From a5d6114baf3daa09dca84a245af7176cf083a163 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Thu, 3 Sep 2026 16:13:03 -0700
Subject: [PATCH 01/43] fix(ssh): stop pane adoption certifying a death from
the relay's not-found union (#18531)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(ssh): stop pane adoption certifying a death from the relay's not-found union
`attachStablePaneOwner` was the last reader that synthesised a runtime exit
from a reattach refusal, and it published code `0` — which
`orca-runtime-on-pty-exit` records as `rememberPtyLivenessVerdict(exited)`, a
death certificate whose only legitimate writer is a host-delivered exit frame.
The refusal it acted on is a union. `pty.attach` answers `PTY "" not found`
both for a pid the relay probed with `isProcessAlive` and for an id its session
map simply never had — which, because ids carry a per-start mint epoch, is every
id minted before a relay restart, checked against nothing. So a relay restart
plus a reconnect certified a shell that was still running under the old daemon's
orphaned process tree, retired the pane binding, and cold-started a second agent
onto the same transcript. The sibling `handlePtyReattachFailure` has always
refused to certify from that union; this path did not.
- The relay marks the one refusal it backed with a liveness check
(`PTY_ATTACH_PROVEN_EXITED_MARKER`). The marker is additive, so an unmarked
answer — including an older relay's — stays ambiguous, which is the safe
direction.
- The client mints that half as `SshPtyProvenExitedOnRelayError`, a subclass so
every existing `isSshPtyAbsentFromRelayError` consumer is unchanged.
- Pane adoption publishes `UNVERIFIED_PROCESS_EXIT_CODE` (-1), the sentinel its
sibling publishes, and passes `hostExitConfirmed` only for evidence that
observed the process: the marked relay refusal, or `SessionNotFoundError` from
the registry that owns the PTY. The ambiguous half now records `unverifiable`
instead of `exited`.
- The gone-branch keys on the error type rather than the bare `PTY ".+" not
found` text, so an untyped string can no longer authorise abandoning a
binding — the discriminator `pty-connect-limits.ts` already documented.
Refs docs/reference/ssh-execution-boundary.md
* test(pty): make the pane-adoption fixtures throw what real providers throw
These four fixtures rejected with bare `new Error('Session not found: ...')` and
`new Error('PTY "..." not found')`. No provider produces either untyped:
`local-pty-spawn` and `decodeDaemonResponseError` both mint
`SessionNotFoundError`, and the SSH reattach path types the relay's wire text
before any pane sees it. Fixtures that skip the type were the reason a
message-shaped gate looked adequate.
The exit-code expectations move with it: the pane path now publishes the -1
stop sentinel plus `hostExitConfirmed`, so a certificate follows the evidence
rather than a synthesized zero.
---
docs/reference/ssh-execution-boundary.md | 2 +
src/main/ipc/pty-dead-owner-respawn.test.ts | 23 ++-
.../pty-pane-reservation-settlement.test.ts | 8 +-
.../pty-persisted-incarnation-repair.test.ts | 14 +-
src/main/ipc/pty/pane/stable-owner.ts | 17 +-
...ble-pane-absence-death-certificate.test.ts | 185 ++++++++++++++++++
src/main/ipc/pty/provider/liveness.ts | 31 ++-
src/main/providers/ssh-pty-errors.ts | 37 +++-
...ty-reattach-absence-discrimination.test.ts | 17 ++
.../providers/ssh-pty-session-reattach.ts | 9 +
.../ssh-expired-lease-pane-readoption.test.ts | 5 +-
src/relay/pty-handler-attach-replay.test.ts | 21 +-
src/relay/pty-handler.ts | 7 +-
src/shared/pty-attach-absence-evidence.ts | 17 ++
14 files changed, 362 insertions(+), 31 deletions(-)
create mode 100644 src/main/ipc/pty/pane/stable-pane-absence-death-certificate.test.ts
create mode 100644 src/shared/pty-attach-absence-evidence.ts
diff --git a/docs/reference/ssh-execution-boundary.md b/docs/reference/ssh-execution-boundary.md
index 070aae61d66..88a4a3c0a0e 100644
--- a/docs/reference/ssh-execution-boundary.md
+++ b/docs/reference/ssh-execution-boundary.md
@@ -66,6 +66,8 @@ A verdict needs evidence from the host that owns the process. Apply these tests
**Does the termination event match the current identity?** A host-delivered exit for the live PTY incarnation and provider generation, while its siblings still report, establishes `exited`. A stale event, an event for a superseded incarnation, or one quiet terminal with no host evidence does not.
+**Did the answer carry its evidence, or only the same wording?** `pty.attach` refuses with `PTY "" not found` both for a pid the relay probed and found gone and for an id its session map never had — which is every id minted before a relay restart, since ids carry a per-start mint epoch. Only the probed refusal carries `PTY_ATTACH_PROVEN_EXITED_MARKER` (`src/shared/pty-attach-absence-evidence.ts`) and reaches the client as `SshPtyProvenExitedOnRelayError`; the unmarked union arrives as `SshPtyAbsentFromRelayError`, which licenses retiring the client's own route to the PTY and nothing more. A missing marker is never evidence — an older relay omits it too.
+
**Is a returned status actually a claim of success?** An operation that reports failure may have succeeded, and one that reports success may not have run — check the durable state it should have changed rather than trusting the return.
Anything short of positive host evidence is `unverifiable`. Reporting it as `exited` is the error this document exists to prevent: it orphans live work and can cold-start a duplicate over the same worktree.
diff --git a/src/main/ipc/pty-dead-owner-respawn.test.ts b/src/main/ipc/pty-dead-owner-respawn.test.ts
index 8f47fa6fcdf..4669d7e5528 100644
--- a/src/main/ipc/pty-dead-owner-respawn.test.ts
+++ b/src/main/ipc/pty-dead-owner-respawn.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { setupPtyIpcSuite } from './pty-ipc-test-harness'
+import { SessionNotFoundError } from '../daemon/daemon-errors'
import { makePaneKey } from '../../shared/stable-pane-id'
import { registerPtyHandlers, setLocalPtyProvider } from './pty'
@@ -59,7 +60,7 @@ describe('registerPtyHandlers', () => {
const providerSpawn = vi.fn(
async (options: { attachOnly?: boolean; command?: string; sessionId?: string }) => {
if (options.attachOnly) {
- throw new Error('Session not found: pty-proven-absent-owner')
+ throw new SessionNotFoundError('pty-proven-absent-owner')
}
return { id: 'pty-fresh-proven', incarnationId: 'inc-fresh-proven' }
}
@@ -161,10 +162,13 @@ describe('registerPtyHandlers', () => {
expect(providerSpawn.mock.calls[1]?.[0]).toMatchObject({
command: 'codex resume proven-absent-session'
})
+ // The registry that owns the PTY answered, so this exit is confirmed — but the code stays the
+ // -1 sentinel; a synthesized zero would be indistinguishable from a clean shell exit.
expect(runtime.onPtyExit).toHaveBeenCalledWith(
'pty-proven-absent-owner',
- 0,
- 'inc-proven-absent-owner'
+ -1,
+ 'inc-proven-absent-owner',
+ { hostExitConfirmed: true }
)
expect(store.setWorkspaceSession).toHaveBeenCalledOnce()
expect(store.flushOrThrow).toHaveBeenCalledOnce()
@@ -178,7 +182,7 @@ describe('registerPtyHandlers', () => {
const providerSpawn = vi.fn(
async (options: { attachOnly?: boolean; command?: string; sessionId?: string }) => {
if (options.attachOnly) {
- throw new Error('Session not found: pty-probe-blip-owner')
+ throw new SessionNotFoundError('pty-probe-blip-owner')
}
return { id: 'pty-fresh-probe-blip', incarnationId: 'inc-fresh-probe-blip' }
}
@@ -282,8 +286,9 @@ describe('registerPtyHandlers', () => {
expect(providerSpawn).toHaveBeenCalledTimes(2)
expect(runtime.onPtyExit).toHaveBeenCalledWith(
'pty-probe-blip-owner',
- 0,
- 'inc-probe-blip-owner'
+ -1,
+ 'inc-probe-blip-owner',
+ { hostExitConfirmed: true }
)
})
// Why: a parked pane (stopped with keepHistory) leaves the runtime holding the binding while
@@ -299,7 +304,7 @@ describe('registerPtyHandlers', () => {
const providerSpawn = vi.fn(
async (options: { attachOnly?: boolean; command?: string; sessionId?: string }) => {
if (options.attachOnly) {
- throw new Error('Session not found: pty-already-retired-owner')
+ throw new SessionNotFoundError('pty-already-retired-owner')
}
return { id: 'pty-fresh-already-retired', incarnationId: 'inc-fresh-already-retired' }
}
@@ -420,6 +425,8 @@ describe('registerPtyHandlers', () => {
expect(providerSpawn.mock.calls[1]?.[0]).toMatchObject({
command: 'codex resume already-retired-session'
})
- expect(runtime.onPtyExit).toHaveBeenCalledWith('pty-already-retired-owner', 0, undefined)
+ expect(runtime.onPtyExit).toHaveBeenCalledWith('pty-already-retired-owner', -1, undefined, {
+ hostExitConfirmed: true
+ })
})
})
diff --git a/src/main/ipc/pty-pane-reservation-settlement.test.ts b/src/main/ipc/pty-pane-reservation-settlement.test.ts
index c894eeaeddd..64a4441f376 100644
--- a/src/main/ipc/pty-pane-reservation-settlement.test.ts
+++ b/src/main/ipc/pty-pane-reservation-settlement.test.ts
@@ -2,6 +2,10 @@ import { describe, expect, it, vi } from 'vitest'
import { spawnMock, registerPtyMock } from './pty-ipc-mock-registry'
import { setupPtyIpcSuite } from './pty-ipc-test-harness'
import { makePaneKey } from '../../shared/stable-pane-id'
+import {
+ SSH_SESSION_EXPIRED_ERROR,
+ SshPtyProvenExitedOnRelayError
+} from '../providers/ssh-pty-errors'
import {
registerPtyHandlers,
registerSshPtyProvider,
@@ -67,7 +71,9 @@ describe('registerPtyHandlers', () => {
const freshPtyId = `ssh:${connectionId}@@fresh-relay-pty`
const remoteSpawn = vi.fn(async (options: { attachOnly?: boolean; command?: string }) => {
if (options.attachOnly) {
- throw new Error('PTY "dead-relay-pty" not found')
+ // The relay's raw wire text never reaches a pane untyped; the SSH reattach path mints the
+ // proven-exit class for the one refusal the relay backed with a pid probe.
+ throw new SshPtyProvenExitedOnRelayError(`${SSH_SESSION_EXPIRED_ERROR}: dead-relay-pty`)
}
return { id: freshPtyId, incarnationId: 'inc-fresh-ssh-owner' }
})
diff --git a/src/main/ipc/pty-persisted-incarnation-repair.test.ts b/src/main/ipc/pty-persisted-incarnation-repair.test.ts
index 8f565f1c08e..743963d0189 100644
--- a/src/main/ipc/pty-persisted-incarnation-repair.test.ts
+++ b/src/main/ipc/pty-persisted-incarnation-repair.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { statSyncMock } from './pty-ipc-mock-registry'
import { setupPtyIpcSuite } from './pty-ipc-test-harness'
-import { TerminalSessionOwnerUnverifiedError } from '../daemon/daemon-errors'
+import { SessionNotFoundError, TerminalSessionOwnerUnverifiedError } from '../daemon/daemon-errors'
import { makePaneKey } from '../../shared/stable-pane-id'
import { registerPtyHandlers, clearProviderPtyState, setLocalPtyProvider } from './pty'
@@ -230,7 +230,7 @@ describe('registerPtyHandlers', () => {
const providerSpawn = vi.fn(
async (options: { attachOnly?: boolean; command?: string; sessionId?: string }) => {
if (options.attachOnly) {
- throw new Error('Session not found: pty-dead-persisted-owner')
+ throw new SessionNotFoundError('pty-dead-persisted-owner')
}
return { id: 'pty-fresh-recovery', incarnationId: 'inc-fresh-recovery' }
}
@@ -352,8 +352,9 @@ describe('registerPtyHandlers', () => {
expect(store.setWorkspaceSession).toHaveBeenCalledOnce()
expect(runtime.onPtyExit).toHaveBeenCalledWith(
'pty-dead-persisted-owner',
- 0,
- 'inc-dead-persisted-owner'
+ -1,
+ 'inc-dead-persisted-owner',
+ { hostExitConfirmed: true }
)
return
}
@@ -376,8 +377,9 @@ describe('registerPtyHandlers', () => {
expect(store.flushOrThrow).toHaveBeenCalledOnce()
expect(runtime.onPtyExit).toHaveBeenCalledWith(
'pty-dead-persisted-owner',
- 0,
- 'inc-dead-persisted-owner'
+ -1,
+ 'inc-dead-persisted-owner',
+ { hostExitConfirmed: true }
)
}
)
diff --git a/src/main/ipc/pty/pane/stable-owner.ts b/src/main/ipc/pty/pane/stable-owner.ts
index 9442e914e1f..731065e59ab 100644
--- a/src/main/ipc/pty/pane/stable-owner.ts
+++ b/src/main/ipc/pty/pane/stable-owner.ts
@@ -1,5 +1,6 @@
import { toSshExecutionHostId } from '../../../../shared/execution-host'
import { makePaneKey, parsePaneKey } from '../../../../shared/stable-pane-id'
+import { UNVERIFIED_PROCESS_EXIT_CODE } from '../../../../shared/terminal-exit-cause'
import type { Store } from '../../../persistence'
import { retireTerminalSurfaceFromPersistence } from '../../../runtime/mobile-session-terminal-persistence-retirement'
import type { OrcaRuntimeService } from '../../../runtime/orca-runtime'
@@ -11,7 +12,7 @@ import {
TerminalSessionOwnerUnverifiedError
} from '../../../daemon/daemon-errors'
import { ptyIncarnationById, ptyOwnership } from '../provider/ownership-state'
-import { isPtyAlreadyGoneError } from '../provider/liveness'
+import { isHostReportedPtyAbsenceError, isObservedPtyExitEvidence } from '../provider/liveness'
import { clearProviderPtyState } from '../provider/state-cleanup'
export type StablePaneOwner = {
@@ -239,7 +240,7 @@ export async function attachStablePaneOwner(
if (isDaemonEndpointGoneError(error)) {
throw new TerminalHostGoneError()
}
- if (!isPtyAlreadyGoneError(error)) {
+ if (!isHostReportedPtyAbsenceError(error)) {
throw error
}
const ownerBeforeRetire = args.resolveOwner?.()
@@ -252,7 +253,17 @@ export async function attachStablePaneOwner(
) {
throw new Error('terminal_pane_owner_changed')
}
- runtime?.onPtyExit(owner.ptyId, 0, owner.incarnationId)
+ // `pty.attach` answers absent both for a pid the relay probed and found gone and for an id its
+ // session map never had — every id minted before a relay restart, checked against nothing. Only
+ // the marked half observed the process, so only it may certify a death; the rest publishes the
+ // stop sentinel its sibling handlePtyReattachFailure publishes, which every reader resolves to
+ // `stop_unverified` (docs/reference/ssh-execution-boundary.md).
+ runtime?.onPtyExit(
+ owner.ptyId,
+ UNVERIFIED_PROCESS_EXIT_CODE,
+ owner.incarnationId,
+ isObservedPtyExitEvidence(error) ? { hostExitConfirmed: true } : {}
+ )
clearProviderPtyState(owner.ptyId)
ptyOwnership.delete(owner.ptyId)
if (
diff --git a/src/main/ipc/pty/pane/stable-pane-absence-death-certificate.test.ts b/src/main/ipc/pty/pane/stable-pane-absence-death-certificate.test.ts
new file mode 100644
index 00000000000..972f479b492
--- /dev/null
+++ b/src/main/ipc/pty/pane/stable-pane-absence-death-certificate.test.ts
@@ -0,0 +1,185 @@
+// `attachStablePaneOwner` is the last reader that synthesised a runtime exit from a reattach
+// refusal, and it published code 0 — which `orca-runtime-on-pty-exit` records as a death
+// certificate. The refusal it acts on is a union: `pty.attach` answers absent both for a pid the
+// relay probed and found gone, and for an id its session map never had, which is every id minted
+// before a relay restart. Certifying the union orphans a live remote shell and cold-starts a second
+// agent onto its transcript (docs/reference/ssh-execution-boundary.md).
+//
+// The sibling handlePtyReattachFailure has always refused to certify from that union. These pin the
+// same rule here, and pin that the marked half — the one refusal the relay backed with a pid probe
+// — still earns the certificate, so a genuinely dead PTY is not left `unverifiable` forever.
+import { describe, expect, it, vi } from 'vitest'
+import { getDefaultWorkspaceSession } from '../../../../shared/constants'
+import { makePaneKey } from '../../../../shared/stable-pane-id'
+import { SSH_EXIT_UNCONFIRMED_REASON } from '../../../../shared/pty-liveness-verdict'
+import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types'
+import { SessionNotFoundError } from '../../../daemon/daemon-errors'
+import type { Store } from '../../../persistence'
+import {
+ SSH_SESSION_EXPIRED_ERROR,
+ SshPtyAbsentFromRelayError,
+ SshPtyProvenExitedOnRelayError
+} from '../../../providers/ssh-pty-errors'
+import type { IPtyProvider } from '../../../providers/types'
+import { OrcaRuntimeService } from '../../../runtime/orca-runtime'
+import { resolvePersistedStablePaneOwner, spawnForStablePane } from './stable-owner'
+
+const CONNECTION = 'conn-1'
+const WORKTREE = 'repo-1::/tmp/pane-absence'
+const TAB = 'tab-1'
+const LEAF = '1b3f2c4d-5e6a-4b7c-8d9e-0f1a2b3c4d5e'
+const SIBLING_LEAF = '2c4d3e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f'
+// Ids carry the relay's per-start mint epoch, so this one names a PTY the CURRENT relay never minted.
+const PTY_ID = 'ssh:conn-1@@pty2:epoch-a:1'
+const OWNER = { tabId: TAB, leafId: LEAF, ptyId: PTY_ID, hasPersistedBinding: true as const }
+
+function paneStore(): { store: Store; read: () => WorkspaceSessionState } {
+ let session = {
+ ...getDefaultWorkspaceSession(),
+ tabsByWorktree: {
+ [WORKTREE]: [{ id: TAB, type: 'terminal', worktreeId: WORKTREE, ptyId: PTY_ID }]
+ },
+ terminalLayoutsByTabId: {
+ [TAB]: {
+ root: {
+ type: 'split',
+ direction: 'row',
+ first: { type: 'leaf', leafId: LEAF },
+ second: { type: 'leaf', leafId: SIBLING_LEAF }
+ },
+ activeLeafId: LEAF,
+ ptyIdsByLeafId: { [LEAF]: PTY_ID, [SIBLING_LEAF]: 'ssh:conn-1@@pty2:epoch-a:2' }
+ }
+ }
+ } as unknown as WorkspaceSessionState
+ return {
+ read: () => session,
+ store: {
+ getWorkspaceSession: () => session,
+ setWorkspaceSession: (next: WorkspaceSessionState) => {
+ session = next
+ },
+ flushOrThrow: () => {},
+ getRepos: () => [
+ {
+ id: 'repo-1',
+ path: '/tmp/pane-absence',
+ displayName: 'pane-absence',
+ badgeColor: '#000000',
+ addedAt: 0
+ }
+ ],
+ getAllWorktreeMeta: () => ({}),
+ getWorktreeMeta: () => undefined,
+ setWorktreeMeta: () => {},
+ removeWorktreeMeta: () => {},
+ getSettings: () => ({ workspaceDir: '/tmp/workspaces' }),
+ getProjects: () => []
+ } as unknown as Store
+ }
+}
+
+function runtimeOwning(store: Store): OrcaRuntimeService {
+ const runtime = new OrcaRuntimeService(store as never)
+ runtime.setPtyController({
+ write: () => true,
+ kill: () => true,
+ hasPty: () => null,
+ listProcesses: async () => [],
+ getForegroundProcess: async () => null
+ } as never)
+ runtime.attachWindow(1)
+ runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
+ runtime.registerPty(PTY_ID, WORKTREE, CONNECTION)
+ return runtime
+}
+
+async function adoptAfterAttachRefusal(error: unknown): Promise<{
+ runtime: OrcaRuntimeService
+ store: Store
+ read: () => WorkspaceSessionState
+ spawn: ReturnType
+}> {
+ const { store, read } = paneStore()
+ const runtime = runtimeOwning(store)
+ const spawn = vi
+ .fn()
+ .mockRejectedValueOnce(error)
+ .mockResolvedValueOnce({ id: 'ssh:conn-1@@pty2:epoch-b:1', isReattach: false })
+ await spawnForStablePane({
+ runtime,
+ store,
+ provider: { spawn } as unknown as IPtyProvider,
+ spawnOptions: { cols: 80, rows: 24 },
+ owner: OWNER,
+ worktreeId: WORKTREE,
+ connectionId: CONNECTION,
+ resolveOwner: () => null
+ })
+ return { runtime, store, read, spawn }
+}
+
+describe('a stable pane whose reattach was refused', () => {
+ it('records no death certificate when the relay merely does not know the id', async () => {
+ const { runtime, spawn } = await adoptAfterAttachRefusal(
+ new SshPtyAbsentFromRelayError(`${SSH_SESSION_EXPIRED_ERROR}: pty2:epoch-a:1`)
+ )
+
+ expect(spawn).toHaveBeenCalledTimes(2)
+ // The shell may well still be running under the previous daemon's orphaned process tree, so the
+ // register must keep saying "we could not observe it" — not "it ended".
+ expect(runtime.getPtyLivenessVerdict(PTY_ID)).toEqual({
+ status: 'unverifiable',
+ reason: SSH_EXIT_UNCONFIRMED_REASON
+ })
+ })
+
+ it('still certifies the death the relay proved with a pid probe', async () => {
+ const { runtime, store, spawn } = await adoptAfterAttachRefusal(
+ new SshPtyProvenExitedOnRelayError(`${SSH_SESSION_EXPIRED_ERROR}: pty2:epoch-a:1`)
+ )
+
+ expect(spawn).toHaveBeenCalledTimes(2)
+ expect(runtime.getPtyLivenessVerdict(PTY_ID)).toEqual({ status: 'exited' })
+ // If nothing ever retired, a proven-dead pane would reattach to a corpse on every adoption.
+ expect(
+ resolvePersistedStablePaneOwner(store, makePaneKey(TAB, LEAF), WORKTREE, CONNECTION)
+ ).toBeNull()
+ })
+
+ it('certifies an absence reported by the process registry that owns the PTY', async () => {
+ // The daemon (or the in-process map) answering here is the owner of the process, and an
+ // endpoint that had gone raises TerminalHostGoneError above, so this absence is an observation
+ // rather than a lost route.
+ const { runtime } = await adoptAfterAttachRefusal(new SessionNotFoundError(PTY_ID))
+
+ expect(runtime.getPtyLivenessVerdict(PTY_ID)).toEqual({ status: 'exited' })
+ })
+
+ it('refuses to abandon the binding on an untyped "not found" string', async () => {
+ // The relay's raw wire wording. The SSH reattach path types it before any pane sees it, so an
+ // untyped one reached this gate having lost every distinction the type carries — including
+ // whether the answer came from the host that owns the process at all.
+ const { store, read } = paneStore()
+ const before = JSON.stringify(read())
+ const runtime = runtimeOwning(store)
+ const spawn = vi.fn().mockRejectedValue(new Error(`PTY "pty2:epoch-a:1" not found`))
+
+ await expect(
+ spawnForStablePane({
+ runtime,
+ store,
+ provider: { spawn } as unknown as IPtyProvider,
+ spawnOptions: { cols: 80, rows: 24 },
+ owner: OWNER,
+ worktreeId: WORKTREE,
+ connectionId: CONNECTION,
+ resolveOwner: () => null
+ })
+ ).rejects.toThrow('not found')
+
+ expect(spawn).toHaveBeenCalledTimes(1)
+ expect(runtime.getPtyLivenessVerdict(PTY_ID)).toBeNull()
+ expect(JSON.stringify(read())).toBe(before)
+ })
+})
diff --git a/src/main/ipc/pty/provider/liveness.ts b/src/main/ipc/pty/provider/liveness.ts
index cc764143384..8a362b74aaf 100644
--- a/src/main/ipc/pty/provider/liveness.ts
+++ b/src/main/ipc/pty/provider/liveness.ts
@@ -2,10 +2,12 @@ import { isRemoteAgentHooksEnabled } from '../../../../shared/agent-hook-relay'
import type { AgentSessionOwnerBinding } from '../../../../shared/agent-session-host-authority'
import { agentSessionOwnerBindingsEqual } from '../../../../shared/claimed-agent-pty-owner'
import { addNodePtyRecoveryHint } from '../../../daemon/node-pty-error-hints'
+import { SessionNotFoundError } from '../../../daemon/daemon-errors'
import type { Store } from '../../../persistence'
import {
isSshPtyAbsentFromRelayError,
- isSshPtyNotFoundError
+ isSshPtyNotFoundError,
+ isSshPtyProvenExitedOnRelayError
} from '../../../providers/ssh-pty-errors'
import type { IPtyProvider } from '../../../providers/types'
import { markClaudePtyExited } from '../../../claude-accounts/live-pty-gate'
@@ -66,6 +68,33 @@ export function isPtyAlreadyGoneError(err: unknown): boolean {
)
}
+/**
+ * Narrower than {@link isPtyAlreadyGoneError}, for the one caller that retires a durable pane
+ * binding rather than just releasing in-memory state: only a typed answer from the host that owns
+ * the process may authorise that. The bare `PTY ".+" not found` text is the relay's raw wire
+ * wording, which the SSH reattach path always types before it reaches a pane; matching the text
+ * instead would let any untyped string carrying that phrase unbind a live pane
+ * (docs/reference/ssh-execution-boundary.md).
+ */
+export function isHostReportedPtyAbsenceError(err: unknown): boolean {
+ return isSshPtyAbsentFromRelayError(err) || err instanceof SessionNotFoundError
+}
+
+/**
+ * The half of {@link isHostReportedPtyAbsenceError} that actually observed the process, and so the
+ * only half that may certify an exit.
+ *
+ * The relay's plain absence answer is excluded because `pty.attach` gives it for an id its session
+ * map never had as readily as for a pid it probed — after a relay restart, every id the previous
+ * one minted. `SessionNotFoundError` is included because the process answering is the one that owns
+ * the PTY: the in-process registry itself, or a daemon whose endpoint is live (a gone endpoint
+ * raises `isDaemonEndpointGoneError` instead), so its absence is an observation rather than a lost
+ * route (docs/reference/ssh-execution-boundary.md).
+ */
+export function isObservedPtyExitEvidence(err: unknown): boolean {
+ return isSshPtyProvenExitedOnRelayError(err) || err instanceof SessionNotFoundError
+}
+
export function delay(ms: number): Promise {
return new Promise((resolve) => {
const timer = setTimeout(resolve, ms)
diff --git a/src/main/providers/ssh-pty-errors.ts b/src/main/providers/ssh-pty-errors.ts
index 92ee84941ad..4d312caa8b1 100644
--- a/src/main/providers/ssh-pty-errors.ts
+++ b/src/main/providers/ssh-pty-errors.ts
@@ -20,12 +20,16 @@ export function isSshPtyIdentityMismatchError(error: unknown): boolean {
}
/**
- * A reachable relay answered for this exact PTY id and reported it absent — positive evidence of
- * absence from the execution host, so `exited` rather than `unverifiable`
- * (docs/reference/ssh-execution-boundary.md). Deliberately NOT raised for a transport failure, a
- * request timeout, a disposed multiplexer, an identity mismatch (the id names a live PTY belonging
- * to another pane), or `restoreRequired` (the PTY is live, only its source stream is not) — none of
- * those observe the process, and treating them as absence orphans live remote work.
+ * A reachable relay answered for this exact PTY id and reported it absent, so the client may retire
+ * its own route to it. Deliberately NOT raised for a transport failure, a request timeout, a
+ * disposed multiplexer, an identity mismatch (the id names a live PTY belonging to another pane),
+ * or `restoreRequired` (the PTY is live, only its source stream is not) — none of those observe the
+ * process, and treating them as absence orphans live remote work.
+ *
+ * This is NOT itself a death certificate. `pty.attach` answers absent for an id its session map
+ * never had as readily as for a pid it probed — and after a relay restart that is every id the
+ * previous one minted. Certifying `exited` needs {@link SshPtyProvenExitedOnRelayError}
+ * (docs/reference/ssh-execution-boundary.md).
*
* Carries the same `SSH_SESSION_EXPIRED` message so message-based consumers are unaffected; only
* callers that can act on the stronger verdict test the class.
@@ -40,3 +44,24 @@ export class SshPtyAbsentFromRelayError extends Error {
export function isSshPtyAbsentFromRelayError(error: unknown): boolean {
return error instanceof SshPtyAbsentFromRelayError
}
+
+/**
+ * The narrow half of {@link SshPtyAbsentFromRelayError}: the relay probed the pid and found it gone
+ * before answering absent, so this is the one attach refusal that observed the process and the only
+ * one that may certify a death.
+ *
+ * The parent class is raised for the whole union, which also contains "this session map has no such
+ * id" — every id minted before a relay restart, checked against nothing. Callers that only release
+ * client-side bookkeeping keep testing the parent; a caller about to record `exited` must test this
+ * (docs/reference/ssh-execution-boundary.md).
+ */
+export class SshPtyProvenExitedOnRelayError extends SshPtyAbsentFromRelayError {
+ constructor(message: string) {
+ super(message)
+ this.name = 'SshPtyProvenExitedOnRelayError'
+ }
+}
+
+export function isSshPtyProvenExitedOnRelayError(error: unknown): boolean {
+ return error instanceof SshPtyProvenExitedOnRelayError
+}
diff --git a/src/main/providers/ssh-pty-reattach-absence-discrimination.test.ts b/src/main/providers/ssh-pty-reattach-absence-discrimination.test.ts
index 79ed97a8f19..bfcc174a4b6 100644
--- a/src/main/providers/ssh-pty-reattach-absence-discrimination.test.ts
+++ b/src/main/providers/ssh-pty-reattach-absence-discrimination.test.ts
@@ -14,9 +14,11 @@
import { describe, expect, it, vi } from 'vitest'
import {
isSshPtyAbsentFromRelayError,
+ isSshPtyProvenExitedOnRelayError,
SSH_PTY_SOURCE_RESTORE_REQUIRED_ERROR,
SSH_SESSION_EXPIRED_ERROR
} from './ssh-pty-errors'
+import { PTY_ATTACH_PROVEN_EXITED_MARKER } from '../../shared/pty-attach-absence-evidence'
import { reattachSshPtySessionForSpawn } from './ssh-pty-session-reattach'
import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer'
@@ -55,6 +57,20 @@ describe('an SSH reattach refusal says whether the host observed the PTY', () =>
expect(error.message).toContain(SSH_SESSION_EXPIRED_ERROR)
expect(isSshPtyAbsentFromRelayError(error)).toBe(true)
+ // ...but absence from the relay is a union. An unmarked answer is also what a restarted relay
+ // gives for every id the previous one minted, checked against nothing, so it may not certify a
+ // death (docs/reference/ssh-execution-boundary.md).
+ expect(isSshPtyProvenExitedOnRelayError(error)).toBe(false)
+ })
+
+ it('separates the refusal the relay backed with a pid probe', async () => {
+ const error = await refusalFrom(async () => {
+ throw new Error(`PTY "${SESSION}" not found (${PTY_ATTACH_PROVEN_EXITED_MARKER})`)
+ })
+
+ expect(error.message).toContain(SSH_SESSION_EXPIRED_ERROR)
+ expect(isSshPtyAbsentFromRelayError(error)).toBe(true)
+ expect(isSshPtyProvenExitedOnRelayError(error)).toBe(true)
})
it('gives a restoreRequired refusal its own token instead of the expiry text', async () => {
@@ -79,5 +95,6 @@ describe('an SSH reattach refusal says whether the host observed the PTY', () =>
expect(error.message).toContain(SSH_SESSION_EXPIRED_ERROR)
expect(isSshPtyAbsentFromRelayError(error)).toBe(false)
+ expect(isSshPtyProvenExitedOnRelayError(error)).toBe(false)
})
})
diff --git a/src/main/providers/ssh-pty-session-reattach.ts b/src/main/providers/ssh-pty-session-reattach.ts
index 530135ad310..f05373a03dc 100644
--- a/src/main/providers/ssh-pty-session-reattach.ts
+++ b/src/main/providers/ssh-pty-session-reattach.ts
@@ -5,9 +5,11 @@ import {
SSH_PTY_SOURCE_RESTORE_REQUIRED_ERROR,
SSH_SESSION_EXPIRED_ERROR,
SshPtyAbsentFromRelayError,
+ SshPtyProvenExitedOnRelayError,
isSshPtyIdentityMismatchError,
isSshPtyNotFoundError
} from './ssh-pty-errors'
+import { isProvenExitedPtyAttachRefusal } from '../../shared/pty-attach-absence-evidence'
import { toAppSshPtyId, toRelaySshPtyId } from './ssh-pty-id'
import type { PtySpawnOptions, PtySpawnResult } from './types'
import type { SshPtySpawnExitRaceTracker } from './ssh-pty-spawn-exit-race'
@@ -238,6 +240,13 @@ export async function reattachSshPtySession(args: {
// Why the class: the relay answered for this exact id, so callers holding a pane binding may
// retire it and spawn fresh. Plain `SSH_SESSION_EXPIRED` cannot say that — a restarted relay
// renumbers from pty-1, so the message alone is indistinguishable from a lost link.
+ //
+ // Why the subclass: the relay marks the one refusal it backed with a pid probe. Without the
+ // marker the answer is the "no such id" union, which is not evidence the shell ended, so the
+ // narrow class is minted only when the relay said so (docs/reference/ssh-execution-boundary.md).
+ if (isProvenExitedPtyAttachRefusal(error)) {
+ throw new SshPtyProvenExitedOnRelayError(`${SSH_SESSION_EXPIRED_ERROR}: ${relaySessionId}`)
+ }
throw new SshPtyAbsentFromRelayError(`${SSH_SESSION_EXPIRED_ERROR}: ${relaySessionId}`)
}
throw error
diff --git a/src/main/ssh-expired-lease-pane-readoption.test.ts b/src/main/ssh-expired-lease-pane-readoption.test.ts
index 4c2c34b5f62..11bf5fe2083 100644
--- a/src/main/ssh-expired-lease-pane-readoption.test.ts
+++ b/src/main/ssh-expired-lease-pane-readoption.test.ts
@@ -7,6 +7,7 @@ import { resolvePersistedStablePaneOwner } from './ipc/pty/pane/stable-owner'
import { adoptStablePane } from './ipc/pty/pane/adopt-stable'
import { sshProviders } from './ipc/pty/provider/registry'
import type { IPtyProvider } from './providers/types'
+import { SSH_SESSION_EXPIRED_ERROR, SshPtyAbsentFromRelayError } from './providers/ssh-pty-errors'
import { testState, createStore, makeTerminalTab } from './persistence-test-harness'
import { TEST_LEAF_1 } from './persistence-session-fixtures'
@@ -141,11 +142,13 @@ describe('recovery through createTerminal reattaches before it respawns', () =>
expect(adopted?.owner).toMatchObject({ ptyId: APP_PTY_ID, hasPersistedBinding: true })
})
+ // The typed refusal is what the SSH reattach path raises; the raw `PTY "…" not found` wire text
+ // never reaches a pane untyped, and an untyped one no longer authorises abandoning the binding.
it('falls through to a fresh spawn once the host answers that the PTY is absent', async () => {
const store = storeWithBoundRemotePane()
store.markSshRemotePtyLease(TARGET, APP_PTY_ID, 'expired')
const spawn = vi.fn(async () => {
- throw new Error('PTY "remote-pty" not found')
+ throw new SshPtyAbsentFromRelayError(`${SSH_SESSION_EXPIRED_ERROR}: remote-pty`)
})
sshProviders.set(TARGET, { spawn } as unknown as IPtyProvider)
diff --git a/src/relay/pty-handler-attach-replay.test.ts b/src/relay/pty-handler-attach-replay.test.ts
index 6af9faec047..e289547bf0c 100644
--- a/src/relay/pty-handler-attach-replay.test.ts
+++ b/src/relay/pty-handler-attach-replay.test.ts
@@ -1,5 +1,9 @@
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import * as ptyShellUtils from './pty-shell-utils'
+import {
+ PTY_ATTACH_PROVEN_EXITED_MARKER,
+ isProvenExitedPtyAttachRefusal
+} from '../shared/pty-attach-absence-evidence'
const { mockPtySpawn, mockPtyInstance, mockCreateShellPromptReadinessProbe } = vi.hoisted(() => ({
mockPtySpawn: vi.fn(),
@@ -87,7 +91,7 @@ describe('PtyHandler', () => {
try {
await expect(
dispatcher.callRequest('pty.attach', { id: PTY_1, suppressReplayNotification: true })
- ).rejects.toThrow(`PTY "${PTY_1}" not found`)
+ ).rejects.toThrow(`PTY "${PTY_1}" not found (${PTY_ATTACH_PROVEN_EXITED_MARKER})`)
} finally {
aliveSpy.mockRestore()
}
@@ -96,9 +100,18 @@ describe('PtyHandler', () => {
// is freed so a later attach also cleanly reports not-found.
expect(exits).toEqual([{ id: PTY_1, paneKey: 'tab-dead:0' }])
expect(handler.activePtyCount).toBe(0)
- await expect(
- dispatcher.callRequest('pty.attach', { id: PTY_1, suppressReplayNotification: true })
- ).rejects.toThrow(`PTY "${PTY_1}" not found`)
+ const unknownId = await dispatcher
+ .callRequest('pty.attach', { id: PTY_1, suppressReplayNotification: true })
+ .then(
+ () => new Error('expected the attach to be refused'),
+ (error: Error) => error
+ )
+
+ // The second refusal is the shape a restarted relay gives for every id the previous one minted:
+ // same words, no liveness check behind them. Only the probed one may be read as a death
+ // (docs/reference/ssh-execution-boundary.md).
+ expect(unknownId.message).toContain(`PTY "${PTY_1}" not found`)
+ expect(isProvenExitedPtyAttachRefusal(unknownId)).toBe(false)
})
it('settles concurrent immediate shutdown when attach proves the shell exited', async () => {
diff --git a/src/relay/pty-handler.ts b/src/relay/pty-handler.ts
index 1c213325bb8..42f8bdc0a77 100644
--- a/src/relay/pty-handler.ts
+++ b/src/relay/pty-handler.ts
@@ -35,6 +35,7 @@ import {
type RelaySpawnCwdResolution
} from './pty-spawn-cwd'
import { PhysicalExitTracker } from '../shared/physical-exit-tracker'
+import { PTY_ATTACH_PROVEN_EXITED_MARKER } from '../shared/pty-attach-absence-evidence'
import { SHELL_READY_MARKER_PREFIX } from '../main/shell-ready-marker-scanner'
import {
createShellStartupOutputScanState,
@@ -2055,7 +2056,11 @@ export class PtyHandler {
// Why: verify liveness because shells can exit without node-pty onExit.
if (this.reapPtyProvenExited(managed)) {
- throw new Error(`PTY "${id}" not found`)
+ // Why the marker: this is the ONLY not-found answer backed by a liveness check. The unmarked
+ // one above is also thrown for an id this session map never had — every id minted before a
+ // relay restart — so a client that cannot tell them apart certifies deaths it never observed
+ // (docs/reference/ssh-execution-boundary.md).
+ throw new Error(`PTY "${id}" not found (${PTY_ATTACH_PROVEN_EXITED_MARKER})`)
}
// Why: legacy `pty-N` ids repeated across relay generations; reject conflicting identities.
diff --git a/src/shared/pty-attach-absence-evidence.ts b/src/shared/pty-attach-absence-evidence.ts
new file mode 100644
index 00000000000..2b306ca40f0
--- /dev/null
+++ b/src/shared/pty-attach-absence-evidence.ts
@@ -0,0 +1,17 @@
+/**
+ * `pty.attach` refuses with `PTY "" not found` for two unrelated situations: a pid the relay
+ * probed and found gone, and an id its session map simply never had — which is every id minted
+ * before a relay restart, since ids carry a per-start mint epoch. Only the first observes the
+ * process, so only the first carries this marker.
+ *
+ * The marker is additive on purpose: an answer without it means "ambiguous", which is also what an
+ * older relay's unmarked answer means, so a client may never read a missing marker as evidence of
+ * anything (docs/reference/ssh-execution-boundary.md).
+ */
+export const PTY_ATTACH_PROVEN_EXITED_MARKER = 'process exited'
+
+const PROVEN_EXITED_ATTACH_REFUSAL = /PTY ".+" not found \(process exited\)/i
+
+export function isProvenExitedPtyAttachRefusal(error: unknown): boolean {
+ return PROVEN_EXITED_ATTACH_REFUSAL.test(error instanceof Error ? error.message : String(error))
+}
From 04ae62202ae05036ad7332e71083d37ae29f8f6a Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Thu, 3 Sep 2026 16:13:07 -0700
Subject: [PATCH 02/43] fix(ssh): close the macOS relay's per-terminal pty fd
leak (#18534)
The relay asset from #17920 only rewrote the forkpty `default:` call site, which
sits in the `#else` arm of PtyFork's `#if defined(__APPLE__)`. macOS takes
`pty_posix_spawn`, so the asset had never patched anything a Mac executes -- and
`applyNodePtyMasterCloexecPatch` returned 'fixed' for any non-Linux host without
running the script at all, which is what publishes a tree to the shared
native-deps cache.
Stock `pty_posix_spawn` opens up to three throwaway ptys to push the real master
off fds 0-2 and never closes them: the cleanup loop is `for (; count > 0;
count--)`, but the first `posix_openpt()` in a running process already returns
>= 2, so it breaks with `count == 0` and the body never runs -- and where it does
run it closes `low_fds[count]`, never `low_fds[0]`. One orphaned /dev/ptmx fd per
terminal, for the life of the relay.
Ports the `low_fds` fix and the Apple-branch `pty_cloexec(master)` call from the
app's `config/patches/node-pty@1.1.0.patch`, byte-identical, and runs the gate on
darwin. macOS needs a different build layout than Linux: it has no `build/` at
all, so the fallback moved aside is `prebuilds/darwin-` -- which is also
what makes node-pty's install script fall through from "prebuild found" to
node-gyp -- and the compile writes a `build/Release` the loader checks first.
Verification is per-platform too: Linux's leak is inheritance (/proc), macOS's is
self-held (lsof).
Also corrects the asset's claim that "macOS re-opens the tty through uv_tty_init's
cloexec dup". Measured false: FD_CLOEXEC is not set on the master. What protects
it is POSIX_SPAWN_CLOEXEC_DEFAULT, one option away from gone since uid/gid drops
libuv back to fork()/exec() -- so the master is now marked there too.
Measured on darwin-arm64, one PTY per open/close cycle in a relay-shaped dir
running the relay's own commands:
before cycle:ptmx 1:1 2:2 3:3 ... 10:10 (10 after a settle)
after cycle:ptmx 1:0 2:0 3:0 ... 10:0 (0 after a settle)
Linux re-verified in docker node:22: inherited before, isolated after,
`already-patched` on the second run.
Refs #17915
Refs #8362
---
.../node-pty-1.1.0-master-cloexec-patch.cjs | 202 ++++++++++++++----
.../node-pty-master-cloexec-patch.test.mjs | 138 ++++++++++--
src/main/ssh/ssh-relay-deploy.ts | 37 ++--
...h-relay-pty-master-cloexec-install.test.ts | 55 +++--
4 files changed, 343 insertions(+), 89 deletions(-)
diff --git a/config/relay-assets/node-pty-1.1.0-master-cloexec-patch.cjs b/config/relay-assets/node-pty-1.1.0-master-cloexec-patch.cjs
index f4f4f87619a..40bc0350d86 100644
--- a/config/relay-assets/node-pty-1.1.0-master-cloexec-patch.cjs
+++ b/config/relay-assets/node-pty-1.1.0-master-cloexec-patch.cjs
@@ -1,16 +1,34 @@
/**
- * Relay-side pty-master close-on-exec patch for node-pty 1.1.0 (#17915).
+ * Relay-side pty fd-leak patch for node-pty 1.1.0 (#17915).
*
* The app gets this through pnpm `patchedDependencies`; the relay installs stock
- * node-pty from npm onto the host, where no pnpm patch reaches. Without it every
- * later child of the relay -- pty children, git helpers, probes, agent CLIs --
- * inherits each live master fd and keeps its /dev/pts device alive for the life
- * of the relay (#8362).
+ * node-pty from npm onto the host, where no pnpm patch reaches. Stock 1.1.0 leaks
+ * a pty fd on both Unix relay platforms, by two unrelated bugs on two code paths.
*
- * Linux only, deliberately: it is the only relay platform that takes forkpty()'s
- * no-atomic-O_CLOEXEC path, and the only one that already compiles node-pty at
- * install time, so the rebuild costs a second compile rather than a first one.
- * macOS re-opens the tty through uv_tty_init's cloexec dup and Windows has no fds.
+ * Linux takes forkpty(), which has no atomic O_CLOEXEC, so every later child of
+ * the relay -- pty children, git helpers, probes, agent CLIs -- inherits each live
+ * master and keeps its /dev/pts device alive for the life of the relay (#8362).
+ *
+ * macOS takes pty_posix_spawn(), which opens up to three throwaway ptys to push
+ * the real master off fds 0-2 and then never closes them: the cleanup loop is
+ * `for (; count > 0; count--)`, but in any running process the first posix_openpt()
+ * already returns >= 2, so the loop breaks with count == 0 and its body never runs
+ * -- and where it does run it closes low_fds[count], never low_fds[0]. Measured on
+ * darwin-arm64: one orphaned /dev/ptmx fd per terminal, never returned.
+ *
+ * macOS does not inherit the master into spawned children today, but not because it
+ * is marked: FD_CLOEXEC is not set on it (`lsof +fg` shows R,W,NB, no CX). What
+ * closes it is POSIX_SPAWN_CLOEXEC_DEFAULT in pty_posix_spawn's spawn flags, an
+ * Apple-only flag that closes every fd in the child. That is one option away from
+ * gone -- setting uid/gid drops libuv back to fork()/exec(), which honors nothing
+ * but FD_CLOEXEC -- so the master is marked on the Apple path too, exactly as the
+ * app's pnpm patch marks it. Windows has no fds and is excluded.
+ *
+ * The compile it buys differs by platform. Linux relays already run node-gyp at
+ * install time (1.1.0 ships no linux prebuild), so this is a second compile on a
+ * path that already compiles. macOS runs the shipped darwin prebuild and has no
+ * build/ at all, so this is its first compile -- the price of the only fix there
+ * is, since the bug is in the source that prebuild was built from.
*
* Non-fatal by construction: the working build is moved aside before anything is
* touched and moved back on any failure, and a failed attempt drops a skip marker
@@ -31,7 +49,7 @@ const { dirname, join, resolve } = require('node:path')
const EXPECTED_NODE_PTY_VERSION = '1.1.0'
const ORIGINAL_SOURCE_SHA256 = '5e1005d6bdcfbe97b486ee415419fe7adae99035047f07340fbad36419e0bae6'
-const PATCHED_SOURCE_SHA256 = '97dea52199216c01b62070758f0f38621ae53adc16c221271dd35ae2d8ee3482'
+const PATCHED_SOURCE_SHA256 = '3e6bc1a688aae187d231687130cfc0a11781c672f5f616d73183d471ee8ee65c'
const STATUS_PREFIX = 'ORCA-NPTY-CLOEXEC:'
const SKIP_MARKER_FILENAME = '.node-pty-cloexec-skip'
@@ -97,7 +115,56 @@ const FORKPTY_CALL_SITE = [
`
]
-const REPLACEMENTS = [FORWARD_DECLARATION, DEFINITION, FORKPTY_CALL_SITE]
+// Apple never reaches FORKPTY_CALL_SITE: `default:` sits in the `#else` arm of PtyFork's
+// `#if defined(__APPLE__)`, so before this pair the asset patched nothing macOS executes.
+const POSIX_SPAWN_CALL_SITE = [
+ ` if (pty_nonblock(master) == -1) {
+ throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking.");
+ }
+#else
+`,
+ ` if (pty_nonblock(master) == -1) {
+ throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking.");
+ }
+ if (pty_cloexec(master) == -1) {
+ throw Napi::Error::New(napiEnv, "Could not set master fd to close-on-exec.");
+ }
+#else
+`
+]
+
+// The throwaway ptys pty_posix_spawn opens to keep the real master off fds 0-2. Byte-identical to
+// the app's pnpm patch, so both trees compile the same cleanup.
+const LOW_FDS_DECLARATION = [
+ ` int low_fds[3];
+ size_t count = 0;
+`,
+ ` int low_fds[3] = {-1, -1, -1};
+ size_t count = 0;
+`
+]
+
+const LOW_FDS_CLEANUP = [
+ ` for (; count > 0; count--) {
+ close(low_fds[count]);
+ }
+`,
+ ` for (size_t i = 0; i <= count && i < 3; i++) {
+ if (low_fds[i] != -1) {
+ close(low_fds[i]);
+ }
+ }
+`
+]
+
+const REPLACEMENTS = [
+ FORWARD_DECLARATION,
+ DEFINITION,
+ POSIX_SPAWN_CALL_SITE,
+ FORKPTY_CALL_SITE,
+ LOW_FDS_DECLARATION,
+ LOW_FDS_CLEANUP
+]
function sourceSha256(source) {
return createHash('sha256').update(source).digest('hex')
@@ -188,10 +255,12 @@ function rebuildNodePty(relayDir) {
}
}
-// Why a child: a bad build can abort the process on require, which would strand the
-// moved-aside working build. Why the reachability check: a host without /proc cannot
-// show inheritance, and an unobservable flag is not evidence the rebuild was wrong.
-const VERIFY_SCRIPT = `
+// Why a child, for both scripts below: a bad build can abort the process on require, which would
+// strand the moved-aside working build. Why each ends in a reachability check: a host that cannot
+// show its fds says nothing, and an unobservable flag is not evidence the rebuild was wrong.
+//
+// Linux's leak is inheritance, so the observation is a later plain child's /proc/self/fd.
+const VERIFY_INHERITANCE_SCRIPT = `
const pty = require(process.argv[1]);
const term = pty.spawn('/bin/sh', ['-c', 'exit 0'], {
name: 'xterm-256color', cols: 80, rows: 24, cwd: process.cwd(), env: process.env
@@ -200,13 +269,39 @@ const probe = require('node:child_process').spawnSync('/bin/sh', ['-c', 'ls -l /
try { term.kill() } catch {}
const listing = probe.stdout || '';
if (probe.status !== 0 || !listing.includes('->')) { console.log('UNVERIFIED'); process.exit(0) }
-console.log(listing.includes('ptmx') ? 'INHERITED' : 'ISOLATED');
+console.log(listing.includes('ptmx') ? 'LEAKED' : 'ISOLATED');
process.exit(0);
`
-/** 'isolated' when a later plain child no longer inherits the master, 'unverified' when /proc cannot say. */
-function verifyMasterNotInheritedByLaterChild(relayDir) {
- const result = spawnSync(process.execPath, ['-e', VERIFY_SCRIPT, nodePtyDir(relayDir)], {
+// Apple's leak is self-held, not inherited, so the observation is this process's own fd table:
+// N live ptys must account for exactly N /dev/ptmx rows. A stock build shows 2N -- the master plus
+// the throwaway pty_posix_spawn opened and never closed. lsof, not /proc, because macOS has no
+// /proc; a host without lsof cannot say, which is 'unverified', not a failed patch.
+const VERIFY_SELF_FDS_SCRIPT = `
+const pty = require(process.argv[1]);
+const terms = [];
+for (let i = 0; i < 3; i++) {
+ terms.push(pty.spawn('/bin/sh', ['-c', 'sleep 30'], {
+ name: 'xterm-256color', cols: 80, rows: 24, cwd: process.cwd(), env: process.env
+ }));
+}
+const probe = require('node:child_process').spawnSync('/bin/sh', ['-c', 'lsof -p ' + process.pid], { encoding: 'utf8', maxBuffer: 1 << 24 });
+for (const term of terms) { try { term.kill() } catch {} }
+const rows = (probe.stdout || '').split('\\n').filter((line) => line.includes('/dev/ptmx'));
+if (probe.status !== 0 || rows.length < terms.length) { console.log('UNVERIFIED'); process.exit(0) }
+console.log(rows.length > terms.length ? 'LEAKED' : 'ISOLATED');
+process.exit(0);
+`
+
+const LEAK_MESSAGE = {
+ darwin: 'rebuilt node-pty still leaks a throwaway pty fd per spawn',
+ linux: 'rebuilt node-pty still leaks the pty master into later children'
+}
+
+/** 'isolated' when the platform's leak is gone, 'unverified' when the host cannot show it. */
+function verifyNoPtyFdLeak(relayDir, platform) {
+ const script = platform === 'darwin' ? VERIFY_SELF_FDS_SCRIPT : VERIFY_INHERITANCE_SCRIPT
+ const result = spawnSync(process.execPath, ['-e', script, nodePtyDir(relayDir)], {
cwd: relayDir,
encoding: 'utf8',
timeout: VERIFY_TIMEOUT_MS,
@@ -219,22 +314,53 @@ function verifyMasterNotInheritedByLaterChild(relayDir) {
`rebuilt node-pty did not load: ${tail || result.error?.message || result.signal}`
)
}
- if (output.includes('INHERITED')) {
- throw new Error('rebuilt node-pty still leaks the pty master into later children')
+ if (output.includes('LEAKED')) {
+ throw new Error(LEAK_MESSAGE[platform] || LEAK_MESSAGE.linux)
}
return output.includes('ISOLATED') ? 'isolated' : 'unverified'
}
-function rollback(relayDir, releaseDir, backupDir) {
- rmSync(releaseDir, { recursive: true, force: true })
+/**
+ * What gets moved aside before the compile, and where the compile writes.
+ *
+ * Linux ships no prebuild, so `build/Release` is both the working build and the compile's output,
+ * and moving it aside only arms the rollback. macOS runs `prebuilds/darwin-` and has no
+ * `build/` at all, so the compile writes a new `build/Release` -- which node-pty's loader checks
+ * ahead of `prebuilds`. Moving `prebuilds` aside does double duty there: it arms the rollback and
+ * it is what makes node-pty's install script fall through from "prebuild found" to `node-gyp
+ * rebuild`. Deliberately not `npm_config_build_from_source`, which deletes the prebuilds outright
+ * and would leave nothing to roll back to.
+ */
+function buildLayout(relayDir, platform, arch) {
+ const ptyDir = nodePtyDir(relayDir)
+ const compiledDir = join(ptyDir, 'build', 'Release')
+ if (platform === 'darwin') {
+ const prebuildsDir = join(ptyDir, 'prebuilds')
+ return {
+ compiledDir,
+ movedDir: prebuildsDir,
+ workingBuildPath: join(prebuildsDir, `darwin-${arch}`, 'pty.node'),
+ missingStatus: 'skipped:no-prebuild'
+ }
+ }
+ return {
+ compiledDir,
+ movedDir: compiledDir,
+ workingBuildPath: join(compiledDir, 'pty.node'),
+ missingStatus: 'skipped:no-compiled-build'
+ }
+}
+
+function rollback(relayDir, layout, backupDir) {
+ rmSync(layout.compiledDir, { recursive: true, force: true })
try {
revertNodePtyMasterCloexecSource(relayDir)
} catch {
// The build that is about to be restored predates the patch either way.
}
if (existsSync(backupDir)) {
- mkdirSync(dirname(releaseDir), { recursive: true })
- renameSync(backupDir, releaseDir)
+ mkdirSync(dirname(layout.movedDir), { recursive: true })
+ renameSync(backupDir, layout.movedDir)
}
}
@@ -244,16 +370,17 @@ function rollback(relayDir, releaseDir, backupDir) {
*/
function applyNodePtyMasterCloexecPatch(relayDir = process.cwd(), options = {}) {
const platform = options.platform || process.platform
+ const arch = options.arch || process.arch
const rebuild = options.rebuild || rebuildNodePty
- const verify = options.verify || verifyMasterNotInheritedByLaterChild
- if (platform !== 'linux') {
- return 'skipped:not-linux'
+ const verify = options.verify || verifyNoPtyFdLeak
+ if (platform !== 'linux' && platform !== 'darwin') {
+ return 'skipped:unsupported-platform'
}
const skipMarkerPath = join(relayDir, SKIP_MARKER_FILENAME)
if (existsSync(skipMarkerPath)) {
return 'skipped:earlier-attempt-failed'
}
- const releaseDir = join(nodePtyDir(relayDir), 'build', 'Release')
+ const layout = buildLayout(relayDir, platform, arch)
const backupDir = join(nodePtyDir(relayDir), BACKUP_DIRNAME)
// A backup stranded by a connection that died mid-rebuild is stale by definition:
// whatever repaired node-pty since built from the source now on disk.
@@ -272,25 +399,28 @@ function applyNodePtyMasterCloexecPatch(relayDir = process.cwd(), options = {})
if (hash !== ORIGINAL_SOURCE_SHA256) {
return 'skipped:unexpected-source'
}
- // No compiled build means the host runs a prebuild or nothing at all; rebuilding
- // could only take away the artifact the probe just proved loadable.
- if (!existsSync(join(releaseDir, 'pty.node'))) {
- return 'skipped:no-compiled-build'
+ // Nothing to fall back on means the host runs neither a compile nor the prebuild
+ // this platform expects; rebuilding could only take away the artifact the probe
+ // just proved loadable.
+ if (!existsSync(layout.workingBuildPath)) {
+ return layout.missingStatus
}
try {
- renameSync(releaseDir, backupDir)
+ renameSync(layout.movedDir, backupDir)
} catch (err) {
return `skipped:${err.message}`
}
try {
patchNodePtyMasterCloexecSource(relayDir)
rebuild(relayDir)
- const verdict = verify(relayDir)
+ const verdict = verify(relayDir, platform)
+ // Discarded, not restored: a tree that gets published must hold no unpatched binary the
+ // loader could still fall back to. A later repair recompiles from the patched source.
rmSync(backupDir, { recursive: true, force: true })
return verdict === 'isolated' ? 'patched' : 'patched-unverified'
} catch (err) {
- rollback(relayDir, releaseDir, backupDir)
+ rollback(relayDir, layout, backupDir)
// Bounded on purpose: one compile attempt per relay directory, never a retry loop.
try {
writeFileSync(skipMarkerPath, `${new Date().toISOString()} ${err.message}\n`)
diff --git a/config/scripts/node-pty-master-cloexec-patch.test.mjs b/config/scripts/node-pty-master-cloexec-patch.test.mjs
index 16013cbf0a3..e323c5d61c8 100644
--- a/config/scripts/node-pty-master-cloexec-patch.test.mjs
+++ b/config/scripts/node-pty-master-cloexec-patch.test.mjs
@@ -27,7 +27,7 @@ afterEach(() => {
}
})
-describe('SSH relay node-pty pty-master close-on-exec patch', () => {
+describe('SSH relay node-pty pty fd-leak patch', () => {
it('adds the forkpty close-on-exec call and reverts to the published bytes', () => {
const fixture = writeRelayFixture()
@@ -44,6 +44,27 @@ describe('SSH relay node-pty pty-master close-on-exec patch', () => {
expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE)
})
+ it('rewrites the Apple branch, which is the only one macOS executes', () => {
+ const fixture = writeRelayFixture()
+ patchNodePtyMasterCloexecSource(fixture.root)
+ const patched = readFileSync(fixture.sourcePath, 'utf8')
+
+ // Stock's cleanup never runs: the first posix_openpt() already returns >= 2, so the loop
+ // breaks with count == 0 -- and where it does run it closes low_fds[count], never low_fds[0].
+ expect(STOCK_SOURCE).toContain('for (; count > 0; count--) {')
+ expect(patched).not.toContain('for (; count > 0; count--) {')
+ expect(patched).toContain('int low_fds[3] = {-1, -1, -1};')
+ expect(patched).toContain('for (size_t i = 0; i <= count && i < 3; i++) {')
+
+ // `default:` sits in the `#else` arm of PtyFork's `#if defined(__APPLE__)`, so marking only
+ // the forkpty call site left the master macOS actually opens unmarked.
+ expect(patched).toContain(
+ ' if (pty_cloexec(master) == -1) {\n' +
+ ' throw Napi::Error::New(napiEnv, "Could not set master fd to close-on-exec.");\n' +
+ ' }\n#else\n'
+ )
+ })
+
it('refuses a different node-pty version or an unrecognized source', () => {
const wrongVersion = writeRelayFixture({ version: '1.2.0-beta.4' })
expect(() => patchNodePtyMasterCloexecSource(wrongVersion.root)).toThrow('expected 1.1.0')
@@ -139,19 +160,81 @@ describe('SSH relay node-pty pty-master close-on-exec patch', () => {
expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE)
})
- it('never compiles on a platform that does not leak', () => {
- for (const platform of ['darwin', 'win32']) {
- const fixture = writeRelayFixture()
- const calls = []
- const status = applyNodePtyMasterCloexecPatch(fixture.root, {
- platform,
- rebuild: () => calls.push('rebuild'),
- verify: () => 'isolated'
- })
- expect(status).toBe('skipped:not-linux')
- expect(calls).toEqual([])
- expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE)
- }
+ it('never compiles on a platform with no pty fds to leak', () => {
+ const fixture = writeRelayFixture()
+ const calls = []
+ const status = applyNodePtyMasterCloexecPatch(fixture.root, {
+ platform: 'win32',
+ rebuild: () => calls.push('rebuild'),
+ verify: () => 'isolated'
+ })
+ expect(status).toBe('skipped:unsupported-platform')
+ expect(calls).toEqual([])
+ expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE)
+ })
+
+ it('compiles a macOS install out from under its shipped prebuild', () => {
+ // macOS has no build/ at all: node-pty runs `prebuilds/darwin-`, built from the leaky
+ // source. Moving `prebuilds` aside is what both arms the rollback and makes node-pty's own
+ // install script fall through from "prebuild found" to node-gyp.
+ const fixture = writeRelayFixture({ platform: 'darwin' })
+ const prebuildsPresentDuringRebuild = []
+
+ const status = applyNodePtyMasterCloexecPatch(fixture.root, {
+ platform: 'darwin',
+ arch: fixture.arch,
+ rebuild: () => {
+ prebuildsPresentDuringRebuild.push(existsSync(fixture.prebuildsDir))
+ writeCompiledBuild(fixture, 'patched-build')
+ },
+ verify: () => 'isolated'
+ })
+
+ expect(status).toBe('patched')
+ expect(prebuildsPresentDuringRebuild).toEqual([false])
+ expect(readFileSync(fixture.compiledPath, 'utf8')).toBe('patched-build')
+ // The published tree must hold no unpatched binary: node-pty's loader checks build/Release
+ // first, but falls back to a prebuild if that ever fails to load.
+ expect(existsSync(fixture.prebuildsDir)).toBe(false)
+ expect(existsSync(fixture.backupDir)).toBe(false)
+ })
+
+ it('restores the macOS prebuild when the first compile fails', () => {
+ // A macOS host has no toolchain guarantee at all, so this is the common failure, not the rare
+ // one -- and the relay has to come back on the prebuild exactly as it was installed.
+ const fixture = writeRelayFixture({ platform: 'darwin' })
+
+ const status = applyNodePtyMasterCloexecPatch(fixture.root, {
+ platform: 'darwin',
+ arch: fixture.arch,
+ rebuild: () => {
+ writeCompiledBuild(fixture, 'half-built')
+ throw new Error('npm rebuild node-pty exited 1: no C++ toolchain')
+ },
+ verify: () => 'isolated'
+ })
+
+ expect(status).toContain('failed:')
+ expect(readFileSync(fixture.buildPath, 'utf8')).toBe('stock-build')
+ expect(existsSync(fixture.compiledPath)).toBe(false)
+ expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE)
+ expect(existsSync(fixture.skipMarkerPath)).toBe(true)
+ })
+
+ it('will not rebuild a macOS install that has no prebuild to fall back on', () => {
+ const fixture = writeRelayFixture({ platform: 'darwin', build: false })
+ const calls = []
+
+ const status = applyNodePtyMasterCloexecPatch(fixture.root, {
+ platform: 'darwin',
+ arch: fixture.arch,
+ rebuild: () => calls.push('rebuild'),
+ verify: () => 'isolated'
+ })
+
+ expect(status).toBe('skipped:no-prebuild')
+ expect(calls).toEqual([])
+ expect(readFileSync(fixture.sourcePath, 'utf8')).toBe(STOCK_SOURCE)
})
it('leaves an already patched install alone', () => {
@@ -201,19 +284,35 @@ describe('SSH relay node-pty pty-master close-on-exec patch', () => {
})
})
-function writeRelayFixture({ version = '1.1.0', source = STOCK_SOURCE, build = true } = {}) {
+/**
+ * `buildPath` is the working build the patch has to be able to fall back on, which differs by
+ * platform: Linux compiles into build/Release at install time, macOS runs a shipped prebuild and
+ * has no build/ at all. `compiledPath` is where the rebuild writes on either.
+ */
+function writeRelayFixture({
+ version = '1.1.0',
+ source = STOCK_SOURCE,
+ build = true,
+ platform = 'linux',
+ arch = 'arm64'
+} = {}) {
const root = mkdtempSync(join(projectDir, '.node-pty-cloexec-patch-test-'))
cleanupDirs.push(root)
const nodePtyDir = join(root, 'node_modules', 'node-pty')
const sourcePath = join(nodePtyDir, 'src', 'unix', 'pty.cc')
- const buildPath = join(nodePtyDir, 'build', 'Release', 'pty.node')
+ const compiledPath = join(nodePtyDir, 'build', 'Release', 'pty.node')
+ const prebuildsDir = join(nodePtyDir, 'prebuilds')
mkdirSync(join(nodePtyDir, 'src', 'unix'), { recursive: true })
writeFileSync(join(nodePtyDir, 'package.json'), JSON.stringify({ version }))
writeFileSync(sourcePath, source)
const fixture = {
root,
+ arch,
sourcePath,
- buildPath,
+ compiledPath,
+ prebuildsDir,
+ buildPath:
+ platform === 'darwin' ? join(prebuildsDir, `darwin-${arch}`, 'pty.node') : compiledPath,
backupDir: join(nodePtyDir, '.orca-cloexec-prepatch-release'),
skipMarkerPath: join(root, SKIP_MARKER_FILENAME)
}
@@ -227,3 +326,8 @@ function writeBuild(fixture, contents) {
mkdirSync(resolve(fixture.buildPath, '..'), { recursive: true })
writeFileSync(fixture.buildPath, contents)
}
+
+function writeCompiledBuild(fixture, contents) {
+ mkdirSync(resolve(fixture.compiledPath, '..'), { recursive: true })
+ writeFileSync(fixture.compiledPath, contents)
+}
diff --git a/src/main/ssh/ssh-relay-deploy.ts b/src/main/ssh/ssh-relay-deploy.ts
index c5cb2dd552c..257eb984caa 100644
--- a/src/main/ssh/ssh-relay-deploy.ts
+++ b/src/main/ssh/ssh-relay-deploy.ts
@@ -52,6 +52,7 @@ import {
RELAY_DEPLOY_TIMEOUT_MS
} from './ssh-relay-deploy-timing'
import { createSshOperationAbortError, shellEscape } from './ssh-connection-utils'
+import { isWindowsRelayPlatform } from '../../shared/relay-artifacts'
import {
probeBuildToolchain,
formatMissingToolchainError,
@@ -745,27 +746,29 @@ const NODE_PTY_CONSOLE_LIST_PATCH_FILENAME = 'node-pty-1.1.0-console-list-agent-
const NODE_PTY_MASTER_CLOEXEC_PATCH_FILENAME = 'node-pty-1.1.0-master-cloexec-patch.cjs'
const NODE_PTY_CLOEXEC_STATUS_PREFIX = 'ORCA-NPTY-CLOEXEC:'
/**
- * Whether the tree the patch left behind still leaks the pty master into every later child.
- * `fixed` is the only outcome a shared cache entry may be published from.
+ * Whether the tree the patch left behind still leaks a pty fd -- the master into every later child
+ * on Linux, a throwaway /dev/ptmx per spawn on macOS. `fixed` is the only outcome a shared cache
+ * entry may be published from.
*/
type NodePtyMasterCloexecOutcome = 'fixed' | 'unfixed'
/**
* The statuses that leave a non-leaking tree. Deliberately an allowlist, not a `failed:` denylist:
- * the script's `skipped:` family is mixed. `skipped:not-linux` is a platform that never leaks, but
- * `skipped:earlier-attempt-failed`, `skipped:no-compiled-build`, `skipped:unexpected-source` and
- * the two `skipped:` forms all mean the patch was refused and the leaky build is still on
- * disk -- indistinguishable from `failed:` as far as what gets published.
+ * the script's `skipped:` family is mixed. `skipped:unsupported-platform` is a platform that never
+ * leaks, but `skipped:earlier-attempt-failed`, `skipped:no-compiled-build`, `skipped:no-prebuild`,
+ * `skipped:unexpected-source` and the two `skipped:` forms all mean the patch was refused
+ * and the leaky build is still on disk -- indistinguishable from `failed:` as far as what gets
+ * published.
*/
const NODE_PTY_CLOEXEC_FIXED_STATUSES: ReadonlySet = new Set([
'patched',
- // The rebuild ran from patched source; only the isolation check could not observe the result.
- // An unobservable check is not a failed patch, and treating it as one would disable the shared
- // cache on every host without `lsof`.
+ // The rebuild ran from patched source; only the leak check could not observe the result. An
+ // unobservable check is not a failed patch, and treating it as one would disable the shared
+ // cache on every host without `/proc` or `lsof`.
'patched-unverified',
'already-patched',
- // Unreachable while the platform gate below short-circuits first, but it is the one `skipped:`
- // that means "nothing to fix" rather than "would not fix it".
- 'skipped:not-linux'
+ // Unreachable while the platform gate below short-circuits Windows first, but it is the one
+ // `skipped:` that means "nothing to fix" rather than "would not fix it".
+ 'skipped:unsupported-platform'
])
// Exported for the relay-native-dependency-coverage test, which asserts every
// native addon the relay bundle imports is either installed here or explicitly
@@ -1300,7 +1303,7 @@ async function installNativeDeps(
}
/**
- * Re-apply the pty-master FD_CLOEXEC patch the app gets from pnpm to the host's npm copy (#17915).
+ * Re-apply the pty fd-leak patch the app gets from pnpm to the host's npm copy (#17915).
*
* Why it is safe to rebuild under a live relay: this only runs from installNativeDeps, so only on a
* freshly created directory or a locked repair, and a relay already serving PTYs has pty.node mapped
@@ -1324,9 +1327,11 @@ async function applyNodePtyMasterCloexecPatch(
nodePath: string,
signal?: AbortSignal
): Promise {
- // Linux is the only relay platform that takes forkpty()'s no-O_CLOEXEC path; macOS and Windows
- // ship prebuilds, so forcing a rebuild there would add a first compile to fix nothing.
- if (isWindowsRemoteHost(hostPlatform) || !platform.startsWith('linux')) {
+ // Both Unix relay platforms leak, by different bugs: Linux inherits the master through forkpty()'s
+ // no-O_CLOEXEC path, macOS orphans one throwaway /dev/ptmx fd per spawn in pty_posix_spawn. Only
+ // Windows, which has no fds, is short-circuited -- and answering 'fixed' from a gate that ran
+ // nothing is exactly how a leaking darwin tree got published to the shared cache.
+ if (isWindowsRemoteHost(hostPlatform) || isWindowsRelayPlatform(platform)) {
return 'fixed'
}
try {
diff --git a/src/main/ssh/ssh-relay-pty-master-cloexec-install.test.ts b/src/main/ssh/ssh-relay-pty-master-cloexec-install.test.ts
index 66af01fa43c..b0ca39ec2b4 100644
--- a/src/main/ssh/ssh-relay-pty-master-cloexec-install.test.ts
+++ b/src/main/ssh/ssh-relay-pty-master-cloexec-install.test.ts
@@ -88,11 +88,12 @@ import {
const PATCH_ASSET = 'node-pty-1.1.0-master-cloexec-patch.cjs'
/**
- * The relay installs stock node-pty from npm, so the app's pnpm patch never reaches it and every
- * later child of the relay inherits a live pty master (#17915). The compile that closes it sits on
+ * The relay installs stock node-pty from npm, so the app's pnpm patch never reaches it and the
+ * relay leaks a pty fd per terminal (#17915) -- the master into every later child on Linux, an
+ * orphaned /dev/ptmx throwaway in `pty_posix_spawn` on macOS. The compile that closes both sits on
* the connect path, so what these specs pin is the blast radius, not the patch itself.
*/
-describe('relay pty-master close-on-exec patch on the install path', () => {
+describe('relay pty fd-leak patch on the install path', () => {
const sftpCapture: SftpWriteCapture = {
paths: [],
contents: {},
@@ -211,9 +212,9 @@ describe('relay pty-master close-on-exec patch on the install path', () => {
})
it('does not publish a tree the patch refused to touch', async () => {
- // `skipped:` is not one verdict. Every form except `skipped:not-linux` means the patch was
- // declined and the leaky build is still on disk, which is indistinguishable from `failed:`
- // as far as what would get published.
+ // `skipped:` is not one verdict. Every form except `skipped:unsupported-platform` means the
+ // patch was declined and the leaky build is still on disk, which is indistinguishable from
+ // `failed:` as far as what would get published.
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
const conn = makeMockConnection(sftpCapture)
@@ -281,25 +282,39 @@ describe('relay pty-master close-on-exec patch on the install path', () => {
expect(patchCommands()).toEqual([])
})
- it('never adds a compile to a macOS relay, which does not leak the master', async () => {
+ it('runs the patch on a macOS relay, which orphans a /dev/ptmx fd per spawn', async () => {
+ // macOS takes `pty_posix_spawn`, not forkpty, so the asset's original replacements rewrote
+ // nothing macOS executes -- and this gate answered 'fixed' without running anything, which is
+ // exactly what publishes to the shared cache. Every later host on the machine then linked a
+ // tree that leaks one /dev/ptmx fd per terminal, measured +1 per open/close cycle on
+ // darwin-arm64. macOS pays a first compile here, unlike Linux's second, and that is the price.
vi.mocked(parseUnameToRelayPlatform).mockReturnValue('darwin-arm64')
const conn = makeMockConnection(sftpCapture)
- feed([
- ...makeStagedFirstInstallExecPrefix(),
- '', // npm install native deps
- '', // chmod prebuilds
- 'ORCA-NPTY-PROBE-OK\n',
- '', // rm probe stderr
- '', // promote into the shared native-deps cache
- '', // clean stage root
- 'DEAD',
- '', // publish the per-launch credential
- 'READY'
- ])
+ feed(makeExecResponses({ npmInstall: 'ok', probe: 'ok' }))
await deployAndLaunchRelay(conn)
- expect(patchCommands()).toEqual([])
+ expect(patchCommands()).toHaveLength(1)
+ expect(promoted()).toBe(true)
+ })
+
+ it('does not publish a macOS tree whose compile failed', async () => {
+ // The darwin rollback restores the shipped prebuild, so the relay still works -- and that is
+ // precisely why the status, not the exit code, has to decide publishability: a rolled-back
+ // macOS tree probes loadable and still leaks.
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
+ try {
+ vi.mocked(parseUnameToRelayPlatform).mockReturnValue('darwin-arm64')
+ const conn = makeMockConnection(sftpCapture)
+ feed(firstInstallReporting('failed:npm rebuild node-pty exited 1: gyp ERR! not ok'))
+
+ await deployAndLaunchRelay(conn)
+
+ expect(patchCommands()).toHaveLength(1)
+ expect(promoted()).toBe(false)
+ } finally {
+ warn.mockRestore()
+ }
})
it('connects anyway when the patch command fails outright', async () => {
From 3c914048206a333aa106eba3598464ea5efa5b83 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Thu, 3 Sep 2026 16:21:52 -0700
Subject: [PATCH 03/43] fix(worktrees): route managed worktree removal by
resolved execution host (#18529)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`removeManagedWorktree` resolved its host once — for the metadata prune
(`cleanupHostId ?? getRepoExecutionHostId(repo)`) — and then read raw
`repo.connectionId` for every step that touches the filesystem: the
`git worktree list` deciding whether the path is registered, the provider handed
to the unregistered-removal branch, the registered-remote-vs-local fork, and the
PTY/history teardown. One function, two spellings.
For a row naming its owner only as `executionHostId: 'ssh:'` — the exact
class #18296 names — the list ran on the client against a remote path,
`removeRuntimeUnregisteredWorktree` was entered with `provider: null`, and the
metadata was pruned under `ssh:` while a same-named *local* directory was
the one considered for deletion (#11163). #18358 made this reachable: it migrated
the cleanup scan, so `executionHostId`-only rows now surface as removable
candidates, but removal did not move with it.
Routing is now one answer for the whole removal, taken from the host the prune
already used, through #18296's host-keyed dispatch. The ambiguous
`provider: SshGitProvider | null` carrier is deleted from the callees rather than
supplemented, so every remaining reader is a compile error in the typed modules
that do the destructive work (`runtime-unregistered-worktree-removal`,
`runtime-registered-remote-worktree-removal`, `runtime-worktree-filesystem`).
The orchestrator itself carries `@ts-nocheck` from its mechanical split, so that
guarantee does not reach it — tests cover it instead.
Every change is in the refusing direction; nothing became more aggressive:
- an `ssh:` host with no registered provider throws instead of deleting a
client-side path (`requireSshGitProvider` already threw for rows that spelled
the same host as `connectionId`);
- `runtime:` throws rather than dialling a same-named target in this
client's namespace, matching `workspace-cleanup-git-route` and
`runtime-git-command-target`;
- the folder-workspace teardown resolves its connection instead of reading the
raw field, so a `runtime:` row stops dialling the wrong namespace.
No wire or persistence change: `removeWorktreeMetadataAndHistory` already took
the resolved host, and the removal RPC result shape is untouched.
---
.../orca-runtime-remove-managed-worktree.ts | 49 ++--
...untime-remove-orphan-or-folder-worktree.ts | 6 +-
...removal-and-reconciliation-part-03.spec.ts | 64 ++++--
.../worktree-removal-execution-host.spec.ts | 216 ++++++++++++++++++
src/main/runtime/orca-runtime.test.ts | 1 +
...time-registered-remote-worktree-removal.ts | 5 +-
.../runtime-unregistered-worktree-removal.ts | 45 ++--
.../runtime/runtime-worktree-filesystem.ts | 20 +-
...ktree-removal-execution-host-route.test.ts | 100 ++++++++
.../worktree-removal-execution-host-route.ts | 81 +++++++
10 files changed, 521 insertions(+), 66 deletions(-)
create mode 100644 src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts
create mode 100644 src/main/worktree-removal-execution-host-route.test.ts
create mode 100644 src/main/worktree-removal-execution-host-route.ts
diff --git a/src/main/runtime/orca-runtime-remove-managed-worktree.ts b/src/main/runtime/orca-runtime-remove-managed-worktree.ts
index 5b45abd13fa..25a294c2a90 100644
--- a/src/main/runtime/orca-runtime-remove-managed-worktree.ts
+++ b/src/main/runtime/orca-runtime-remove-managed-worktree.ts
@@ -10,8 +10,7 @@ import { preservedBranchCleanupScopeKey } from '../../shared/preserved-branch-cl
import { getRuntimeWorktreeRemovalOptionsKey } from './runtime-worktree-selection'
import { withWorktreeSpan } from '../observability/instrumentation'
import { invalidateAuthorizedRootsCache } from '../ipc/filesystem-auth'
-import { requireSshGitProvider } from '../providers/ssh-git-dispatch'
-import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
+import { resolveWorktreeRemovalRoute } from '../worktree-removal-execution-host-route'
import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
import { listWorktreesStrict } from '../git/worktree'
import { findRegisteredDeletableWorktree } from '../worktree-removal-safety'
@@ -79,22 +78,24 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM
if (orphanOrFolderResult) {
return orphanOrFolderResult
}
- const provider = repo.connectionId ? requireSshGitProvider(repo.connectionId) : null
- const fsProvider = repo.connectionId ? getSshFilesystemProvider(repo.connectionId) : null
- const localWorktreeGitOptions = repo.connectionId
- ? {}
- : getLocalProjectWorktreeGitOptions(this.requireStore(), repo)
+ // One host for the whole removal. Listing on a different host from the one the prune and
+ // the delete use is how an `executionHostId: 'ssh:*'`-only row got listed remotely and
+ // deleted here; the route refuses rather than falling back to this machine.
+ const route = resolveWorktreeRemovalRoute(removalHostId)
+ const localWorktreeGitOptions =
+ route.kind === 'ssh' ? {} : getLocalProjectWorktreeGitOptions(this.requireStore(), repo)
const hasLocalWorktreeGitOptions = Object.keys(localWorktreeGitOptions).length > 0
- const registeredWorktrees = repo.connectionId
- ? await provider!.listWorktrees(repo.path)
- : hasLocalWorktreeGitOptions
- ? await listWorktreesStrict(repo.path, localWorktreeGitOptions)
- : await listWorktreesStrict(repo.path)
+ const registeredWorktrees =
+ route.kind === 'ssh'
+ ? await route.provider.listWorktrees(repo.path)
+ : hasLocalWorktreeGitOptions
+ ? await listWorktreesStrict(repo.path, localWorktreeGitOptions)
+ : await listWorktreesStrict(repo.path)
const removedMeta = resolveWorktreeRemovalMetadata(
store,
removalTarget.repoId,
removalTarget.id,
- cleanupHostId ?? getRepoExecutionHostId(repo)
+ removalHostId
)
const removedPushTarget = removedMeta?.pushTarget ?? removalTarget.pushTarget
const registeredWorktree = findRegisteredDeletableWorktree(
@@ -111,8 +112,7 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM
removedPushTarget,
force,
allowUnverifiedPtyStop,
- provider,
- fsProvider: fsProvider ?? null,
+ route,
localOptions: localWorktreeGitOptions,
store,
acquireWatcherRemoval: this.acquireFileWatcherRemoval,
@@ -123,7 +123,7 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM
}),
deleteHistory: () =>
deleteRemoteWorktreeHistory(
- repo.connectionId ? this.getSshProviderFn?.(repo.connectionId) : undefined,
+ route.kind === 'ssh' ? this.getSshProviderFn?.(route.connectionId) : undefined,
removalTarget.id
),
finishRemoval: () => {
@@ -145,13 +145,17 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM
throw new Error(formatWorktreeRemovalError(error, canonicalWorktreePath, force))
}
if (
- !repo.connectionId &&
+ route.kind === 'local' &&
force === true &&
process.platform === 'win32' &&
(isWindowsAbsolutePathLike(canonicalWorktreePath) ||
!!localWorktreeGitOptions.wslDistro) &&
removedMeta &&
- (await isRuntimeWorktreePathMissing(repo, canonicalWorktreePath, localWorktreeGitOptions))
+ (await isRuntimeWorktreePathMissing(
+ route.hostId,
+ canonicalWorktreePath,
+ localWorktreeGitOptions
+ ))
) {
const removalResult = await removeStaleLocalWorktreeRegistrationAfterFilesystemRemoval({
canonicalWorktreePath,
@@ -182,26 +186,27 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM
this.notifyWorktreesChanged(repo.id)
return removalResult ?? {}
}
- if (repo.connectionId) {
+ if (route.kind === 'ssh') {
return removeRuntimeRegisteredRemoteWorktree({
repo,
target: removalTarget,
registeredWorktree,
removedPushTarget,
store,
- provider: provider!,
+ provider: route.provider,
+ connectionId: route.connectionId,
force,
allowUnverifiedPtyStop,
deleteBranch,
acquireWatcherRemoval: this.acquireFileWatcherRemoval,
stopPtys: () =>
this.stopPtysForDestructiveWorktreeRemoval(removalTarget.id, {
- connectionId: repo.connectionId!,
+ connectionId: route.connectionId,
allowUnverifiedStop: allowUnverifiedPtyStop
}),
deleteHistory: () =>
deleteRemoteWorktreeHistory(
- this.getSshProviderFn?.(repo.connectionId!),
+ this.getSshProviderFn?.(route.connectionId),
removalTarget.id
),
preserveBranchHead: (result, fallbackHead) =>
diff --git a/src/main/runtime/orca-runtime-remove-orphan-or-folder-worktree.ts b/src/main/runtime/orca-runtime-remove-orphan-or-folder-worktree.ts
index d49dc410cd5..01f4f305803 100644
--- a/src/main/runtime/orca-runtime-remove-orphan-or-folder-worktree.ts
+++ b/src/main/runtime/orca-runtime-remove-orphan-or-folder-worktree.ts
@@ -91,7 +91,11 @@ export async function removeOrphanOrFolderWorktree({
if (removalTarget.id === getRuntimeFolderWorkspaceRootId(repo)) {
throw new Error('Cannot delete the project root workspace. Remove the folder project instead.')
}
- const folderConnectionId = repo.connectionId?.trim() || null
+ // Resolved, not raw: a folder repo naming its owner only as `executionHostId: 'ssh:*'` used to
+ // tear down its PTYs and history on the client. A `runtime:` host answers null — its nested
+ // target is addressable only inside that environment, never from this client's SSH table.
+ const folderHost = parseExecutionHostId(removalHostId)
+ const folderConnectionId = folderHost?.kind === 'ssh' ? folderHost.targetId : null
const folderSshPtyProvider = folderConnectionId
? runtime.getSshProviderFn?.(folderConnectionId)
: undefined
diff --git a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec.ts
index beaa91036d0..06982cf179e 100644
--- a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec.ts
+++ b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec.ts
@@ -193,41 +193,69 @@ describe('OrcaRuntimeService', () => {
})
it('does not coalesce concurrent same-id removals on different hosts', async () => {
+ const baseRepo = store.getRepos()[0]!
+ // The second owner names its host only in the migrated spelling, so its removal must reach the
+ // SSH host rather than joining the local one and running `git worktree remove` here.
+ const remoteRepo = { ...baseRepo, path: '/remote/repo', executionHostId: 'ssh:host-b' }
const runtimeStore = {
...store,
- getRepos: () => [
- { ...store.getRepos()[0], executionHostId: 'local' },
- { ...store.getRepos()[0], executionHostId: 'runtime:env-1' }
- ]
+ getRepos: () => [{ ...baseRepo, executionHostId: 'local' }, remoteRepo]
}
const runtime = createWorktreeRemovalRuntime(runtimeStore)
vi.spyOn(runtime, 'acquireFileWatcherRemoval').mockResolvedValue({ finish: vi.fn() })
const bothStarted = deferred()
const finishRemovals = deferred()
let startedCount = 0
- vi.mocked(removeWorktree).mockImplementation(async () => {
+ const startRemoval = async (): Promise> => {
startedCount += 1
if (startedCount === 2) {
bothStarted.resolve()
}
await finishRemovals.promise
return {}
- })
+ }
+ vi.mocked(removeWorktree).mockImplementation(startRemoval)
+ const provider = {
+ exec: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }),
+ listWorktrees: vi.fn().mockResolvedValue([
+ {
+ path: remoteRepo.path,
+ head: 'main',
+ branch: 'main',
+ isBare: false,
+ isMainWorktree: true
+ },
+ {
+ path: TEST_WORKTREE_PATH,
+ head: 'def456',
+ branch: 'feature/test',
+ isBare: false,
+ isMainWorktree: false
+ }
+ ]),
+ removeWorktree: vi.fn().mockImplementation(startRemoval)
+ }
+ registerSshGitProvider('host-b', provider as never)
- const local = runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'local')
- const paired = runtime.removeManagedWorktree(
- TEST_WORKTREE_ID,
- true,
- false,
- false,
- 'runtime:env-1'
- )
+ try {
+ const local = runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'local')
+ const remote = runtime.removeManagedWorktree(
+ TEST_WORKTREE_ID,
+ true,
+ false,
+ false,
+ 'ssh:host-b'
+ )
- await bothStarted.promise
- expect(removeWorktree).toHaveBeenCalledTimes(2)
+ await bothStarted.promise
+ expect(removeWorktree).toHaveBeenCalledTimes(1)
+ expect(provider.removeWorktree).toHaveBeenCalledTimes(1)
- finishRemovals.resolve()
- await expect(Promise.all([local, paired])).resolves.toEqual([{}, {}])
+ finishRemovals.resolve()
+ await expect(Promise.all([local, remote])).resolves.toEqual([{}, {}])
+ } finally {
+ unregisterSshGitProvider('host-b')
+ }
})
it('rejects concurrent runtime worktree removals for the same id with different options', async () => {
diff --git a/src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts
new file mode 100644
index 00000000000..8ec38e0e5e8
--- /dev/null
+++ b/src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts
@@ -0,0 +1,216 @@
+import {
+ listWorktrees,
+ listWorktreesStrict,
+ registerSshFilesystemProvider,
+ registerSshGitProvider,
+ removeWorktree,
+ unregisterSshFilesystemProvider,
+ unregisterSshGitProvider
+} from '../orca-runtime-test-mocks.spec'
+import type { WorktreeMeta } from '../orca-runtime-test-mocks.spec'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import {
+ TEST_WORKTREE_ID,
+ TEST_WORKTREE_PATH,
+ makeWorktreeMeta,
+ store
+} from '../orca-runtime-test-fixtures.spec'
+import { createWorktreeRemovalRuntime } from '../orca-runtime-test-scenario-builders.spec'
+import type { ExecutionHostId } from '../../../shared/execution-host'
+
+const REMOTE_REPO_PATH = '/remote/repo'
+
+function missingPath(): never {
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
+}
+
+function makeGitProvider(worktrees: readonly unknown[]) {
+ return {
+ exec: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }),
+ listWorktrees: vi.fn().mockResolvedValue(worktrees),
+ removeWorktree: vi.fn().mockResolvedValue({})
+ }
+}
+
+function makeRemoteRepoStore(
+ executionHostId: ExecutionHostId,
+ extraRepoFields: Record = {},
+ metaOverrides: Partial = {}
+) {
+ const repo = {
+ ...store.getRepos()[0]!,
+ path: REMOTE_REPO_PATH,
+ executionHostId,
+ ...extraRepoFields
+ }
+ const metaById: Record = {
+ [TEST_WORKTREE_ID]: makeWorktreeMeta({ hostId: executionHostId, ...metaOverrides })
+ }
+ const removeWorktreeMeta = vi.fn((worktreeId: string, hostId?: string) => {
+ if (!hostId || metaById[worktreeId]?.hostId === hostId) {
+ delete metaById[worktreeId]
+ }
+ })
+ return {
+ repo,
+ metaById,
+ removeWorktreeMeta,
+ runtimeStore: {
+ ...store,
+ getRepos: () => [repo],
+ getRepo: (id: string) => (id === repo.id ? repo : undefined),
+ getAllWorktreeMeta: () => metaById,
+ getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
+ setWorktreeMeta: (worktreeId: string, meta: Partial) => {
+ metaById[worktreeId] = { ...(metaById[worktreeId] ?? makeWorktreeMeta()), ...meta }
+ return metaById[worktreeId]
+ },
+ removeWorktreeMeta
+ }
+ }
+}
+
+const REPO_ROOT_ENTRY = {
+ path: REMOTE_REPO_PATH,
+ head: 'main',
+ branch: 'main',
+ isBare: false,
+ isMainWorktree: true
+}
+
+const REGISTERED_ENTRY = {
+ path: TEST_WORKTREE_PATH,
+ head: 'def456',
+ branch: 'feature/test',
+ isBare: false,
+ isMainWorktree: false
+}
+
+describe('OrcaRuntimeService worktree removal execution host', () => {
+ beforeEach(() => {
+ vi.mocked(listWorktrees).mockClear()
+ vi.mocked(listWorktreesStrict).mockClear()
+ vi.mocked(removeWorktree).mockClear()
+ })
+
+ it('removes a migrated-spelling SSH row on its own host, never this machine', async () => {
+ // No `connectionId` at all: the row names its owner only as `executionHostId: 'ssh:target-a'`.
+ const { runtimeStore, removeWorktreeMeta, metaById } = makeRemoteRepoStore('ssh:target-a')
+ const provider = makeGitProvider([REPO_ROOT_ENTRY, REGISTERED_ENTRY])
+ registerSshGitProvider('target-a', provider as never)
+ const runtime = createWorktreeRemovalRuntime(runtimeStore)
+ vi.spyOn(runtime, 'acquireFileWatcherRemoval').mockResolvedValue({ finish: vi.fn() })
+
+ try {
+ await runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'ssh:target-a')
+
+ expect(provider.listWorktrees).toHaveBeenCalledWith(REMOTE_REPO_PATH)
+ expect(provider.removeWorktree).toHaveBeenCalledWith(TEST_WORKTREE_PATH, true)
+ expect(listWorktreesStrict).not.toHaveBeenCalled()
+ expect(removeWorktree).not.toHaveBeenCalled()
+ expect(removeWorktreeMeta).toHaveBeenCalledWith(TEST_WORKTREE_ID, 'ssh:target-a')
+ expect(metaById[TEST_WORKTREE_ID]).toBeUndefined()
+ } finally {
+ unregisterSshGitProvider('target-a')
+ }
+ })
+
+ it('runs the cleanup path for a migrated-spelling row entirely on its host', async () => {
+ // #18358 made an `executionHostId`-only row a removable cleanup candidate. Every step of the
+ // removal it starts — list, existence probe, delete, prune — must name the same host.
+ const { runtimeStore, removeWorktreeMeta } = makeRemoteRepoStore('ssh:target-a')
+ const provider = makeGitProvider([REPO_ROOT_ENTRY])
+ const fsProvider = { stat: vi.fn(missingPath), deletePath: vi.fn() }
+ registerSshGitProvider('target-a', provider as never)
+ registerSshFilesystemProvider('target-a', fsProvider as never)
+ const runtime = createWorktreeRemovalRuntime(runtimeStore)
+
+ try {
+ await expect(
+ runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'ssh:target-a')
+ ).resolves.toEqual({})
+
+ expect(provider.listWorktrees).toHaveBeenCalledWith(REMOTE_REPO_PATH)
+ expect(fsProvider.stat).toHaveBeenCalledWith(TEST_WORKTREE_PATH)
+ expect(listWorktreesStrict).not.toHaveBeenCalled()
+ expect(removeWorktree).not.toHaveBeenCalled()
+ expect(fsProvider.deletePath).not.toHaveBeenCalled()
+ expect(removeWorktreeMeta).toHaveBeenCalledWith(TEST_WORKTREE_ID, 'ssh:target-a')
+ } finally {
+ unregisterSshFilesystemProvider('target-a')
+ unregisterSshGitProvider('target-a')
+ }
+ })
+
+ it('keeps two simultaneously registered SSH hosts off each other paths', async () => {
+ const { runtimeStore } = makeRemoteRepoStore('ssh:target-b')
+ const providerA = makeGitProvider([REPO_ROOT_ENTRY, REGISTERED_ENTRY])
+ const providerB = makeGitProvider([REPO_ROOT_ENTRY, REGISTERED_ENTRY])
+ registerSshGitProvider('target-a', providerA as never)
+ registerSshGitProvider('target-b', providerB as never)
+ const runtime = createWorktreeRemovalRuntime(runtimeStore)
+ vi.spyOn(runtime, 'acquireFileWatcherRemoval').mockResolvedValue({ finish: vi.fn() })
+
+ try {
+ await runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'ssh:target-b')
+
+ expect(providerB.removeWorktree).toHaveBeenCalledWith(TEST_WORKTREE_PATH, true)
+ expect(providerA.listWorktrees).not.toHaveBeenCalled()
+ expect(providerA.removeWorktree).not.toHaveBeenCalled()
+ } finally {
+ unregisterSshGitProvider('target-b')
+ unregisterSshGitProvider('target-a')
+ }
+ })
+
+ it('refuses an SSH row whose host is unreachable instead of deleting here', async () => {
+ const { runtimeStore, metaById } = makeRemoteRepoStore('ssh:target-a')
+ const runtime = createWorktreeRemovalRuntime(runtimeStore)
+
+ await expect(
+ runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'ssh:target-a')
+ ).rejects.toThrow('Remote connection dropped')
+
+ expect(listWorktreesStrict).not.toHaveBeenCalled()
+ expect(removeWorktree).not.toHaveBeenCalled()
+ expect(metaById[TEST_WORKTREE_ID]).toBeDefined()
+ })
+
+ it('refuses a runtime row with no nested SSH target rather than deleting locally', async () => {
+ const { runtimeStore, metaById } = makeRemoteRepoStore('runtime:env-1')
+ const runtime = createWorktreeRemovalRuntime(runtimeStore)
+
+ await expect(
+ runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'runtime:env-1')
+ ).rejects.toThrow('not dispatched by this process')
+
+ expect(listWorktreesStrict).not.toHaveBeenCalled()
+ expect(removeWorktree).not.toHaveBeenCalled()
+ expect(metaById[TEST_WORKTREE_ID]).toBeDefined()
+ })
+
+ it('refuses a runtime row whose nested SSH target is dialable in this namespace', async () => {
+ // `connectionId: 'target-a'` names a target inside env-1, not the one registered here. The raw
+ // read dialled this client's same-named host and removed a worktree on the wrong machine.
+ const { runtimeStore, metaById } = makeRemoteRepoStore('runtime:env-1', {
+ connectionId: 'target-a'
+ })
+ const provider = makeGitProvider([REPO_ROOT_ENTRY, REGISTERED_ENTRY])
+ registerSshGitProvider('target-a', provider as never)
+ const runtime = createWorktreeRemovalRuntime(runtimeStore)
+
+ try {
+ await expect(
+ runtime.removeManagedWorktree(TEST_WORKTREE_ID, true, false, false, 'runtime:env-1')
+ ).rejects.toThrow('not dispatched by this process')
+
+ // Selector resolution still lists through the raw field before removal begins — a read on
+ // the wrong namespace, tracked separately. Nothing destructive reaches it.
+ expect(provider.removeWorktree).not.toHaveBeenCalled()
+ expect(removeWorktree).not.toHaveBeenCalled()
+ expect(metaById[TEST_WORKTREE_ID]).toBeDefined()
+ } finally {
+ unregisterSshGitProvider('target-a')
+ }
+ })
+})
diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts
index aabfeb899c8..573c5ba979d 100644
--- a/src/main/runtime/orca-runtime.test.ts
+++ b/src/main/runtime/orca-runtime.test.ts
@@ -104,5 +104,6 @@ await import('./orca-runtime-tests/worktree-removal-and-reconciliation.spec')
await import('./orca-runtime-tests/worktree-removal-and-reconciliation-part-02.spec')
await import('./orca-runtime-tests/worktree-removal-and-reconciliation-part-03.spec')
await import('./orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec')
+await import('./orca-runtime-tests/worktree-removal-execution-host.spec')
await import('./orca-runtime-tests/targeting-and-resilience.spec')
await import('./orca-runtime-tests/worktree-scan-cache-ttl.spec')
diff --git a/src/main/runtime/runtime-registered-remote-worktree-removal.ts b/src/main/runtime/runtime-registered-remote-worktree-removal.ts
index 09c7ac362f4..6eec18d52c8 100644
--- a/src/main/runtime/runtime-registered-remote-worktree-removal.ts
+++ b/src/main/runtime/runtime-registered-remote-worktree-removal.ts
@@ -13,6 +13,8 @@ export async function removeRuntimeRegisteredRemoteWorktree(args: {
removedPushTarget: GitPushTarget | undefined
store: RuntimeStore
provider: SshGitProvider
+ /** From the resolved removal route; `repo.connectionId!` answered null for an `ssh:`-only row. */
+ connectionId: string
force: boolean
allowUnverifiedPtyStop: boolean
deleteBranch: boolean
@@ -28,8 +30,7 @@ export async function removeRuntimeRegisteredRemoteWorktree(args: {
) => RemoveWorktreeResult
finishRemoval: (result: RemoveWorktreeResult) => void
}): Promise {
- const { repo, target, registeredWorktree, provider } = args
- const connectionId = repo.connectionId!
+ const { repo, target, registeredWorktree, provider, connectionId } = args
const removeOptions = !args.deleteBranch ? { deleteBranch: args.deleteBranch } : {}
const gate = await args.acquireWatcherRemoval(registeredWorktree.path, connectionId)
let rawResult: RemoveWorktreeResult | undefined
diff --git a/src/main/runtime/runtime-unregistered-worktree-removal.ts b/src/main/runtime/runtime-unregistered-worktree-removal.ts
index 3d21998ef55..01de8820c1c 100644
--- a/src/main/runtime/runtime-unregistered-worktree-removal.ts
+++ b/src/main/runtime/runtime-unregistered-worktree-removal.ts
@@ -2,8 +2,10 @@ import type { GitPushTarget, GitWorktreeInfo } from '../../shared/worktree/types
import type { Repo } from '../../shared/repo-types'
import type { WorktreeMeta } from '../../shared/worktree/meta-types'
import type { LocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
-import type { IFilesystemProvider } from '../providers/types'
-import type { SshGitProvider } from '../providers/ssh-git-provider'
+import {
+ getWorktreeRemovalConnectionId,
+ type WorktreeRemovalRoute
+} from '../worktree-removal-execution-host-route'
import {
getLocalWorktreePathAccess,
removeLocalWorktreePath,
@@ -39,8 +41,8 @@ export async function removeRuntimeUnregisteredWorktree(args: {
removedPushTarget: GitPushTarget | undefined
force: boolean
allowUnverifiedPtyStop: boolean
- provider: SshGitProvider | null
- fsProvider: IFilesystemProvider | null
+ /** One resolved host for the whole removal, replacing the `provider` whose `null` also meant local. */
+ route: WorktreeRemovalRoute
localOptions: LocalProjectWorktreeGitOptions
store: RuntimeStore
acquireWatcherRemoval: (path: string, connectionId?: string) => Promise
@@ -48,21 +50,23 @@ export async function removeRuntimeUnregisteredWorktree(args: {
deleteHistory: () => Promise
finishRemoval: () => void
}): Promise<{}> {
- const { repo, target, registeredWorktrees, removedMeta } = args
+ const { repo, target, registeredWorktrees, removedMeta, route } = args
let canCleanOrphanedDirectory = false
if (canCleanupUnregisteredOrcaWorktreeDirectory({ meta: removedMeta })) {
- if (repo.connectionId) {
- if (!args.fsProvider) {
+ if (route.kind === 'ssh') {
+ const fsProvider = route.fsProvider
+ if (!fsProvider) {
throw new Error('SSH filesystem provider unavailable')
}
- if (!args.fsProvider.lstat) {
+ const lstat = fsProvider.lstat
+ if (!lstat) {
throw new Error('SSH filesystem provider lstat unavailable')
}
canCleanOrphanedDirectory = await canSafelyRemoveOrphanedWorktreeDirectory(
target.path,
repo.path,
- (path) => args.fsProvider!.lstat!(path),
- (path) => args.fsProvider!.readFile(path)
+ (path) => lstat(path),
+ (path) => fsProvider.readFile(path)
)
} else {
const access = getLocalWorktreePathAccess(args.localOptions)
@@ -85,7 +89,7 @@ export async function removeRuntimeUnregisteredWorktree(args: {
args.finishRemoval()
return {}
}
- if (!repo.connectionId) {
+ if (route.kind === 'local') {
const access = getLocalWorktreePathAccess(args.localOptions)
const runtimeWorktreePath = toLocalWorktreeRuntimePath(target.path, args.localOptions)
if (
@@ -108,7 +112,7 @@ export async function removeRuntimeUnregisteredWorktree(args: {
return {}
}
}
- if (await isRuntimeWorktreePathMissing(repo, target.path, args.localOptions)) {
+ if (await isRuntimeWorktreePathMissing(route.hostId, target.path, args.localOptions)) {
if (!args.force && !removedMeta) {
throw new Error(UNREGISTERED_MISSING_WORKTREE_MESSAGE)
}
@@ -123,14 +127,19 @@ export async function removeRuntimeUnregisteredWorktree(args: {
async function deleteUnregisteredDirectory(
args: Parameters[0]
): Promise {
- const connectionId = args.repo.connectionId?.trim() || undefined
+ const route = args.route
+ const connectionId = getWorktreeRemovalConnectionId(route)
const gate = await args.acquireWatcherRemoval(args.target.path, connectionId)
let completed = false
try {
await args.stopPtys(args.target.id, connectionId, args.allowUnverifiedPtyStop)
- await (connectionId
- ? args.fsProvider!.deletePath(args.target.path, true)
- : removeLocalWorktreePath(args.target.path, args.localOptions))
+ if (route.kind === 'local') {
+ await removeLocalWorktreePath(args.target.path, args.localOptions)
+ } else if (route.fsProvider) {
+ await route.fsProvider.deletePath(args.target.path, true)
+ } else {
+ throw new Error('SSH filesystem provider unavailable')
+ }
completed = true
} finally {
await gate.finish(completed)
@@ -142,9 +151,9 @@ async function deleteUnregisteredDirectory(
async function cleanupPushTarget(
args: Parameters[0]
): Promise {
- await (args.repo.connectionId
+ await (args.route.kind === 'ssh'
? cleanupUnusedWorktreePushTargetRemoteSsh(
- args.provider!,
+ args.route.provider,
args.repo.path,
args.target.id,
args.removedPushTarget,
diff --git a/src/main/runtime/runtime-worktree-filesystem.ts b/src/main/runtime/runtime-worktree-filesystem.ts
index c2c5ac711c5..4f29919ca95 100644
--- a/src/main/runtime/runtime-worktree-filesystem.ts
+++ b/src/main/runtime/runtime-worktree-filesystem.ts
@@ -9,9 +9,12 @@ import {
getLocalWorktreePathAccess,
toLocalWorktreeRuntimePath
} from '../local-worktree-filesystem'
-import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch'
+import {
+ ExecutionHostNotDispatchableError,
+ resolveFilesystemRouteForHost
+} from '../providers/execution-host-provider-dispatch'
import { isWorktreePathMissing } from '../worktree-removal-safety'
-import { getRepoExecutionHostId } from '../../shared/execution-host'
+import { getRepoExecutionHostId, type ExecutionHostId } from '../../shared/execution-host'
import { getRepoOwnedWorktreeMeta } from '../worktree-metadata-ownership'
import type { WorktreeMeta } from '../../shared/worktree/meta-types'
import {
@@ -22,19 +25,26 @@ import {
import type { RuntimeStore } from './runtime-store-contract'
import { gitStatusErrorMeansNotRepository } from './runtime-worktree-selection'
+// Takes the resolved host rather than the repo: reading `repo.connectionId` answered "stat this on
+// the client" for a row that names its owner only as `executionHostId: 'ssh:'`, which is
+// the evidence a forced removal prunes registrations on.
export async function isRuntimeWorktreePathMissing(
- repo: Repo,
+ hostId: ExecutionHostId,
worktreePath: string,
localWorktreeGitOptions: { wslDistro?: string } = {}
): Promise {
- if (!repo.connectionId) {
+ const route = resolveFilesystemRouteForHost(hostId)
+ if (route.kind === 'runtime') {
+ throw new ExecutionHostNotDispatchableError(route.hostId)
+ }
+ if (route.kind === 'local') {
const access = getLocalWorktreePathAccess(localWorktreeGitOptions)
return isWorktreePathMissing(
toLocalWorktreeRuntimePath(worktreePath, localWorktreeGitOptions),
access.statPath
)
}
- const fsProvider = getSshFilesystemProvider(repo.connectionId)
+ const fsProvider = route.provider
return fsProvider ? isWorktreePathMissing(worktreePath, (path) => fsProvider.stat(path)) : false
}
diff --git a/src/main/worktree-removal-execution-host-route.test.ts b/src/main/worktree-removal-execution-host-route.test.ts
new file mode 100644
index 00000000000..5fbbfbf9540
--- /dev/null
+++ b/src/main/worktree-removal-execution-host-route.test.ts
@@ -0,0 +1,100 @@
+import { afterEach, describe, expect, it } from 'vitest'
+import {
+ registerSshGitProvider,
+ SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE,
+ unregisterSshGitProvider
+} from './providers/ssh-git-dispatch'
+import {
+ registerSshFilesystemProvider,
+ unregisterSshFilesystemProvider
+} from './providers/ssh-filesystem-dispatch'
+import { ExecutionHostNotDispatchableError } from './providers/execution-host-provider-dispatch'
+import {
+ getWorktreeRemovalConnectionId,
+ resolveWorktreeRemovalRoute
+} from './worktree-removal-execution-host-route'
+
+const HOST_A = 'target-a'
+const HOST_B = 'target-b'
+
+function gitProvider(name: string): never {
+ return { name } as never
+}
+
+function fsProvider(name: string): never {
+ return { name } as never
+}
+
+afterEach(() => {
+ unregisterSshGitProvider(HOST_A)
+ unregisterSshGitProvider(HOST_B)
+ unregisterSshFilesystemProvider(HOST_A)
+ unregisterSshFilesystemProvider(HOST_B)
+})
+
+describe('resolveWorktreeRemovalRoute', () => {
+ it('routes a local host to this machine with no connection', () => {
+ const route = resolveWorktreeRemovalRoute('local')
+
+ expect(route).toEqual({ kind: 'local', hostId: 'local' })
+ expect(getWorktreeRemovalConnectionId(route)).toBeUndefined()
+ })
+
+ it('keeps two simultaneously registered SSH hosts on their own providers', () => {
+ registerSshGitProvider(HOST_A, gitProvider('git-a'))
+ registerSshGitProvider(HOST_B, gitProvider('git-b'))
+ registerSshFilesystemProvider(HOST_A, fsProvider('fs-a'))
+ registerSshFilesystemProvider(HOST_B, fsProvider('fs-b'))
+
+ const routeA = resolveWorktreeRemovalRoute('ssh:target-a')
+ const routeB = resolveWorktreeRemovalRoute('ssh:target-b')
+
+ expect(routeA).toMatchObject({
+ kind: 'ssh',
+ hostId: 'ssh:target-a',
+ connectionId: HOST_A,
+ provider: { name: 'git-a' },
+ fsProvider: { name: 'fs-a' }
+ })
+ expect(routeB).toMatchObject({
+ kind: 'ssh',
+ hostId: 'ssh:target-b',
+ connectionId: HOST_B,
+ provider: { name: 'git-b' },
+ fsProvider: { name: 'fs-b' }
+ })
+ expect(getWorktreeRemovalConnectionId(routeA)).toBe(HOST_A)
+ expect(getWorktreeRemovalConnectionId(routeB)).toBe(HOST_B)
+ })
+
+ it('carries a null filesystem provider without falling back to the local one', () => {
+ registerSshGitProvider(HOST_A, gitProvider('git-a'))
+
+ expect(resolveWorktreeRemovalRoute('ssh:target-a')).toMatchObject({
+ kind: 'ssh',
+ fsProvider: null
+ })
+ })
+
+ it('refuses an unreachable SSH host instead of answering local', () => {
+ expect(() => resolveWorktreeRemovalRoute('ssh:target-a')).toThrow(
+ SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE
+ )
+ })
+
+ it('refuses a runtime host with no nested SSH target', () => {
+ expect(() => resolveWorktreeRemovalRoute('runtime:env-1')).toThrow(
+ ExecutionHostNotDispatchableError
+ )
+ })
+
+ it('refuses a runtime host even when a same-named target is dialable here', () => {
+ // The nested target lives in the environment's namespace; a same-named local one is a
+ // different machine, and removing a worktree through it deletes the wrong checkout.
+ registerSshGitProvider(HOST_A, gitProvider('git-a'))
+
+ expect(() => resolveWorktreeRemovalRoute('runtime:target-a')).toThrow(
+ ExecutionHostNotDispatchableError
+ )
+ })
+})
diff --git a/src/main/worktree-removal-execution-host-route.ts b/src/main/worktree-removal-execution-host-route.ts
new file mode 100644
index 00000000000..529eed71af5
--- /dev/null
+++ b/src/main/worktree-removal-execution-host-route.ts
@@ -0,0 +1,81 @@
+/**
+ * Which execution host a destructive worktree removal runs against.
+ *
+ * `removeManagedWorktree` resolved its host once, for metadata pruning
+ * (`cleanupHostId ?? getRepoExecutionHostId(repo)`), and then read raw `repo.connectionId` for
+ * every step that actually touches the filesystem: the `git worktree list` that decides whether the
+ * path is registered, the provider handed to the unregistered-removal branch, the
+ * registered-remote-vs-local fork, and the PTY/history teardown. One function, two spellings —
+ * so a row naming its owner only as `executionHostId: 'ssh:'` listed a *remote* checkout on
+ * this client, entered the unregistered branch with `provider: null`, and deleted a same-named
+ * local directory while metadata was pruned under `ssh:` (#11163). #18358 made that
+ * reachable by migrating the cleanup scan, so those rows now surface as removable candidates.
+ *
+ * Routing is now one answer for the whole removal, taken from the host the prune already used, so
+ * list, remove and prune cannot disagree. The ambiguous `provider: SshGitProvider | null` carrier
+ * is deleted from the callees rather than supplemented, which makes every remaining reader a
+ * compile error in the typed modules that do the destructive work.
+ *
+ * `runtime:` is not a variant. Its files live on that environment's own server and the SSH
+ * target on its repo row is that server's nested one, addressable only as the pair
+ * (environmentId, targetId); handing it to this client's SSH table would `git worktree remove` a
+ * same-named path on the wrong machine. It throws, matching `workspace-cleanup-git-route` and
+ * `runtime-git-command-target`.
+ *
+ * An `ssh:` host with no registered provider also throws. Loss of contact is never evidence that
+ * the checkout is local (docs/reference/ssh-execution-boundary.md); refusing leaves a remote
+ * worktree in place, while the incumbent fallback deleted a client-side path.
+ */
+
+import type { ExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host'
+import {
+ ExecutionHostNotDispatchableError,
+ resolveFilesystemRouteForHost,
+ resolveGitRouteForHost
+} from './providers/execution-host-provider-dispatch'
+import { SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE } from './providers/ssh-git-dispatch'
+import type { SshGitProvider } from './providers/ssh-git-provider'
+import type { IFilesystemProvider } from './providers/types'
+
+export type WorktreeRemovalRoute =
+ | { kind: 'local'; hostId: typeof LOCAL_EXECUTION_HOST_ID }
+ | {
+ kind: 'ssh'
+ hostId: `ssh:${string}`
+ connectionId: string
+ provider: SshGitProvider
+ /**
+ * Still nullable: the incumbent read `getSshFilesystemProvider` (not `require…`) and the
+ * directory branches raise their own message when they need it. Narrowing it here would
+ * refuse removals that never touch the filesystem provider.
+ */
+ fsProvider: IFilesystemProvider | null
+ }
+
+export function resolveWorktreeRemovalRoute(hostId: ExecutionHostId): WorktreeRemovalRoute {
+ const route = resolveGitRouteForHost(hostId)
+ switch (route.kind) {
+ case 'local':
+ return { kind: 'local', hostId: route.hostId }
+ case 'runtime':
+ throw new ExecutionHostNotDispatchableError(route.hostId)
+ case 'ssh': {
+ if (!route.provider) {
+ throw new Error(SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE)
+ }
+ const fsRoute = resolveFilesystemRouteForHost(hostId)
+ return {
+ kind: 'ssh',
+ hostId: route.hostId,
+ connectionId: route.connectionId,
+ provider: route.provider,
+ fsProvider: fsRoute.kind === 'ssh' ? fsRoute.provider : null
+ }
+ }
+ }
+}
+
+/** The connection to teardown PTYs, watchers and history against — `undefined` on a local host. */
+export function getWorktreeRemovalConnectionId(route: WorktreeRemovalRoute): string | undefined {
+ return route.kind === 'ssh' ? route.connectionId : undefined
+}
From 5d8532f6d3eb2afdc2fb7dc7ec910e15535f189b Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Thu, 3 Sep 2026 16:27:13 -0700
Subject: [PATCH 04/43] fix(worktrees): resolve the execution host at both
worktree-create entry points (#18545)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two entry points create the same workspace and disagreed about how to read its
host. `orca-runtime-create-managed-worktree.ts:63` resolved through
`getRepoSshConnectionId` and then normalized the row; the `worktrees:create` IPC
handler branched on raw `repo.connectionId`
(`register-worktree-create-handlers.ts:66-69`). So a repo naming its owner only
as `executionHostId: 'ssh:'` created remotely through the runtime and ran
`git worktree add` on the client against a remote path through IPC (#11163).
Same repo, two entry points, different answers.
Both now take one route, resolved through the existing layer
(`getRepoExecutionHostId` -> #18296's `resolveGitRouteForHost`). No new resolver.
The row normalization on the `ssh` variant is kept, and it is a **workaround, not
the pattern**. `createRemoteWorktree` and its callees re-read `repo.connectionId!`
at five depths in `ipc/worktree-remote.ts` (1627, 1847, 1848, 1865, 2029), so the
resolved connection has to reach them through the field they already read. It
travels only as far as that object does — anything downstream that re-reads the
row from the store still sees the unnormalized one, and it cannot express the
`runtime:` refusal on its own. Proper fix, deliberately not done here: give that
pipeline an explicit connection parameter and delete `repo.connectionId!` from it
so every reader becomes a compile error, the technique #18307/#18325 used. That
is a change inside a 2800-line module plus its callers, and it wants its own PR.
Three answers that used to collapse into one, now distinct at both entry points:
- `executionHostId: 'ssh:*'` with no `connectionId` -> that SSH host (IPC used to
create locally);
- `executionHostId: 'local'` with a surviving `connectionId` -> local, since a
local row cannot nest an SSH namespace. This is what `getRepoSshConnectionId`
and therefore the runtime sibling already answered; IPC used to go remote;
- `runtime:` -> refused. Its worktree is created by that environment's own
server and the SSH target on its repo row is that server's nested one,
addressable only as (environmentId, targetId). The renderer already routes
runtime-environment creates over `worktree.create` RPC rather than this IPC
channel, so reaching either entry point with one is a routing mistake. Matches
`workspace-cleanup-git-route` and `runtime-git-command-target`.
Folder-workspace creation is untouched on both sides: it is a registration, not a
filesystem create, so the route is resolved after that branch on the IPC side, and
on the runtime side only the agent trust write consumes it — where a `runtime:`
host now yields `null` instead of the nested target, so the write stops going to a
same-named target in this client's table.
No wire or persistence change: the normalized row is a local value passed to the
create pipeline, never stored, and `CreateWorktreeResult` is untouched.
---
...rees-create-execution-host-routing.test.ts | 229 ++++++++++++++++++
.../register-worktree-create-handlers.ts | 19 +-
.../orca-runtime-create-managed-worktree.ts | 44 ++--
.../worktree-create-execution-host.spec.ts | 61 +++++
src/main/runtime/orca-runtime.test.ts | 1 +
...rktree-create-execution-host-route.test.ts | 106 ++++++++
.../worktree-create-execution-host-route.ts | 69 ++++++
7 files changed, 505 insertions(+), 24 deletions(-)
create mode 100644 src/main/ipc/worktrees-create-execution-host-routing.test.ts
create mode 100644 src/main/runtime/orca-runtime-tests/worktree-create-execution-host.spec.ts
create mode 100644 src/main/worktree-create-execution-host-route.test.ts
create mode 100644 src/main/worktree-create-execution-host-route.ts
diff --git a/src/main/ipc/worktrees-create-execution-host-routing.test.ts b/src/main/ipc/worktrees-create-execution-host-routing.test.ts
new file mode 100644
index 00000000000..36f1e816ea6
--- /dev/null
+++ b/src/main/ipc/worktrees-create-execution-host-routing.test.ts
@@ -0,0 +1,229 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import {
+ addWorktreeMock,
+ getActiveMultiplexerMock,
+ getSshGitProviderMock,
+ listWorktreesMock
+} from './worktrees-test-module-mocks'
+import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness'
+
+vi.mock('electron', async () =>
+ (await import('./worktrees-test-module-mocks')).electronModuleMock()
+)
+vi.mock('../git/worktree', async () =>
+ (await import('./worktrees-test-module-mocks')).gitWorktreeModuleMock()
+)
+vi.mock('../git/runner', async () =>
+ (await import('./worktrees-test-module-mocks')).gitRunnerModuleMock()
+)
+vi.mock('../git/repo', async () =>
+ (await import('./worktrees-test-module-mocks')).gitRepoModuleMock()
+)
+vi.mock('../git/git-username', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ resolveLocalGitUsername: (await import('./worktrees-test-module-mocks'))
+ .resolveLocalGitUsernameMock
+}))
+vi.mock('../github/client', async () =>
+ (await import('./worktrees-test-module-mocks')).githubClientModuleMock()
+)
+vi.mock('../source-control/hosted-review', async () =>
+ (await import('./worktrees-test-module-mocks')).hostedReviewModuleMock()
+)
+vi.mock('../providers/ssh-git-dispatch', async () =>
+ (await import('./worktrees-test-module-mocks')).sshGitDispatchModuleMock()
+)
+vi.mock('../providers/ssh-filesystem-dispatch', async () =>
+ (await import('./worktrees-test-module-mocks')).sshFilesystemDispatchModuleMock()
+)
+vi.mock('./worktree-symlinks', async () =>
+ (await import('./worktrees-test-module-mocks')).worktreeSymlinksModuleMock()
+)
+vi.mock('./ssh', async () => (await import('./worktrees-test-module-mocks')).sshModuleMock())
+vi.mock('../ssh/ssh-target-registry', async () =>
+ (await import('./worktrees-test-module-mocks')).sshTargetRegistryModuleMock()
+)
+vi.mock('../hooks', async () => (await import('./worktrees-test-module-mocks')).hooksModuleMock())
+vi.mock('../setup-runner-script-text', async (importOriginal) =>
+ (await import('./worktrees-test-module-mocks')).setupRunnerScriptTextModuleMock(
+ (await importOriginal()) as Record
+ )
+)
+vi.mock('../worktree-runner-script', async (importOriginal) =>
+ (await import('./worktrees-test-module-mocks')).worktreeRunnerScriptModuleMock(
+ (await importOriginal()) as Record
+ )
+)
+vi.mock('../effective-hook-config', async (importOriginal) =>
+ (await import('./worktrees-test-module-mocks')).effectiveHookConfigModuleMock(
+ (await importOriginal()) as Record
+ )
+)
+vi.mock('../setup-hook-env-vars', async (importOriginal) =>
+ (await import('./worktrees-test-module-mocks')).setupHookEnvVarsModuleMock(
+ (await importOriginal()) as Record
+ )
+)
+vi.mock('./worktree-logic', async (importOriginal) =>
+ (await import('./worktrees-test-module-mocks')).worktreeLogicModuleMock(
+ (await importOriginal()) as Record
+ )
+)
+vi.mock('../terminal-history-deletion', async () =>
+ (await import('./worktrees-test-module-mocks')).terminalHistoryDeletionModuleMock()
+)
+vi.mock('../ports/advertised-url-watcher', async () =>
+ (await import('./worktrees-test-module-mocks')).advertisedUrlWatcherModuleMock()
+)
+vi.mock('../workspace-cleanup-scan-snapshot', async () =>
+ (await import('./worktrees-test-module-mocks')).workspaceCleanupScanSnapshotModuleMock()
+)
+vi.mock('../workspace-space-analysis-snapshot', async () =>
+ (await import('./worktrees-test-module-mocks')).workspaceSpaceAnalysisSnapshotModuleMock()
+)
+vi.mock('../workspace-cleanup-removal-snapshot-prune', async () =>
+ (await import('./worktrees-test-module-mocks')).workspaceCleanupRemovalSnapshotPruneModuleMock()
+)
+vi.mock('../runtime/worktree-teardown', async () =>
+ (await import('./worktrees-test-module-mocks')).worktreeTeardownModuleMock()
+)
+vi.mock('./pty', async () => (await import('./worktrees-test-module-mocks')).ptyModuleMock())
+
+const REMOTE_REPO_PATH = '/remote/repo'
+
+function makeRepo(fields: Record) {
+ return {
+ id: 'repo-1',
+ path: REMOTE_REPO_PATH,
+ displayName: 'repo',
+ badgeColor: '#000',
+ addedAt: 0,
+ worktreeBaseRef: 'origin/main',
+ ...fields
+ }
+}
+
+function makeProvider(worktreePath: string) {
+ return {
+ exec: vi.fn().mockImplementation(async (args: string[]) => {
+ if (args[0] === 'remote') {
+ return { stdout: 'origin\n', stderr: '' }
+ }
+ if (args[0] === 'show-ref') {
+ // A hit here reads as "branch already exists"; the create loop would then rename.
+ throw Object.assign(new Error('missing exact ref'), { code: 1 })
+ }
+ return { stdout: '', stderr: '' }
+ }),
+ fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined),
+ addWorktree: vi.fn().mockResolvedValue(undefined),
+ listWorktrees: vi.fn().mockResolvedValue([
+ {
+ path: worktreePath,
+ head: 'abc123',
+ branch: 'refs/heads/wt',
+ isBare: false,
+ isMainWorktree: false
+ }
+ ])
+ }
+}
+
+function useRepo(repo: ReturnType): void {
+ store.getRepos.mockReturnValue([repo])
+ store.getRepo.mockReturnValue(repo)
+ store.setWorktreeMeta.mockImplementation((_worktreeId: string, meta: unknown) => meta)
+ getActiveMultiplexerMock.mockReturnValue({
+ request: vi.fn().mockResolvedValue(undefined),
+ notify: vi.fn()
+ })
+}
+
+describe('worktrees:create execution host routing', () => {
+ beforeEach(() => {
+ setupWorktreeHandlers()
+ })
+
+ it('creates on the SSH host for a row that names it only as executionHostId', async () => {
+ // No `connectionId`: the raw read answered "local" and ran `git worktree add` on the client
+ // against `/remote/repo`. The runtime sibling already resolved this row remotely.
+ useRepo(makeRepo({ executionHostId: 'ssh:target-a' }))
+ const provider = makeProvider('/remote/repo-wt')
+ getSshGitProviderMock.mockImplementation((connectionId: string) =>
+ connectionId === 'target-a' ? provider : undefined
+ )
+
+ await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'wt' })
+
+ expect(provider.addWorktree).toHaveBeenCalledTimes(1)
+ expect(addWorktreeMock).not.toHaveBeenCalled()
+ })
+
+ it('keeps two simultaneously registered SSH hosts apart', async () => {
+ useRepo(makeRepo({ executionHostId: 'ssh:target-b' }))
+ const providerA = makeProvider('/remote/repo-wt-a')
+ const providerB = makeProvider('/remote/repo-wt-b')
+ getSshGitProviderMock.mockImplementation((connectionId: string) =>
+ connectionId === 'target-a' ? providerA : connectionId === 'target-b' ? providerB : undefined
+ )
+
+ await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'wt' })
+
+ expect(providerB.addWorktree).toHaveBeenCalledTimes(1)
+ expect(providerA.addWorktree).not.toHaveBeenCalled()
+ expect(addWorktreeMock).not.toHaveBeenCalled()
+ })
+
+ it('refuses a runtime row with no nested SSH target instead of creating locally', async () => {
+ useRepo(makeRepo({ executionHostId: 'runtime:env-1' }))
+
+ await expect(
+ handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'wt' })
+ ).rejects.toThrow('not dispatched by this process')
+
+ expect(addWorktreeMock).not.toHaveBeenCalled()
+ })
+
+ it('refuses a runtime row whose nested SSH target is dialable in this namespace', async () => {
+ // `target-a` names a target inside env-1. A same-named one registered here is another machine,
+ // so creating through it lands the checkout on the wrong host.
+ useRepo(makeRepo({ executionHostId: 'runtime:env-1', connectionId: 'target-a' }))
+ const provider = makeProvider('/remote/repo-wt')
+ getSshGitProviderMock.mockImplementation((connectionId: string) =>
+ connectionId === 'target-a' ? provider : undefined
+ )
+
+ await expect(
+ handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'wt' })
+ ).rejects.toThrow('not dispatched by this process')
+
+ expect(provider.addWorktree).not.toHaveBeenCalled()
+ expect(addWorktreeMock).not.toHaveBeenCalled()
+ })
+
+ it('answers local for a row that declares itself local while carrying a connection', async () => {
+ // A contradictory row: `getRepoSshConnectionId` lets `local` win, and the runtime sibling has
+ // always read it that way. The raw field sent it remote, so the two entry points disagreed.
+ useRepo(
+ makeRepo({ path: '/workspace/repo', executionHostId: 'local', connectionId: 'target-a' })
+ )
+ listWorktreesMock.mockResolvedValue([
+ {
+ path: '/workspace/wt',
+ head: 'abc123',
+ branch: 'wt',
+ isBare: false,
+ isMainWorktree: false
+ }
+ ])
+ const provider = makeProvider('/remote/repo-wt')
+ getSshGitProviderMock.mockImplementation((connectionId: string) =>
+ connectionId === 'target-a' ? provider : undefined
+ )
+
+ await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'wt' })
+
+ expect(addWorktreeMock).toHaveBeenCalledTimes(1)
+ expect(provider.addWorktree).not.toHaveBeenCalled()
+ })
+})
diff --git a/src/main/ipc/worktrees/create/register-worktree-create-handlers.ts b/src/main/ipc/worktrees/create/register-worktree-create-handlers.ts
index f371529963c..1f94598208b 100644
--- a/src/main/ipc/worktrees/create/register-worktree-create-handlers.ts
+++ b/src/main/ipc/worktrees/create/register-worktree-create-handlers.ts
@@ -29,6 +29,7 @@ import { normalizeLinkedWorkItemFields } from '../ipc-context-schemas'
import type { CreateWorktreeArgsWithSystemProvenance } from '../ipc-context-schemas'
import { createFolderWorkspace } from './folder-workspace-creation'
import { findExactRepoOwner, isCapturedRepoCurrent } from '../listing/worktree-host-ownership'
+import { requireWorktreeCreateRoute } from '../../../worktree-create-execution-host-route'
import type { WorktreeIpcContext } from '../worktree-ipc-context'
export function registerWorktreeCreateHandlers(context: WorktreeIpcContext): void {
@@ -62,11 +63,19 @@ export function registerWorktreeCreateHandlers(context: WorktreeIpcContext): voi
let result: CreateWorktreeResult
try {
// Why: wrap only the helpers; the pre-validation throws above are IPC-shape bugs, not the git/filesystem failures the funnel tracks.
- result = isFolderRepo(repo)
- ? createFolderWorkspace(createArgs, repo, store)
- : repo.connectionId
- ? await createRemoteWorktree(createArgs, repo, store, mainWindow)
- : await createLocalWorktree(createArgs, repo, store, mainWindow, runtime)
+ if (isFolderRepo(repo)) {
+ // A folder workspace is a registration, not a filesystem create, so it is host-agnostic.
+ result = createFolderWorkspace(createArgs, repo, store)
+ } else {
+ // Resolve the host rather than reading the raw field: an `executionHostId: 'ssh:*'`-only
+ // row read as local here and ran `git worktree add` on the client against a remote path,
+ // while the runtime sibling on the same repo already resolved.
+ const createRoute = requireWorktreeCreateRoute(repo)
+ result =
+ createRoute.kind === 'ssh'
+ ? await createRemoteWorktree(createArgs, createRoute.repo, store, mainWindow)
+ : await createLocalWorktree(createArgs, repo, store, mainWindow, runtime)
+ }
} catch (error) {
releaseAutomationWorkspaceProvenanceRequest(args.automationProvenanceRequest)
track('workspace_create_failed', {
diff --git a/src/main/runtime/orca-runtime-create-managed-worktree.ts b/src/main/runtime/orca-runtime-create-managed-worktree.ts
index f9116f73405..f17a2285b83 100644
--- a/src/main/runtime/orca-runtime-create-managed-worktree.ts
+++ b/src/main/runtime/orca-runtime-create-managed-worktree.ts
@@ -4,7 +4,8 @@ import type { RuntimeManagedWorktreeCreateArgs } from './runtime-managed-worktre
import type { CreateWorktreeResult } from '../../shared/worktree/create-types'
import { isTuiAgentEnabled } from '../../shared/tui-agent-selection'
import { isFolderRepo } from '../../shared/repo-kind'
-import { getRepoSshConnectionId } from '../../shared/execution-host'
+import { resolveWorktreeCreateRoute } from '../worktree-create-execution-host-route'
+import { ExecutionHostNotDispatchableError } from '../providers/execution-host-provider-dispatch'
import { createRuntimeFolderWorktree } from './runtime-folder-worktree-create'
import { createRuntimeLocalManagedWorktree } from './runtime-local-worktree-create'
import { prepareRuntimeLocalWorktreeSetup } from './runtime-local-worktree-setup'
@@ -57,10 +58,14 @@ export class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWithGetWork
draftStartup?.agent ??
(requestedAgentEnabled ? requestedAgent : undefined))
const effectiveDraftPaste = args.startupDraftPaste ?? draftStartup?.draftPaste
- // Resolve the execution host once: SSH ownership has two spellings, and reading the raw
- // `connectionId` field routes an `executionHostId: 'ssh:*'`-only repo down the local path,
- // which runs `git worktree add` on the client against a remote path.
- const sshConnectionId = getRepoSshConnectionId(repo)
+ // Resolve the execution host once, shared with the `worktrees:create` IPC entry point so the
+ // two cannot answer differently for the same repo. Reading the raw `connectionId` field routes
+ // an `executionHostId: 'ssh:*'`-only repo down the local path, which runs `git worktree add` on
+ // the client against a remote path.
+ const createRoute = resolveWorktreeCreateRoute(repo)
+ // `null` on a `runtime:` host is deliberate: its nested target is addressable only inside that
+ // environment, so the trust write must not go to a same-named target in this client's table.
+ const sshConnectionId = createRoute.kind === 'ssh' ? createRoute.connectionId : null
if (isFolderRepo(repo)) {
// A folder workspace is a registration, not a filesystem create, so it is host-agnostic —
// except for the agent trust write, which must land on the host that will run the agent.
@@ -97,20 +102,21 @@ export class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWithGetWork
const lineageInput =
args.lineage || args.comment ? { ...args.lineage, comment: args.comment } : undefined
const lineageResolution = await this.resolveLineageForWorktreeCreate(lineageInput)
- if (sshConnectionId) {
- // Why normalize the row: the remote-create pipeline reads `repo.connectionId!` at every
- // depth, so hand it the connection the resolved host actually names.
- const result = await this.createManagedRemoteWorktree(
- { ...repo, connectionId: sshConnectionId },
- {
- ...args,
- activate: args.activate,
- ...(effectiveStartup ? { startup: effectiveStartup } : {}),
- ...(effectiveStartupFollowup ? { startupFollowup: effectiveStartupFollowup } : {}),
- ...(effectiveCreatedWithAgent ? { createdWithAgent: effectiveCreatedWithAgent } : {}),
- ...(effectiveDraftPaste ? { startupDraftPaste: effectiveDraftPaste } : {})
- }
- )
+ if (createRoute.kind === 'runtime') {
+ throw new ExecutionHostNotDispatchableError(createRoute.hostId)
+ }
+ if (createRoute.kind === 'ssh') {
+ // `createRoute.repo` carries the resolved connection in `connectionId`, because the
+ // remote-create pipeline still reads `repo.connectionId!` at every depth. See the workaround
+ // note in worktree-create-execution-host-route.ts.
+ const result = await this.createManagedRemoteWorktree(createRoute.repo, {
+ ...args,
+ activate: args.activate,
+ ...(effectiveStartup ? { startup: effectiveStartup } : {}),
+ ...(effectiveStartupFollowup ? { startupFollowup: effectiveStartupFollowup } : {}),
+ ...(effectiveCreatedWithAgent ? { createdWithAgent: effectiveCreatedWithAgent } : {}),
+ ...(effectiveDraftPaste ? { startupDraftPaste: effectiveDraftPaste } : {})
+ })
const recordedLineage = this.recordCreatedWorktreeLineage(result.worktree, lineageResolution)
this.emitWorktreeLifecycle({
kind: 'created',
diff --git a/src/main/runtime/orca-runtime-tests/worktree-create-execution-host.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-create-execution-host.spec.ts
new file mode 100644
index 00000000000..981e9860463
--- /dev/null
+++ b/src/main/runtime/orca-runtime-tests/worktree-create-execution-host.spec.ts
@@ -0,0 +1,61 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import {
+ OrcaRuntimeService,
+ addWorktree,
+ registerSshGitProvider,
+ unregisterSshGitProvider
+} from '../orca-runtime-test-mocks.spec'
+import { store } from '../orca-runtime-test-fixtures.spec'
+
+const RUNTIME_REPO_PATH = '/remote/repo'
+
+function makeRuntimeHostedStore(extraRepoFields: Record = {}) {
+ const repo = {
+ ...store.getRepos()[0]!,
+ path: RUNTIME_REPO_PATH,
+ executionHostId: 'runtime:env-1',
+ ...extraRepoFields
+ }
+ return {
+ ...store,
+ getRepos: () => [repo],
+ getRepo: (id: string) => (id === repo.id ? repo : undefined)
+ }
+}
+
+describe('OrcaRuntimeService worktree create execution host', () => {
+ beforeEach(() => {
+ vi.mocked(addWorktree).mockClear()
+ })
+
+ it('refuses to create for a runtime-hosted repo with no nested SSH target', async () => {
+ const runtime = new OrcaRuntimeService(makeRuntimeHostedStore() as never)
+
+ await expect(
+ runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'wt' })
+ ).rejects.toThrow('not dispatched by this process')
+
+ expect(addWorktree).not.toHaveBeenCalled()
+ })
+
+ it('refuses a runtime-hosted repo whose nested SSH target is dialable in this namespace', async () => {
+ // `target-a` names a target inside env-1. The same-named one registered here is another
+ // machine, so creating through it would put the checkout on the wrong host.
+ const provider = { exec: vi.fn(), addWorktree: vi.fn(), listWorktrees: vi.fn() }
+ registerSshGitProvider('target-a', provider as never)
+ const runtime = new OrcaRuntimeService(
+ makeRuntimeHostedStore({ connectionId: 'target-a' }) as never
+ )
+
+ try {
+ await expect(
+ runtime.createManagedWorktree({ repoSelector: 'id:repo-1', name: 'wt' })
+ ).rejects.toThrow('not dispatched by this process')
+
+ expect(provider.addWorktree).not.toHaveBeenCalled()
+ expect(addWorktree).not.toHaveBeenCalled()
+ } finally {
+ unregisterSshGitProvider('target-a')
+ }
+ })
+})
diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts
index 573c5ba979d..9d223233ef2 100644
--- a/src/main/runtime/orca-runtime.test.ts
+++ b/src/main/runtime/orca-runtime.test.ts
@@ -18,6 +18,7 @@ await import('./orca-runtime-tests/terminal-listing.spec')
await import('./orca-runtime-tests/worktree-selector-resolution.spec')
await import('./orca-runtime-tests/local-worktree-creation.spec')
await import('./orca-runtime-tests/local-worktree-creation-part-02.spec')
+await import('./orca-runtime-tests/worktree-create-execution-host.spec')
await import('./orca-runtime-tests/ssh-worktree-lifecycle.spec')
await import('./orca-runtime-tests/ssh-worktree-lifecycle-part-02.spec')
await import('./orca-runtime-tests/ssh-worktree-lifecycle-part-03.spec')
diff --git a/src/main/worktree-create-execution-host-route.test.ts b/src/main/worktree-create-execution-host-route.test.ts
new file mode 100644
index 00000000000..ec4ecd2899e
--- /dev/null
+++ b/src/main/worktree-create-execution-host-route.test.ts
@@ -0,0 +1,106 @@
+import { afterEach, describe, expect, it } from 'vitest'
+import { registerSshGitProvider, unregisterSshGitProvider } from './providers/ssh-git-dispatch'
+import { ExecutionHostNotDispatchableError } from './providers/execution-host-provider-dispatch'
+import type { Repo } from '../shared/repo-types'
+import {
+ requireWorktreeCreateRoute,
+ resolveWorktreeCreateRoute
+} from './worktree-create-execution-host-route'
+
+const HOST_A = 'target-a'
+const HOST_B = 'target-b'
+
+function repoRow(fields: Partial): Repo {
+ return {
+ id: 'repo-1',
+ path: '/remote/repo',
+ displayName: 'repo',
+ badgeColor: '#000',
+ addedAt: 0,
+ ...fields
+ } as Repo
+}
+
+afterEach(() => {
+ unregisterSshGitProvider(HOST_A)
+ unregisterSshGitProvider(HOST_B)
+})
+
+describe('resolveWorktreeCreateRoute', () => {
+ it('routes a row that names its host only as executionHostId to that SSH target', () => {
+ registerSshGitProvider(HOST_A, { name: 'git-a' } as never)
+
+ expect(resolveWorktreeCreateRoute(repoRow({ executionHostId: 'ssh:target-a' }))).toMatchObject({
+ kind: 'ssh',
+ hostId: 'ssh:target-a',
+ connectionId: HOST_A,
+ repo: { connectionId: HOST_A }
+ })
+ })
+
+ it('normalizes the row for a legacy connectionId-only repo without changing its answer', () => {
+ expect(resolveWorktreeCreateRoute(repoRow({ connectionId: HOST_A }))).toMatchObject({
+ kind: 'ssh',
+ connectionId: HOST_A,
+ repo: { connectionId: HOST_A }
+ })
+ })
+
+ it('keeps two simultaneously registered SSH hosts on their own connections', () => {
+ registerSshGitProvider(HOST_A, { name: 'git-a' } as never)
+ registerSshGitProvider(HOST_B, { name: 'git-b' } as never)
+
+ expect(resolveWorktreeCreateRoute(repoRow({ executionHostId: 'ssh:target-a' }))).toMatchObject({
+ connectionId: HOST_A,
+ repo: { connectionId: HOST_A }
+ })
+ expect(resolveWorktreeCreateRoute(repoRow({ executionHostId: 'ssh:target-b' }))).toMatchObject({
+ connectionId: HOST_B,
+ repo: { connectionId: HOST_B }
+ })
+ })
+
+ it('lets an explicit local host win over a surviving connectionId', () => {
+ // A contradictory row. `getRepoExecutionHostId` answers `local`, which is what the runtime
+ // create sibling has always done; the raw read sent it remote.
+ expect(
+ resolveWorktreeCreateRoute(repoRow({ executionHostId: 'local', connectionId: HOST_A }))
+ ).toEqual({ kind: 'local', hostId: 'local' })
+ })
+
+ it('answers runtime for a runtime row with no nested SSH target', () => {
+ expect(resolveWorktreeCreateRoute(repoRow({ executionHostId: 'runtime:env-1' }))).toEqual({
+ kind: 'runtime',
+ hostId: 'runtime:env-1',
+ environmentId: 'env-1'
+ })
+ })
+
+ it('answers runtime for a runtime row whose nested target is dialable here', () => {
+ registerSshGitProvider(HOST_A, { name: 'git-a' } as never)
+
+ expect(
+ resolveWorktreeCreateRoute(
+ repoRow({ executionHostId: 'runtime:env-1', connectionId: HOST_A })
+ )
+ ).toMatchObject({ kind: 'runtime', environmentId: 'env-1' })
+ })
+})
+
+describe('requireWorktreeCreateRoute', () => {
+ it('refuses a runtime host rather than creating through this client', () => {
+ expect(() => requireWorktreeCreateRoute(repoRow({ executionHostId: 'runtime:env-1' }))).toThrow(
+ ExecutionHostNotDispatchableError
+ )
+ })
+
+ it('passes local and SSH hosts through unchanged', () => {
+ registerSshGitProvider(HOST_A, { name: 'git-a' } as never)
+
+ expect(requireWorktreeCreateRoute(repoRow({}))).toEqual({ kind: 'local', hostId: 'local' })
+ expect(requireWorktreeCreateRoute(repoRow({ executionHostId: 'ssh:target-a' }))).toMatchObject({
+ kind: 'ssh',
+ connectionId: HOST_A
+ })
+ })
+})
diff --git a/src/main/worktree-create-execution-host-route.ts b/src/main/worktree-create-execution-host-route.ts
new file mode 100644
index 00000000000..2831a2fcbeb
--- /dev/null
+++ b/src/main/worktree-create-execution-host-route.ts
@@ -0,0 +1,69 @@
+/**
+ * Which execution host a worktree create runs on.
+ *
+ * Two entry points create the same workspace and disagreed about how to read its host. The runtime
+ * path resolved (`orca-runtime-create-managed-worktree.ts`) and then normalized the row; the IPC
+ * handler branched on raw `repo.connectionId`, so a row naming its owner only as
+ * `executionHostId: 'ssh:'` ran `git worktree add` on the client against a remote path
+ * (#11163). Same repo, two entry points, two answers.
+ *
+ * Both now take this one route.
+ *
+ * The `repo` on the `ssh` variant is a normalization, and it is a workaround rather than the
+ * pattern: `createRemoteWorktree` and its callees re-read `repo.connectionId!` at five depths
+ * (`ipc/worktree-remote.ts`), so the resolved connection has to be handed to them through the field
+ * they already read. It travels only as far as this object does — anything downstream that re-reads
+ * the row from the store still sees the unnormalized one. The real fix is to give that pipeline an
+ * explicit connection parameter and delete `repo.connectionId!` from it, which is a separate change.
+ */
+
+import { getRepoExecutionHostId, type LOCAL_EXECUTION_HOST_ID } from '../shared/execution-host'
+import type { Repo } from '../shared/repo-types'
+import {
+ ExecutionHostNotDispatchableError,
+ resolveGitRouteForHost
+} from './providers/execution-host-provider-dispatch'
+
+export type WorktreeCreateRoute =
+ | { kind: 'local'; hostId: typeof LOCAL_EXECUTION_HOST_ID }
+ | {
+ kind: 'ssh'
+ hostId: `ssh:${string}`
+ connectionId: string
+ /** The row with `connectionId` set to the resolved target; see the workaround note above. */
+ repo: Repo
+ }
+ | { kind: 'runtime'; hostId: `runtime:${string}`; environmentId: string }
+
+export function resolveWorktreeCreateRoute(repo: Repo): WorktreeCreateRoute {
+ const route = resolveGitRouteForHost(getRepoExecutionHostId(repo))
+ switch (route.kind) {
+ case 'local':
+ return { kind: 'local', hostId: route.hostId }
+ case 'ssh':
+ return {
+ kind: 'ssh',
+ hostId: route.hostId,
+ connectionId: route.connectionId,
+ repo: { ...repo, connectionId: route.connectionId }
+ }
+ case 'runtime':
+ return { kind: 'runtime', hostId: route.hostId, environmentId: route.environmentId }
+ }
+}
+
+/**
+ * For the two create forks that put files on a host. `runtime:` is not one of them: the
+ * environment's own server creates the worktree, and the SSH target on its repo row is that
+ * server's nested one, addressable only as (environmentId, targetId). Creating through this
+ * client's SSH table would `git worktree add` on a same-named target on the wrong machine.
+ */
+export function requireWorktreeCreateRoute(
+ repo: Repo
+): Exclude {
+ const route = resolveWorktreeCreateRoute(repo)
+ if (route.kind === 'runtime') {
+ throw new ExecutionHostNotDispatchableError(route.hostId)
+ }
+ return route
+}
From e85ebb0086e7b867e64ada9feae3aa99d9402e1c Mon Sep 17 00:00:00 2001
From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Date: Thu, 3 Sep 2026 16:28:45 -0700
Subject: [PATCH 05/43] feat(native-chat): restore the terminal/chat switcher
for bridge chat only (#18532)
* feat(native-chat): restore the terminal/chat switcher for bridge chat only
#16729 removed every user-facing terminal<->chat switching affordance as a
side effect of the structured Codex restructure ("renderer switching
affordances and their dead leftovers"). That was right for structured Codex
sessions, which render their own transcript with no live TUI underneath, but
it also took the switcher away from bridge native chat, which still reads the
terminal and has one to return to.
Restore all four surfaces, each gated so structured sessions keep the removal:
- pane header chat/terminal button (TerminalPaneHeaderOverlay)
- pane context-menu "Switch to chat/terminal view" (TerminalContextMenu)
- tab context-menu equivalent (SortableTabContextMenu)
- the keyboard chord, whose hook had survived uncalled since #16729
Gating is one rule in one place: `canSwitchNativeChatView` refuses whenever a
`structuredSessionId` is present, over the existing `canToggleNativeChat`
eligibility. Standalone structured tabs are already excluded by the
`contentType === 'terminal'` check; the new guard covers a terminal tab that
adopted a structured session. The shortcut hook applies the same rule.
The state plumbing (`viewMode`, `setTabViewMode`, `toggleTabViewMode`, host
mirroring, `native_chat_toggled` telemetry) was never removed, so this rewires
live actions rather than reintroducing logic.
SortableTab.tsx sat exactly at its 400-line cap, so its inline-rename state and
the window rename-request listener move to `use-sortable-tab-rename.ts` to make
room. No behavior change; its rename tests pass unmodified.
Two ratchets move for real, explained in place:
- store-subscription budget: per-pane listeners stay pinned at 17 (the folded
action bundle is still one listener); only the counterfactual pre-fold
constant grows 48 -> 49 for the added `toggleTabViewMode` key.
- hook-order parity: 204 -> 208 hooks for the four added `useCallback`s,
useMemo count unchanged at 8.
* fix(native-chat): restore bridge chat escape hatch
* test: update pane agent identity inventory
---------
Co-authored-by: Merge Sim
---
.../native-chat/NativeChatResolvedView.tsx | 1 +
.../native-chat-availability.test.ts | 36 +++++-
.../native-chat/native-chat-availability.ts | 13 +++
.../use-native-chat-context-menu.test.tsx | 110 ++++++++++++++++++
.../use-native-chat-context-menu.tsx | 22 +++-
.../use-native-chat-toggle-shortcut.ts | 15 ++-
.../src/components/tab-bar/SortableTab.tsx | 98 +++++-----------
.../tab-bar/SortableTabContextMenu.tsx | 47 +++++++-
.../native-chat-tab-agent-evidence.test.ts | 38 ++++++
.../tab-bar/native-chat-tab-agent-evidence.ts | 18 +++
.../tab-bar/tab-bar-item-surface.tsx | 37 +++++-
.../tab-bar/use-sortable-tab-rename.ts | 94 +++++++++++++++
.../TerminalContextMenu.test.tsx | 3 +
.../terminal-pane/TerminalContextMenu.tsx | 29 ++++-
.../TerminalPaneHeaderOverlay.tsx | 62 +++++++++-
.../TerminalPaneOverlayLayer.tsx | 3 +
.../terminal-pane/TerminalPaneSurface.tsx | 12 ++
.../terminal-pane-hook-order-parity.test.ts | 7 +-
...al-pane-store-subscription-budget.test.tsx | 8 +-
.../use-terminal-pane-chat-state.ts | 50 +++++++-
.../use-terminal-pane-projection.ts | 23 +++-
.../use-terminal-pane-store-actions.ts | 2 +
.../pane-agent-identity-inventory.test.ts | 2 +-
23 files changed, 637 insertions(+), 93 deletions(-)
create mode 100644 src/renderer/src/components/native-chat/use-native-chat-context-menu.test.tsx
create mode 100644 src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.test.ts
create mode 100644 src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.ts
create mode 100644 src/renderer/src/components/tab-bar/use-sortable-tab-rename.ts
diff --git a/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx b/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx
index bc170a98a2b..dc152df4c70 100644
--- a/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatResolvedView.tsx
@@ -130,6 +130,7 @@ export function NativeChatResolvedView({
})
const contextMenu = useNativeChatContextMenu({
rootRef,
+ onSwitchToTerminal,
actions: {
onPaste: pasteClipboardIntoComposer,
...(contextMenuActions ?? emptyNativeChatContextMenuActions)
diff --git a/src/renderer/src/components/native-chat/native-chat-availability.test.ts b/src/renderer/src/components/native-chat/native-chat-availability.test.ts
index a08bc476d2f..409f8c7329f 100644
--- a/src/renderer/src/components/native-chat/native-chat-availability.test.ts
+++ b/src/renderer/src/components/native-chat/native-chat-availability.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
-import { canToggleNativeChat } from './native-chat-availability'
+import { canSwitchNativeChatView, canToggleNativeChat } from './native-chat-availability'
import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability'
describe('canToggleNativeChat', () => {
@@ -225,3 +225,37 @@ describe('canToggleNativeChat', () => {
).toBe(false)
})
})
+
+describe('canSwitchNativeChatView', () => {
+ it('allows bridge chat to expose a terminal/chat switcher', () => {
+ expect(
+ canSwitchNativeChatView({
+ experimentalNativeChatEnabled: true,
+ contentType: 'terminal',
+ launchAgent: 'claude'
+ })
+ ).toBe(true)
+ })
+
+ it('keeps structured sessions free of terminal/chat switchers', () => {
+ expect(
+ canSwitchNativeChatView({
+ experimentalNativeChatEnabled: true,
+ contentType: 'terminal',
+ launchAgent: 'codex',
+ structuredSessionId: 'thread-1'
+ })
+ ).toBe(false)
+ })
+
+ it('keeps structured sessions hidden even when toggling back', () => {
+ expect(
+ canSwitchNativeChatView({
+ experimentalNativeChatEnabled: true,
+ contentType: 'terminal',
+ isChatViewMode: true,
+ structuredSessionId: 'thread-1'
+ })
+ ).toBe(false)
+ })
+})
diff --git a/src/renderer/src/components/native-chat/native-chat-availability.ts b/src/renderer/src/components/native-chat/native-chat-availability.ts
index 3cad7fc157f..1f883630f13 100644
--- a/src/renderer/src/components/native-chat/native-chat-availability.ts
+++ b/src/renderer/src/components/native-chat/native-chat-availability.ts
@@ -58,3 +58,16 @@ export function canToggleNativeChat(input: NativeChatAvailabilityInput): boolean
}
return isNativeChatSupportedAgent(agent)
}
+
+/** Whether a user-facing terminal⇄chat switcher may be offered. A structured
+ * session IS the conversation — it owns the surface with no live TUI beneath
+ * it — so only terminal-backed (bridge) chat, which renders a terminal we can
+ * return to, gets the switch. */
+export function canSwitchNativeChatView(
+ input: NativeChatAvailabilityInput & { structuredSessionId?: string | null }
+): boolean {
+ if (input.structuredSessionId) {
+ return false
+ }
+ return canToggleNativeChat(input)
+}
diff --git a/src/renderer/src/components/native-chat/use-native-chat-context-menu.test.tsx b/src/renderer/src/components/native-chat/use-native-chat-context-menu.test.tsx
new file mode 100644
index 00000000000..f4e8b3efc72
--- /dev/null
+++ b/src/renderer/src/components/native-chat/use-native-chat-context-menu.test.tsx
@@ -0,0 +1,110 @@
+/**
+ * @vitest-environment happy-dom
+ */
+import React, { createRef, type ReactNode } from 'react'
+import { renderToStaticMarkup } from 'react-dom/server'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import {
+ emptyNativeChatContextMenuActions,
+ useNativeChatContextMenu,
+ type NativeChatContextMenuActions
+} from './use-native-chat-context-menu'
+
+type ItemProps = { onSelect?: () => void; children?: ReactNode }
+
+const items = vi.hoisted(() => ({ list: [] as ItemProps[] }))
+
+vi.mock('@/components/ui/dropdown-menu', () => ({
+ DropdownMenu: ({ children }: { children?: ReactNode }) => children,
+ DropdownMenuContent: ({ children }: { children?: ReactNode }) => children,
+ DropdownMenuItem: (props: ItemProps) => {
+ items.list.push(props)
+ return props.children
+ },
+ DropdownMenuLabel: ({ children }: { children?: ReactNode }) => children,
+ DropdownMenuSeparator: () => null,
+ DropdownMenuShortcut: ({ children }: { children?: ReactNode }) => children,
+ DropdownMenuSub: ({ children }: { children?: ReactNode }) => children,
+ DropdownMenuSubContent: ({ children }: { children?: ReactNode }) => children,
+ DropdownMenuSubTrigger: ({ children }: { children?: ReactNode }) => children,
+ DropdownMenuTrigger: ({ children }: { children?: ReactNode }) => children
+}))
+
+vi.mock('lucide-react', () => {
+ const Icon = () => null
+ return {
+ Clipboard: Icon,
+ Copy: Icon,
+ GitFork: Icon,
+ Maximize2: Icon,
+ MessageSquarePlus: Icon,
+ Minimize2: Icon,
+ PanelBottomClose: Icon,
+ PanelsTopLeft: Icon,
+ PanelRightClose: Icon,
+ Pencil: Icon,
+ SquareTerminal: Icon,
+ X: Icon
+ }
+})
+
+vi.mock('@/i18n/i18n', () => ({
+ translate: (_key: string, fallback: string) => fallback
+}))
+
+function childrenText(children: ReactNode): string {
+ return React.Children.toArray(children)
+ .map((child) => {
+ if (typeof child === 'string') {
+ return child
+ }
+ return React.isValidElement<{ children?: ReactNode }>(child)
+ ? childrenText(child.props.children)
+ : ''
+ })
+ .join('')
+}
+
+function Harness({ onSwitchToTerminal }: { onSwitchToTerminal?: () => void }) {
+ const rootRef = createRef()
+ const { menu } = useNativeChatContextMenu({
+ rootRef,
+ onSwitchToTerminal,
+ actions: {
+ ...emptyNativeChatContextMenuActions,
+ onPaste: vi.fn()
+ } satisfies NativeChatContextMenuActions
+ })
+ return menu
+}
+
+describe('useNativeChatContextMenu', () => {
+ beforeEach(() => {
+ items.list = []
+ })
+
+ it('restores the bridge switch-to-terminal action when supplied', () => {
+ const onSwitchToTerminal = vi.fn()
+
+ renderToStaticMarkup( )
+
+ // Keep the assertions tied to the mocked menu item's semantic children.
+ const labels = items.list.map((candidate) => childrenText(candidate.children))
+
+ expect(labels.some((label) => label.startsWith('Switch to terminal view'))).toBe(true)
+ const item = items.list.find((candidate) =>
+ childrenText(candidate.children).startsWith('Switch to terminal view')
+ )
+ expect(item).toBeDefined()
+ item?.onSelect?.()
+ expect(onSwitchToTerminal).toHaveBeenCalledTimes(1)
+ })
+
+ it('does not render a terminal switch action without a bridge callback', () => {
+ renderToStaticMarkup( )
+
+ expect(
+ items.list.some((candidate) => childrenText(candidate.children) === 'Switch to terminal view')
+ ).toBe(false)
+ })
+})
diff --git a/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx b/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx
index 8e939401a47..a2d7dae4c93 100644
--- a/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx
+++ b/src/renderer/src/components/native-chat/use-native-chat-context-menu.tsx
@@ -17,6 +17,7 @@ import {
PanelsTopLeft,
PanelRightClose,
Pencil,
+ SquareTerminal,
X
} from 'lucide-react'
import {
@@ -28,7 +29,7 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { translate } from '@/i18n/i18n'
-import { isMacPlatform } from './native-chat-shortcut'
+import { isMacPlatform, nativeChatToggleShortcutLabel } from './native-chat-shortcut'
type NativeChatContextMenuState = {
open: boolean
@@ -38,6 +39,8 @@ type NativeChatContextMenuState = {
type UseNativeChatContextMenuArgs = {
rootRef: RefObject
+ /** Bridge-only escape hatch; structured sessions never mount this menu. */
+ onSwitchToTerminal?: () => void
actions: NativeChatContextMenuActions
}
@@ -83,7 +86,11 @@ export const emptyNativeChatContextMenuActions: Omit {}
}
-export function useNativeChatContextMenu({ rootRef, actions }: UseNativeChatContextMenuArgs): {
+export function useNativeChatContextMenu({
+ rootRef,
+ onSwitchToTerminal,
+ actions
+}: UseNativeChatContextMenuArgs): {
onContextMenuCapture: MouseEventHandler
onSelectionCapture: () => void
menu: React.JSX.Element
@@ -95,6 +102,7 @@ export function useNativeChatContextMenu({ rootRef, actions }: UseNativeChatCont
point: { x: 0, y: 0 },
selectedText: ''
})
+ const shortcutLabel = nativeChatToggleShortcutLabel(isMacPlatform())
const rememberCurrentSelection = useCallback(() => {
const selectedText = getNativeChatSelectedText(rootRef.current)
@@ -161,6 +169,16 @@ export function useNativeChatContextMenu({ rootRef, actions }: UseNativeChatCont
{translate('auto.components.terminal.pane.TerminalContextMenu.0a917b591a', 'Paste')}
+ {onSwitchToTerminal ? (
+
+
+ {translate(
+ 'components.tab.bar.SortableTabContextMenu.switchToTerminalView',
+ 'Switch to terminal view'
+ )}
+ {shortcutLabel}
+
+ ) : null}
{actions.canContinueAgentSessionInNewSession ? (
diff --git a/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts b/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts
index 90c84470cee..272e0ba81eb 100644
--- a/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts
+++ b/src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts
@@ -2,7 +2,7 @@ import { useEffect } from 'react'
import { useAppStore } from '../../store'
import type { AgentType } from '../../../../shared/agent-status-types'
import type { TerminalLayoutSnapshot } from '../../../../shared/terminal-tab-types'
-import { resolveCommittedTitleAgentType } from '@/lib/pane-agent-evidence'
+import { resolveNativeChatTabAgentEvidence } from '../tab-bar/native-chat-tab-agent-evidence'
import { canToggleNativeChat } from './native-chat-availability'
import { isNativeChatTranscriptLocalReadable } from '@/lib/native-chat-transcript-readability'
import { isMacPlatform, matchesNativeChatToggleShortcut } from './native-chat-shortcut'
@@ -58,7 +58,10 @@ export function useNativeChatToggleShortcut(worktreeId: string, isWorktreeActive
const tab = (state.unifiedTabsByWorktree[worktreeId] ?? []).find(
(candidate) => candidate.id === group.activeTabId
)
- if (!tab || tab.contentType !== 'terminal') {
+ // contentType gates out standalone structured (agent-session) tabs;
+ // structuredSessionId gates out a terminal tab that adopted one, which
+ // renders the structured surface with no TUI to switch back to.
+ if (!tab || tab.contentType !== 'terminal' || tab.structuredSessionId) {
return
}
const terminalTab = (state.tabsByWorktree[worktreeId] ?? []).find(
@@ -75,10 +78,10 @@ export function useNativeChatToggleShortcut(worktreeId: string, isWorktreeActive
terminalLayout,
agentStatusByPaneKey: state.agentStatusByPaneKey
})
- const titleFallbackAgent = tabWideFallbackSafe
- ? (resolveCommittedTitleAgentType(tab.label ?? '') ??
- (terminalTab ? resolveCommittedTitleAgentType(terminalTab.title) : null))
- : null
+ const titleFallbackAgent =
+ tabWideFallbackSafe && terminalTab
+ ? resolveNativeChatTabAgentEvidence(terminalTab, tab)
+ : null
if (
!canToggleNativeChat({
experimentalNativeChatEnabled: state.settings?.experimentalNativeChat === true,
diff --git a/src/renderer/src/components/tab-bar/SortableTab.tsx b/src/renderer/src/components/tab-bar/SortableTab.tsx
index cb5966d1e23..8e793f96773 100644
--- a/src/renderer/src/components/tab-bar/SortableTab.tsx
+++ b/src/renderer/src/components/tab-bar/SortableTab.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { X, Minimize2, Pin } from 'lucide-react'
import { stripLeadingAgentTitleDecoration } from '../../../../shared/agent-title-decoration'
@@ -17,10 +17,7 @@ import {
type DropIndicator
} from './drop-indicator'
import { preventMiddleButtonDefault } from './middle-button-default-guard'
-import {
- RENAME_TERMINAL_TAB_EVENT,
- type RenameTerminalTabDetail
-} from './terminal-tab-rename-request'
+import { useSortableTabRename } from './use-sortable-tab-rename'
import { SortableTabContextMenu } from './SortableTabContextMenu'
import { translate } from '@/i18n/i18n'
import { TAB_CONTAINER_WIDTH_CLASSES, TAB_LABEL_WIDTH_CLASSES } from './tab-width-rules'
@@ -55,6 +52,12 @@ type SortableTabProps = {
dragData: TabDragItemData
dropIndicator?: DropIndicator
includeTopTabBorder?: boolean
+ /** True when this agent terminal can switch between the terminal and native chat views; surfaces the "Switch view" context-menu item. */
+ canToggleViewMode?: boolean
+ /** True when the tab is currently showing the native chat view. */
+ isChatView?: boolean
+ /** Toggle the tab between terminal and native chat view. */
+ onToggleViewMode?: () => void
}
export const CLOSE_ALL_CONTEXT_MENUS_EVENT = 'orca-close-all-context-menus'
@@ -80,7 +83,10 @@ export default function SortableTab({
onToggleExpand,
dragData,
dropIndicator,
- includeTopTabBorder = true
+ includeTopTabBorder = true,
+ canToggleViewMode = false,
+ isChatView = false,
+ onToggleViewMode
}: SortableTabProps): React.JSX.Element {
// Why: agent-completion unread exists even with terminal-attention off; collapse both sources to one primitive so unrelated tabs don't re-render.
const hasUnreadActivity = useAppStore((s) =>
@@ -121,72 +127,23 @@ export default function SortableTab({
// Why: no transform/transition/opacity so tabs stay anchored during drag, only the insertion bar moves (see TabBar.tsx).
const [menuOpen, setMenuOpen] = useState(false)
const [menuPoint, setMenuPoint] = useState({ x: 0, y: 0 })
- const [isEditing, setIsEditing] = useState(false)
+ const {
+ isEditing,
+ renameValue,
+ setRenameValue,
+ handleRenameOpen,
+ commitRename,
+ cancelRename,
+ setRenameInputElement
+ } = useSortableTabRename({
+ tabId: tab.id,
+ title: tab.title,
+ customTitle: tab.customTitle,
+ onSetCustomTitle
+ })
// Why: a live working/needs-input state is newer than a prior-turn unread, so it owns the icon until the turn ends.
const showUnreadActivity =
hasUnreadActivity && !isEditing && !isTerminalTabActivityLive(activityStatus)
- const [renameValue, setRenameValue] = useState('')
- const renameFocusFrameRef = useRef(null)
- // Why: onBlur fires during Input unmount; mark rename resolved so it can't re-commit and overwrite discarded edits.
- const committedOrCancelledRef = useRef(false)
-
- const handleRenameOpen = useCallback(() => {
- committedOrCancelledRef.current = false
- // Why: snapshot title once; don't refresh if tab.title changes mid-edit (e.g. OSC) so the user's edits aren't overwritten.
- setRenameValue(tab.customTitle ?? tab.title)
- setIsEditing(true)
- }, [tab.customTitle, tab.title])
-
- const commitRename = useCallback(() => {
- if (committedOrCancelledRef.current) {
- return
- }
- committedOrCancelledRef.current = true
- const trimmed = renameValue.trim()
- onSetCustomTitle(tab.id, trimmed.length > 0 ? trimmed : null)
- setIsEditing(false)
- }, [renameValue, onSetCustomTitle, tab.id])
-
- const cancelRename = useCallback(() => {
- committedOrCancelledRef.current = true
- setIsEditing(false)
- }, [])
-
- const setRenameInputElement = useCallback((input: HTMLInputElement | null) => {
- if (renameFocusFrameRef.current !== null) {
- cancelAnimationFrame(renameFocusFrameRef.current)
- renameFocusFrameRef.current = null
- }
- if (!input) {
- return
- }
- // Why: defer past Radix menu teardown/focus restore; key off input mount so title updates don't re-select edited text.
- renameFocusFrameRef.current = requestAnimationFrame(() => {
- renameFocusFrameRef.current = null
- input.focus()
- input.select()
- })
- }, [])
-
- // Why the ref: keeps the listener subscribed to tab.id alone, so OSC title churn can't
- // resubscribe it mid-edit. Written from an Effect, not in render -- a render React discards
- // must not leave a stale handler behind for the next commit to fire.
- const handleRenameOpenRef = useRef(handleRenameOpen)
- useEffect(() => {
- handleRenameOpenRef.current = handleRenameOpen
- }, [handleRenameOpen])
-
- useEffect(() => {
- const onRenameRequest = (event: Event): void => {
- const detail = (event as CustomEvent).detail
- if (detail?.tabId !== tab.id) {
- return
- }
- handleRenameOpenRef.current()
- }
- window.addEventListener(RENAME_TERMINAL_TAB_EVENT, onRenameRequest)
- return () => window.removeEventListener(RENAME_TERMINAL_TAB_EVENT, onRenameRequest)
- }, [tab.id])
useEffect(() => {
const closeMenu = (): void => setMenuOpen(false)
@@ -439,6 +396,9 @@ export default function SortableTab({
onRenameOpen={handleRenameOpen}
onSetTabColor={onSetTabColor}
onTogglePin={onTogglePin}
+ canToggleViewMode={canToggleViewMode}
+ isChatView={isChatView}
+ onToggleViewMode={onToggleViewMode}
/>
>
)
diff --git a/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx b/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx
index 53b31012d39..5b6caaecb55 100644
--- a/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx
+++ b/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx
@@ -1,4 +1,14 @@
-import { PanelLeftClose, PanelRightClose, Pin, PinOff, Pencil, X, ListX } from 'lucide-react'
+import {
+ MessageSquare,
+ PanelLeftClose,
+ PanelRightClose,
+ Pin,
+ PinOff,
+ Pencil,
+ SquareTerminal,
+ X,
+ ListX
+} from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
@@ -97,6 +107,15 @@ type SortableTabContextMenuProps = {
onRenameOpen: () => void
onSetTabColor: (tabId: string, color: string | null) => void
onTogglePin: () => void
+ /** True when this tab is an agent terminal that can switch between the terminal
+ * and native chat views; gates the "Switch view" menu item. Structured
+ * sessions never qualify — they have no terminal underneath. */
+ canToggleViewMode?: boolean
+ /** True when the tab is currently showing the native chat view (drives the
+ * item's label/icon between "chat" and "terminal"). */
+ isChatView?: boolean
+ /** Toggle the tab between terminal and native chat view. */
+ onToggleViewMode?: () => void
}
export function SortableTabContextMenu({
@@ -118,7 +137,10 @@ export function SortableTabContextMenu({
onCloseToLeft,
onRenameOpen,
onSetTabColor,
- onTogglePin
+ onTogglePin,
+ canToggleViewMode = false,
+ isChatView = false,
+ onToggleViewMode
}: SortableTabContextMenuProps): React.JSX.Element {
const keybindings = useAppStore((state) => state.keybindings)
const splitRightShortcut = formatShortcutLabel('terminal.splitRight', keybindings)
@@ -147,6 +169,27 @@ export function SortableTabContextMenu({
splitRightShortcut={splitRightShortcut}
splitDownShortcut={splitDownShortcut}
/>
+ {canToggleViewMode && onToggleViewMode ? (
+ <>
+
+
+ {isChatView ? (
+
+ ) : (
+
+ )}
+ {isChatView
+ ? translate(
+ 'components.tab.bar.SortableTabContextMenu.switchToTerminalView',
+ 'Switch to terminal view'
+ )
+ : translate(
+ 'components.tab.bar.SortableTabContextMenu.switchToChatView',
+ 'Switch to chat view'
+ )}
+
+ >
+ ) : null}
{isPinned ? (
diff --git a/src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.test.ts b/src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.test.ts
new file mode 100644
index 00000000000..0ebf19e6e9b
--- /dev/null
+++ b/src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.test.ts
@@ -0,0 +1,38 @@
+import { describe, expect, it } from 'vitest'
+import { resolveNativeChatTabAgentEvidence } from './native-chat-tab-agent-evidence'
+
+describe('resolveNativeChatTabAgentEvidence', () => {
+ it('uses the retained provider identity when a generated title masks the process title', () => {
+ expect(
+ resolveNativeChatTabAgentEvidence(
+ {
+ title: 'Summarize recent commits',
+ aiVaultTitle: null
+ },
+ {
+ label: 'Summarize recent commits',
+ aiVaultTitle: {
+ agent: 'codex',
+ sessionId: 'thread-1',
+ title: 'Summarize recent commits'
+ }
+ }
+ )
+ ).toBe('codex')
+ })
+
+ it('keeps the committed process-title signal ahead of retained metadata', () => {
+ expect(
+ resolveNativeChatTabAgentEvidence(
+ {
+ title: 'Claude Code',
+ aiVaultTitle: { agent: 'codex', sessionId: 'thread-1', title: 'Old title' }
+ },
+ {
+ label: 'Old title',
+ aiVaultTitle: null
+ }
+ )
+ ).toBe('claude')
+ })
+})
diff --git a/src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.ts b/src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.ts
new file mode 100644
index 00000000000..54b97ea775b
--- /dev/null
+++ b/src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.ts
@@ -0,0 +1,18 @@
+import type { Tab } from '../../../../shared/tab-types'
+import type { TerminalTab } from '../../../../shared/terminal-tab-types'
+import type { TuiAgent } from '../../../../shared/tui-agent'
+import { resolveCommittedTitleAgentType } from '@/lib/pane-agent-evidence'
+
+/** Resolve durable tab metadata used before a live pane status arrives. */
+export function resolveNativeChatTabAgentEvidence(
+ tab: Pick,
+ unifiedTab?: Pick
+): TuiAgent | null {
+ return (
+ resolveCommittedTitleAgentType(unifiedTab?.label ?? '') ??
+ resolveCommittedTitleAgentType(tab.title) ??
+ unifiedTab?.aiVaultTitle?.agent ??
+ tab.aiVaultTitle?.agent ??
+ null
+ )
+}
diff --git a/src/renderer/src/components/tab-bar/tab-bar-item-surface.tsx b/src/renderer/src/components/tab-bar/tab-bar-item-surface.tsx
index c0af60cf6fc..24114f42a16 100644
--- a/src/renderer/src/components/tab-bar/tab-bar-item-surface.tsx
+++ b/src/renderer/src/components/tab-bar/tab-bar-item-surface.tsx
@@ -4,6 +4,8 @@ import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import type { TuiAgent } from '../../../../shared/tui-agent'
import { isAgentSessionHandleProvider } from '../../../../shared/agent-session-provider-handle'
import type { OpenFile } from '../../store/slices/editor'
+import { canSwitchNativeChatView } from '../native-chat/native-chat-availability'
+import { resolveNativeChatTabAgentEvidence } from './native-chat-tab-agent-evidence'
import SortableTab from './SortableTab'
import EditorFileTab from './EditorFileTab'
import BrowserTab from './BrowserTab'
@@ -56,7 +58,17 @@ export function renderTabBarItems({
onCloseAllFiles,
onMakePreviewFilePermanent
} = props
- const { resolvedGroupId, generatedTabTitlesEnabled, statusByRelativePath } = runtime
+ const {
+ resolvedGroupId,
+ generatedTabTitlesEnabled,
+ unifiedTabByVisibleId,
+ nativeChatEnabled,
+ tabAgentTypesByTabId,
+ nativeChatTabWideFallbackUnsafeTabsById,
+ nativeChatTranscriptIsLocalReadable,
+ toggleTabViewMode,
+ statusByRelativePath
+ } = runtime
// A selected client-hosted row covers the pane, so the tab it covers must stop looking active —
// the group's own activeTabId never moves for it, and two underlines would show at once.
@@ -89,6 +101,24 @@ export function renderTabBarItems({
...item.data,
title: resolveTerminalTabTitle(item.data, generatedTabTitlesEnabled, item.data.title)
}
+ const unifiedTabForItem = unifiedTabByVisibleId.get(item.id)
+ // Carry the agent *identity* (not just "an agent exists") so the native-chat gate can reject agents like Grok.
+ const resolvedAgent = resolveNativeChatTabAgentEvidence(terminalTab, unifiedTabForItem)
+ // Key the live-agent lookup by the backing terminal tab id: agent-status pane keys use it, not the unified tab id.
+ const detectedAgent = tabAgentTypesByTabId[terminalTab.id] ?? null
+ const tabWideFallbackSafe = nativeChatTabWideFallbackUnsafeTabsById[terminalTab.id] !== true
+ const canToggleViewMode =
+ unifiedTabForItem !== undefined &&
+ canSwitchNativeChatView({
+ experimentalNativeChatEnabled: nativeChatEnabled,
+ contentType: 'terminal',
+ launchAgent: tabWideFallbackSafe ? terminalTab.launchAgent : null,
+ detectedAgent,
+ resolvedAgent: tabWideFallbackSafe ? resolvedAgent : null,
+ nativeChatTranscriptIsLocalReadable,
+ isChatViewMode: unifiedTabForItem.viewMode === 'chat',
+ structuredSessionId: unifiedTabForItem.structuredSessionId ?? null
+ })
return (
toggleTabViewMode(unifiedTabForItem.id) : undefined
+ }
hasTabsToRight={index < items.length - 1}
hasTabsToLeft={index > 0}
isActive={
diff --git a/src/renderer/src/components/tab-bar/use-sortable-tab-rename.ts b/src/renderer/src/components/tab-bar/use-sortable-tab-rename.ts
new file mode 100644
index 00000000000..05b85d226bb
--- /dev/null
+++ b/src/renderer/src/components/tab-bar/use-sortable-tab-rename.ts
@@ -0,0 +1,94 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+import {
+ RENAME_TERMINAL_TAB_EVENT,
+ type RenameTerminalTabDetail
+} from './terminal-tab-rename-request'
+
+/** Inline tab-title rename: snapshots the title on open so mid-edit OSC churn
+ * cannot overwrite the user's text, commits at most once, and answers the
+ * window rename request addressed to this tab. */
+export function useSortableTabRename({
+ tabId,
+ title,
+ customTitle,
+ onSetCustomTitle
+}: {
+ tabId: string
+ title: string
+ customTitle?: string | null
+ onSetCustomTitle: (tabId: string, title: string | null) => void
+}) {
+ const [isEditing, setIsEditing] = useState(false)
+ const [renameValue, setRenameValue] = useState('')
+ const renameFocusFrameRef = useRef(null)
+ // Why: onBlur fires during Input unmount; mark rename resolved so it can't re-commit and overwrite discarded edits.
+ const committedOrCancelledRef = useRef(false)
+
+ const handleRenameOpen = useCallback(() => {
+ committedOrCancelledRef.current = false
+ // Why: snapshot title once; don't refresh if tab.title changes mid-edit (e.g. OSC) so the user's edits aren't overwritten.
+ setRenameValue(customTitle ?? title)
+ setIsEditing(true)
+ }, [customTitle, title])
+
+ const commitRename = useCallback(() => {
+ if (committedOrCancelledRef.current) {
+ return
+ }
+ committedOrCancelledRef.current = true
+ const trimmed = renameValue.trim()
+ onSetCustomTitle(tabId, trimmed.length > 0 ? trimmed : null)
+ setIsEditing(false)
+ }, [renameValue, onSetCustomTitle, tabId])
+
+ const cancelRename = useCallback(() => {
+ committedOrCancelledRef.current = true
+ setIsEditing(false)
+ }, [])
+
+ const setRenameInputElement = useCallback((input: HTMLInputElement | null) => {
+ if (renameFocusFrameRef.current !== null) {
+ cancelAnimationFrame(renameFocusFrameRef.current)
+ renameFocusFrameRef.current = null
+ }
+ if (!input) {
+ return
+ }
+ // Why: defer past Radix menu teardown/focus restore; key off input mount so title updates don't re-select edited text.
+ renameFocusFrameRef.current = requestAnimationFrame(() => {
+ renameFocusFrameRef.current = null
+ input.focus()
+ input.select()
+ })
+ }, [])
+
+ // Why the ref: keeps the listener subscribed to tabId alone, so OSC title churn can't
+ // resubscribe it mid-edit. Written from an Effect, not in render -- a render React discards
+ // must not leave a stale handler behind for the next commit to fire.
+ const handleRenameOpenRef = useRef(handleRenameOpen)
+ useEffect(() => {
+ handleRenameOpenRef.current = handleRenameOpen
+ }, [handleRenameOpen])
+
+ useEffect(() => {
+ const onRenameRequest = (event: Event): void => {
+ const detail = (event as CustomEvent).detail
+ if (detail?.tabId !== tabId) {
+ return
+ }
+ handleRenameOpenRef.current()
+ }
+ window.addEventListener(RENAME_TERMINAL_TAB_EVENT, onRenameRequest)
+ return () => window.removeEventListener(RENAME_TERMINAL_TAB_EVENT, onRenameRequest)
+ }, [tabId])
+
+ return {
+ isEditing,
+ renameValue,
+ setRenameValue,
+ handleRenameOpen,
+ commitRename,
+ cancelRename,
+ setRenameInputElement
+ }
+}
diff --git a/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx b/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx
index d8dc54c954b..58a39f28a50 100644
--- a/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx
@@ -77,6 +77,9 @@ function renderMenu(overrides: Record = {}): string {
canContinueAgentSessionInNewSession: false,
onContinueAgentSessionInNewSession: vi.fn(),
onForkAgentSession: vi.fn(),
+ canToggleNativeChat: false,
+ isNativeChatView: false,
+ onToggleNativeChat: vi.fn(),
onCopyAgentSessionContext: vi.fn(),
quickCommandHosts: [
{ hostId: 'local' as const, label: 'Local Linux', repoCommands: [], globalCommands: [] }
diff --git a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx
index 8311fb3df9d..2cc76cd7164 100644
--- a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx
@@ -6,11 +6,13 @@ import {
Eraser,
GitFork,
Maximize2,
+ MessageSquare,
Minimize2,
PanelBottomClose,
PanelsTopLeft,
PanelRightClose,
Pencil,
+ SquareTerminal,
TextSelect,
X
} from 'lucide-react'
@@ -28,6 +30,7 @@ import type { ExecutionHostId } from '../../../../shared/execution-host'
import { formatPrimaryShortcutLabel } from '@/hooks/useShortcutLabel'
import type { KeybindingOverrides } from '../../../../shared/keybindings'
import { translate } from '@/i18n/i18n'
+import { isMacPlatform, nativeChatToggleShortcutLabel } from '../native-chat/native-chat-shortcut'
import { AgentSessionContinuationMenuItem } from './AgentSessionContinuationMenuItem'
import type { TerminalQuickCommandMenuHost } from '@/hooks/use-terminal-quick-command-hosts'
import { TerminalQuickCommandsSubmenu } from './TerminalQuickCommandsSubmenu'
@@ -53,6 +56,11 @@ type TerminalContextMenuProps = {
canContinueAgentSessionInNewSession: boolean
onContinueAgentSessionInNewSession: () => void
onForkAgentSession: () => void
+ /** True when this pane may switch between the terminal and native chat views.
+ * Structured sessions are excluded — they have no terminal underneath. */
+ canToggleNativeChat: boolean
+ isNativeChatView: boolean
+ onToggleNativeChat: () => void
onCopyAgentSessionContext: () => void
quickCommandHosts: TerminalQuickCommandMenuHost[]
quickCommandHostLoadFailed: boolean
@@ -91,6 +99,9 @@ export default function TerminalContextMenu({
canContinueAgentSessionInNewSession,
onContinueAgentSessionInNewSession,
onForkAgentSession,
+ canToggleNativeChat,
+ isNativeChatView,
+ onToggleNativeChat,
onCopyAgentSessionContext,
quickCommandHosts,
quickCommandHostLoadFailed,
@@ -119,7 +130,8 @@ export default function TerminalContextMenu({
expand: formatPrimaryShortcutLabel('terminal.expandPane', keybindings),
setTitle: formatPrimaryShortcutLabel('terminal.setTitle', keybindings),
clearPaneTitle: formatPrimaryShortcutLabel('terminal.clearPaneTitle', keybindings),
- close: formatPrimaryShortcutLabel('terminal.closePane', keybindings)
+ close: formatPrimaryShortcutLabel('terminal.closePane', keybindings),
+ nativeChat: nativeChatToggleShortcutLabel(isMacPlatform())
}),
[keybindings]
)
@@ -209,6 +221,21 @@ export default function TerminalContextMenu({
'Copy Context'
)}
+ {canToggleNativeChat ? (
+
+ {isNativeChatView ? : }
+ {isNativeChatView
+ ? translate(
+ 'components.tab.bar.SortableTabContextMenu.switchToTerminalView',
+ 'Switch to terminal view'
+ )
+ : translate(
+ 'components.tab.bar.SortableTabContextMenu.switchToChatView',
+ 'Switch to chat view'
+ )}
+ {shortcuts.nativeChat}
+
+ ) : null}
diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx
index 6b246701c43..6256bc64c07 100644
--- a/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx
@@ -1,5 +1,11 @@
import type { CSSProperties, RefObject } from 'react'
-import { MessageSquarePlus, SquareSplitVertical, X } from 'lucide-react'
+import {
+ MessageSquare,
+ MessageSquarePlus,
+ SquareSplitVertical,
+ SquareTerminal,
+ X
+} from 'lucide-react'
import type { ManagedPane, PaneManager } from '@/lib/pane-manager/pane-manager'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
@@ -36,6 +42,16 @@ type TerminalPaneHeaderOverlayProps = {
hiddenStartupStyle: CSSProperties
managerRef: RefObject
paneTransportsRef: RefObject>
+ /** When true, this pane can switch between the terminal and the native chat
+ * view; renders a chat/terminal toggle as the first button in the pane header
+ * actions row (beside split/close). The caller gates it to the active pane to
+ * avoid duplicating it across splits, and to bridge chat only — a structured
+ * session has no terminal underneath to switch to. */
+ canToggleNativeChat?: boolean
+ /** True when the active pane is currently showing the native chat view. */
+ isChatViewMode?: boolean
+ /** Flip the active pane between the terminal and the native chat view. */
+ onToggleNativeChat?: () => void
canContinueAgentSessionInNewSession?: boolean
onContinueAgentSessionInNewSession?: (pane: ManagedPane) => void
onSplitPane: (pane: ManagedPane, direction: 'vertical' | 'horizontal') => void
@@ -71,6 +87,9 @@ export default function TerminalPaneHeaderOverlay({
hiddenStartupStyle,
managerRef,
paneTransportsRef,
+ canToggleNativeChat,
+ isChatViewMode,
+ onToggleNativeChat,
canContinueAgentSessionInNewSession,
onContinueAgentSessionInNewSession,
onSplitPane,
@@ -255,6 +274,47 @@ export default function TerminalPaneHeaderOverlay({
) : null}
+ {canToggleNativeChat && isActivePane ? (
+
+
+ {
+ event.stopPropagation()
+ onToggleNativeChat?.()
+ }}
+ >
+ {isChatViewMode ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isChatViewMode
+ ? translate('components.native-chat.toggle.showTerminal', 'Show terminal')
+ : translate('components.native-chat.toggle.showChat', 'Show chat view')}
+
+
+ ) : null}
{showAlwaysOnHeaders && showSplitButton ? (
diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.tsx
index 79d0f42a14e..2cb20783f53 100644
--- a/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.tsx
@@ -8,6 +8,7 @@ import {
type ActivityTerminalPortalTarget
} from '../activity/activity-terminal-portal'
import { shouldMountBackgroundWorktreeTab } from '../terminal/background-terminal-worktree-mount'
+import { useNativeChatToggleShortcut } from '../native-chat/use-native-chat-toggle-shortcut'
import { TerminalOverlaySlot } from './TerminalOverlaySlot'
import { useTerminalTabColdParking } from './use-terminal-tab-cold-parking'
@@ -59,6 +60,8 @@ const TerminalPaneOverlayLayer = memo(function TerminalPaneOverlayLayer({
const setActiveWorktree = useAppStore((state) => state.setActiveWorktree)
const reconcileWorktreeTabModel = useAppStore((state) => state.reconcileWorktreeTabModel)
+ useNativeChatToggleShortcut(worktreeId, isWorktreeActive)
+
const leaveWorktreeIfEmpty = useCallback(() => {
const state = useAppStore.getState()
if (state.activeWorktreeId !== worktreeId) {
diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx
index fc33cab3f18..1773aa48d20 100644
--- a/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx
@@ -32,6 +32,8 @@ export function TerminalPaneSurface({
const {
activePane,
activePaneCanContinueInNewSession,
+ activePaneCanToggleChat,
+ activePaneIsChatLeaf,
activatePaneTitleInteraction,
agentSessionContinuation,
agentSessionFork,
@@ -39,6 +41,8 @@ export function TerminalPaneSurface({
closeTerminalLinkActions,
contextMenu,
contextMenuCanContinueInNewSession,
+ contextMenuCanToggleChat,
+ contextMenuIsChatView,
cwd,
daemonActions,
dismissTerminalError,
@@ -46,6 +50,7 @@ export function TerminalPaneSurface({
expandedPaneId,
handleCancelClose,
handleConfirmClose,
+ handleContextMenuToggleNativeChat,
handlePrimarySelectionAuxClick,
handlePrimarySelectionMiddleMouseDown,
handleRemoveTitle,
@@ -54,6 +59,7 @@ export function TerminalPaneSurface({
handleRenameSubmit,
handleRequestClosePane,
handleStartRename,
+ handleToggleNativeChat,
hiddenStartupStyle,
isActive,
keybindings,
@@ -231,6 +237,9 @@ export function TerminalPaneSurface({
canContinueAgentSessionInNewSession={contextMenuCanContinueInNewSession}
onContinueAgentSessionInNewSession={contextMenu.onContinueAgentSessionInNewSession}
onForkAgentSession={() => void contextMenu.onForkAgentSession()}
+ canToggleNativeChat={contextMenuCanToggleChat}
+ isNativeChatView={contextMenuIsChatView}
+ onToggleNativeChat={handleContextMenuToggleNativeChat}
onCopyAgentSessionContext={() => void contextMenu.onCopyAgentSessionContext()}
quickCommandHosts={visibleQuickCommandHosts}
quickCommandHostLoadFailed={quickCommandHostLoadFailed}
@@ -303,6 +312,9 @@ export function TerminalPaneSurface({
hiddenStartupStyle={hiddenStartupStyle}
managerRef={managerRef}
paneTransportsRef={paneTransportsRef}
+ canToggleNativeChat={activePaneCanToggleChat}
+ isChatViewMode={activePaneIsChatLeaf}
+ onToggleNativeChat={handleToggleNativeChat}
canContinueAgentSessionInNewSession={activePaneCanContinueInNewSession}
onContinueAgentSessionInNewSession={(pane) =>
contextMenu.runForPane(pane.id, contextMenu.onContinueAgentSessionInNewSession)
diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-hook-order-parity.test.ts b/src/renderer/src/components/terminal-pane/terminal-pane-hook-order-parity.test.ts
index 6151594ff83..a6b96168047 100644
--- a/src/renderer/src/components/terminal-pane/terminal-pane-hook-order-parity.test.ts
+++ b/src/renderer/src/components/terminal-pane/terminal-pane-hook-order-parity.test.ts
@@ -11,8 +11,11 @@ const TERMINAL_PANE_HOOK_SOURCE_PATTERN =
// split-cwd changes; the pane session-ID projection added one render hook (230 hooks).
// Then 27 stable-action `useAppStore` subscriptions folded into four
// `useTerminalPaneStoreActions()` calls, each one `useMemo` (204 hooks, 8 useMemo).
+// Restoring the terminal/chat switcher added four `useCallback`s -- three in
+// chat-state (can-toggle, toggle-for-leaf, toggle-active) and the context-menu
+// toggle in projection (208 hooks, still 8 useMemo).
const PRE_REFACTOR_HOOK_ORDER_SHA256 =
- 'b541d26ff66a1db0b7150ced2a4340c5cf025aba79a651a2ad8d2fa68d403ae3'
+ '983ad067c9feca82c5435eb1b865674344489c368ec2007dc7bb40c81aef037c'
const sourceFiles = readdirSync(__dirname)
.filter((name) => TERMINAL_PANE_HOOK_SOURCE_PATTERN.test(name))
@@ -77,7 +80,7 @@ function readFlattenedHookOrder(): string[] {
describe('TerminalPane refactor hook parity', () => {
it('preserves the recursively flattened render hook order', () => {
const hooks = readFlattenedHookOrder()
- expect(hooks).toHaveLength(204)
+ expect(hooks).toHaveLength(208)
expect(hooks.filter((hook) => hook === 'useMemo')).toHaveLength(8)
expect(createHash('sha256').update(hooks.join('\n')).digest('hex')).toBe(
PRE_REFACTOR_HOOK_ORDER_SHA256
diff --git a/src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx b/src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx
index 9562c46c772..2418a5da6d3 100644
--- a/src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx
+++ b/src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx
@@ -4,8 +4,8 @@
* synchronously on every publication, so the per-pane subscription count is a
* direct multiplier on agent-status burn (docs/reference/renderer-agent-status-performance.md).
*
- * On `main` one mounted pane opened 48 listeners; 31 of them selected values that
- * can never change — 27 store actions and 4 duplicate reads of one unified tab.
+ * On `main` one mounted pane opened 49 listeners; 32 of them selected values that
+ * can never change — 28 store actions and 4 duplicate reads of one unified tab.
*/
import { act, createRef, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
@@ -27,7 +27,7 @@ import {
*/
const TERMINAL_PANE_LISTENER_BUDGET = 17
/** What the same mount cost before the stable-action and unified-tab folds. */
-const PRE_FOLD_LISTENERS_PER_PANE = 48
+const PRE_FOLD_LISTENERS_PER_PANE = 49
const originalState = useAppStore.getState()
@@ -103,7 +103,7 @@ describe('TerminalPane store subscription budget', () => {
expect(perPane).toBe(TERMINAL_PANE_LISTENER_BUDGET)
expect(perPane).toBeLessThan(PRE_FOLD_LISTENERS_PER_PANE)
- // 27 stable actions plus four duplicate unified-tab reads.
+ // 28 stable actions plus four duplicate unified-tab reads.
expect(PRE_FOLD_LISTENERS_PER_PANE - perPane).toBe(TERMINAL_PANE_STORE_ACTION_KEYS.length + 4)
unmount()
diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts
index 92bc444e299..2fd75f3f093 100644
--- a/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts
+++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts
@@ -37,7 +37,8 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController
setTabCanExpandPane,
setTabPaneExpanded,
setTabViewMode,
- suppressPtyExit
+ suppressPtyExit,
+ toggleTabViewMode
} = useTerminalPaneStoreActions()
const pendingCodexPaneRestartIds = useAppStore((store) => store.pendingCodexPaneRestartIds)
// Why one selector: five separate subscriptions each re-read the same unified
@@ -207,6 +208,50 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController
onAgentExitedRef.current = handleConfirmedAgentExit
// oxlint-disable-next-line react-hooks/exhaustive-deps -- Preserve the pre-split dependency contract.
}, [handleConfirmedAgentExit])
+ const canToggleChatForLeaf = useCallback(
+ (leafId: string | null): boolean => {
+ // A structured session renders its own transcript with no TUI beneath it,
+ // so the switcher stays off for it while bridge chat keeps it.
+ if (structuredSessionId) {
+ return false
+ }
+ // Scope the "always allow toggling back" rule to the leaf showing chat; must not make an unsupported sibling look eligible.
+ const isChatViewForLeaf = effectiveChatViewMode && leafId !== null && chatLeafId === leafId
+ return (nativeChatEnabled && isChatViewForLeaf) || isChatEligibleForLeaf(leafId)
+ },
+ [
+ chatLeafId,
+ effectiveChatViewMode,
+ isChatEligibleForLeaf,
+ nativeChatEnabled,
+ structuredSessionId
+ ]
+ )
+ const toggleNativeChatForLeaf = useCallback(
+ (leafId: string) => {
+ if (!unifiedTabId) {
+ return
+ }
+ if (effectiveChatViewMode && chatLeafId === leafId) {
+ setChatLeafId(null)
+ toggleTabViewMode(unifiedTabId)
+ return
+ }
+ setChatLeafId(leafId)
+ if (!effectiveChatViewMode) {
+ toggleTabViewMode(unifiedTabId)
+ }
+ },
+ [chatLeafId, effectiveChatViewMode, setChatLeafId, toggleTabViewMode, unifiedTabId]
+ )
+ const handleToggleNativeChat = useCallback(() => {
+ const activeLeafId = managerRef.current?.getActivePane()?.leafId ?? null
+ if (!activeLeafId) {
+ return
+ }
+ toggleNativeChatForLeaf(activeLeafId)
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- managerRef is a stable ref container.
+ }, [toggleNativeChatForLeaf])
const switchNativeChatToTerminal = useCallback(() => {
if (chatLeafId && unifiedTabId) {
setChatLeafId(null)
@@ -249,6 +294,9 @@ export function useTerminalPaneChatState(controller: TerminalPaneTitleController
getTabWideAgentHintLeafIdRef,
resolveTitleAgentForLeaf,
isChatEligibleForLeaf,
+ canToggleChatForLeaf,
+ toggleNativeChatForLeaf,
+ handleToggleNativeChat,
applyNativeChatLeafRoute,
switchNativeChatToTerminal,
readNativeChatTerminalScreen
diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts
index ea179b6925e..7ef23834873 100644
--- a/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts
+++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-projection.ts
@@ -1,4 +1,4 @@
-import { useEffect, useMemo } from 'react'
+import { useCallback, useEffect, useMemo } from 'react'
import type { CSSProperties } from 'react'
import {
DEFAULT_TERMINAL_DIVIDER_DARK,
@@ -23,10 +23,13 @@ import { resolvePaneAgentSessionId } from './pane-agent-session-id'
export function useTerminalPaneProjection(controller: TerminalPaneMobileController) {
const {
applyNativeChatLeafRoute,
+ canToggleChatForLeaf,
chatLeafId,
chatPaneDispatchStatus,
contextMenu,
contextMenuLeafId,
+ effectiveChatViewMode,
+ getContextMenuLeafId,
getNativeChatLeafIds,
getTabWideAgentHintLeafId,
isActive,
@@ -34,6 +37,7 @@ export function useTerminalPaneProjection(controller: TerminalPaneMobileControll
isChatViewMode,
isVisible,
managerRef,
+ toggleNativeChatForLeaf,
paneTitles,
paneTransportsRef,
resolveTitleAgentForLeaf,
@@ -177,6 +181,17 @@ export function useTerminalPaneProjection(controller: TerminalPaneMobileControll
const contextMenuCanContinueInNewSession = canContinueAgentSessionInNewSession(
resolveAgentForLeaf(contextMenuLeafId)
)
+ // Each switcher gates on its own leaf (header=active, menu=opened-over), so mixed splits show it only where chat can render.
+ const activePaneCanToggleChat = canToggleChatForLeaf(activePane?.leafId ?? null)
+ const contextMenuCanToggleChat = canToggleChatForLeaf(contextMenuLeafId)
+ const contextMenuIsChatView = effectiveChatViewMode && contextMenuLeafId === chatLeafId
+ const handleContextMenuToggleNativeChat = useCallback(() => {
+ const leafId = getContextMenuLeafId()
+ if (!leafId) {
+ return
+ }
+ toggleNativeChatForLeaf(leafId)
+ }, [getContextMenuLeafId, toggleNativeChatForLeaf])
return {
effectiveAppearance,
terminalBackground,
@@ -204,7 +219,11 @@ export function useTerminalPaneProjection(controller: TerminalPaneMobileControll
activePaneIsChatLeaf,
resolveAgentForLeaf,
activePaneCanContinueInNewSession,
- contextMenuCanContinueInNewSession
+ contextMenuCanContinueInNewSession,
+ activePaneCanToggleChat,
+ contextMenuCanToggleChat,
+ contextMenuIsChatView,
+ handleContextMenuToggleNativeChat
}
}
diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-store-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-store-actions.ts
index 5eb20c1559d..02c58e96049 100644
--- a/src/renderer/src/components/terminal-pane/use-terminal-pane-store-actions.ts
+++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-store-actions.ts
@@ -37,6 +37,7 @@ export function useTerminalPaneStoreActions() {
setTabLayout: state.setTabLayout,
setTabPaneExpanded: state.setTabPaneExpanded,
setTabViewMode: state.setTabViewMode,
+ toggleTabViewMode: state.toggleTabViewMode,
suppressPtyExit: state.suppressPtyExit,
updateSettings: state.updateSettings,
updateTabPtyId: state.updateTabPtyId,
@@ -72,6 +73,7 @@ export const TERMINAL_PANE_STORE_ACTION_KEYS = [
'setTabLayout',
'setTabPaneExpanded',
'setTabViewMode',
+ 'toggleTabViewMode',
'suppressPtyExit',
'updateSettings',
'updateTabPtyId',
diff --git a/src/shared/pane-agent-identity-inventory.test.ts b/src/shared/pane-agent-identity-inventory.test.ts
index 739ec6987f8..6871a38eb6d 100644
--- a/src/shared/pane-agent-identity-inventory.test.ts
+++ b/src/shared/pane-agent-identity-inventory.test.ts
@@ -161,7 +161,7 @@ const INVENTORY: readonly InventoryGroup[] = [
helper: 'resolveCommittedTitleAgentType',
classification: 'action-consumer',
paths: [
- ['src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts', 3],
+ ['src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.ts', 3],
['src/renderer/src/components/terminal-pane/pty-connection/connect-pane-pty.ts', 2],
['src/renderer/src/components/terminal-pane/terminal-ctrl-enter.ts', 2],
['src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.ts', 2],
From e42c60e8a399831263dd8628ecd085ef34b2c9eb Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Thu, 3 Sep 2026 16:47:32 -0700
Subject: [PATCH 06/43] fix(ssh): resolve a pane's binding from the target
partition, not the stale local copy (#18546)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
One SSH pane accumulated one extra reattachable lease per relay restart (2, 3, 4,
5, 6 across five), and every one of them costs a `pty.attach` round trip on every
later connect, forever. Nothing prunes `sshRemotePtyLeases`, so the fan-out only
grows.
`supersedeSiblingLeasesForPane` is fenced on the PTY the pane is durably bound to,
and `durablyBoundPtyIdForPane` read `state.workspaceSession` (local) before
`workspaceSessionsByHostId['ssh:']`. But `persistPtyBinding(binding, hostId)`
updates ONLY the host partition:
AFTER-PERSIST local= ssh:t@@pty2:old:1 host= ssh:t@@pty2:new:1
So for the length of a reconnect the local copy still names the predecessor, the
fence resolved to it, supersession took an already-`expired` lease as its winner,
and returned having marked nothing. Both partitions agree again once the renderer
republishes its layout, which is why the settled store looks consistent and hid
this.
Read both partitions as an ordered list, target's own first, and test the fence by
membership rather than by equality with whichever was read first. Pick the winner
preferring a lease this client still has a route to, since the stale partition
names an expired one. Never retire a lease that is both bound and live, so a
partition disagreement can't strand a running remote process.
Superseded predecessors stay `expired` and are never `terminated`: losing a lease
is not evidence the shell died (docs/reference/ssh-execution-boundary.md). A pane
with no binding is skipped rather than pruned, so a genuine orphan stays askable.
Also re-runs supersession from the binding side after each spawn commit's binding
write, so the lease/binding order at a call site no longer decides, and reconciles
every pane for a target immediately before `reattachKnownPtys` reads the set it
feeds to `pty.attach` — that repairs stores which already accumulated these rows.
The guard suite could not catch this: every assertion bound the pane BEFORE
upserting the lease, an order no caller uses. Rewritten to the spawn commits' real
order (lease, then binding, then the binding-side trigger); it fails 8 assertions
without this change. Added a suite that drives the real `persistPtyIpcSpawnCommit`
rather than the store primitives, including the exact stale-partition state written
by production's own binding writer.
Verified on the Docker SSH lane: five `relay.js` SIGKILLs with recovery between
each, reattachable leases flat at one per pane.
Note: this bounds the reattach SET, not the store. `sshRemotePtyLeases` still has
no cap or TTL and rows still accumulate; pruning is left alone deliberately, since
an `expired` row without `supersededBy` is a genuine orphan and must not be dropped
on age.
---
.../pty-controller-ownership-routing.test.ts | 1 +
.../pty-daemon-spawn-session-identity.test.ts | 1 +
.../pty-pane-reservation-settlement.test.ts | 2 +
...ty-runtime-ssh-binding-persistence.test.ts | 5 +
.../pty-serializer-settlement-mapping.test.ts | 2 +
...pty-session-liveness-and-ownership.test.ts | 1 +
src/main/ipc/pty/ipc/spawn-commit-persist.ts | 7 +
...spawn-commit-ssh-lease-cardinality.test.ts | 289 ++++++++++++++++++
src/main/ipc/pty/pane/ssh-pane-lease-claim.ts | 44 +++
src/main/ipc/pty/runtime/spawn-commit.ts | 27 +-
src/main/ipc/ssh-ipc-test-harness.ts | 2 +
.../ssh-pty-lease-operations.ts | 88 +-----
.../ssh-pty-pane-supersession.ts | 201 ++++++++++++
.../ssh-lease-recovery-operations.ts | 26 ++
.../ssh-reattach-pane-cardinality.test.ts | 60 ++--
.../ssh/ssh-orphan-relay-pty-sweep.test.ts | 3 +-
...ay-session-agent-hooks.integration.test.ts | 1 +
.../ssh-relay-session-terminal-error.test.ts | 1 +
.../ssh/ssh-relay-session-test-fixtures.ts | 1 +
src/main/ssh/ssh-relay-session.ts | 6 +
...ssh-docker-transport-drop-recovery.spec.ts | 146 ++++++++-
21 files changed, 777 insertions(+), 137 deletions(-)
create mode 100644 src/main/ipc/pty/ipc/spawn-commit-ssh-lease-cardinality.test.ts
create mode 100644 src/main/ipc/pty/pane/ssh-pane-lease-claim.ts
create mode 100644 src/main/persistence/leasing-ssh-ptys/ssh-pty-pane-supersession.ts
diff --git a/src/main/ipc/pty-controller-ownership-routing.test.ts b/src/main/ipc/pty-controller-ownership-routing.test.ts
index 0d236e63cb2..2101d17f26b 100644
--- a/src/main/ipc/pty-controller-ownership-routing.test.ts
+++ b/src/main/ipc/pty-controller-ownership-routing.test.ts
@@ -324,6 +324,7 @@ describe('registerPtyHandlers', () => {
}
const store = {
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
persistPtyBinding: vi.fn(),
removeSshRemotePtyLease: vi.fn(),
markSshRemotePtyLease: vi.fn(),
diff --git a/src/main/ipc/pty-daemon-spawn-session-identity.test.ts b/src/main/ipc/pty-daemon-spawn-session-identity.test.ts
index 90ea56049fa..b1ead181530 100644
--- a/src/main/ipc/pty-daemon-spawn-session-identity.test.ts
+++ b/src/main/ipc/pty-daemon-spawn-session-identity.test.ts
@@ -345,6 +345,7 @@ describe('registerPtyHandlers', () => {
)
const store = {
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
persistPtyBinding: vi.fn()
}
registerSshPtyProvider('ssh-1', {
diff --git a/src/main/ipc/pty-pane-reservation-settlement.test.ts b/src/main/ipc/pty-pane-reservation-settlement.test.ts
index 64a4441f376..06e8679ace4 100644
--- a/src/main/ipc/pty-pane-reservation-settlement.test.ts
+++ b/src/main/ipc/pty-pane-reservation-settlement.test.ts
@@ -124,6 +124,7 @@ describe('registerPtyHandlers', () => {
flushOrThrow: vi.fn(),
persistPtyBinding: vi.fn(),
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
removeSshRemotePtyLease: vi.fn(),
markSshRemotePtyLease: vi.fn(),
clearSshRemotePtyKillIntent: vi.fn()
@@ -482,6 +483,7 @@ describe('registerPtyHandlers', () => {
} as never)
const store = {
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
persistPtyBinding: vi.fn(),
removeSshRemotePtyLease: vi.fn(),
markSshRemotePtyLease: vi.fn(),
diff --git a/src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts b/src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts
index 3fd1f89b11d..d1078477d12 100644
--- a/src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts
+++ b/src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts
@@ -154,6 +154,7 @@ describe('registerPtyHandlers', () => {
} as never)
const store = {
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
persistPtyBinding: vi.fn(),
removeSshRemotePtyLease: vi.fn(),
markSshRemotePtyLease: vi.fn(),
@@ -260,6 +261,7 @@ describe('registerPtyHandlers', () => {
} as never)
const store = {
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
persistPtyBinding: vi.fn()
}
let controller: RuntimeSpawnController | null = null
@@ -370,6 +372,7 @@ describe('registerPtyHandlers', () => {
} as never)
const store = {
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
persistPtyBinding: vi.fn(() => {
throw new Error('disk full')
}),
@@ -471,6 +474,7 @@ describe('registerPtyHandlers', () => {
} as never)
const store = {
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
persistPtyBinding: vi.fn(),
removeSshRemotePtyLease: vi.fn(),
markSshRemotePtyLease: vi.fn(),
@@ -577,6 +581,7 @@ describe('registerPtyHandlers', () => {
} as never)
const store = {
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
persistPtyBinding: vi.fn(),
removeSshRemotePtyLease: vi.fn(),
markSshRemotePtyLease: vi.fn(),
diff --git a/src/main/ipc/pty-serializer-settlement-mapping.test.ts b/src/main/ipc/pty-serializer-settlement-mapping.test.ts
index 4af42f1dbd5..53755214238 100644
--- a/src/main/ipc/pty-serializer-settlement-mapping.test.ts
+++ b/src/main/ipc/pty-serializer-settlement-mapping.test.ts
@@ -108,6 +108,7 @@ describe('registerPtyHandlers', () => {
} as never)
const store = {
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
persistPtyBinding: vi.fn(),
removeSshRemotePtyLease: vi.fn(),
markSshRemotePtyLease: vi.fn(),
@@ -212,6 +213,7 @@ describe('registerPtyHandlers', () => {
} as never)
const store = {
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
persistPtyBinding: vi.fn(() => {
throw new Error('disk full')
}),
diff --git a/src/main/ipc/pty-session-liveness-and-ownership.test.ts b/src/main/ipc/pty-session-liveness-and-ownership.test.ts
index fb88fc4b6cb..574e83c1220 100644
--- a/src/main/ipc/pty-session-liveness-and-ownership.test.ts
+++ b/src/main/ipc/pty-session-liveness-and-ownership.test.ts
@@ -386,6 +386,7 @@ describe('registerPtyHandlers', () => {
it('ignores fire-and-forget IPC for detached SSH PTYs without a provider', async () => {
const store = {
upsertSshRemotePtyLease: vi.fn(),
+ supersedeSshRemotePtyLeasesForBoundPane: vi.fn(),
persistPtyBinding: vi.fn(),
markSshRemotePtyLease: vi.fn(),
clearSshRemotePtyKillIntent: vi.fn()
diff --git a/src/main/ipc/pty/ipc/spawn-commit-persist.ts b/src/main/ipc/pty/ipc/spawn-commit-persist.ts
index be9cb31394a..540ada9397d 100644
--- a/src/main/ipc/pty/ipc/spawn-commit-persist.ts
+++ b/src/main/ipc/pty/ipc/spawn-commit-persist.ts
@@ -134,6 +134,13 @@ export async function persistPtyIpcSpawnCommit(ctx: PtyIpcSpawnState): Promise<{
})
}
}
+ // Why here and not at the upsert: this path leases before it binds, so supersession fenced on the
+ // pane's binding still named the predecessor and bailed on every reconnect — one more reattachable
+ // lease, and one more `pty.attach`, per reconnect forever. Runs after whichever binding write this
+ // commit made, so the lease/binding order no longer decides.
+ if (ctx.deps.store && args.connectionId && ctx.validatedLeafId !== null) {
+ ctx.deps.store.supersedeSshRemotePtyLeasesForBoundPane(args.connectionId, ctx.validatedLeafId)
+ }
// Why: when the renderer has declared it will own the serializer for this paneKey, suppress the daemon-snapshot seed so its hydration path is sole authority (keyed on paneKey since the ptyId isn't known yet). See docs/mobile-prefer-renderer-scrollback.md.
const rendererPreSignaled = ctx.validatedPaneKey
? pendingByPaneKey.has(ctx.validatedPaneKey)
diff --git a/src/main/ipc/pty/ipc/spawn-commit-ssh-lease-cardinality.test.ts b/src/main/ipc/pty/ipc/spawn-commit-ssh-lease-cardinality.test.ts
new file mode 100644
index 00000000000..6266faf0c02
--- /dev/null
+++ b/src/main/ipc/pty/ipc/spawn-commit-ssh-lease-cardinality.test.ts
@@ -0,0 +1,289 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { rmSync, mkdtempSync } from 'node:fs'
+import { join } from 'node:path'
+import { tmpdir } from 'node:os'
+import { testState, createStore } from '../../../persistence-test-harness'
+import { TEST_LEAF_1, TEST_LEAF_2 } from '../../../persistence-session-fixtures'
+import { sshRemotePtyLeaseAllowsReattach } from '../../../../shared/ssh-types'
+import { toAppSshPtyId } from '../../../providers/ssh-pty-id'
+import { toSshExecutionHostId } from '../../../../shared/execution-host'
+import type { PtySpawnIpcArgs, PtySpawnIpcDeps } from './spawn-types'
+import { createPtyIpcSpawnState } from './spawn-state'
+import { persistPtyIpcSpawnCommit } from './spawn-commit-persist'
+
+vi.mock('electron', () => ({
+ app: { getPath: () => testState.dir },
+ safeStorage: { isEncryptionAvailable: () => false }
+}))
+
+const TARGET = 'ssh-1'
+const WORKTREE = 'repo1::/worktree'
+const TAB = 'tab-1'
+
+/**
+ * Drives the shipped IPC spawn commit rather than the store primitives it calls.
+ *
+ * The store-level suite could not catch this: it exercised bind-then-upsert, and this path does the
+ * opposite — it writes the lease row first so a force-quit in the renderer's debounce window cannot
+ * strand a running remote shell without one, then binds the pane. Supersession is fenced on the
+ * pane's binding, so under this real order it bailed on the predecessor every time and never re-ran,
+ * and each reconnect left one more reattachable lease for `reattachKnownPtys` to `pty.attach`.
+ */
+async function commitSshSpawn(
+ store: ReturnType,
+ args: { relayPtyId: string; leafId: string }
+): Promise {
+ const deps = { store } as unknown as PtySpawnIpcDeps
+ const spawnArgs = {
+ cols: 80,
+ rows: 24,
+ worktreeId: WORKTREE,
+ tabId: TAB,
+ leafId: args.leafId,
+ connectionId: TARGET
+ } as unknown as PtySpawnIpcArgs
+ const ctx = createPtyIpcSpawnState(deps, spawnArgs)
+ ctx.result = { id: toAppSshPtyId(TARGET, args.relayPtyId) }
+ ctx.validatedLeafId = args.leafId
+ await persistPtyIpcSpawnCommit(ctx)
+}
+
+/** One pane's layout, so the two host partitions can be given different bindings for one leaf. */
+function sessionBinding(ptyId: string) {
+ return {
+ activeRepoId: 'repo1',
+ activeWorktreeId: WORKTREE,
+ activeTabId: TAB,
+ tabsByWorktree: {},
+ terminalLayoutsByTabId: {
+ [TAB]: {
+ root: { type: 'leaf' as const, leafId: TEST_LEAF_1 },
+ activeLeafId: TEST_LEAF_1,
+ expandedLeafId: null,
+ ptyIdsByLeafId: { [TEST_LEAF_1]: ptyId }
+ }
+ }
+ }
+}
+
+function bulkReattachPtyIds(store: ReturnType): string[] {
+ return store
+ .getSshRemotePtyLeases(TARGET)
+ .filter(sshRemotePtyLeaseAllowsReattach)
+ .map((lease) => lease.ptyId)
+ .sort()
+}
+
+describe('the IPC spawn commit keeps one reattachable lease per SSH pane', () => {
+ beforeEach(() => {
+ testState.dir = mkdtempSync(join(tmpdir(), 'orca-test-'))
+ })
+ afterEach(() => {
+ rmSync(testState.dir, { recursive: true, force: true })
+ })
+
+ // QA's measurement, driven through the real path: five relay restarts, one pane, N+1 leases.
+ it('holds the reattach set flat across five reconnects of one pane', async () => {
+ const store = await createStore()
+
+ for (let reconnect = 0; reconnect < 5; reconnect++) {
+ // A relay renumbers from `pty-1` on every start; a reconnect therefore re-leases the same
+ // pane under an id it has never used before.
+ await commitSshSpawn(store, { relayPtyId: `pty-${reconnect}`, leafId: TEST_LEAF_1 })
+ }
+
+ expect(bulkReattachPtyIds(store)).toEqual(['pty-4'])
+ })
+
+ it('retires each predecessor as `expired` with the winner recorded, never `terminated`', async () => {
+ const store = await createStore()
+
+ await commitSshSpawn(store, { relayPtyId: 'pty-0', leafId: TEST_LEAF_1 })
+ await commitSshSpawn(store, { relayPtyId: 'pty-1', leafId: TEST_LEAF_1 })
+
+ const predecessor = store.getSshRemotePtyLeases(TARGET).find((entry) => entry.ptyId === 'pty-0')
+ // `expired`, not `terminated`: losing the lease is not evidence the remote shell died, and the
+ // process is deliberately left running (docs/reference/ssh-execution-boundary.md).
+ expect(predecessor).toMatchObject({ state: 'expired', supersededBy: 'pty-1' })
+ })
+
+ // The failure that would be worse than the fan-out: over-superseding strands a live remote
+ // process behind a pane that can no longer find it.
+ it('leaves a genuine orphan reattachable while superseding the pane that re-leased', async () => {
+ const store = await createStore()
+
+ await commitSshSpawn(store, { relayPtyId: 'orphan-pty', leafId: TEST_LEAF_2 })
+ // The orphan's client lost its route; nothing observed the shell, so it stays askable.
+ store.markSshRemotePtyLease(TARGET, 'orphan-pty', 'expired')
+
+ await commitSshSpawn(store, { relayPtyId: 'pty-0', leafId: TEST_LEAF_1 })
+ await commitSshSpawn(store, { relayPtyId: 'pty-1', leafId: TEST_LEAF_1 })
+
+ const orphan = store.getSshRemotePtyLeases(TARGET).find((entry) => entry.ptyId === 'orphan-pty')
+ expect(orphan?.supersededBy).toBeUndefined()
+ expect(bulkReattachPtyIds(store)).toEqual(['orphan-pty', 'pty-1'])
+ })
+
+ /**
+ * The shape the Docker lane exposed, and the reason a spawn-time trigger is not enough on its
+ * own. When the spawn commit writes no binding, the renderer's debounced layout publish does it
+ * later — so at commit time the pane still names the predecessor and supersession correctly
+ * declines. Nothing revisited it afterwards, and the predecessor stayed reattachable forever.
+ *
+ * Measured rows agreed on target, worktree, tab and leaf and still carried no `supersededBy`.
+ */
+ it('retires a predecessor whose successor bound the pane after the spawn commit', async () => {
+ const store = await createStore()
+
+ await commitSshSpawn(store, { relayPtyId: 'pty2:aaa:1', leafId: TEST_LEAF_1 })
+ // What `handlePtyReattachFailure` writes when a restarted relay disowns the id.
+ store.markSshRemotePtyLease(TARGET, 'pty2:aaa:1', 'expired')
+
+ // The successor leases without binding the pane; the binding catches up afterwards, exactly as
+ // the renderer's debounced publish does.
+ store.upsertSshRemotePtyLease({
+ targetId: TARGET,
+ ptyId: 'pty2:bbb:1',
+ worktreeId: WORKTREE,
+ tabId: TAB,
+ leafId: TEST_LEAF_1,
+ state: 'attached'
+ })
+ expect(bulkReattachPtyIds(store)).toEqual(['pty2:aaa:1', 'pty2:bbb:1'])
+ store.persistPtyBinding({
+ worktreeId: WORKTREE,
+ tabId: TAB,
+ leafId: TEST_LEAF_1,
+ ptyId: toAppSshPtyId(TARGET, 'pty2:bbb:1')
+ })
+
+ // What the connect path does before reading the set it feeds to `pty.attach`.
+ store.reconcileSshRemotePtyLeasesForTarget(TARGET)
+
+ expect(bulkReattachPtyIds(store)).toEqual(['pty2:bbb:1'])
+ })
+
+ // Reconciliation must not invent evidence: with no binding naming the pane, nothing says which
+ // shell owns it, so every lease stays askable.
+ it('leaves leases reattachable when no binding names the pane', async () => {
+ const store = await createStore()
+
+ store.upsertSshRemotePtyLease({
+ targetId: TARGET,
+ ptyId: 'unbound-a',
+ worktreeId: WORKTREE,
+ tabId: TAB,
+ leafId: TEST_LEAF_1,
+ state: 'expired'
+ })
+ store.upsertSshRemotePtyLease({
+ targetId: TARGET,
+ ptyId: 'unbound-b',
+ worktreeId: WORKTREE,
+ tabId: TAB,
+ leafId: TEST_LEAF_1,
+ state: 'expired'
+ })
+
+ store.reconcileSshRemotePtyLeasesForTarget(TARGET)
+
+ expect(bulkReattachPtyIds(store)).toEqual(['unbound-a', 'unbound-b'])
+ })
+
+ /**
+ * The measured defect, reduced to its cause.
+ *
+ * Main writes an SSH pane's binding to the `ssh:` partition, but a stale copy of the same
+ * leaf survives in `local`. Reading `local` first named the PREDECESSOR as the pane's current
+ * PTY, so supersession took an already-expired lease as its winner and returned having marked
+ * nothing — once per relay restart, forever. Both partitions name the same PTY again once the
+ * renderer republishes, which is why the finished store looks consistent and hides this.
+ */
+ it('supersedes when the local partition still names the predecessor', async () => {
+ const store = await createStore()
+ const hostId = toSshExecutionHostId(TARGET)
+ const predecessor = toAppSshPtyId(TARGET, 'pty2:old:1')
+ const successor = toAppSshPtyId(TARGET, 'pty2:new:1')
+
+ store.upsertSshRemotePtyLease({
+ targetId: TARGET,
+ ptyId: 'pty2:old:1',
+ worktreeId: WORKTREE,
+ tabId: TAB,
+ leafId: TEST_LEAF_1,
+ state: 'expired'
+ })
+ // Both partitions start on the predecessor, as they do before a relay restart.
+ store.setWorkspaceSession(sessionBinding(predecessor))
+ store.setWorkspaceSession(sessionBinding(predecessor), hostId)
+ store.upsertSshRemotePtyLease({
+ targetId: TARGET,
+ ptyId: 'pty2:new:1',
+ worktreeId: WORKTREE,
+ tabId: TAB,
+ leafId: TEST_LEAF_1,
+ state: 'attached'
+ })
+ // Production's writer for an SSH pane binding, and the whole point: it updates ONLY the host
+ // partition, so `local` is left naming the predecessor until the renderer republishes.
+ store.persistPtyBinding(
+ { worktreeId: WORKTREE, tabId: TAB, leafId: TEST_LEAF_1, ptyId: successor },
+ hostId
+ )
+ expect(
+ store.getWorkspaceSession().terminalLayoutsByTabId?.[TAB]?.ptyIdsByLeafId?.[TEST_LEAF_1]
+ ).toBe(predecessor)
+
+ store.supersedeSshRemotePtyLeasesForBoundPane(TARGET, TEST_LEAF_1)
+
+ const retired = store.getSshRemotePtyLeases(TARGET).find((l) => l.ptyId === 'pty2:old:1')
+ expect(retired).toMatchObject({ state: 'expired', supersededBy: 'pty2:new:1' })
+ expect(bulkReattachPtyIds(store)).toEqual(['pty2:new:1'])
+ })
+
+ // The mirror: a live shell the pane is still bound to must never be retired, whichever partition
+ // names it. Over-superseding strands a running remote process.
+ it('never retires a live lease the pane is still bound to', async () => {
+ const store = await createStore()
+ const live = toAppSshPtyId(TARGET, 'pty2:live:1')
+
+ store.upsertSshRemotePtyLease({
+ targetId: TARGET,
+ ptyId: 'pty2:live:1',
+ worktreeId: WORKTREE,
+ tabId: TAB,
+ leafId: TEST_LEAF_1,
+ state: 'attached'
+ })
+ // Bound BEFORE the stray lease arrives, which is the order that makes the binding meaningful:
+ // with no binding at all, an arriving lease is the only evidence there is and does win.
+ store.setWorkspaceSession(sessionBinding(live))
+ store.setWorkspaceSession(sessionBinding(live), toSshExecutionHostId(TARGET))
+ store.upsertSshRemotePtyLease({
+ targetId: TARGET,
+ ptyId: 'pty2:other:1',
+ worktreeId: WORKTREE,
+ tabId: TAB,
+ leafId: TEST_LEAF_1,
+ state: 'attached'
+ })
+
+ store.supersedeSshRemotePtyLeasesForBoundPane(TARGET, TEST_LEAF_1)
+
+ const stillLive = store.getSshRemotePtyLeases(TARGET).find((l) => l.ptyId === 'pty2:live:1')
+ expect(stillLive).toMatchObject({ state: 'attached' })
+ expect(stillLive?.supersededBy).toBeUndefined()
+ })
+
+ // Panes are independent, and supersession keys on the leaf: a second live pane on the same
+ // target must survive its neighbour reconnecting.
+ it('does not touch a sibling pane on the same target', async () => {
+ const store = await createStore()
+
+ await commitSshSpawn(store, { relayPtyId: 'sibling-pty', leafId: TEST_LEAF_2 })
+ await commitSshSpawn(store, { relayPtyId: 'pty-0', leafId: TEST_LEAF_1 })
+ await commitSshSpawn(store, { relayPtyId: 'pty-1', leafId: TEST_LEAF_1 })
+
+ expect(bulkReattachPtyIds(store)).toEqual(['pty-1', 'sibling-pty'])
+ })
+})
diff --git a/src/main/ipc/pty/pane/ssh-pane-lease-claim.ts b/src/main/ipc/pty/pane/ssh-pane-lease-claim.ts
new file mode 100644
index 00000000000..5746814a5f7
--- /dev/null
+++ b/src/main/ipc/pty/pane/ssh-pane-lease-claim.ts
@@ -0,0 +1,44 @@
+import { isTerminalLeafId } from '../../../../shared/stable-pane-id'
+import { getRelayPtyId } from '../provider/registry'
+import type { Store } from '../../../persistence'
+
+/**
+ * Claim a remote PTY for a pane: record the lease, then retire the pane's predecessors.
+ *
+ * The lease keeps the RELAY id, because reconnect calls `pty.attach` with target-local ids, while
+ * the pane binding keeps the app-facing id used for hydration.
+ *
+ * Supersession is a second step rather than something `upsertSshRemotePtyLease` finishes on its own
+ * because it is fenced on the pane's durable binding — it refuses to retire a predecessor the pane
+ * is still bound to, which would detach a live pane. A caller that leases BEFORE it binds therefore
+ * trips that fence on every reconnect and, with the upsert as the only trigger, never re-runs: one
+ * more reattachable lease, and one more `pty.attach` round trip on every later connect, forever.
+ * Re-running it here from the binding side is what makes the two writes commute.
+ */
+export function claimSshPaneLease(args: {
+ store: Store | undefined
+ connectionId: string | null | undefined
+ ptyId: string
+ worktreeId: string | undefined
+ tabId: string | undefined
+ leafId: string | undefined
+}): void {
+ const { store, connectionId } = args
+ if (!store || !connectionId) {
+ return
+ }
+ const leafId =
+ typeof args.leafId === 'string' && isTerminalLeafId(args.leafId) ? args.leafId : null
+ store.upsertSshRemotePtyLease({
+ targetId: connectionId,
+ ptyId: getRelayPtyId(connectionId, args.ptyId),
+ ...(typeof args.worktreeId === 'string' ? { worktreeId: args.worktreeId } : {}),
+ ...(typeof args.tabId === 'string' ? { tabId: args.tabId } : {}),
+ ...(leafId ? { leafId } : {}),
+ state: 'attached',
+ lastAttachedAt: Date.now()
+ })
+ if (leafId) {
+ store.supersedeSshRemotePtyLeasesForBoundPane(connectionId, leafId)
+ }
+}
diff --git a/src/main/ipc/pty/runtime/spawn-commit.ts b/src/main/ipc/pty/runtime/spawn-commit.ts
index 0b4e8804ac0..23592604bea 100644
--- a/src/main/ipc/pty/runtime/spawn-commit.ts
+++ b/src/main/ipc/pty/runtime/spawn-commit.ts
@@ -1,8 +1,6 @@
import { isValidTerminalTabId } from '../../../../shared/terminal-tab-id'
-import { isTerminalLeafId } from '../../../../shared/stable-pane-id'
import { ptyOwnership, ptyIncarnationById, deletePtyOwnership } from '../provider/ownership-state'
import { ptySizes } from '../delivery/visibility-state'
-import { getRelayPtyId } from '../provider/registry'
import {
shouldSkipCodexHomeEnvForWindowsShell,
recordCodexPaneAccountForSpawn,
@@ -25,6 +23,7 @@ import {
requestKindSchema
} from '../../../../shared/telemetry-events'
import { persistAdmittedStablePaneBinding } from '../pane/stable-owner'
+import { claimSshPaneLease } from '../pane/ssh-pane-lease-claim'
import {
isNativeWindowsLocalPtySpawn,
markNativeWindowsConptyPty
@@ -114,23 +113,15 @@ export async function commitRuntimePtySpawn(ctx: RuntimePtySpawnState) {
) {
markNativeWindowsConptyPty(ctx.result.id)
}
- const persistSshLease = (): void => {
- if (!ctx.deps.store || !args.connectionId) {
- return
- }
- // Why: SSH leases keep relay ids for remote reconciliation, while session bindings keep app-facing ids for hydration.
- ctx.deps.store.upsertSshRemotePtyLease({
- targetId: args.connectionId,
- ptyId: getRelayPtyId(args.connectionId, ctx.result.id),
- ...(typeof args.worktreeId === 'string' ? { worktreeId: args.worktreeId } : {}),
- ...(typeof args.tabId === 'string' ? { tabId: args.tabId } : {}),
- ...(typeof args.leafId === 'string' && isTerminalLeafId(args.leafId)
- ? { leafId: args.leafId }
- : {}),
- state: 'attached',
- lastAttachedAt: Date.now()
+ const persistSshLease = (): void =>
+ claimSshPaneLease({
+ store: ctx.deps.store,
+ connectionId: args.connectionId,
+ ptyId: ctx.result.id,
+ worktreeId: args.worktreeId,
+ tabId: args.tabId,
+ leafId: args.leafId
})
- }
if (!ctx.hostSessionBinding) {
persistSshLease()
}
diff --git a/src/main/ipc/ssh-ipc-test-harness.ts b/src/main/ipc/ssh-ipc-test-harness.ts
index 84802fe83a0..581fcc916ca 100644
--- a/src/main/ipc/ssh-ipc-test-harness.ts
+++ b/src/main/ipc/ssh-ipc-test-harness.ts
@@ -25,6 +25,7 @@ export type SshLeaseStoreMock = {
upsertSshPtyConsumerRecovery: Mock
removeSshPtyConsumerRecovery: Mock
getSshRemotePtyLeases: Mock
+ reconcileSshRemotePtyLeasesForTarget: Mock
markSshRemotePtyLease: Mock
markSshRemotePtyLeases: Mock
markSshRemotePtyLeasesAsync: Mock
@@ -102,6 +103,7 @@ export function createSshIpcHarness(mocks: SshIpcMocks): SshIpcHarness {
upsertSshPtyConsumerRecovery: vi.fn(),
removeSshPtyConsumerRecovery: vi.fn(),
getSshRemotePtyLeases: vi.fn().mockReturnValue([]),
+ reconcileSshRemotePtyLeasesForTarget: vi.fn(),
markSshRemotePtyLease: vi.fn(),
markSshRemotePtyLeases: vi.fn(),
markSshRemotePtyLeasesAsync: vi.fn(),
diff --git a/src/main/persistence/leasing-ssh-ptys/ssh-pty-lease-operations.ts b/src/main/persistence/leasing-ssh-ptys/ssh-pty-lease-operations.ts
index 46db8c4f0c7..f0628cb25e5 100644
--- a/src/main/persistence/leasing-ssh-ptys/ssh-pty-lease-operations.ts
+++ b/src/main/persistence/leasing-ssh-ptys/ssh-pty-lease-operations.ts
@@ -1,9 +1,8 @@
-import { toSshExecutionHostId } from '../../../shared/execution-host'
import type { PersistedState } from '../../../shared/persisted-state-types'
import type { SshRemotePtyLease } from '../../../shared/ssh-types'
import { isTerminalLeafId } from '../../../shared/stable-pane-id'
-import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
import { invalidateLocalWorktreeMetadataPruneInputs } from '../../local-worktree-metadata-prune-gate'
+import { supersedeSiblingLeasesForPane } from './ssh-pty-pane-supersession'
export type SshPtyLeaseOperations = {
state: PersistedState
@@ -15,91 +14,6 @@ export type SshPtyLeaseOperations = {
flushDurableStateOrThrowAsync: () => Promise
}
-/**
- * The PTY a pane is durably bound to, keyed on the leaf alone — the only remint-stable half of a
- * pane key, since `detachTerminalPaneToTab` moves a live pane and leaves its lease naming the tab
- * it left.
- *
- * Reads both partitions deliberately. Main writes some SSH pane bindings to `ssh:` and
- * some to `local`, so a reader that consulted one would see "unbound" for a live pane and expire
- * its lease. Reading both makes this fence correct whichever partition the binding landed in.
- */
-function durablyBoundPtyIdForPane(
- operations: SshPtyLeaseOperations,
- targetId: string,
- leafId: string
-): string | undefined {
- const findLeafBinding = (session: WorkspaceSessionState | undefined): string | undefined =>
- Object.values(session?.terminalLayoutsByTabId ?? {}).find(
- (layout) => layout?.ptyIdsByLeafId?.[leafId]
- )?.ptyIdsByLeafId?.[leafId]
- const boundPtyId =
- findLeafBinding(operations.state.workspaceSession) ??
- findLeafBinding(operations.state.workspaceSessionsByHostId?.[toSshExecutionHostId(targetId)])
- return boundPtyId ? operations.toComparablePtyId(targetId, boundPtyId) : undefined
-}
-
-/**
- * One pane owns at most one live remote PTY. Lease identity is `(targetId, ptyId)` alone, so a
- * pane re-leasing under a new relay id leaves its predecessor live with nothing to retire it and
- * the next reattach fans out over both — the reported 2 -> 19 -> 20 across three reconnects.
- *
- * Superseded leases are marked `expired`, never `terminated`: losing a lease is not evidence the
- * shell died, so the remote process is deliberately left running. They also carry `supersededBy`,
- * which is what keeps them out of the bulk reattach set now that plain `expired` no longer does —
- * the winner's ptyId is already in hand here, so recording it needs no relay-start identity.
- */
-function supersedeSiblingLeasesForPane(
- operations: SshPtyLeaseOperations,
- winner: SshRemotePtyLease,
- now: number
-): void {
- if (!winner.worktreeId || !winner.leafId) {
- return
- }
- if (winner.state === 'terminated' || winner.state === 'expired') {
- return
- }
- // At upsert time the arriving lease may not be the one the pane is bound to yet. Expiring the
- // bound predecessor would detach a live pane, so leave both live and let reattach arbitrate
- // with the binding in hand.
- const boundPtyId = durablyBoundPtyIdForPane(operations, winner.targetId, winner.leafId)
- if (boundPtyId && boundPtyId !== winner.ptyId) {
- return
- }
- const superseded: SshRemotePtyLease[] = []
- for (const lease of operations.state.sshRemotePtyLeases ?? []) {
- if (
- lease.ptyId === winner.ptyId ||
- lease.targetId !== winner.targetId ||
- lease.worktreeId !== winner.worktreeId ||
- // Leaf only: a lease freezes its tabId, so a pane broken out into a new tab would otherwise
- // never compete with its own predecessor — which is the reported cardinality growth.
- lease.leafId !== winner.leafId ||
- lease.state === 'terminated'
- ) {
- continue
- }
- if (lease.state === 'expired') {
- // An already-expired predecessor is superseded by the same evidence, and marking it is what
- // bounds the reattach set: without this, every past orphan for this pane stays reattachable
- // forever. `updatedAt` stays put — bumping it would make a stale lease look recent to
- // `getRecentExpiredSshLease`.
- lease.supersededBy = winner.ptyId
- continue
- }
- lease.state = 'expired'
- lease.supersededBy = winner.ptyId
- lease.updatedAt = now
- superseded.push(lease)
- }
- if (superseded.length > 0) {
- // Why: matching on lease ptyId first means this scrubs only the predecessor's stale binding —
- // the winner's own binding cannot match and is left intact.
- operations.clearBindingsForLeases(winner.targetId, superseded)
- }
-}
-
/**
* Only `terminated` unbinds a pane. It is the operator-close state and the one written after a
* host-acknowledged stop; `expired` records that the CLIENT lost its route and says nothing about
diff --git a/src/main/persistence/leasing-ssh-ptys/ssh-pty-pane-supersession.ts b/src/main/persistence/leasing-ssh-ptys/ssh-pty-pane-supersession.ts
new file mode 100644
index 00000000000..61d6b934db5
--- /dev/null
+++ b/src/main/persistence/leasing-ssh-ptys/ssh-pty-pane-supersession.ts
@@ -0,0 +1,201 @@
+import { toSshExecutionHostId } from '../../../shared/execution-host'
+import type { SshRemotePtyLease } from '../../../shared/ssh-types'
+import { isTerminalLeafId } from '../../../shared/stable-pane-id'
+import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
+import type { SshPtyLeaseOperations } from './ssh-pty-lease-operations'
+
+/**
+ * Every PTY id any partition binds to this pane, most authoritative first.
+ *
+ * Keyed on the leaf alone — the only remint-stable half of a pane key, since
+ * `detachTerminalPaneToTab` moves a live pane and leaves its lease naming the tab it left.
+ *
+ * Returns a LIST, and reads the target's own partition first, because the two partitions disagree
+ * for the length of a reconnect and this resolved that disagreement backwards. Main writes an SSH
+ * pane's binding to `ssh:`, while a stale copy of the same leaf survives in `local`;
+ * consulting `local` first therefore named the PREDECESSOR as the pane's current PTY on every relay
+ * restart. Supersession then took that expired predecessor as its winner and returned without
+ * marking anything — the per-reconnect lease growth. Both partitions are still read, because a
+ * reader that consulted only one would see "unbound" for a live pane and expire its lease.
+ */
+function durablyBoundPtyIdsForPane(
+ operations: SshPtyLeaseOperations,
+ targetId: string,
+ leafId: string
+): string[] {
+ const findLeafBindings = (session: WorkspaceSessionState | undefined): string[] =>
+ Object.values(session?.terminalLayoutsByTabId ?? {})
+ .map((layout) => layout?.ptyIdsByLeafId?.[leafId])
+ .filter((ptyId): ptyId is string => Boolean(ptyId))
+ const ordered = [
+ ...findLeafBindings(
+ operations.state.workspaceSessionsByHostId?.[toSshExecutionHostId(targetId)]
+ ),
+ ...findLeafBindings(operations.state.workspaceSession)
+ ]
+ return [...new Set(ordered.map((ptyId) => operations.toComparablePtyId(targetId, ptyId)))]
+}
+
+/** A lease this client still holds a route to, as opposed to one it has already lost. */
+function isLiveLeaseState(state: SshRemotePtyLease['state']): boolean {
+ return state === 'attached' || state === 'detached'
+}
+
+/**
+ * One pane owns at most one live remote PTY. Lease identity is `(targetId, ptyId)` alone, so a
+ * pane re-leasing under a new relay id leaves its predecessor live with nothing to retire it and
+ * the next reattach fans out over both — the reported 2 -> 19 -> 20 across three reconnects.
+ *
+ * Superseded leases are marked `expired`, never `terminated`: losing a lease is not evidence the
+ * shell died, so the remote process is deliberately left running. They also carry `supersededBy`,
+ * which is what keeps them out of the bulk reattach set now that plain `expired` no longer does —
+ * the winner's ptyId is already in hand here, so recording it needs no relay-start identity.
+ */
+export function supersedeSiblingLeasesForPane(
+ operations: SshPtyLeaseOperations,
+ winner: SshRemotePtyLease,
+ now: number
+): boolean {
+ if (!winner.worktreeId || !winner.leafId) {
+ return false
+ }
+ if (winner.state === 'terminated' || winner.state === 'expired') {
+ return false
+ }
+ // At upsert time the arriving lease may not be the one the pane is bound to yet. Expiring the
+ // bound predecessor would detach a live pane, so leave both live and let reattach arbitrate
+ // with the binding in hand. `supersedeSshRemotePtyLeasesForBoundPane` re-runs this once the
+ // binding write lands, so a caller that upserts before it binds is not left bailed forever.
+ // Membership rather than equality: during a reconnect the two partitions name different PTYs for
+ // the same leaf, and requiring the winner to match the FIRST one read is what made this bail.
+ const boundPtyIds = durablyBoundPtyIdsForPane(operations, winner.targetId, winner.leafId)
+ if (boundPtyIds.length > 0 && !boundPtyIds.includes(winner.ptyId)) {
+ return false
+ }
+ let marked = false
+ const superseded: SshRemotePtyLease[] = []
+ for (const lease of operations.state.sshRemotePtyLeases ?? []) {
+ if (
+ lease.ptyId === winner.ptyId ||
+ lease.targetId !== winner.targetId ||
+ lease.worktreeId !== winner.worktreeId ||
+ // Leaf only: a lease freezes its tabId, so a pane broken out into a new tab would otherwise
+ // never compete with its own predecessor — which is the reported cardinality growth.
+ lease.leafId !== winner.leafId ||
+ lease.state === 'terminated' ||
+ // Never retire a shell the pane is BOTH still bound to and still routable to. The stale
+ // partition can name a predecessor, and retiring that is the point; retiring a live one
+ // would strand a running remote process behind a pane that can no longer reach it.
+ (boundPtyIds.includes(lease.ptyId) && isLiveLeaseState(lease.state))
+ ) {
+ continue
+ }
+ if (lease.state === 'expired') {
+ // An already-expired predecessor is superseded by the same evidence, and marking it is what
+ // bounds the reattach set: without this, every past orphan for this pane stays reattachable
+ // forever. `updatedAt` stays put — bumping it would make a stale lease look recent to
+ // `getRecentExpiredSshLease`.
+ marked ||= lease.supersededBy !== winner.ptyId
+ lease.supersededBy = winner.ptyId
+ continue
+ }
+ lease.state = 'expired'
+ lease.supersededBy = winner.ptyId
+ lease.updatedAt = now
+ marked = true
+ superseded.push(lease)
+ }
+ if (superseded.length > 0) {
+ // Why: matching on lease ptyId first means this scrubs only the predecessor's stale binding —
+ // the winner's own binding cannot match and is left intact.
+ operations.clearBindingsForLeases(winner.targetId, superseded)
+ }
+ return marked
+}
+
+/**
+ * Supersede from the lease the pane's binding names — preferring a LIVE one when the partitions
+ * disagree, since a reconnect leaves the stale partition naming an already-expired predecessor and
+ * an expired winner supersedes nothing.
+ */
+function supersedeFromBoundPane(
+ operations: SshPtyLeaseOperations,
+ targetId: string,
+ leafId: string,
+ now: number
+): boolean {
+ if (!isTerminalLeafId(leafId)) {
+ return false
+ }
+ const boundPtyIds = durablyBoundPtyIdsForPane(operations, targetId, leafId)
+ if (boundPtyIds.length === 0) {
+ // No binding names this pane, so nothing here is evidence about which shell owns it. Leaving
+ // every lease reattachable is the deliberate direction: an orphan must stay askable.
+ return false
+ }
+ const candidates = (operations.state.sshRemotePtyLeases ?? []).filter(
+ (lease) =>
+ lease.targetId === targetId && lease.leafId === leafId && boundPtyIds.includes(lease.ptyId)
+ )
+ const winner = candidates.find((lease) => isLiveLeaseState(lease.state))
+ const marked = winner ? supersedeSiblingLeasesForPane(operations, winner, now) : false
+ return marked
+}
+
+/**
+ * The binding-side trigger for supersession, and the reason the two writes that together claim a
+ * pane are commutative.
+ *
+ * `upsertSshRemotePtyLease` is the only other trigger, and it bails whenever the pane's durable
+ * binding still names the predecessor. A spawn path that upserts its lease BEFORE it writes the
+ * binding therefore bails and never re-runs on its own. Re-resolving the winner from the binding
+ * is safe in the other direction too: it supersedes only from the lease the pane is actually bound
+ * to, so it can never strand a live orphan.
+ */
+export function supersedeSshRemotePtyLeasesForBoundPane(
+ operations: SshPtyLeaseOperations,
+ targetId: string,
+ leafId: string
+): void {
+ if (supersedeFromBoundPane(operations, targetId, leafId, Date.now())) {
+ operations.flush()
+ }
+}
+
+/**
+ * Bound the reattach set to one lease per pane, re-derived from each pane's CURRENT binding.
+ *
+ * The spawn-side trigger cannot be sufficient alone, and measuring the shipped path is what showed
+ * it: a pane's binding has several writers — the spawn commit, the relay's reattach bind, and the
+ * renderer's debounced layout publish — and the last of those lands well after the spawn commit
+ * that leased the pty. A predecessor that was still bound when its successor was claimed therefore
+ * keeps its reattachability forever, because nothing revisits it once the binding catches up. The
+ * observed rows agreed on target, worktree, tab and leaf and still carried no mark.
+ *
+ * Running this immediately before the reattach set is read makes the answer independent of which
+ * writer bound the pane and when. It also repairs stores written by earlier builds, where these
+ * rows have already accumulated and no spawn-time trigger would ever revisit them.
+ *
+ * Panes with no binding are skipped rather than pruned: absence of a binding is not evidence about
+ * which shell owns the pane, and a genuine orphan has to stay askable
+ * (docs/reference/ssh-execution-boundary.md).
+ */
+export function reconcileSshRemotePtyLeasesForTarget(
+ operations: SshPtyLeaseOperations,
+ targetId: string
+): void {
+ const leafIds = new Set()
+ for (const lease of operations.state.sshRemotePtyLeases ?? []) {
+ if (lease.targetId === targetId && lease.leafId) {
+ leafIds.add(lease.leafId)
+ }
+ }
+ const now = Date.now()
+ let changed = false
+ for (const leafId of leafIds) {
+ changed = supersedeFromBoundPane(operations, targetId, leafId, now) || changed
+ }
+ if (changed) {
+ operations.flush()
+ }
+}
diff --git a/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts b/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts
index 8768a47e50a..75aa19ad24b 100644
--- a/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts
+++ b/src/main/persistence/loading-store/ssh-lease-recovery-operations.ts
@@ -23,6 +23,10 @@ import {
type SshPtyLeaseOperations,
upsertSshRemotePtyLease as upsertSshRemotePtyLeaseOperation
} from '../leasing-ssh-ptys/ssh-pty-lease-operations'
+import {
+ reconcileSshRemotePtyLeasesForTarget as reconcileSshRemotePtyLeasesForTargetOperation,
+ supersedeSshRemotePtyLeasesForBoundPane as supersedeSshRemotePtyLeasesForBoundPaneOperation
+} from '../leasing-ssh-ptys/ssh-pty-pane-supersession'
import {
getSshPtyConsumerRecovery as getSshPtyConsumerRecoveryOperation,
removeSshPtyConsumerRecovery as removeSshPtyConsumerRecoveryOperation,
@@ -95,6 +99,28 @@ export class SshLeaseRecoveryOperations {
upsertSshRemotePtyLeaseOperation(getSshPtyLeaseOperations(this), lease)
}
+ /**
+ * Re-run pane supersession from the binding rather than from an arriving lease. Spawn commits
+ * call this after their binding write so it does not matter whether the lease or the binding
+ * landed first; see `supersedeSshRemotePtyLeasesForBoundPane`.
+ */
+ supersedeSshRemotePtyLeasesForBoundPane(targetId: string, leafId: string): void {
+ supersedeSshRemotePtyLeasesForBoundPaneOperation(
+ getSshPtyLeaseOperations(this),
+ targetId,
+ leafId
+ )
+ }
+
+ /**
+ * Re-derive one reattachable lease per pane from each pane's current binding. Called on the
+ * connect path immediately before the reattach set is read; see
+ * `reconcileSshRemotePtyLeasesForTarget`.
+ */
+ reconcileSshRemotePtyLeasesForTarget(targetId: string): void {
+ reconcileSshRemotePtyLeasesForTargetOperation(getSshPtyLeaseOperations(this), targetId)
+ }
+
markSshRemotePtyLeases(targetId: string, state: SshRemotePtyLease['state']): void {
markSshRemotePtyLeasesOperation(getSshPtyLeaseOperations(this), targetId, state)
}
diff --git a/src/main/ssh-reattach-pane-cardinality.test.ts b/src/main/ssh-reattach-pane-cardinality.test.ts
index 5827ec1ee68..d7179703360 100644
--- a/src/main/ssh-reattach-pane-cardinality.test.ts
+++ b/src/main/ssh-reattach-pane-cardinality.test.ts
@@ -82,24 +82,38 @@ function relayReattachBinds(
}
/**
- * Both binding writers land before the lease upsert — spawn asserts that ordering directly, and
- * the relay's reattach binds the pane before `markSshRemotePtyLeasesAttachedAsync`. Supersession
- * therefore sees a session already naming the arriving shell.
+ * One reconnect's worth of writes, in the order the spawn commits actually issue them: the lease
+ * row first — so a force-quit in the renderer's debounce window cannot leave a running remote shell
+ * with no lease to reattach it — then the binding, then the binding-side supersession trigger.
+ *
+ * This suite used to bind BEFORE upserting, an order no caller uses. Under that order supersession
+ * always saw a session already naming the arriving shell and passed; under production's order it
+ * bailed on the predecessor's binding every time and never re-ran, so the guard could not catch the
+ * per-reconnect lease growth it exists to pin.
*
* Goes through `persistPtyBinding` rather than `setWorkspaceSession` because that is the writer
* production uses; a raw session write is reconciled back to the attached lease's PTY by binding
* recovery, which would make the fixture disagree with the real flow.
*/
-function paneBindsTo(
+function paneSpawnCommits(
store: ReturnType,
- args: { tabId: string; leafId: string; ptyId: string }
+ args: { tabId: string; leafId: string; ptyId: string; leaseTabId?: string }
): void {
+ store.upsertSshRemotePtyLease({
+ targetId: TARGET,
+ ptyId: args.ptyId,
+ worktreeId: WORKTREE,
+ tabId: args.leaseTabId ?? args.tabId,
+ leafId: args.leafId,
+ state: 'attached'
+ })
store.persistPtyBinding({
worktreeId: WORKTREE,
tabId: args.tabId,
leafId: args.leafId,
ptyId: args.ptyId
})
+ store.supersedeSshRemotePtyLeasesForBoundPane(TARGET, args.leafId)
}
function liveLeasePtyIds(store: ReturnType): string[] {
@@ -300,8 +314,7 @@ describe('STA-3077: one pane keeps at most one live remote lease', () => {
const lease = { targetId: TARGET, worktreeId: WORKTREE, tabId: TAB, leafId: TEST_LEAF_1 }
store.upsertSshRemotePtyLease({ ...lease, ptyId: 'pty-1', state: 'attached' })
- paneBindsTo(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
- store.upsertSshRemotePtyLease({ ...lease, ptyId: 'pty-2', state: 'attached' })
+ paneSpawnCommits(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
expect(liveLeasePtyIds(store)).toEqual(['pty-2'])
})
@@ -314,8 +327,7 @@ describe('STA-3077: one pane keeps at most one live remote lease', () => {
const lease = { targetId: TARGET, worktreeId: WORKTREE, tabId: TAB, leafId: TEST_LEAF_1 }
store.upsertSshRemotePtyLease({ ...lease, ptyId: 'pty-1', state: 'attached' })
- paneBindsTo(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
- store.upsertSshRemotePtyLease({ ...lease, ptyId: 'pty-2', state: 'attached' })
+ paneSpawnCommits(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
const predecessor = store.getSshRemotePtyLeases(TARGET).find((entry) => entry.ptyId === 'pty-1')
expect(predecessor?.state).toBe('expired')
@@ -325,11 +337,9 @@ describe('STA-3077: one pane keeps at most one live remote lease', () => {
it('holds the live lease count flat across ten reconnects of one pane', async () => {
const store = await createStore()
store.setWorkspaceSession(sessionWithPane({ tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-0' }))
- const lease = { targetId: TARGET, worktreeId: WORKTREE, tabId: TAB, leafId: TEST_LEAF_1 }
for (let reconnect = 0; reconnect < 10; reconnect++) {
- paneBindsTo(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: `pty-${reconnect}` })
- store.upsertSshRemotePtyLease({ ...lease, ptyId: `pty-${reconnect}`, state: 'attached' })
+ paneSpawnCommits(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: `pty-${reconnect}` })
}
expect(liveLeasePtyIds(store)).toEqual(['pty-9'])
@@ -349,17 +359,14 @@ describe('STA-3077: one pane keeps at most one live remote lease', () => {
state: 'attached'
})
- paneBindsTo(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
// The successor's lease names the tab the pane sits in NOW; the predecessor's still names the
// one it was written in. Only the leaf is common, so keying on the tab would stop the two
// competing and leave both live — the cardinality growth.
- store.upsertSshRemotePtyLease({
- targetId: TARGET,
- ptyId: 'pty-2',
- worktreeId: WORKTREE,
- tabId: OTHER_TAB,
+ paneSpawnCommits(store, {
+ tabId: TAB,
leafId: TEST_LEAF_1,
- state: 'attached'
+ ptyId: 'pty-2',
+ leaseTabId: OTHER_TAB
})
expect(liveLeasePtyIds(store)).toEqual(['pty-2'])
@@ -394,8 +401,7 @@ describe('STA-3077: one pane keeps at most one live remote lease', () => {
const lease = { targetId: TARGET, worktreeId: WORKTREE, tabId: TAB, leafId: TEST_LEAF_1 }
store.upsertSshRemotePtyLease({ ...lease, ptyId: 'pty-1', state: 'attached' })
- paneBindsTo(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
- store.upsertSshRemotePtyLease({ ...lease, ptyId: 'pty-2', state: 'attached' })
+ paneSpawnCommits(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
expect(liveLeasePtyIds(store).sort()).toEqual(['pty-2', 'sibling-pty'])
})
@@ -455,8 +461,7 @@ describe('STA-3077: `expired` separates a superseded sibling from an orphan', ()
it('never bulk-reattaches a superseded sibling', async () => {
const store = await storeWithPane('pty-1')
- paneBindsTo(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
- store.upsertSshRemotePtyLease({ ...paneLease, ptyId: 'pty-2', state: 'attached' })
+ paneSpawnCommits(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
const predecessor = store.getSshRemotePtyLeases(TARGET).find((entry) => entry.ptyId === 'pty-1')
expect(predecessor).toMatchObject({ state: 'expired', supersededBy: 'pty-2' })
@@ -469,8 +474,7 @@ describe('STA-3077: `expired` separates a superseded sibling from an orphan', ()
store.setWorkspaceSession(sessionWithPane({ tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-0' }))
for (let reconnect = 0; reconnect < 10; reconnect++) {
- paneBindsTo(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: `pty-${reconnect}` })
- store.upsertSshRemotePtyLease({ ...paneLease, ptyId: `pty-${reconnect}`, state: 'attached' })
+ paneSpawnCommits(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: `pty-${reconnect}` })
}
expect(bulkReattachPtyIds(store)).toEqual(['pty-9'])
@@ -496,8 +500,7 @@ describe('STA-3077: `expired` separates a superseded sibling from an orphan', ()
store.markSshRemotePtyLease(TARGET, 'pty-1', 'expired')
const orphanUpdatedAt = store.getSshRemotePtyLeases(TARGET)[0].updatedAt
- paneBindsTo(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
- store.upsertSshRemotePtyLease({ ...paneLease, ptyId: 'pty-2', state: 'attached' })
+ paneSpawnCommits(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
const predecessor = store.getSshRemotePtyLeases(TARGET).find((entry) => entry.ptyId === 'pty-1')
expect(predecessor).toMatchObject({ state: 'expired', supersededBy: 'pty-2' })
@@ -510,8 +513,7 @@ describe('STA-3077: `expired` separates a superseded sibling from an orphan', ()
// belongs to the lease that lost, never to whatever claims the id next.
it('clears the supersession mark when the id is re-upserted as a live lease', async () => {
const store = await storeWithPane('pty-1')
- paneBindsTo(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
- store.upsertSshRemotePtyLease({ ...paneLease, ptyId: 'pty-2', state: 'attached' })
+ paneSpawnCommits(store, { tabId: TAB, leafId: TEST_LEAF_1, ptyId: 'pty-2' })
// A restarted relay hands `pty-1` to a new shell for a different pane.
store.upsertSshRemotePtyLease({
diff --git a/src/main/ssh/ssh-orphan-relay-pty-sweep.test.ts b/src/main/ssh/ssh-orphan-relay-pty-sweep.test.ts
index 3201f8709fe..afb365c075c 100644
--- a/src/main/ssh/ssh-orphan-relay-pty-sweep.test.ts
+++ b/src/main/ssh/ssh-orphan-relay-pty-sweep.test.ts
@@ -53,7 +53,8 @@ function createHarness(
shutdown
} as unknown as IPtyProvider
const store = {
- getSshRemotePtyLeases: vi.fn().mockReturnValue(leases)
+ getSshRemotePtyLeases: vi.fn().mockReturnValue(leases),
+ reconcileSshRemotePtyLeasesForTarget: vi.fn()
} as unknown as Store
return { provider, store, shutdown }
}
diff --git a/src/main/ssh/ssh-relay-session-agent-hooks.integration.test.ts b/src/main/ssh/ssh-relay-session-agent-hooks.integration.test.ts
index 49fc1e98ccd..76d92e77e9a 100644
--- a/src/main/ssh/ssh-relay-session-agent-hooks.integration.test.ts
+++ b/src/main/ssh/ssh-relay-session-agent-hooks.integration.test.ts
@@ -157,6 +157,7 @@ function createSession(targetId: string): InstanceType {
upsertSshPtyConsumerRecovery: vi.fn(),
removeSshPtyConsumerRecovery: vi.fn(),
getSshRemotePtyLeases: vi.fn().mockReturnValue([]),
+ reconcileSshRemotePtyLeasesForTarget: vi.fn(),
markSshRemotePtyLease: vi.fn(),
markSshRemotePtyLeases: vi.fn(),
markSshRemotePtyLeasesAsync: vi.fn(),
diff --git a/src/main/ssh/ssh-relay-session-terminal-error.test.ts b/src/main/ssh/ssh-relay-session-terminal-error.test.ts
index 9bb14618319..4cfa657e401 100644
--- a/src/main/ssh/ssh-relay-session-terminal-error.test.ts
+++ b/src/main/ssh/ssh-relay-session-terminal-error.test.ts
@@ -104,6 +104,7 @@ function createMockDeps(): {
upsertSshPtyConsumerRecovery: vi.fn(),
removeSshPtyConsumerRecovery: vi.fn(),
getSshRemotePtyLeases: vi.fn().mockReturnValue([]),
+ reconcileSshRemotePtyLeasesForTarget: vi.fn(),
markSshRemotePtyLease: vi.fn(),
markSshRemotePtyLeases: vi.fn(),
markSshRemotePtyLeasesAsync: vi.fn(),
diff --git a/src/main/ssh/ssh-relay-session-test-fixtures.ts b/src/main/ssh/ssh-relay-session-test-fixtures.ts
index efdee31c406..ba0782b3a20 100644
--- a/src/main/ssh/ssh-relay-session-test-fixtures.ts
+++ b/src/main/ssh/ssh-relay-session-test-fixtures.ts
@@ -21,6 +21,7 @@ export function createMockDeps(): SshRelaySessionTestDeps {
upsertSshPtyConsumerRecovery: vi.fn(),
removeSshPtyConsumerRecovery: vi.fn(),
getSshRemotePtyLeases: vi.fn().mockReturnValue([]),
+ reconcileSshRemotePtyLeasesForTarget: vi.fn(),
getWorkspaceSession: vi.fn(),
markSshRemotePtyLease: vi.fn(),
markSshRemotePtyLeases: vi.fn(),
diff --git a/src/main/ssh/ssh-relay-session.ts b/src/main/ssh/ssh-relay-session.ts
index bca3632b83b..99a2ce9fbf9 100644
--- a/src/main/ssh/ssh-relay-session.ts
+++ b/src/main/ssh/ssh-relay-session.ts
@@ -2322,6 +2322,12 @@ export class SshRelaySession {
if (!shouldContinue()) {
return
}
+ // Why immediately before the read: a pane's binding is written by several writers, and the
+ // renderer's debounced layout publish lands long after the spawn commit that leased the pty —
+ // so a predecessor that was still bound at spawn time never gets marked by a spawn-side
+ // trigger. Re-deriving from each pane's CURRENT binding here is what actually bounds this set,
+ // and it repairs stores that already accumulated these rows.
+ this.store.reconcileSshRemotePtyLeasesForTarget(this.targetId)
// Why not `state !== 'expired'`: that state covers both a superseded sibling (re-adopting it is
// the 2 -> 19 -> 20 fan-out) and an orphan whose reattach merely lost contact. Only the first
// carries a retirement mark, and only it has to be skipped.
diff --git a/tests/e2e/ssh-docker-transport-drop-recovery.spec.ts b/tests/e2e/ssh-docker-transport-drop-recovery.spec.ts
index 9f8434ed51a..e18336d12c5 100644
--- a/tests/e2e/ssh-docker-transport-drop-recovery.spec.ts
+++ b/tests/e2e/ssh-docker-transport-drop-recovery.spec.ts
@@ -1,5 +1,9 @@
-import type { Page } from '@playwright/test'
+import path from 'node:path'
+import { readFileSync } from 'node:fs'
+import type { ElectronApplication, Page } from '@playwright/test'
import { test, expect } from './helpers/orca-app'
+import { DEFAULT_LOCAL_ORCA_PROFILE_ID } from '../../src/shared/orca-profiles'
+import { sshRemotePtyLeaseAllowsReattach, type SshRemotePtyLease } from '../../src/shared/ssh-types'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
execInTerminal,
@@ -49,6 +53,59 @@ async function readSshStatus(orcaPage: Page, targetId: string) {
)
}
+/**
+ * Every lease `reattachKnownPtys` would feed to `pty.attach` on the next connect, read from the
+ * durable store rather than from the renderer — leases are main-owned and never published.
+ *
+ * Goes through the shipped `sshRemotePtyLeaseAllowsReattach` predicate so the measurement cannot
+ * drift from the fan-out it exists to bound.
+ */
+function readSshLeases(userDataDir: string, targetId: string): SshRemotePtyLease[] {
+ const dataPath = path.join(
+ userDataDir,
+ 'profiles',
+ DEFAULT_LOCAL_ORCA_PROFILE_ID,
+ 'orca-data.json'
+ )
+ const parsed = JSON.parse(readFileSync(dataPath, 'utf8')) as {
+ sshRemotePtyLeases?: SshRemotePtyLease[]
+ }
+ return (parsed.sshRemotePtyLeases ?? []).filter((lease) => lease.targetId === targetId)
+}
+
+function readReattachablePtyIds(userDataDir: string, targetId: string): string[] {
+ return readSshLeases(userDataDir, targetId)
+ .filter(sshRemotePtyLeaseAllowsReattach)
+ .map((lease) => lease.ptyId)
+ .sort()
+}
+
+/**
+ * Everything a cardinality failure needs to be diagnosable from the report alone.
+ *
+ * Worth keeping rather than reducing to a count: when this first failed, the count said only "2",
+ * and it was the per-row fields that ruled out the obvious causes — the rows agreed on worktree,
+ * tab and leaf, so the pane identity was never the problem.
+ */
+function describeSshLeases(userDataDir: string, targetId: string): string {
+ return JSON.stringify(
+ readSshLeases(userDataDir, targetId).map((lease) => ({
+ ptyId: lease.ptyId,
+ state: lease.state,
+ worktreeId: lease.worktreeId,
+ leafId: lease.leafId,
+ tabId: lease.tabId,
+ supersededBy: lease.supersededBy,
+ relayIdRecycled: lease.relayIdRecycled,
+ reattachable: sshRemotePtyLeaseAllowsReattach(lease)
+ }))
+ )
+}
+
+function readUserDataDir(electronApp: ElectronApplication): Promise {
+ return electronApp.evaluate(({ app }) => app.getPath('userData'))
+}
+
/**
* Not covered here on purpose: park-then-reveal after a reconnect. ssh-terminal-parking already
* covers the park/reveal round trip, and driving a park deterministically from this lane proved
@@ -117,7 +174,9 @@ test.describe('SSH transport drop recovery', () => {
// run after the flood produces no output within the poll budget. Same shape as #18018 (deaf pane
// after a stalled host resumes), and not caused by this spec. Tracked there; the three verdict
// assertions around it stay enforced.
- test.fixme('stays bounded when a disconnected shell floods its pty', async ({ orcaPage }, testInfo) => {
+ test.fixme('stays bounded when a disconnected shell floods its pty', async ({
+ orcaPage
+ }, testInfo) => {
test.slow()
// Timeouts here are deliberately generous: this guards memory, not latency. A 48MB flood plus a
// reconnect lands near 60s wall-clock end to end, so a 60s bind timeout was marginal and made
@@ -261,6 +320,89 @@ test.describe('SSH transport drop recovery', () => {
}
})
+ /**
+ * The cardinality half of the same fault, which the verdict test above cannot see: it asserts the
+ * pane is re-backed, not what the pane's PREVIOUS shells left behind in the store.
+ *
+ * A pane re-leases under a new relay pty id on every relay restart, and nothing else retires the
+ * predecessor. When supersession fails, each generation leaves one more `expired`-but-unsuperseded
+ * lease that `reattachKnownPtys` still asks about — one extra `pty.attach` round trip on every
+ * later connect, forever, growing linearly with reconnect count. Measured as leases rather than
+ * as latency because latency hides the growth until it is already large.
+ *
+ * The reattachable set must stay at exactly one per pane. It must not go to zero either: a lease
+ * wrongly superseded is a running remote shell the pane can no longer find, which is the worse
+ * failure (docs/reference/ssh-execution-boundary.md).
+ */
+ test('keeps one reattachable lease per pane across repeated relay restarts', async ({
+ orcaPage,
+ electronApp
+ }, testInfo) => {
+ test.slow()
+ let target: DockerSshRelayTarget | null = null
+ try {
+ target = startDockerSshRelayTarget(testInfo)
+ enableDockerSshRelayTargetShellTitle(target)
+ await waitForSessionReady(orcaPage)
+ await waitForActiveWorktree(orcaPage)
+ const remote = await connectDockerSshRelayTarget(orcaPage, target)
+ await ensureTerminalVisible(orcaPage, 45_000)
+ await waitForActiveTerminalManager(orcaPage, 60_000)
+ await waitForActivePanePtyId(orcaPage, 60_000)
+
+ const userDataDir = await readUserDataDir(electronApp)
+ const generations: string[][] = []
+
+ for (let generation = 1; generation <= 5; generation++) {
+ expect(
+ killDockerSshRelayDaemon(target),
+ 'no relay process was found to kill'
+ ).toBeGreaterThan(0)
+ await expect
+ .poll(() => readSshStatus(orcaPage, remote.targetId), {
+ timeout: 120_000,
+ message: `SSH target never reconnected after relay kill ${generation}`
+ })
+ .toBe('connected')
+ await waitForActiveTerminalManager(orcaPage, 120_000)
+ // The pane must be usable again before the count is meaningful: recovery is what mints the
+ // successor lease that retires the generation before it.
+ const ptyId = await waitForActivePanePtyId(orcaPage, 120_000)
+ const marker = `LEASE_GEN_${generation}_${Date.now()}`
+ await execInTerminal(orcaPage, ptyId, `printf '%s\\n' ${marker}`)
+ await waitForTerminalOutput(orcaPage, marker, 60_000)
+
+ try {
+ await expect
+ .poll(() => readReattachablePtyIds(userDataDir, remote.targetId).length, {
+ timeout: 60_000
+ })
+ .toBe(1)
+ } catch (error) {
+ // Why re-thrown with the rows: the count alone cannot say WHICH predecessor stayed
+ // reattachable, and the user-data dir is torn down before the report is read.
+ throw new Error(
+ `reattachable lease count never settled at 1 in generation ${generation}; leases: ${describeSshLeases(userDataDir, remote.targetId)}`,
+ { cause: error }
+ )
+ }
+ generations.push(readReattachablePtyIds(userDataDir, remote.targetId))
+ }
+
+ // Stated as the whole sequence so a regression reports the growth, not just its endpoint —
+ // the reported shape was 2, 3, 4, 5, 6 across five restarts.
+ expect(
+ generations.map((ptyIds) => ptyIds.length),
+ `reattachable lease count per generation: ${JSON.stringify(generations)}`
+ ).toEqual([1, 1, 1, 1, 1])
+ } finally {
+ if (target) {
+ clearDockerSshRelayFaults(target)
+ cleanupDockerSshRelayTarget(target)
+ }
+ }
+ })
+
/**
* The third fault shape: silence with the socket still established. `docker pause` freezes the
* container, so nothing is closed or reset — the client simply stops hearing from a host that is
From dec1a1d788899499ddf0c15217ac685b62c9e662 Mon Sep 17 00:00:00 2001
From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
Date: Thu, 3 Sep 2026 20:32:48 -0400
Subject: [PATCH 07/43] fix(i18n): ship onboarding integration capability
strings in boot catalog
Jinwoo-H <73622457+Jinwoo-H@users.noreply.github.com>
---
src/renderer/src/i18n/en-runtime-required.json | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json
index 99b7f1a7870..6f3c49a21e3 100644
--- a/src/renderer/src/i18n/en-runtime-required.json
+++ b/src/renderer/src/i18n/en-runtime-required.json
@@ -3717,6 +3717,16 @@
"readyOneOne": "1 workspace found, with 1 cleanup suggestion."
}
}
+ },
+ "onboarding": {
+ "integrations": {
+ "capabilities": {
+ "browseIssues": "Browse GitHub issues and pull requests in the Tasks view without leaving Orca",
+ "managePullRequests": "Read, comment on, and merge pull requests without leaving Orca",
+ "reviewStatus": "See issue state, review status, and CI checks on every worktree",
+ "startWorkspaceFromIssue": "Start a workspace from any GitHub issue or pull request, prefilled with its title and context"
+ }
+ }
}
},
"dashboard": {
From 912463c2780237d7f51b254e50dd271684302631 Mon Sep 17 00:00:00 2001
From: Trevin Chow
Date: Thu, 3 Sep 2026 17:32:51 -0700
Subject: [PATCH 08/43] docs: document localization workflow
tmchow <517103+tmchow@users.noreply.github.com>
---
AGENTS.md | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/AGENTS.md b/AGENTS.md
index 8b0156ba6b1..6915b246bcc 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -53,6 +53,18 @@ Orca targets macOS, Linux, and Windows. Keep all platform-dependent behavior beh
- **WSL commands**: build argv with `buildWslExecArgs` (always `--exec` — under `--`, `wsl.exe` expands `$name` in every argument and silently rewrites the script), and fence anything whose stdout you parse with `buildWslCapturedLoginShellCommand`, because the interactive login shell prints the distro banner to stdout. See [`docs/reference/wsl-command-execution.md`](./docs/reference/wsl-command-execution.md).
- **Linux native modules**: keep the glibc floor at Ubuntu 20.04 / glibc 2.31. A module compiled from source on a newer runner can reference symbol versions absent on the floor and crash the app on startup. See [`docs/reference/linux-glibc-compatibility.md`](./docs/reference/linux-glibc-compatibility.md); packaging fails if a bundled native binary needs newer glibc.
+## Localization (i18n)
+
+All user-facing copy is localized. `src/renderer/src/i18n/locales/en.json` is the source of truth; the `zh`, `ja`, `ko`, and `es` catalogs mirror its keys. Strings reach the UI through `translate('auto.', 'English fallback')` — never hardcode display text.
+
+When you touch user-facing copy, keep all five catalogs in sync:
+
+- **New strings** — wrap them in `translate(...)` with an English fallback, run `pnpm sync:localization-catalog` to register the keys in `en.json` and add placeholders to every other locale, then `pnpm bootstrap:-catalog` (e.g. `bootstrap:ja-catalog`) to translate the placeholders.
+- **Reworded strings** — changing the value of an existing key updates only `en.json`. The other locales keep the key with its old translation, and **the lint checks will not catch this**: `verify:localization-catalog` enforces key _parity_, not translation _freshness_. Update the same key in `zh/ja/ko/es` by hand, or re-translate it via `bootstrap:-catalog`.
+- **Removed strings** — delete the key from _every_ locale; the parity check rejects a key that exists in one catalog but not another.
+
+Before pushing copy changes, run `pnpm verify:localization-catalog` and `pnpm verify:localization-coverage` (both also run in `pnpm lint`).
+
## SSH Use Case
All changes must consider the SSH use case. Don't assume local-only execution. Before changing anything that reports on, stops, or lists remote work, follow [`docs/reference/ssh-execution-boundary.md`](./docs/reference/ssh-execution-boundary.md): the execution host owns everything that touches execution, and loss of contact is never evidence of process death — the verdict vocabulary is `live` / `unverifiable` / `exited`, with no synonyms.
From 48cb575db58a8e66f63c8e8c375ecce5f144df4a Mon Sep 17 00:00:00 2001
From: hwantage <82494320+hwantage@users.noreply.github.com>
Date: Fri, 4 Sep 2026 09:32:55 +0900
Subject: [PATCH 09/43] feat(i18n): localize Orca Account settings and
navigation to Korean
hwantage <82494320+hwantage@users.noreply.github.com>
---
src/renderer/src/i18n/locales/ko.json | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index d05754d176e..f494ff29a58 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -10289,6 +10289,28 @@
"updateServer": "소스 기본값을 구성하려면 이 서버를 업데이트하세요.",
"updateServerDefaults": "표시 기본값을 구성하려면 이 서버를 업데이트하세요."
},
+ "orcaAccount": {
+ "connected": "연결됨",
+ "reconnectRequired": "세션이 만료되었습니다. 클라우드 기능을 사용하려면 다시 로그인하세요.",
+ "unavailable": "이 빌드에서는 Orca 로그인을 사용할 수 없습니다.",
+ "signedOut": "로그인하여 아티팩트 및 Orca Relay를 비롯한 클라우드 기능으로 Orca를 확장하세요.",
+ "checking": "계정 상태 확인 중…",
+ "account": "Orca 계정",
+ "signOut": "로그아웃",
+ "signingIn": "로그인 중…",
+ "signInAgain": "다시 로그인",
+ "signIn": "Orca 로그인",
+ "title": "Orca 계정",
+ "description": "작업을 즉시 공유하고 어디서나 Orca Mobile로 데스크톱에 연결하세요.",
+ "searchDescription": "아티팩트 및 Orca Relay에서 사용하는 계정에 로그인하거나 로그아웃합니다.",
+ "benefitsTitle": "계정에 포함된 기능",
+ "artifactsTitle": "아티팩트 공유",
+ "artifactsDescription": "HTML 및 Markdown 파일을 게시하고 Orca에서 모든 공유 링크를 관리합니다.",
+ "relayTitle": "Orca Relay",
+ "relayDescription": "셀룰러 또는 모든 Wi-Fi 네트워크를 통해 Orca Mobile을 이 데스크톱에 연결합니다.",
+ "skillsTitle": "스킬 공유",
+ "skillsDescription": "단일 스킬 또는 전체 세트를 비공개 링크로 공유하고 사용하는 모든 기기에 설치하세요."
+ },
"automations": {
"title": "자동화",
"description": "에이전트 작업을 예약하고 사이드바에 자동화 표시 여부를 선택합니다.",
From 49d6d35b16c998ee2e135385e6e710405b3148a7 Mon Sep 17 00:00:00 2001
From: foXaCe
Date: Fri, 4 Sep 2026 02:32:59 +0200
Subject: [PATCH 10/43] feat(i18n): add French UI locale
foXaCe <290678+foXaCe@users.noreply.github.com>
---
.gitignore | 1 +
config/scripts/bootstrap-locale-catalog.mjs | 5 +
config/scripts/locale-translation-policy.mjs | 39 +-
src/main/i18n/main-i18n.ts | 1 +
src/renderer/src/i18n/i18n.ts | 1 +
src/renderer/src/i18n/locales/en.json | 3 +-
src/renderer/src/i18n/locales/es.json | 3 +-
src/renderer/src/i18n/locales/fr.json | 16601 +++++++++++++++++
src/renderer/src/i18n/locales/ja.json | 3 +-
src/renderer/src/i18n/locales/ko.json | 3 +-
src/renderer/src/i18n/locales/zh.json | 3 +-
src/renderer/src/i18n/supported-languages.ts | 7 +-
src/shared/ui-language.test.ts | 4 +-
src/shared/ui-language.ts | 5 +-
src/shared/ui-locale.test.ts | 16 +-
src/shared/ui-locale.ts | 6 +-
16 files changed, 16685 insertions(+), 16 deletions(-)
create mode 100644 src/renderer/src/i18n/locales/fr.json
diff --git a/.gitignore b/.gitignore
index 3fb72a6486a..8be3fc5b6f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -158,6 +158,7 @@ src/renderer/src/i18n/locales/.zh-catalog-cache.json
src/renderer/src/i18n/locales/.ko-catalog-cache.json
src/renderer/src/i18n/locales/.ja-catalog-cache.json
src/renderer/src/i18n/locales/.es-catalog-cache.json
+src/renderer/src/i18n/locales/.fr-catalog-cache.json
# Bench result JSONs are working artifacts
tests/tools/benchmarks/results/terminal-pipeline-*.json
diff --git a/config/scripts/bootstrap-locale-catalog.mjs b/config/scripts/bootstrap-locale-catalog.mjs
index 05739b4da9c..e5c5fb69a81 100644
--- a/config/scripts/bootstrap-locale-catalog.mjs
+++ b/config/scripts/bootstrap-locale-catalog.mjs
@@ -34,6 +34,11 @@ const LOCALE_CONFIG = {
targetLanguage: 'es',
displayName: 'Spanish',
cacheFile: '.es-catalog-cache.json'
+ },
+ fr: {
+ targetLanguage: 'fr',
+ displayName: 'French',
+ cacheFile: '.fr-catalog-cache.json'
}
}
diff --git a/config/scripts/locale-translation-policy.mjs b/config/scripts/locale-translation-policy.mjs
index 9fd4350ee45..cec2ebf63ad 100644
--- a/config/scripts/locale-translation-policy.mjs
+++ b/config/scripts/locale-translation-policy.mjs
@@ -217,10 +217,41 @@ export const NEVER_TRANSLATE_VALUES = new Set([
])
export const NATIVE_PICKER_LABELS = {
- zh: { chinese: '中文(简体)', korean: '한국어', japanese: '日本語', spanish: 'Español' },
- ko: { chinese: '中文(简体)', korean: '한국어', japanese: '日本語', spanish: 'Español' },
- ja: { chinese: '中文(简体)', korean: '한국어', japanese: '日本語', spanish: 'Español' },
- es: { chinese: '中文(简体)', korean: '한국어', japanese: '日本語', spanish: 'Español' }
+ zh: {
+ chinese: '中文(简体)',
+ korean: '한국어',
+ japanese: '日本語',
+ spanish: 'Español',
+ french: 'Français'
+ },
+ ko: {
+ chinese: '中文(简体)',
+ korean: '한국어',
+ japanese: '日本語',
+ spanish: 'Español',
+ french: 'Français'
+ },
+ ja: {
+ chinese: '中文(简体)',
+ korean: '한국어',
+ japanese: '日本語',
+ spanish: 'Español',
+ french: 'Français'
+ },
+ es: {
+ chinese: '中文(简体)',
+ korean: '한국어',
+ japanese: '日本語',
+ spanish: 'Español',
+ french: 'Français'
+ },
+ fr: {
+ chinese: '中文(简体)',
+ korean: '한국어',
+ japanese: '日本語',
+ spanish: 'Español',
+ french: 'Français'
+ }
}
const CJK_LATIN_SPACED_TERM_PATTERN = CJK_LATIN_SPACED_TERMS.join('|')
diff --git a/src/main/i18n/main-i18n.ts b/src/main/i18n/main-i18n.ts
index 8ebe21e0545..4fb548986b4 100644
--- a/src/main/i18n/main-i18n.ts
+++ b/src/main/i18n/main-i18n.ts
@@ -25,6 +25,7 @@ const LAZY_LOCALE_LOADERS: Record<
() => Promise<{ default: Record }>
> = {
es: () => import('../../renderer/src/i18n/locales/es.json'),
+ fr: () => import('../../renderer/src/i18n/locales/fr.json'),
ja: () => import('../../renderer/src/i18n/locales/ja.json'),
ko: () => import('../../renderer/src/i18n/locales/ko.json'),
zh: () => import('../../renderer/src/i18n/locales/zh.json')
diff --git a/src/renderer/src/i18n/i18n.ts b/src/renderer/src/i18n/i18n.ts
index f7fd94f2a55..725a40e9aaa 100644
--- a/src/renderer/src/i18n/i18n.ts
+++ b/src/renderer/src/i18n/i18n.ts
@@ -26,6 +26,7 @@ const NON_DEFAULT_LOCALE_LOADERS: Record<
() => Promise<{ default: Record }>
> = {
es: () => import('./locales/es.json'),
+ fr: () => import('./locales/fr.json'),
ja: () => import('./locales/ja.json'),
ko: () => import('./locales/ko.json'),
zh: () => import('./locales/zh.json')
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 57db52974bd..87fcb50a2a1 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -119,7 +119,8 @@
"chinese": "中文(简体)",
"korean": "한국어",
"japanese": "日本語",
- "spanish": "Español"
+ "spanish": "Español",
+ "french": "Français"
},
"statusBar": {
"claudeToggleDescription": "Show Claude token and cost usage for the active workspace.",
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index 0ac5d624ede..9999634daee 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -20,7 +20,8 @@
"chinese": "中文(简体)",
"korean": "한국어",
"japanese": "日本語",
- "spanish": "Español"
+ "spanish": "Español",
+ "french": "Français"
},
"statusBar": {
"claudeToggleDescription": "Muestra el consumo de tokens y el costo de Claude para el espacio de trabajo activo.",
diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json
new file mode 100644
index 00000000000..ed9307b30db
--- /dev/null
+++ b/src/renderer/src/i18n/locales/fr.json
@@ -0,0 +1,16601 @@
+{
+ "app": {
+ "recoverableError": {
+ "rootTitle": "Orca a rencontré une erreur de rendu.",
+ "rootDescription": "Le shell de l'application n'a pas pu terminer son rendu. Réessayez pour le remonter, ou relancez Orca si l'erreur persiste.",
+ "webTitle": "Orca web a rencontré une erreur de rendu.",
+ "webDescription": "Réessayez le client web ou reconnectez-vous au runtime appairé."
+ }
+ },
+ "browser": {
+ "loadFailure": {
+ "connectionNotSecure": "La connexion n'est pas sécurisée",
+ "cantReachHost": "Impossible d'accéder à {{value0}}",
+ "cantLoadPage": "Impossible de charger cette page",
+ "certificateNameMismatch": "Le certificat ne correspond pas à {{value0}}.",
+ "certificateDateInvalid": "Le certificat de {{value0}} n'est pas valide à la date et à l'heure actuelles.",
+ "certificateAuthorityInvalid": "Orca ne fait pas confiance à l'autorité qui a émis le certificat de {{value0}}.",
+ "certificateVerificationFailed": "Orca n'a pas pu vérifier le certificat de {{value0}}.",
+ "trustedCertificateGuidance": "Pour le développement local, utilisez si possible un certificat local de confiance.",
+ "retry": "Réessayer",
+ "copyAddress": "Copier l'adresse",
+ "addressCopied": "Adresse de la page actuelle copiée.",
+ "openExternally": "Ouvrir en externe",
+ "tryHttps": "Essayer HTTPS",
+ "proceedUnsafe": "Continuer quand même (non sécurisé)",
+ "connecting": "Connexion…",
+ "certificateChallengeExpired": "Cette approbation de certificat a expiré. Rechargez la page pour en demander une nouvelle.",
+ "certificateChallengeChanged": "La demande de certificat a changé. Rechargez la page et examinez le nouvel avertissement.",
+ "certificateChallengeUnavailable": "Cette demande de certificat n'est plus disponible. Rechargez la page pour en demander une nouvelle.",
+ "certificateProceedFailed": "Orca n'a pas pu approuver cette demande de certificat. Rechargez la page et réessayez."
+ },
+ "guestRecovery": {
+ "title": "Page du navigateur arrêtée",
+ "failed": "La page du navigateur s'est arrêtée de manière inattendue. Réessayez pour la restaurer."
+ }
+ },
+ "githubChecks": {
+ "retrying": "Nouvelle tentative…"
+ },
+ "settings": {
+ "appearance": {
+ "language": {
+ "title": "Langue",
+ "description": "Choisissez la langue utilisée par l'interface d'Orca.",
+ "system": "Système",
+ "english": "English",
+ "chinese": "中文(简体)",
+ "korean": "한국어",
+ "japanese": "日本語",
+ "spanish": "Español",
+ "french": "Français"
+ },
+ "statusBar": {
+ "claudeToggleDescription": "Afficher l'utilisation des tokens et des coûts de Claude pour l'espace de travail actif.",
+ "codexToggleDescription": "Afficher l'utilisation des tokens et des coûts de Codex pour l'espace de travail actif.",
+ "geminiToggleDescription": "Afficher l'utilisation des tokens et des coûts de Gemini pour l'espace de travail actif.",
+ "opencodeGoToggleDescription": "Afficher l'utilisation des tokens et des coûts d'OpenCode Go pour l'espace de travail actif.",
+ "kimiToggleDescription": "Afficher l'utilisation de l'abonnement Kimi pour l'espace de travail actif.",
+ "minimaxToggleDescription": "Afficher l'utilisation de l'abonnement MiniMax pour l'espace de travail actif.",
+ "sshToggleDescription": "Afficher les hôtes SSH et les hôtes Orca distants configurés lorsqu'ils sont disponibles.",
+ "resourceUsageToggleDescription": "Afficher le gestionnaire de ressources. Cliquez dessus pour le CPU, la mémoire, les sessions, les contrôles des daemons et les analyses disque des espaces de travail.",
+ "portsToggleDescription": "Afficher les ports actifs de l'espace de travail. Cliquez dessus pour les ports par espace de travail et les écouteurs externes.",
+ "antigravityToggleDescription": "Afficher l'utilisation de l'abonnement Antigravity pour l'espace de travail actif.",
+ "grokToggleDescription": "Afficher l'utilisation des crédits de l'abonnement Grok lorsque vous êtes connecté via Grok CLI."
+ },
+ "menuBarIcon": {
+ "title": "Afficher l'icône dans la barre de menus",
+ "description": "Garder un raccourci Orca et un indicateur d'activité dans la barre de menus macOS."
+ }
+ }
+ },
+ "menu": {
+ "checkForUpdates": "Rechercher les mises à jour...",
+ "settings": "Paramètres",
+ "exploreOrca": "Découvrir Orca",
+ "gettingStarted": "Premiers pas avec Orca",
+ "reportCrash": "Signaler un plantage...",
+ "file": "Fichier",
+ "exit": "Quitter",
+ "edit": "Édition",
+ "appearance": "Apparence",
+ "toggleLeftSidebar": "Basculer la barre latérale gauche",
+ "toggleRightSidebar": "Basculer la barre latérale droite",
+ "showStatusBar": "Afficher la barre d'état",
+ "showTasksButton": "Afficher le bouton Tâches",
+ "showAutomationsButton": "Afficher le bouton Automatisations",
+ "showMobileButton": "Afficher le bouton Orca Mobile",
+ "showTitlebarAppName": "Afficher le nom de l'app dans la barre de titre",
+ "view": "Affichage",
+ "reload": "Recharger",
+ "forceReload": "Forcer le rechargement",
+ "resetSize": "Réinitialiser la taille",
+ "zoomIn": "Zoom avant",
+ "zoomOut": "Zoom arrière",
+ "openWorktreePalette": "Ouvrir la palette des worktrees",
+ "window": "Fenêtre",
+ "help": "Aide",
+ "paste": "Coller",
+ "copy": "Copier",
+ "selectAll": "Tout sélectionner"
+ },
+ "tray": {
+ "openOrca": "Ouvrir Orca",
+ "quit": "Quitter",
+ "minimizeNotice": {
+ "body": "Orca continue de tourner dans la zone de notification"
+ },
+ "activityWaiting": "Orca - activité en attente",
+ "activityWaitingSuffix": "activité en attente"
+ },
+ "worktreeJumpPalette": {
+ "matchLabel": {
+ "comment": "Commentaire",
+ "issue": "Issue",
+ "mr": "MR",
+ "port": "Port",
+ "pr": "PR",
+ "task": "Tâche",
+ "automation": "Exécution"
+ },
+ "linearIssue": {
+ "createLabel": "Créer un worktree à partir de l'issue Linear {{value0}} : {{value1}}",
+ "pendingLabel": "Créer un worktree à partir de l'issue Linear {{value0}}",
+ "loadingLabel": "Chargement de l'issue Linear {{value0}}",
+ "createHint": "Créer un worktree à partir d'une issue Linear",
+ "loadingHint": "Chargement de l'issue Linear…"
+ },
+ "renderCapOverflow": "{{value0}} autres — faites défiler ou continuez à taper pour affiner",
+ "filter": {
+ "emptyTitle": "Aucun résultat ne correspond au filtre actif",
+ "emptySubtitle": "Effacez le filtre ci-dessus, ou élargissez-le à davantage d'hôtes et de projets.",
+ "tabKey": "Tab",
+ "label": "Filtre",
+ "activeCount": "{{value0}} actif(s)",
+ "removeChip": "Supprimer le filtre {{value0}}",
+ "chipOverflow": "+{{value0}}",
+ "clearAll": "Tout effacer",
+ "hosts": "Hôtes",
+ "projects": "Projets",
+ "trigger": "Filtrer les résultats",
+ "search": "Filtrer par hôte ou projet...",
+ "searchHosts": "Filtrer les hôtes...",
+ "searchProjects": "Filtrer les projets...",
+ "noOptions": "Aucun hôte ni projet correspondant",
+ "noHosts": "Aucun hôte correspondant",
+ "noProjects": "Aucun projet correspondant",
+ "moreOptions": "{{value0}} autres - continuez à taper pour affiner",
+ "selectAllMatching": "Sélectionner toutes les correspondances ({{value0}})",
+ "selectedCollapsed": "{{value0}} sélectionnés — supprimez-les via les chips ou via Effacer",
+ "clearHosts": "Effacer les hôtes",
+ "clearProjects": "Effacer les projets",
+ "back": "Retour",
+ "ariaActive": "Filtre : {{value0}} actif(s)."
+ },
+ "taskUrl": {
+ "loadingHint": "Chargement de {{value0}}…",
+ "createHint": "Créer un worktree à partir de {{value0}}"
+ }
+ },
+ "auto": {
+ "App": {
+ "221a95ba38": "Relancez l'onboarding, ou fermez-le et continuez dans l'app.",
+ "f02d37278a": "Une erreur est survenue dans l'onboarding.",
+ "acd66311dc": "Utilisez le menu Aide après avoir réessayé si vous avez toujours besoin d'un diagnostic.",
+ "722d03aa62": "La boîte de dialogue de rapport de plantage a rencontré une erreur.",
+ "8a023cea1f": "Réessayez la barre d'état pour remonter ses contrôles.",
+ "2e8ff36f94": "La barre d'état a rencontré une erreur.",
+ "7cbfbf622f": "Réessayez l'espace de travail flottant, ou fermez-le et rouvrez-le.",
+ "1b3024bcd6": "L'espace de travail flottant a rencontré une erreur.",
+ "8d1e160ed1": "Réessayez la barre latérale ou changez d'onglet pour recharger cette vue.",
+ "ed6b168d00": "La barre latérale droite a rencontré une erreur.",
+ "03a14f6b5b": "Réessayez la page ou naviguez vers une autre vue Orca.",
+ "b7a714db1e": "Cette page a rencontré une erreur.",
+ "98d4ea2823": "Le rendu du terminal, du navigateur ou de l'éditeur a échoué dans cet espace de travail. Réessayez pour le remonter.",
+ "5a9519aef0": "Le workbench de l'espace de travail a rencontré une erreur.",
+ "cba0fafda5": "La page active reste ouverte. Réessayez la liste ou changez de vue.",
+ "1468601e7b": "La liste des espaces de travail a rencontré une erreur.",
+ "bdc71dddc9": "L'espace de travail actif reste ouvert. Réessayez la liste ou changez de vue.",
+ "c1cf0b0e4a": "Réduire le volet",
+ "8504ddf267": "L'app tourne toujours. Réessayez le shell ou utilisez le menu pour signaler les détails du plantage.",
+ "df1d56bf87": "Le shell de l'espace de travail a rencontré une erreur.",
+ "9e0b441a91": "Basculer la barre latérale droite",
+ "e81217c1b7": "Masquer le nom de l'app",
+ "5096cbbc86": "Orca",
+ "8b0b8eb54f": "Menu de l'application",
+ "caea5b51b9": "Redémarrer maintenant",
+ "0a9e810705": "Les modifications ne seront enregistrées qu'après redémarrage. Vos onglets précédents sont en sécurité sur le disque.",
+ "12e77cf12b": "Échec de la restauration de session",
+ "332dbfa497": "Espace de travail téléversé",
+ "e960d18540": "Fermer",
+ "c9d6f98459": "Agrandir",
+ "66f0a552e5": "Restaurer",
+ "bbb7f90669": "Réduire",
+ "d54e66004c": "terminal",
+ "9f0152563e": "mobile",
+ "62ca9895a7": "espace",
+ "844eb0f4f4": "activité",
+ "3443924e91": "automatisations",
+ "4f08ae8311": "tâches",
+ "ca6c6eece7": "skills",
+ "1b9d9d065f": "paramètres",
+ "c184e056de": "Basculer la barre latérale droite ({{value0}})",
+ "f7aa73e785": "Suivant ({{value0}})",
+ "cf9099fe98": "Suivant",
+ "fe21e8f6f5": "Précédent ({{value0}})",
+ "064bd07810": "Précédent",
+ "ce37cf5279": "Basculer la barre latérale ({{value0}})",
+ "e4b9e7dff7": "Basculer la barre latérale",
+ "pluginCommandFailed": "Impossible d'exécuter la commande du plugin."
+ },
+ "web": {
+ "WebConnect": {
+ "b411ec0069": "Se connecter",
+ "2cf9e5a294": "Effacer le serveur enregistré",
+ "4a4c017be1": "Point de terminaison :",
+ "27393856e4": "orca://pair?code=...",
+ "7a566540de": "URL ou code d'appairage",
+ "cb4d287238": "Nom du serveur",
+ "3affe7de3a": "Collez une URL d'appairage provenant d'un serveur Orca accessible depuis ce navigateur.",
+ "e3bcd082ac": "Se connecter à Orca",
+ "mobileScopeRejected": "Ce code QR accorde un accès limité (mobile). Pour utiliser l'application web complète, ouvrez le lien d'accès navigateur depuis Paramètres → Environnements d'exécution → Partager ce serveur Orca → Nouveau lien."
+ },
+ "webPreloadApi": {
+ "aiVaultUnavailableForHost": "L'historique des sessions d'agent n'est pas disponible pour cet hôte d'exécution.",
+ "runtimeEnvironmentManuallyDisconnected": "L'environnement d'exécution est déconnecté manuellement.",
+ "loopbackPairingBlocked": "Ce lien d'accès pointe vers cet appareil lui-même.",
+ "remotePairingUnreachable": "Impossible de joindre Orca à l'adresse {{endpoint}}.",
+ "remotePairingInvalidDetails": "Ce lien d'accès contient des détails de connexion invalides.",
+ "remotePairingSaveFailed": "Orca a vérifié l'hôte mais n'a pas pu l'enregistrer. Vérifiez le stockage du navigateur et réessayez."
+ },
+ "web": {
+ "preload": {
+ "api": {
+ "31bfe8ae1a": "Indisponible dans le client web.",
+ "67ec964791": "L'import de cookies est indisponible dans le client web.",
+ "275a776357": "L'extraction au survol est indisponible dans le client web.",
+ "8dfcb7a351": "Les captures de sélection sont indisponibles dans le client web.",
+ "31bea294d5": "Le mode capture est indisponible dans le client web.",
+ "b8a1618172": "La génération du détail de pull request est indisponible dans le client web.",
+ "e57c82d276": "La découverte des modèles de messages de commit est indisponible dans le client web.",
+ "9fc90740b6": "La génération des messages de commit est indisponible dans le client web.",
+ "52bee9d8a0": "Des raccourcis personnalisés en conflit ont été ignorés : {{value0}}.",
+ "32f15bdb0f": "Plateforme inconnue « {{value0}} » ignorée.",
+ "0a69fcd8bc": "« platforms » doit être un objet avec des sections darwin, linux ou win32.",
+ "10898045f3": "Raccourci pour « {{value0}} » ignoré : utilisez un tableau de chaînes.",
+ "36761d9604": "Action de raccourci inconnue « {{value0}} » ignorée.",
+ "d2e43e426a": "{{value0}} doit être un objet.",
+ "fb290366b2": "Indisponible sur le web.",
+ "76122208ca": "Raccourci pour « {{value0}} » ignoré : {{value1}}"
+ }
+ },
+ "runtime": {
+ "environment": {
+ "07f788de83": "WebSocket"
+ }
+ }
+ }
+ },
+ "store": {
+ "slices": {
+ "browser": {
+ "d175274b6d": "Nouvel onglet de navigateur",
+ "08fc23631d": "Navigateur",
+ "remoteCookieImportUnavailable": "L'import manuel d'un fichier de cookies est indisponible tant qu'un runtime distant est actif."
+ },
+ "editor": {
+ "dcb521ed29": "Ce fichier est en état de conflit, mais aucun fichier de l'arbre de travail n'est disponible à l'édition.",
+ "conflictPlaceholderGuidance": "Résolvez le conflit dans Git ou restaurez l'un des deux côtés avant de le rouvrir.",
+ "51f15c37d3": "Impossible d'ouvrir le répertoire : {{value0}}",
+ "f2e00db373": "Fichier introuvable : {{value0}}",
+ "checkRunDetailsUnavailable": "Aucun détail disponible pour cette vérification.",
+ "checkRunDetailsLoadFailed": "Échec du chargement des détails de la vérification.",
+ "checkRunDetailsRepoUnavailable": "Les détails du dépôt sont indisponibles pour cette vérification."
+ },
+ "github": {
+ "f129c42773": "GitHub n'a pas renvoyé le nouveau commentaire.",
+ "683a21264b": "La ligne n'a pas de owner/repo/number.",
+ "83f9b126ad": "Le type d'issue ne peut être défini que sur des issues.",
+ "f963485d37": "Ligne introuvable",
+ "a967f23983": "Vue de projet non chargée",
+ "87020f6605": "La ligne n'a pas de owner/repo/number — impossible de modifier l'élément sous-jacent",
+ "d49ef4b944": "Échec de l'enregistrement de la préférence de source d'issue"
+ },
+ "sparse": {
+ "presets": {
+ "ef13e994e6": "Les préréglages doivent être chargés avant l'enregistrement.",
+ "6ed7d6010a": "Échec de la suppression du préréglage",
+ "ee434d7941": "Préréglage supprimé",
+ "c96b770172": "Échec de l'enregistrement du préréglage",
+ "811be06b57": "Échec de la mise à jour du préréglage",
+ "0696d13e56": "Préréglage enregistré",
+ "e10f097822": "Préréglage mis à jour"
+ }
+ },
+ "store": {
+ "test": {
+ "helpers": {
+ "b9a8117c33": "Terminal 1"
+ }
+ }
+ },
+ "workspace": {
+ "cleanup": {
+ "9d6e531da6": "L'espace de travail n'existe plus.",
+ "changedSinceConfirmation": "L'espace de travail a changé après confirmation. Actualisez pour le passer en revue avant de le supprimer.",
+ "hostCollision": "Erreur : cet espace de travail existe sur plusieurs hôtes au même chemin",
+ "hostUnresolved": "Orca ne peut pas déterminer quel hôte possède cet espace de travail. Actualisez les projets et examinez-le à nouveau.",
+ "gitStatusUnavailable": "Orca n'a pas pu vérifier le statut git de cet espace de travail. Réessayez, ou supprimez-le depuis la barre latérale propre à son hôte ou depuis la liste des projets.",
+ "gitStatusUnavailableOlderPeer": "Orca n'a pas pu rattacher cet échec de statut git à un hôte. Mettez à jour l'homologue connecté le plus ancien, ou supprimez l'espace de travail depuis la barre latérale propre à son hôte ou depuis la liste des projets.",
+ "forceNeedsApproval": "Examinez et confirmez cet espace de travail avant de le supprimer de force."
+ }
+ },
+ "worktrees": {
+ "5a58e03a26": "« {{value0}} » supprimé.",
+ "d1d78a7baa": "Git n'a pas pu supprimer la branche « {{value0}} »{{value1}} sans risque, Orca l'a donc conservée pour éviter de perdre des commits locaux.",
+ "4e6496f3d2": "{{value0}} supprimé, branche conservée",
+ "e50495aae6": "Forcer la suppression de la branche",
+ "889487d8bb": "Ignorer",
+ "f4503ca505": "Ouvrez Paramètres > Git et réessayez.",
+ "34a03a6565": "Garder {{value0}} à jour",
+ "fa9299a66f": "Votre nouveau worktree est à jour, mais le {{value0}} local est en retard de {{value1}} {{value2}}. Les diffs IA peuvent omettre des commits récents.",
+ "14bc053a47": "Le {{value0}} local n'a pas été actualisé",
+ "localBaseRefRefreshFailedForWorktree": "Le {{value0}} local n'a pas été actualisé pour « {{value1}} »",
+ "4a18052018": "Le {{value0}} local est en retard sur {{value1}}",
+ "903b51c2ed": "Espace de travail créé à partir de {{value0}}, mais Orca n'a pas pu avancer le {{value1}} local en fast-forward. {{value2}}",
+ "localBaseRefRefreshFailedDescriptionNamed": "L'espace de travail « {{value0}} » a été créé à partir de {{value1}}, mais Orca n'a pas pu avancer le {{value2}} local en fast-forward. {{value3}}",
+ "localBaseRefRefreshFailedDetailDirtyNamed": "Le worktree situé dans {{value0}} (où le {{value1}} local est en checkout) contient des changements non commités. Committez-les, stashez-les ou abandonnez-les, puis mettez à jour le {{value1}} local manuellement.",
+ "localBaseRefRefreshFailedDetailDirty": "Le worktree où le {{value0}} local est en checkout contient des changements non commités. Committez-les, stashez-les ou abandonnez-les, puis mettez à jour le {{value0}} local manuellement.",
+ "localBaseRefRefreshFailedDetailNotFastForward": "Le {{value0}} local n'existe pas ou ne peut pas être avancé proprement en fast-forward depuis la base distante. Vérifiez les commits présents uniquement en local avant de le mettre à jour manuellement.",
+ "localBaseRefRefreshFailedDetailError": "Git a renvoyé une erreur lors de la mise à jour du {{value0}} local. Vérifiez que le dépôt n'a pas de refs verrouillées ni d'état de worktree inhabituel, puis mettez à jour le {{value0}} local manuellement.",
+ "0216895fb5": "Échec de la suppression de la branche",
+ "c6cf133786": "Cet espace de travail n'est plus disponible.",
+ "19db0085fb": "Branche locale supprimée",
+ "2b0afc7f14": "Impossible de garder le {{value0}} local à jour",
+ "670864ab52": "Mise à jour du {{value0}} local en cours",
+ "5366d13eec": "Espace de travail supprimé, branche conservée",
+ "2e17f825d4": "Worktree supprimé, branche conservée",
+ "78e08cd877": "Git n'a pas pu supprimer la branche « {{value0}} » sans risque, Orca l'a donc conservée pour éviter de perdre des commits locaux.",
+ "3b57982bf6": "Git n'a pas pu supprimer la branche « {{value0}} » sans risque après la suppression de l'espace de travail « {{value1}} », Orca l'a donc conservée pour éviter de perdre des commits locaux.",
+ "81f13f48d2": "Git n'a pas pu supprimer la branche « {{value0}} » sans risque après la suppression du worktree « {{value1}} », Orca l'a donc conservée pour éviter de perdre des commits locaux.",
+ "runtimeScopeForbiddenTitle": "Cette connexion a un accès limité (mobile)",
+ "runtimeScopeForbiddenDescription": "Les espaces de travail sont indisponibles sur un appairage à périmètre mobile. Reconnectez-vous via le lien d'accès navigateur depuis Paramètres → Environnements d'exécution → Partager ce serveur Orca.",
+ "a17f4d2e93": "Impossible de mettre à jour cet espace de travail.",
+ "preservedBranchCleanupHostAmbiguous": "Plusieurs nettoyages de branches conservées sont en attente pour « {{value0}} » ; précisez l'hôte.",
+ "metadata": {
+ "worktree": {
+ "meta": {
+ "persist": {
+ "877e3638d8": "Mettez à jour le runtime distant pour modifier l'issue liée à cet espace de travail",
+ "4367540861": "Mettez à jour le runtime distant pour lier des issues Linear"
+ }
+ }
+ }
+ }
+ },
+ "repos": {
+ "2975400634": "Mettez à jour le serveur Orca pour ouvrir des dossiers non Git sur ce runtime.",
+ "b7e14472ae": "Échec de l'ajout du dossier",
+ "e649269645": "Utilisez Ajouter un projet pour saisir un chemin sur l'hôte sélectionné.",
+ "c6e022ddfc": "Échec de l'ajout du projet",
+ "90d129b48b": "Dossier ajouté",
+ "8bb3ad7935": "Projet ajouté",
+ "a8e4b3af5b": "Projet déjà ajouté",
+ "6d3318e813": "Échec de l'import des dépôts",
+ "3be0f7df04": "Impossible d'ouvrir le dossier sur le runtime sélectionné",
+ "15cf5319ec": "{{path}} a été vérifié sur {{hostName}}, mais cet hôte n'a pas signalé de dossier exploitable.",
+ "2dcd706774": "Le projet existe aussi dans un autre profil",
+ "presenceProfileOverflow": "{{names}} +{{count}} autres",
+ "removeProjectFailed": "Échec de la suppression du projet"
+ },
+ "settings": {
+ "e12dab333b": "Échec du changement de serveur",
+ "faa8fb83dd": "Enregistrez ou fermez les onglets d'éditeur non enregistrés avant de changer de serveur."
+ },
+ "ui": {
+ "66e3bd7ce6": "Envoyé à {{value0}}",
+ "53883b7bc3": "Impossible d'envoyer à {{value0}}"
+ },
+ "jira": {
+ "856083302c": "La connexion Jira a été remplacée par une requête plus récente."
+ },
+ "linear": {
+ "37d36984d0": "La connexion Linear a été remplacée par une requête plus récente."
+ },
+ "orca": {
+ "profiles": {
+ "612f7f6861": "Échec de la création du profil",
+ "319d7cf39b": "Profil cloud créé",
+ "d6e764e7db": "Reconnecter ce profil",
+ "f0c9e11a6d": "Échec de la création du profil cloud",
+ "8b8fa73174": "La connexion à Orca Cloud n'est pas configurée",
+ "33290e88ed": "Échec de la connexion du profil",
+ "9fcb07a796": "Profil connecté",
+ "2f6c78a039": "Échec de l'actualisation de l'authentification du profil",
+ "a37b5e6d37": "Déconnecté du profil",
+ "83600521e7": "Échec de la déconnexion",
+ "76deec8f58": "Échec du changement d'organisation",
+ "7d4bc516ee": "Échec du changement de profil",
+ "f518e89aa5": "Le projet existe déjà dans ce profil",
+ "f03ae7f27b": "Échec du transfert du projet"
+ }
+ },
+ "runtime": {
+ "status": {
+ "runtimeHostDisconnectedDescription": "Vérifiez qu'Orca tourne sur ce serveur et que votre connexion réseau fonctionne, puis réessayez.",
+ "runtimeHostUnreachableNamed": "Impossible de joindre {{hostName}}",
+ "runtimeHostUnreachable": "Impossible de joindre le serveur Orca",
+ "tryAgain": "Réessayer"
+ }
+ },
+ "terminal": {
+ "quick": {
+ "command": {
+ "hosts": {
+ "5b7d781d67": "Échec de l'enregistrement de la commande rapide"
+ }
+ }
+ }
+ }
+ }
+ },
+ "lib": {
+ "agent": {
+ "catalog": {
+ "5dff448636": "OpenClaw",
+ "8a9ba743cc": "Hermes",
+ "4e63c7b956": "Rovo Dev",
+ "bee242fe3d": "Qwen Code",
+ "ca73055bd0": "Mistral Vibe",
+ "28810273af": "Kimi",
+ "739a930554": "Droid",
+ "667c104cff": "Cursor",
+ "9e2a9bb87b": "Continuer",
+ "6f8056a565": "Command Code",
+ "4238b771b5": "Codebuff",
+ "cbaf0c2e0b": "Cline",
+ "1f8a19e9ad": "Autohand Code",
+ "5e8eff11b3": "Auggie",
+ "9477377a2a": "Charm",
+ "e0247254f2": "Kiro",
+ "918ba4ffed": "Kilocode",
+ "c73c573939": "Amp",
+ "8da11d876c": "Goose",
+ "b32627f09b": "Aider",
+ "691dd11789": "Antigravity",
+ "12e6baa4f7": "Gemini",
+ "09973b4d84": "OMP",
+ "302934c5d9": "Pi",
+ "e7a4ca5103": "OpenCode",
+ "706b0fe68b": "GitHub Copilot",
+ "0baad2d5d2": "Grok",
+ "760bc6883d": "Codex",
+ "a5fc0cb622": "OpenClaude",
+ "bf53f09bf8": "Claude Agent Teams",
+ "0708ed89f1": "Claude",
+ "fc80296033": "Devin",
+ "da41abbdd4": "Ante",
+ "060d152fb5": "Trae",
+ "d443a47995": "Prime Agent",
+ "mimo_code_label": "MiMo Code"
+ },
+ "skill": {
+ "cli": {
+ "prerequisite": {
+ "79371593b0": "Orca CLI n'est pas encore visible dans le PATH",
+ "e99d7dc36f": "L'enregistrement d'Orca CLI demande votre attention",
+ "2db0bd7515": "L'enregistrement d'Orca CLI est indisponible",
+ "8d6eedf97e": "Échec de l'enregistrement d'Orca CLI dans le PATH.",
+ "0f116999f1": "Redémarrez votre shell ou ajoutez le répertoire d'Orca CLI au PATH avant la configuration.",
+ "15cbedc3e3": "Installez Orca CLI avant de lancer la configuration des skills d'agent.",
+ "windowsPathUnknown": "Orca n'a pas pu vérifier votre PATH utilisateur Windows",
+ "refreshCliRegistration": "Actualisez le statut d'enregistrement du CLI et réessayez."
+ }
+ }
+ }
+ },
+ "remotePairingCopy": {
+ "invalidInput": "Saisissez un lien d'accès Orca ou un code d'appairage brut.",
+ "mobileOnly": "Ce lien accorde un accès mobile uniquement. Générez un lien pour un autre client Orca.",
+ "invalidDestination": "Ce lien d'accès contient une destination invalide.",
+ "unsupportedDestination": "Ce lien d'accès contient une destination non prise en charge.",
+ "nonConnectableDestination": "Ce lien d'accès contient une destination non joignable.",
+ "loopback": "Boucle locale",
+ "tailscale": "Adresse Tailscale",
+ "lan": "Adresse LAN privée",
+ "public": "Adresse publique",
+ "custom": "Nom d'hôte personnalisé"
+ },
+ "ensure": {
+ "simulator": {
+ "tab": {
+ "372d21d428": "Émulateur mobile"
+ }
+ }
+ },
+ "fix": {
+ "checks": {
+ "agent": {
+ "launch": {
+ "027228a06b": "Impossible de trouver un espace de travail pour ces vérifications.",
+ "fb6c294e85": "Impossible de construire la commande de lancement de l'agent.",
+ "03c1d61f83": "Impossible d'ouvrir l'espace de travail associé à ces vérifications.",
+ "822bf52295": "Impossible de résoudre la plateforme de lancement de l'espace de travail.",
+ "dfb4dd7c00": "Impossible de trouver l'espace de travail associé à ces vérifications.",
+ "9f00d7df0c": "Le prompt de correction des checks est vide. Mettez à jour les paramètres d'IA du contrôle de code source.",
+ "2ebf794906": "Aucun agent IA activé n'a été détecté sur l'hôte de cet espace de travail.",
+ "4c7f783a7a": "L'agent de checks enregistré n'est pas disponible sur l'hôte de cet espace de travail."
+ }
+ }
+ }
+ },
+ "floating": {
+ "workspace": {
+ "tab": {
+ "creation": {
+ "f3785eddc2": "Nouvel onglet de navigateur"
+ }
+ }
+ }
+ },
+ "launch": {
+ "agent": {
+ "in": {
+ "new": {
+ "tab": {
+ "11cce5cc77": "Impossible de lancer {{value0}} dans un nouveau terminal.",
+ "a5a1f7033f": "Votre {{value0}} n'a pas été envoyé — collez-le dès que l'agent est prêt."
+ }
+ }
+ },
+ "background": {
+ "session": {
+ "4ca0651d56": "Votre prompt d'automatisation n'a pas été envoyé — ouvrez l'espace de travail et collez-le."
+ }
+ }
+ },
+ "worktree": {
+ "background": {
+ "terminals": {
+ "setupTitle": "Configuration"
+ }
+ }
+ },
+ "work": {
+ "item": {
+ "direct": {
+ "3de6371df3": "Impossible de construire la commande de lancement de l'agent.",
+ "67e103dd60": "L'espace de travail a été créé mais n'a pas pu être activé.",
+ "19c7683acf": "L'agent sélectionné n'est pas disponible dans l'espace de travail créé.",
+ "8bc45efdbc": "Échec de la résolution du head de la pull request.",
+ "agent": {
+ "ceeeb509b5": "L'agent a mis trop de temps à démarrer. L'espace de travail est prêt — collez le {{value0}} quand l'agent est inactif."
+ }
+ }
+ }
+ }
+ },
+ "local": {
+ "path": {
+ "open": {
+ "guard": {
+ "edc1908653": "L'ouverture de chemins distants dans l'OS local n'est pas disponible."
+ }
+ }
+ }
+ },
+ "open": {
+ "in": {
+ "app": {
+ "catalog": {
+ "f8b8ca2711": "Zed",
+ "d62b12e98a": "Cursor",
+ "173553f73a": "VS Code"
+ }
+ }
+ },
+ "mobile": {
+ "emulator": {
+ "tab": {
+ "bf4f2a8a72": "Impossible de démarrer l'émulateur. Vérifiez la configuration de l'émulateur iOS ou Android et essayez un autre appareil."
+ }
+ }
+ }
+ },
+ "orchestration": {
+ "usage": {
+ "examples": {
+ "f91fe27f2a": "Scinder un changement volumineux en pull requests plus petites",
+ "9e37a5b1b3": "Exécuter des travaux indépendants en parallèle",
+ "bddc4c09b8": "Exécuter un workflow par phases",
+ "ab0e9803b7": "Transférer vers un autre worktree",
+ "5e0d489fe1": "Transférer une tâche active",
+ "handoffSummary": "Confier la responsabilité à un autre agent avec suffisamment de contexte pour continuer.",
+ "worktreeHandoffSummary": "Déplacer le travail vers un agent qui tourne déjà dans une autre branche.",
+ "childSequenceSummary": "Enchaîner des agents enfants les uns après les autres lorsque chaque phase dépend de la précédente.",
+ "childParallelSummary": "Répartir entre agents enfants des tâches d'investigation ou d'implémentation qui ne se chevauchent pas.",
+ "prSplitSummary": "Donner à chaque agent enfant son propre worktree pour que l'implémentation en parallèle reste relisible."
+ }
+ }
+ },
+ "pr": {
+ "comment": {
+ "audience": {
+ "64deee36a9": "Bots",
+ "a7150a17bc": "Humains",
+ "27ce73211c": "Tous",
+ "empty": {
+ "bot": "Aucun commentaire de bot.",
+ "human": "Aucun commentaire humain.",
+ "all": "Aucun commentaire pour le moment."
+ }
+ }
+ },
+ "bot": {
+ "author": {
+ "overrides": {
+ "6d5d52b53f": "Limite de remplacement d'auteur de bot atteinte"
+ }
+ }
+ }
+ },
+ "resume": {
+ "sleeping": {
+ "agent": {
+ "session": {
+ "f235f604fd": "Cette session d'agent ne peut pas être reprise."
+ }
+ }
+ }
+ },
+ "source": {
+ "control": {
+ "agent": {
+ "action": {
+ "plan": {
+ "3f0ea9aa0d": "Impossible de construire la commande de lancement de l'agent.",
+ "46f1a2c9bd": "La saisie de commande est vide.",
+ "8eb541cc83": "L'agent sélectionné n'a pas été détecté sur l'hôte de cet espace de travail.",
+ "b96e091fc9": "L'agent sélectionné est désactivé dans les Paramètres.",
+ "a7ac8717c7": "Choisissez un agent avant de démarrer."
+ }
+ }
+ },
+ "generation": {
+ "plan": {
+ "dc480d5897": "La saisie de commande est vide."
+ }
+ }
+ }
+ },
+ "sparse": {
+ "preset": {
+ "draft": {
+ "5915a0a1f6": "Utilisez des répertoires relatifs au dépôt, pas la racine, des chemins absolus ni des segments parent.",
+ "efc05d1820": "Ajoutez au moins un répertoire."
+ }
+ }
+ },
+ "terminal": {
+ "shortcut": {
+ "capture": {
+ "notification": {
+ "b0536028c9": "Ouvrir les raccourcis",
+ "141ad6c004": "Raccourci terminal traité",
+ "0ab0cd001a": "size-4 text-muted-foreground"
+ }
+ }
+ }
+ },
+ "workspace": {
+ "create": {
+ "error": {
+ "format": {
+ "37cf0bc991": "Orca n'a pas pu résoudre une ref de base exploitable pour cet espace de travail.",
+ "64555d0014": "Aucune branche de base trouvée"
+ }
+ }
+ },
+ "browser": {
+ "tab": {
+ "open": {
+ "urlFailed": "Impossible d'ouvrir l'URL.",
+ "searchFailed": "Impossible d'effectuer la recherche avec {{value0}}."
+ }
+ }
+ }
+ },
+ "worktree": {
+ "palette": {
+ "search": {
+ "9ccec2316b": "Issue",
+ "ca40ffcbec": "PR",
+ "0b01ff98d2": "Port",
+ "7d732521ec": "Commentaire"
+ }
+ },
+ "activation": {
+ "cannotOpenFolderWorkspace": "Impossible d'ouvrir l'espace de travail de type dossier"
+ }
+ },
+ "folderWorkspacePathStatus": {
+ "title": {
+ "missing": "Dossier introuvable",
+ "notDirectory": "Le chemin n'est pas un dossier",
+ "ambiguousConnection": "Impossible de déterminer la connexion",
+ "unavailable": "Impossible de vérifier le dossier",
+ "unrecognized": "Le dossier n'est pas exploitable"
+ },
+ "description": {
+ "missing": "Orca ne trouve pas {{path}}. Supprimez puis réimportez cet espace de travail de type dossier.",
+ "notDirectory": "{{path}} existe, mais ce n'est pas un dossier.",
+ "ambiguousConnection": "Orca ne peut pas déterminer quelle connexion SSH possède ce périmètre de dossier.",
+ "unavailable": "Orca ne peut pas vérifier ce dossier pour le moment. Vérifiez le runtime ou la connexion SSH, puis réessayez.",
+ "unrecognized": "Orca ne peut pas utiliser {{path}}, et cette version ne reconnaît pas la raison. Mettez Orca à jour pour voir les détails."
+ },
+ "createError": {
+ "title": {
+ "missing": "Dossier introuvable",
+ "notDirectory": "Le chemin n'est pas un dossier",
+ "ambiguousConnection": "Impossible de déterminer la connexion",
+ "unavailable": "Impossible de vérifier le dossier",
+ "generic": "Échec de la création de l'espace de travail de type dossier"
+ },
+ "description": {
+ "missing": "Orca ne trouve pas {{path}}. Supprimez puis réimportez le dossier.",
+ "notDirectory": "{{path}} existe, mais ce n'est pas un dossier.",
+ "ambiguousConnection": "Orca ne peut pas déterminer quelle connexion SSH possède ce périmètre de dossier.",
+ "unavailable": "Orca ne peut pas vérifier ce dossier pour le moment. Vérifiez le runtime ou la connexion SSH, puis réessayez."
+ }
+ }
+ },
+ "projectSkillRuntime": {
+ "wslUnavailable": "Le runtime du projet nécessite WSL avant de pouvoir installer ce skill.",
+ "distroRequired": "Sélectionnez une distro WSL pour ce projet avant d'installer ce skill.",
+ "distroMissing": "La distro WSL sélectionnée est indisponible. Choisissez une distro disponible ou basculez ce projet sur Windows.",
+ "wslDefault": "WSL par défaut"
+ },
+ "sidebarWorktreeActivation": {
+ "wakeEphemeralVmFailed": "Échec du réveil de l'espace de travail VM éphémère"
+ },
+ "ephemeralVmWorkspaceTarget": {
+ "projectRootRegistrationFailed": "Échec de l'enregistrement sur le runtime de la racine de projet créée par la recette.",
+ "provisionedRootRequiresSsh": "Les recettes à racine provisionnée nécessitent actuellement une connexion SSH directe."
+ },
+ "blocked": {
+ "notification": {
+ "fallback": {
+ "de50bef680": "macOS bloque les notifications Orca"
+ }
+ }
+ },
+ "linear": {
+ "usage": {
+ "examples": {
+ "readTicket": "Lire le ticket lié",
+ "postUpdate": "Publier un point d'avancement",
+ "moveState": "Faire avancer le ticket",
+ "attachPr": "Joindre le lien de review",
+ "triageFollowups": "Trier et créer des tickets de suivi",
+ "readTicketSummary": "Récupérer tout le contexte de l'issue Linear liée avant de commencer le travail.",
+ "readTicketPrompt": "Utilisez {{value0}} pour lire l'issue Linear liée à ce worktree, puis résumez l'objectif et les critères d'acceptation avant de commencer.",
+ "postUpdateSummary": "Commenter la progression ou un résumé d'achèvement vers l'issue Linear.",
+ "postUpdatePrompt": "Utilisez {{value0}} pour publier une mise à jour d'achèvement sur l'issue Linear associée, avec ce qui a changé et comment cela a été vérifié.",
+ "moveStateSummary": "Faites avancer l'état du workflow Linear au fur et à mesure de la progression des travaux.",
+ "moveStatePrompt": "Utilisez {{value0}} pour passer l'issue Linear associée à In Review maintenant que la modification est prête.",
+ "attachPrSummary": "Liez la pull request ou merge request à l'issue Linear au moment de l'ouvrir.",
+ "attachPrPrompt": "Utilisez {{value0}} pour rattacher cette pull request ou merge request à l'issue Linear associée.",
+ "triageFollowupsSummary": "Définissez l'assigné, la priorité ou l'estimation, et créez des tickets de suivi rattachés.",
+ "triageFollowupsPrompt": "Utilisez {{value0}} pour trier l'issue Linear associée — définir la priorité et l'estimation — et créer un ticket de suivi rattaché pour le nettoyage différé."
+ }
+ },
+ "issue": {
+ "workspace": {
+ "open": {
+ "4f2c1d8a3b": "Impossible d'ouvrir l'espace de travail associé à cette issue."
+ }
+ }
+ }
+ },
+ "codex": {
+ "session": {
+ "restart": {
+ "4bd4a3a9c7": "Valeur par défaut du système",
+ "9f0b1c2d3e": "Compte Codex"
+ }
+ }
+ },
+ "browser": {
+ "cookie": {
+ "import": {
+ "toast": {
+ "restartFallbackUnavailableNone": "Aucun des {{value0}} cookies n'a pu être chargé, et la solution de repli par redémarrage était indisponible. Les cookies précédents de ce profil ont été remplacés. Réessayez l'importation.",
+ "restartFallbackUnavailablePartial": "{{value0}} cookies sur {{value1}} ont été importés. Le reste n'a pas pu être chargé, et la solution de repli par redémarrage était indisponible. Réessayez l'importation.",
+ "googleCookiesSkipped": "Les cookies Google n'ont pas été importés. Ouvrez un navigateur dans Orca sur {{value0}} avec ce profil, puis connectez-vous à Google.",
+ "undecryptableAppBound": "Orca ne peut pas déchiffrer {{value0}} cookies de ce navigateur car ils utilisent un chiffrement lié à l'application. Vous pouvez importer des cookies depuis un fichier avec « Depuis un fichier… ».",
+ "undecryptableAppBoundMixed": "Orca ne peut pas déchiffrer {{value0}} cookies de ce navigateur car ils utilisent un chiffrement lié à l'application ; {{value1}} autres n'ont pas pu être déchiffrés pour une autre raison. Vous pouvez importer des cookies depuis un fichier avec « Depuis un fichier… ».",
+ "undecryptableKeyring": "{{value0}} cookies n'ont pas pu être déchiffrés car le trousseau système était indisponible. Déverrouillez votre trousseau de connexion (ou installez un fournisseur Secret Service tel que gnome-keyring), puis importez à nouveau.",
+ "undecryptableKeyringMixed": "{{value0}} cookies n'ont pas pu être déchiffrés car le trousseau système était indisponible ; {{value1}} autres n'ont pas pu être déchiffrés pour une autre raison. Déverrouillez votre trousseau de connexion (ou installez un fournisseur Secret Service tel que gnome-keyring), puis importez à nouveau.",
+ "undecryptableUnknown": "{{value0}} cookies n'ont pas pu être déchiffrés et ont été ignorés. Fermez complètement le navigateur source, puis réessayez l'importation.",
+ "unrecognizedWarning": "L'importation des cookies s'est terminée avec un avertissement que cette version d'Orca ne reconnaît pas. Mettez Orca à jour pour voir les détails, puis vérifiez ce profil avant de vous fier à ses cookies.",
+ "undecryptableUnrecognizedReason": "{{value0}} cookies n'ont pas pu être déchiffrés et ont été ignorés pour une raison que cette version d'Orca ne reconnaît pas. Mettez Orca à jour pour voir les détails, puis réessayez l'importation.",
+ "partitionSkipped": "{{value0}} cookies n'ont pas été importés car leur partition de site n'a pas pu être lue. Connectez-vous à nouveau à ces sites dans Orca."
+ }
+ }
+ }
+ },
+ "pluginCommandKeybindings": {
+ "group": "Plugins"
+ },
+ "feedback": {
+ "image": {
+ "attachments": {
+ "fallbackName": "Pièce jointe image",
+ "unsupportedType": "{{fileName}} n'est pas un type d'image pris en charge.",
+ "empty": "{{fileName}} est vide.",
+ "tooLarge": "{{fileName}} dépasse {{maxSize}}.",
+ "tooMany": "Vous pouvez joindre jusqu'à {{maxCount}} images.",
+ "dimensionsTooLarge": "{{fileName}} a des dimensions trop grandes pour un aperçu sans risque.",
+ "invalidImage": "{{fileName}} n'est pas une image valide prise en charge.",
+ "additionalErrors": "{{count}} images supplémentaires n'ont pas pu être jointes."
+ }
+ }
+ },
+ "ephemeralVmWorktreeCreation": {
+ "sparseCheckoutUnsupported": "Les recettes à racine provisionnée ne prennent pas en charge le sparse checkout."
+ }
+ },
+ "hooks": {
+ "useAutomationDispatchEvents": {
+ "59718b120b": "L'espace de travail cible n'est plus disponible.",
+ "16a21d6413": "La reconnexion SSH nécessite des identifiants interactifs.",
+ "386db94f3e": "Le projet cible n'est plus disponible.",
+ "3ad7d77f57": "L'espace de travail cible se trouve sur un hôte différent de la cible de cette exécution d'automatisation."
+ },
+ "useComposerState": {
+ "7eb3f44ff7": "L'agent sélectionné est désactivé. Choisissez un agent activé avant de créer.",
+ "b2ead86962": "Échec de la résolution de la base de la PR.",
+ "a9ff236145": "Certaines pièces jointes n'ont pas pu être envoyées.",
+ "3db83fc58a": "Aucun chemin de projet n'est disponible sur cet hôte pour les pièces jointes.",
+ "ba6cb77082": "Échec de la connexion au projet.",
+ "chooseOrAddProjectBeforeWorkspace": "Choisissez ou ajoutez un projet avant de créer un espace de travail.",
+ "folderWorkspaceCreateFailedTitle": "Échec de la création de l'espace de travail de dossier",
+ "folderWorkspaceCreateFailedMessage": "L'espace de travail de dossier n'a pas pu être créé. Vérifiez les détails de l'erreur ci-dessus, puis réessayez.",
+ "setupAgentStartupPolicySaveFailed": "Échec de l'enregistrement du comportement de démarrage de la configuration.",
+ "5f3d2c8a1b": "Échec de la résolution de la base de la MR."
+ },
+ "useGlobalFileDrop": {
+ "38c9f034ff": "Échec de l'envoi des fichiers déposés.",
+ "d720e2f855": "Certains fichiers déposés n'ont pas pu être envoyés.",
+ "245faa95b9": "Aucun chemin de espace de travail distant n'est disponible pour les fichiers déposés.",
+ "nativeDropTooManyPathsDescription": "Déposez {{value0}} fichiers maximum à la fois.",
+ "nativeDropTooManyPaths": "Le dépôt contient trop de fichiers.",
+ "nativeDropPathsTooLargeDescription": "Déposez moins de fichiers ou utilisez une liste de chemins plus courte.",
+ "nativeDropPathsTooLarge": "La liste de chemins du dépôt est trop grande.",
+ "ownerChanged": "Impossible de vérifier quel hôte possède cet espace de travail. Réessayez après sa reconnexion."
+ },
+ "useIpcEvents": {
+ "0e3cf53060": "Onglet de navigateur {{value0}} introuvable",
+ "2f6637fe6c": "L'onglet de navigateur {{value0}} est épinglé",
+ "a8d2bf8e9e": "Aucun onglet de navigateur actif à fermer",
+ "291c8ed902": "Les onglets de navigateur sont indisponibles tant qu'un runtime distant est actif",
+ "f45fa2b03c": "Les profils de navigateur sont indisponibles tant qu'un runtime distant est actif",
+ "f000b2ff76": "Aucun worktree actif",
+ "56d3ec4203": "Échec de la création du fichier markdown sans titre.",
+ "f6300deb8b": "Nouvel onglet de navigateur",
+ "7a64b31991": "La création d'un terminal local est indisponible tant qu'un runtime distant est actif",
+ "60428567b4": "L'affichage d'un terminal local est indisponible tant qu'un runtime distant est actif",
+ "f8aaf2bde3": "Espace de travail téléversé",
+ "2fe88c2e06": "Synchronisation de l'espace de travail distant indisponible",
+ "workspaceChangedOnAnotherDevice": "Espace de travail modifié sur un autre appareil",
+ "2ec42e1c52": "Pas encore de espace de travail distant",
+ "88214a785b": "La synchronisation de l'espace de travail a attendu l'hydratation de la session locale et a expiré",
+ "4f78ba5885": "Espace de travail synchronisé",
+ "ef223fbb6b": "Un appareil a tenté de se connecter mais n'est pas jumelé",
+ "11992d0337": "S'il s'agissait de votre téléphone ou d'un autre client Orca, réappairez-le depuis Paramètres → Mobile.",
+ "6573cfe955": "Ouvrir les paramètres mobiles",
+ "unresolvedTerminalWorktreeOwner": "La création de terminal est indisponible car le propriétaire du worktree n'a pas pu être déterminé"
+ },
+ "useSettingsNavigationMetadata": {
+ "4a728cd56b": "Nouvelles fonctionnalités encore en cours de construction. Essayez-les.",
+ "225071c560": "Expérimental",
+ "e338c507c1": "Paramètres de compatibilité bas niveau pour le dépannage.",
+ "580a04cd81": "Avancé",
+ "8400cfe1c1": "Données d'utilisation anonymes et contrôles de télémétrie.",
+ "3618579df6": "Confidentialité & télémétrie",
+ "65ec7d1968": "Accès de confidentialité macOS pour les outils de développement lancés depuis le terminal.",
+ "d91ae31fbd": "Autorisations macOS",
+ "95a1886d94": "Contrôlez les terminaux et les agents depuis votre téléphone.",
+ "1cd25673df": "Mobile",
+ "31e57d1c70": "Utilisez des machines existantes via SSH pour les fichiers, les terminaux, Git et les espaces de travail.",
+ "94a5afe910": "Hôtes SSH",
+ "40d80bad8a": "Beta",
+ "de0c2907a1": "Serveurs Orca distants",
+ "b351014180": "Statistiques Orca, plus analyses de tokens Claude, Codex, OpenCode et utilisation d'abonnement Grok.",
+ "d72a58b5b9": "Statistiques & usage",
+ "dcd0d9b74f": "Raccourcis clavier pour les actions courantes.",
+ "94295ebfb3": "Raccourcis",
+ "7682607591": "Notifications natives de bureau pour les événements d'agent et de terminal.",
+ "2eece16ad1": "Notifications",
+ "1f452cbd4c": "Comportement de sélection et d'édition.",
+ "0c6ee88a5f": "Saisie & édition",
+ "b11a5a48a2": "Thème, zoom, apparence de l'app et du terminal, barres latérales et barre d'état.",
+ "93d88d20bf": "Apparence",
+ "2d0659f6f0": "Onglets globaux de terminal, de navigateur et de markdown.",
+ "65b19f5bde": "Espace de travail flottant",
+ "3d65d3f1b9": "Configurez la prise en charge des émulateurs mobiles pour Orca et les agents de codage.",
+ "1e761cff2b": "Émulateur mobile",
+ "e815fd01bd": "Page d'accueil, routage des liens et cookies de session.",
+ "8c197f74a1": "Navigateur",
+ "42ae40842f": "Commandes de terminal enregistrées, globales ou par projet.",
+ "3fc3db144f": "Commandes rapides",
+ "c33bfd664c": "Shells, moteur de rendu, sessions et comportement du terminal.",
+ "a9fb10afca": "Terminal",
+ "5235c215ca": "Choisissez les fournisseurs de tâches affichés dans la page Tâches et la barre latérale.",
+ "85f4fd7710": "Sources de tâches",
+ "ab4b21b58e": "Nommage des branches, refs de base et Git AI Author.",
+ "09607cb0fe": "Git & gestion de code source",
+ "33a5e1d597": "Connectez GitHub, GitLab, Linear et les services d'hébergement de sources.",
+ "2b043783ef": "Intégrations",
+ "2cd4ea75da": "Valeurs par défaut des espaces de travail, configuration de l'app et maintenance.",
+ "13241992bd": "Général",
+ "724c440e72": "prise en main",
+ "0505d0df29": "démarrer avec Orca",
+ "ea0b1bc7b8": "guide de configuration",
+ "17005c73d4": "Ouvrez la checklist d'intégration pour les étapes de configuration et les jalons.",
+ "ded9e9032f": "Checklist d'intégration",
+ "5f32ac08f3": "Terminez la checklist d'intégration pour les workflows essentiels d'Orca.",
+ "8ac3de82f5": "Dictée vocale locale avec modèles embarqués sur l'appareil.",
+ "6a50cdcd7c": "Voix",
+ "0059bd17f3": "Permettez aux agents de contrôler n'importe quelle app de votre ordinateur.",
+ "b35e92364b": "Computer Use",
+ "cd50cec5d7": "Coordonnez plusieurs agents de codage via Orca.",
+ "58a868e8e4": "Orchestration",
+ "7c79d3b7bf": "Facultatif",
+ "b1c2f8b0ac": "Configuration facultative de changement de compte et d'utilisation pour Claude, Codex, Gemini, OpenCode Go, MiniMax et Grok.",
+ "f70ac54d38": "Comptes de fournisseurs d'IA",
+ "4121f7a0a2": "Gérez les agents IA, définissez-en un par défaut et personnalisez les commandes.",
+ "b49abbd2f7": "Agents",
+ "dev": "Outils dev",
+ "devDescription": "Outils réservés au développement pour exercer les états de l'UI.",
+ "devBadge": "Dev",
+ "devSearchNotificationPlayground": "Terrain d'essai des notifications",
+ "devSearchNotificationPlaygroundDescription": "Déclenche des états d'UI représentatifs de toast et de notification.",
+ "devSearchKeywordDev": "dev",
+ "devSearchKeywordToast": "toast",
+ "devSearchKeywordSonner": "sonner",
+ "devSearchKeywordError": "error",
+ "devSearchKeywordNotification": "notification",
+ "projectHostsSummary": "{{value0}} hôtes",
+ "linearTitle": "Linear",
+ "linearDescription": "Fonctionnement de Linear dans Orca, checklist de configuration, skill d'agent et exemples de prompts.",
+ "pluginsTitle": "Plugins",
+ "pluginsDescription": "Installez et gérez les plugins Orca expérimentaux.",
+ "tasksDescription": "Connectez des fournisseurs, installez le skill Linear et choisissez ce qui apparaît dans Tâches.",
+ "artifactsTitle": "Artefacts",
+ "artifactsDescription": "Partagez des fichiers HTML et Markdown avec votre équipe et gérez leurs liens publics.",
+ "automationsTitle": "Automatisations",
+ "automationsDescription": "Planifiez le travail des agents et choisissez si Automatisations apparaît dans la barre latérale.",
+ "shareSkillsTitle": "Partage de skills",
+ "shareSkillsDescription": "Partagez vos skills avec un lien non répertorié. Toute personne qui l'a peut les installer.",
+ "floatingWorkspaceWebDescription": "Onglets globaux de terminal et de markdown."
+ },
+ "useAppMenuPaste": {
+ "pasteTooLarge": "Le collage est trop volumineux."
+ },
+ "useLargeTextControlPaste": {
+ "pasteTooLarge": "Le collage est trop volumineux."
+ },
+ "useInstalledAgentSkills": {
+ "unreadableSkillSource": "Un dossier de skill n'a pas répondu ; cet état peut donc être incomplet."
+ },
+ "useMacosTccPromptNotice": {
+ "title": "Vous voyez des invites « Orca souhaite accéder à… » ?",
+ "description": "Des messages d'autorisation macOS peuvent apparaître quand un agent ou un outil de terminal exécuté dans Orca tente d'accéder à des fichiers protégés. Accordez l'Accès complet au disque dans les paramètres pour réduire ces invites.",
+ "openSettings": "Ouvrir les paramètres",
+ "dismiss": "Ne plus afficher"
+ },
+ "useMacTccAttributionSeveredNotice": {
+ "title": "Les autorisations macOS peuvent ne pas s'appliquer aux terminaux Orca",
+ "description": "Les terminaux Orca en cours d'exécution sont hébergés par un daemon démarré par une installation précédente d'Orca. macOS peut ne pas leur appliquer les autorisations Accessibilité, Automatisation ou fichiers protégés d'Orca. Redémarrez le daemon depuis Gérer les sessions pour restaurer l'accès. Cela fermera tous les terminaux Orca en cours d'exécution.",
+ "openManageSessions": "Ouvrir Gérer les sessions",
+ "dismiss": "Ignorer"
+ }
+ },
+ "components": {
+ "BrowserCookieImportDisclosure": {
+ "title": "Connexions Google non importées",
+ "description": "Connectez-vous à Google directement dans Orca."
+ },
+ "CodexRestartChip": {
+ "a4c8e1b2f7": "Codex est toujours connecté en tant que {{value0}}",
+ "c72a5fb234": "Redémarrer",
+ "9263e75f49": "Codex utilise le compte précédent",
+ "d3e8a1f4b2": "Compte changé",
+ "9375620cc3": "Redémarrez cette session pour utiliser {{value0}}. Elle reste sur le compte précédent tant que vous ne l'avez pas fait.",
+ "6133594b12": "Conserver l'ancien compte",
+ "8f0d5c92a1": "Configuration Codex modifiée",
+ "3ea91b5c07": "Cette session Codex utilise une configuration obsolète",
+ "e6b7139d2a": "Redémarrez cette session pour charger votre configuration Codex actuelle.",
+ "7b1d20f4c8": "Conserver la session actuelle"
+ },
+ "FirstLaunchBanner": {
+ "b9e1b966c7": "Masquer l'avis",
+ "94cc673726": "Compris",
+ "fc5cc29955": "Refuser",
+ "d1deebb050": "Politique de confidentialité",
+ "958d2cc31b": "Des comptages anonymes des fonctionnalités que vous utilisez nous aident à prioriser ce qu'il faut construire. Aucun contenu de fichier, prompt, sortie de terminal ni rien qui vous identifie. Modifiable à tout moment dans Paramètres -> Confidentialité & télémétrie.",
+ "9784b4d7bc": "Aidez-nous à décider quoi construire ensuite",
+ "fcbee32f08": "Avis de télémétrie"
+ },
+ "GitHubItemDialog": {
+ "3ab6ac0fc8": "Prévisualisez et modifiez l'issue ou la pull request GitHub sélectionnée.",
+ "3cd5ae5b7b": "Aucun fichier modifié.",
+ "filesUnavailable": "Impossible de charger les fichiers modifiés.",
+ "filesRetry": "Réessayer",
+ "999b5ad7d9": "Fichiers",
+ "4bd1f5b055": "Vérifications",
+ "e30a5470c9": "Conversation",
+ "474c59b4b3": "Fermer · Esc",
+ "45af57999b": "Fermer l'aperçu",
+ "3fdf777817": "Ouvrir sur GitHub",
+ "c43fe79ee0": "Copier le lien GitHub",
+ "0caac1a18f": "Démarrer un espace de travail à partir de la PR",
+ "8223320f8d": "mis à jour",
+ "10ef1afb8e": "· mis à jour",
+ "55962099bc": "a ouvert cette issue",
+ "0ab4664a8b": "Démarrer un espace de travail à partir de l'issue",
+ "36182aa57f": "Démarrer un nouveau espace de travail",
+ "fe6ff12dc2": "Plus d'actions de espace de travail pour l'issue",
+ "726db41722": "Ouvrir l'espace de travail",
+ "84855fedd0": "Ouvrir l'espace de travail associé à l'issue",
+ "b7bf31b8de": "Échec de la synchronisation de l'état consulté avec GitHub.",
+ "c0253318d6": "Impossible de synchroniser l'état consulté de cette pull request.",
+ "5fea151559": "Échec de la copie du lien GitHub",
+ "2e77dc2053": "Lien GitHub copié",
+ "2ef631437e": "Impossible d'ouvrir l'espace de travail associé à cette issue.",
+ "bf43425540": "Commentaire",
+ "0a73f59e85": "Envoyer le commentaire",
+ "c5c117270e": "Ajouter un commentaire…",
+ "082515176a": "Échec de l'ajout du commentaire",
+ "c6f37a563d": "+ Assigné",
+ "f41ec96c13": "+ Étiquette",
+ "ab050dffec": "Fermé",
+ "dc1ca081a8": "Ouvert",
+ "2e4d806c92": "Espace de travail",
+ "886a64b081": "Aucun pour l'instant",
+ "4ba0132f37": "Modifier les étiquettes",
+ "217e55d87c": "Étiquettes",
+ "c67de9e2fe": "Personne n'est assigné",
+ "76adcf5fe2": "Modifier les assignés",
+ "83ac703dda": "Assignés",
+ "00ccdf9b5a": "Statut",
+ "2aa9acdf34": "Modifier les étiquettes sur GitHub",
+ "e52bed9264": "Aucune vérification signalée pour l'instant",
+ "90020cc1f3": "Cette pull request n'a aucune vérification signalée pour l'instant.",
+ "ecffebc251": "Aucune vérification trouvée",
+ "checkOpenInBrowser": "Ouvrir dans le navigateur",
+ "744197c84d": "Aucune sortie en ligne n'est disponible pour cette vérification.",
+ "08d072664d": "Jobs",
+ "96d8f36798": "Annotations",
+ "485609c4f2": "vérification #",
+ "0f478f5efa": "Terminé",
+ "4812814bc8": "Démarré",
+ "9c3ba11a05": "Statut :",
+ "934d87ab96": "Chargement des détails de la vérification…",
+ "71c11aff84": "Relancer toutes les vérifications",
+ "e31651a224": "Relancer les vérifications échouées",
+ "1b56e28faa": "Relancer",
+ "f4b1292569": "Démarrer l'agent IA par défaut sur ces vérifications",
+ "9a1004fc76": "Actualiser les vérifications",
+ "03e542fcfe": "Échec du démarrage d'un agent IA pour les vérifications cassées : {{value0}}",
+ "28986b3747": "Un agent IA a été démarré pour les vérifications cassées.",
+ "1690fd7f4a": "Aucune vérification cassée à corriger.",
+ "9e7c221b8d": "Échec de la relance des vérifications",
+ "e463ec935f": "Relances des vérifications demandées",
+ "ddafe851e1": "Relance de la vérification demandée",
+ "0bbdc673c1": "Échec de l'actualisation des vérifications",
+ "e7007aa1d8": "Impossible d'actualiser les vérifications sans chemin de dépôt.",
+ "675bc0d638": "Annuler",
+ "a18f669c7a": "{{value0}} {{value1}} réaction{{value2}}",
+ "53fe19aefc": "Ouvrir la zone de merge GitHub",
+ "a2495e4784": "Pull request",
+ "ce360fc318": "Échec de la désactivation de l'auto-merge",
+ "825a8fb8cd": "Échec de l'activation de l'auto-merge",
+ "4b390bd50d": "Auto-merge désactivé",
+ "a35ea5a0f6": "Auto-merge activé",
+ "aba792c8b3": "Échec de la fusion de la pull request",
+ "dbe5e2448e": "Pull request fusionnée",
+ "a27ee5ca1a": "Cela mettra à jour la pull request sur GitHub.",
+ "03d7216d62": "{{value0}} la PR #{{value1}} ?",
+ "e9b7cb7d17": "Impossible de {{value0}} la PR",
+ "bd3b4492a0": "Pull request rouverte",
+ "9f88657c4e": "Pull request fermée",
+ "b6f1b7adbd": "Cela rouvrira la pull request sur GitHub.",
+ "de45fedf7b": "Cela fermera la pull request sur GitHub.",
+ "5a94f3d0e9": "Aucun commentaire pour le moment.",
+ "1506916c09": "Commentaires",
+ "9b9cb55994": "Aucune description fournie.",
+ "52b20b56f7": "Description",
+ "4d555d3796": "Modifier la description",
+ "9df4e74bdf": "Enregistrer",
+ "0ae387d8ca": "par",
+ "228e2f59d3": "Résolu",
+ "a154ec5224": "Ouvrir le commentaire sur GitHub",
+ "bca8eb39ac": "Répondre au commentaire",
+ "68cb993d61": "résolu",
+ "10f4ff5be8": "Réponse publiée.",
+ "745c9089ec": "Impossible de répondre sans chemin de dépôt.",
+ "58c73cb0d8": "Échec de la mise à jour de la description.",
+ "5221548274": "Description mise à jour.",
+ "06c06e58ba": "Afficher plus de lignes en dessous",
+ "307c98e8e3": "Afficher {{value0}} lignes supplémentaires au-dessus",
+ "5664681624": "Afficher plus de lignes au-dessus",
+ "b1574e8ac2": "Réinitialiser le contexte de code",
+ "d43736d09c": "Contrôles du contexte de code",
+ "bd7be7b1fd": "commentaire L",
+ "db61d76cd5": "Chargement du contexte de code…",
+ "f2d02cdf8c": "fichiers consultés",
+ "1257d1435d": "Afficher l'arborescence des fichiers",
+ "a341343303": "Commentaire de review ajouté.",
+ "d1fa2cf888": "Impossible de commenter sans le SHA head de la PR.",
+ "829674460a": "Diff indisponible car les SHA des commits de la PR manquent.",
+ "af924014f8": "Consulté",
+ "2d89a38d9d": "{{value0}} {{value1}} comme consultés",
+ "70e84e3d0b": "Aucun reviewer correspondant.",
+ "1ffce94a8b": "Tous les autres",
+ "c2b21818e1": "Suggestions",
+ "a98433e73d": "Chargement...",
+ "b0b7344684": "Demander jusqu'à 15 reviewers",
+ "934add88b6": "Reviewer",
+ "bb42774171": "Saisissez ou choisissez un utilisateur",
+ "36f9ac4a47": "Aucun reviewer demandé.",
+ "8b15a5e91c": "Retirer le reviewer {{value0}}",
+ "6a45771d47": "Chargement des reviewers",
+ "dc8a092c57": "Reviewers",
+ "e3243d9376": "A récemment modifié ces fichiers",
+ "8c45901789": "Demander le reviewer {{value0}}",
+ "fedc09eeb9": "Retirer la demande pour le reviewer {{value0}}",
+ "73487fb975": "Échec du retrait du reviewer",
+ "2e69540652": "Reviewers retirés",
+ "69515bff81": "Reviewer retiré",
+ "b4af16bf43": "Aucun contexte de dépôt disponible pour cette pull request.",
+ "c42d942b75": "Échec de la demande de reviewer",
+ "c016e4bac3": "Reviewers demandés",
+ "ea985e657f": "Reviewer demandé",
+ "12e761610e": "Vous pouvez demander jusqu'à 15 reviewers",
+ "94ab23a9f9": "Saisissez un reviewer",
+ "3853476a97": "Élément GitHub",
+ "68796dafa0": "pr",
+ "b1157c78ff": "sheet",
+ "038b3d39b1": "Copied",
+ "04539beb48": "issue",
+ "773ff70035": "unknown",
+ "3e544d966d": "Issue",
+ "88fd82474d": "page",
+ "e517b4d641": "closed",
+ "d0a05e73f5": "compact",
+ "7d42606f66": "Annotation",
+ "2511f44bb7": "Corriger les vérifications cassées",
+ "9157d48ddb": "Corriger les vérifications",
+ "06482d6190": "Impossible de créer automatiquement un espace de travail de correction.",
+ "f64dd90102": "Répondre",
+ "5752c25aff": "Publication…",
+ "ec5c4b3ab2": "Rouvrir la PR",
+ "21860b58d0": "Fermer la pull request",
+ "5932578f51": "Le merge exige un dépôt local enregistré",
+ "ce8a85d209": "default",
+ "924c2fe05e": "destructive",
+ "e2bf3e41a9": "comment",
+ "28d0d3374f": "thread",
+ "080d071d48": "Répondre à @{{value0}}",
+ "86f809e2ce": "Répondre dans ce fil de review",
+ "136542c9ba": ":L{{value0}}",
+ "283699bc82": "Échec de la publication de la réponse.",
+ "d1c0dad471": "-L{{value0}}",
+ "31770bef03": "Côte à côte",
+ "6e43a16435": "En ligne",
+ "d00a0a7f8f": "Tout réduire",
+ "3c19ec3069": "Tout développer",
+ "b0b09778c8": "Échec de l'ajout du commentaire de review.",
+ "16c1abe76c": "Marquer comme consulté",
+ "ba8e329d92": "Ne plus marquer comme consulté",
+ "3f79ffc8b7": "Ouvrez les détails de la PR pour voir les reviewers actuels.",
+ "5c1c973855": "Retirer le reviewer",
+ "commentTooLarge": "Le commentaire est trop volumineux pour être envoyé sans risque.",
+ "timeline": {
+ "completed": "comme terminé",
+ "notPlanned": "comme non prévu",
+ "someone": "quelqu'un",
+ "assigned": "a assigné",
+ "unassigned": "a désassigné",
+ "mentioned": "a mentionné ceci",
+ "in": "dans",
+ "closed": "a fermé ceci",
+ "reopened": "a rouvert ceci",
+ "moved": "a déplacé ceci",
+ "from": "de",
+ "to": "vers",
+ "activity": "Activité",
+ "noActivity": "Aucune activité pour l'instant."
+ },
+ "checkActionRequiredHint": "Cette vérification nécessite une action manuelle sur GitHub (par exemple, approuver l'exécution du workflow) avant que le merge soit débloqué.",
+ "e15a8b77ef": "Aucun détail en ligne n'est disponible pour cette vérification.",
+ "e45324fbed": "Échec du chargement des détails de la vérification.",
+ "dcb3c546fe": "Réessayer",
+ "85a2b66f54": "Ouvrir les fichiers sur GitHub",
+ "86d84a17ca": "Ajouter un commentaire de review",
+ "d9fa90b625": "Échec du chargement du diff.",
+ "4aecf121e7": "Fermer",
+ "8812225174": "Rouvrir",
+ "09d67a0f9b": "Échec de la fermeture de la pull request",
+ "88809e79db": "Échec de la réouverture de la pull request",
+ "00f55cc17b": "L'accès au dépôt est indisponible pour cette pull request."
+ },
+ "GitLabItemDialog": {
+ "65e784c1f1": "Rouvrir",
+ "a199eb364b": "Fermer",
+ "16b3412570": "Merge",
+ "131865e231": "Créer un espace de travail",
+ "f2e64d1c20": "Ouvrir dans GitLab",
+ "84012fa8fb": "Commentaire",
+ "c08e1d5a57": "Commenter {{value0}}{{value1}}…",
+ "f11e3e7675": "Aucune exécution de pipeline pour cette MR.",
+ "808b1ca1ba": "Aucun fichier modifié.",
+ "007423f585": "Contenu du diff indisponible.",
+ "a7eb4f4916": "de",
+ "21f8dde18a": "Commentaire en ligne",
+ "7a7204417f": "Ligne",
+ "ceb08a733d": "Fichier",
+ "85a8170279": "Aucun commentaire pour le moment.",
+ "14423484db": "Aucune description.",
+ "da4174b00f": "Édition",
+ "93f79a3fc1": "Enregistrer",
+ "f72fad3b16": "Annuler",
+ "717b706849": "Chargement des étiquettes",
+ "3c0b6ccca7": "bug, backend",
+ "dde24ade55": "Étiquettes",
+ "908d8d2a73": "Description",
+ "89f3f19368": "Titre",
+ "7a2117129a": "Ajouter",
+ "05939e977d": "Ajouter un reviewer",
+ "474b50d988": "Aucun reviewer.",
+ "1b19cdc510": "Retirer le reviewer {{value0}}",
+ "cb55b0390f": "Gérer",
+ "4f9313984d": "Reviewers",
+ "02cbe2de44": "Pipeline",
+ "be3d291837": "Fichiers",
+ "c996e2962c": "Conversation",
+ "b3c156dd51": "Actualiser",
+ "9bfb4a24d7": "par",
+ "30c97083c2": "Détail de l'élément de travail GitLab",
+ "e089f62594": "MR !{{value0}} fusionnée",
+ "865ea2703e": "MR !{{value0}} rouverte",
+ "9b11cd233f": "MR !{{value0}} fermée",
+ "60c13320c4": "Commentaire en ligne ajouté",
+ "ffdd9a78e1": "Les refs de diff de la MR sont indisponibles pour les commentaires en ligne.",
+ "00d0d25825": "Le fichier, la ligne et le commentaire sont requis.",
+ "ceaf7c30c7": "L'id de reviewer est indisponible pour cet utilisateur GitLab.",
+ "f7cb495a12": "{{value0}} relancé(s)",
+ "98718490e4": "Le titre de la MR est requis.",
+ "d600c2619a": "Chargement du log",
+ "028bde664e": "Masquer",
+ "2f9b27f838": "Log du job",
+ "032ae1312b": "Ouvrir le job dans GitLab",
+ "fa3e042203": "Réessayer",
+ "f23ea85341": "résolu",
+ "4186685c78": "rouvrir",
+ "cae2712a23": "clôturer",
+ "881e522e04": "merge",
+ "6de8ce0cc6": "{{value0}} requis",
+ "22511537d2": "Approuvé",
+ "00f3bab87b": " sur {{value0}} requis",
+ "11384f99aa": "nombre",
+ "40c56b95e2": "Il reste {{value0}} approbation{{value1}}",
+ "3a051b8ade": "Élément de travail",
+ "32f8bef818": "Aucune sortie de log.",
+ "4168eb2c51": "Résoudre",
+ "commentTooLarge": "Le commentaire est trop volumineux pour être envoyé sans risque."
+ },
+ "JiraIssueWorkspace": {
+ "b0b92666c9": "Commentaire",
+ "a585fd204e": "Ajouter un commentaire Jira...",
+ "2441be6f9f": "Démarrer l'espace de travail",
+ "9178090e26": "Aucun commentaire pour le moment.",
+ "5cd09beaf9": "Réessayer",
+ "9a980b06b9": "Commentaires",
+ "c4889a47e4": "Aucune description fournie.",
+ "0f3c07a901": "backend, bug",
+ "aee97b6913": "Étiquettes",
+ "444865b4a8": "Titre",
+ "0b6b5646ed": "Non assigné",
+ "51bed73f88": "Aucune priorité",
+ "7a96985ca0": "Fermer",
+ "76513c7898": "Fermer l'aperçu du ticket Jira",
+ "857bd2f88f": "Prévisualisez, modifiez et démarrez le travail depuis le ticket sélectionné.",
+ "0cc62bd690": "Copier le prompt",
+ "80efa101c5": "Copier le nom de branche suggéré",
+ "38839801e8": "Copier la clé",
+ "779bb91ee0": "Copier l'URL",
+ "69da9a208c": "Ouvrir dans Jira",
+ "fa132c8aed": "Échec de l'ajout du commentaire.",
+ "ea21952aa3": "Échec de la mise à jour du ticket Jira.",
+ "6c41a9bcea": "Échec de la copie : {{value0}}",
+ "2ff69a3545": "{{value0}} copié",
+ "666cfdd835": "Inconnu",
+ "9ebee71962": "labels",
+ "def3d0e824": "titre",
+ "b8e2079d96": "assigné",
+ "54649eaeab": "+ Assigné",
+ "2a829a2f00": "priorité",
+ "693be070d0": "transition",
+ "ef21405c6d": "Ticket Jira",
+ "commentTooLarge": "Le commentaire est trop volumineux pour être envoyé sans risque."
+ },
+ "Landing": {
+ "76a95f7f47": "Créer",
+ "f05d237049": "Ajoutez d'abord un projet",
+ "f9eaa9e12d": "Ajouter un projet",
+ "6ca6ff404e": "ORCA",
+ "520304a067": "Logo Orca",
+ "ce44fad849": "Dépendances manquantes",
+ "c1cf168479": "Masquer",
+ "00cee697c1": "Exécutez \"gh auth login\" dans un terminal pour connecter votre compte GitHub.",
+ "9f96d018b7": "GitHub CLI n'est pas authentifiée",
+ "73e1ad4282": "Orca utilise la GitHub CLI (gh) pour afficher les pull requests, les tickets et les vérifications.",
+ "5beaef5f9e": "GitHub CLI n'est pas installée",
+ "b673e7cf1b": "Git est requis pour les projets Git, le contrôle de version et la gestion des espaces de travail.",
+ "e5b7296d9d": "Git n'est pas installé",
+ "cd21242762": "Ajoutez un projet pour commencer.",
+ "9c00bd4adf": "Sélectionnez un espace de travail dans la barre latérale pour commencer.",
+ "16e9e3df89": "favori",
+ "0d0ace8861": "Mettre une étoile sur GitHub",
+ "ec43b38ba7": "Étoile ajoutée sur GitHub",
+ "157bb5ecbb": "Ouvrir GitHub",
+ "preflightDismiss": "Ignorer"
+ },
+ "LinearIssueMarkdownDescriptionEditor": {
+ "d9c47069ef": "Markdown",
+ "a7301a11f3": "enregistrer",
+ "632096eb1c": "Lien",
+ "340160f4e8": "Supprimer le lien",
+ "9eaf02ac01": "Citation",
+ "e2a0267c8c": "Liste de tâches",
+ "d6b2f3d35b": "Liste numérotée",
+ "c82917e06e": "Liste à puces",
+ "ad1869bd54": "Code en ligne",
+ "28fd951b83": "Barré",
+ "5666b4493d": "Italique",
+ "caa88f50d0": "Gras",
+ "dddaa7a0a6": "Titre 2",
+ "e3f741d258": "Titre 1",
+ "68a41d5665": "Texte courant",
+ "7c52151156": "Mise en forme de la description du ticket",
+ "5c16ec8f14": "URL du lien",
+ "4f2fddc2b7": "Aucune description fournie."
+ },
+ "LinearIssueTextEditor": {
+ "947ba2d6f4": "pour enregistrer",
+ "04d73b72dc": "Titre du ticket",
+ "e8ff595db3": "Échec de la mise à jour de {{value0}}",
+ "1e08a1ec80": "Le titre est obligatoire",
+ "00fa439dc7": "titre",
+ "75294b07d1": "description"
+ },
+ "LinearIssueWorkspace": {
+ "ad5dec37b7": "Prévisualisez, modifiez et démarrez le travail depuis le ticket sélectionné.",
+ "c23e79e5c0": "Actions",
+ "b0eac92d85": "Réessayer",
+ "fabbd3f974": "a mis à jour le ticket ·",
+ "543970c87a": "Activité",
+ "df4c86ed12": "Fermer",
+ "7a4997d8bb": "Fermer l'aperçu du ticket Linear",
+ "e1e0a9bca9": "Démarrer l'espace de travail",
+ "30a7f56c0a": "Démarrer un espace de travail à partir de l'issue",
+ "30c1242f3a": "Copier l'identifiant",
+ "9e3c49beb8": "Copier l'identifiant du ticket",
+ "9a9a884236": "Copier l'URL",
+ "97c19a84f1": "Copier l'URL Linear",
+ "f63ef94ea8": "Tickets",
+ "f6c6381593": "Copier le prompt",
+ "5d670ec8dc": "Copier le nom de branche suggéré",
+ "937ba6ad9a": "Chargement des projets",
+ "db3f269d98": "Rechercher des projets",
+ "b51276c8d6": "Projet",
+ "8b5b593053": "Échec de la mise à jour du projet",
+ "f9d4ef9807": "Projet mis à jour",
+ "38b80780c2": "Échec du chargement des projets",
+ "42589845bc": "Créer",
+ "c182e02de5": "Titre du sous-ticket",
+ "8c55d6696a": "Ajouter des sous-tickets",
+ "b25e453c9d": "Échec de la création du sous-ticket",
+ "aeed19d003": "{{value0}} créé",
+ "9a1317cdd3": "Échec du chargement du sous-ticket",
+ "9bcbaa2737": "Échec de la copie : {{value0}}",
+ "7835483c43": "{{value0}} copié",
+ "61f424f8ca": "Ticket Linear",
+ "ca8778c124": "Inconnu",
+ "8a33c85e9c": "Quelqu'un",
+ "f5a6b38a14": "sheet",
+ "65239a714b": "Linear",
+ "af6e02c44a": "page",
+ "76ffd3c937": "Recherchez un projet à ajouter.",
+ "c11b4e3cc2": "Aucun projet trouvé.",
+ "519c3587f3": "Ajouter au projet",
+ "openAttachedWorkspace": "Ouvrir l'espace de travail associé à l'issue",
+ "openWorkspace": "Ouvrir l'espace de travail",
+ "openOnLinear": "Ouvrir dans Linear",
+ "moreWorkspaceActions": "Plus d'actions de espace de travail pour l'issue",
+ "startNewWorkspace": "Démarrer un nouveau espace de travail",
+ "workspaceSection": "Espace de travail",
+ "noWorkspaceYet": "Aucun pour l'instant"
+ },
+ "LinearItemDrawer": {
+ "04008e6c46": "Démarrer un espace de travail à partir de l'issue",
+ "a4fcc57522": "Aucun commentaire pour le moment.",
+ "fde849b2b6": "Commentaires",
+ "9dc54172db": "Fermer · Esc",
+ "0190b760c1": "Ouvrir dans Linear",
+ "04a442f796": "Prévisualisez et modifiez le ticket Linear sélectionné.",
+ "d369841269": "Envoyer le commentaire",
+ "2fcff829a8": "Ajouter un commentaire…",
+ "2820f0f0f0": "Laisser un commentaire...",
+ "6ab35eafd5": "Échec de l'ajout du commentaire",
+ "367f828482": "Aucun label trouvé",
+ "cddd9b04a7": "Chargement des étiquettes",
+ "23886c7eec": "Ajouter un label",
+ "7f7b89b631": "Labels : {{value0}}",
+ "b2376d0179": "Chargement des membres",
+ "866316f22c": "Non assigné",
+ "b5675b0694": "Enregistrer",
+ "ceeb8c6153": "Effacer",
+ "fbb90300e2": "Estimation personnalisée",
+ "780ea6ed89": "Aucun état trouvé",
+ "59b6cd3706": "Chargement des états",
+ "64bfffc4dd": "Étiquettes",
+ "dd304de85a": "Propriétés",
+ "0be31fef8e": "L'estimation doit être un entier non négatif",
+ "48e17e8cbd": "Inconnu",
+ "858d0630da": "Fermer l'aperçu",
+ "39883467f4": "Ticket Linear",
+ "fda549766e": "{{value0}} pour commenter",
+ "d71cd3003e": "+ Assigné",
+ "commentTooLarge": "Le commentaire est trop volumineux pour être envoyé sans risque.",
+ "openAttachedWorkspace": "Ouvrir l'espace de travail associé à l'issue",
+ "openWorkspace": "Ouvrir l'espace de travail",
+ "moreWorkspaceActions": "Plus d'actions de espace de travail pour l'issue",
+ "startNewWorkspace": "Démarrer un nouveau espace de travail"
+ },
+ "NewWorkspaceComposerCard": {
+ "reuseExistingBranch": "Réutiliser la branche",
+ "reuseExistingBranchHint": "Faites un checkout de la branche existante au lieu d'en créer une nouvelle à partir de celle-ci.",
+ "createMultiple": "Créer plus",
+ "cbb47ee0dc": "Disponible uniquement pour les projets Git locaux.",
+ "d861de981b": "Sparse checkout",
+ "090cfedeb4": "Écrire une note",
+ "f8728aa4f9": "Note",
+ "0ee17638fe": "Nom de l'espace de travail",
+ "2688050e4b": "Nom",
+ "f0470c7383": "Avancé",
+ "ba64270bdb": "Configurer les agents",
+ "ab63f25397": "Ouvrir les paramètres des agents",
+ "01d1e8f601": "Agent",
+ "0c5d6a479c": "[Optionnel]",
+ "b5a0796911": "Se connecter",
+ "dccd26d4e4": "Choisir le projet",
+ "d6b0a96f32": "Ajouter un projet",
+ "969a8bff66": "Projet",
+ "23bb365554": "orca.yaml",
+ "a239038146": "Erreur de connexion SSH",
+ "9a70e4859e": "Choisissez si le setup doit être exécuté avant de créer cet espace de travail.",
+ "803b7fe72f": "Vérification de la configuration du setup...",
+ "92e34f0311": "paramètres locaux",
+ "326a578923": "orca.yaml + local",
+ "2132b670da": "les deux",
+ "0e587e31fb": "yaml",
+ "ac3748dcda": "Nom ou « Créer à partir de »",
+ "f660aa1454": "Connexion",
+ "7711ad5122": "Commande de setup locale",
+ "e5db1b0419": "Commande de setup combinée",
+ "runOn": "Exécuter sur",
+ "addProjectBeforeWorkspace": "Ajoutez un projet avant de créer un espace de travail.",
+ "connectProjectFirst": "Connectez d'abord ce projet",
+ "connectSourceLookup": "Se connecter",
+ "reconnectSourceLookup": "Reconnecter",
+ "setupHostExistingFolderTitle": "Configurer {{value0}}",
+ "cloneProjectOnHost": "Cloner le projet",
+ "cloneUrlPlaceholder": "https://github.com/owner/repo.git",
+ "cloneDestinationPlaceholder": "/parent/directory/on/host",
+ "cloningHostSetup": "Clonage...",
+ "cloneHostSetup": "Cloner",
+ "importExistingFolderOnHost": "Importer un dossier existant",
+ "setupHostExistingFolderPlaceholder": "/path/to/project/on/host",
+ "setupKindGit": "Dépôt Git",
+ "setupKindFolder": "Dossier",
+ "setupHostExistingFolderHelp": "Reliez un checkout déjà présent à cet emplacement, puis créez cet espace de travail sur cet hôte.",
+ "importingHostSetup": "Import...",
+ "importHostSetup": "Importer",
+ "sshNotConnected": "SSH non connecté",
+ "connectingSsh": "Connexion SSH...",
+ "sshAuthenticationFailed": "Échec de l'authentification SSH",
+ "preparingSshConnection": "Préparation de la connexion SSH...",
+ "connected": "Connecté",
+ "reconnectingSsh": "Reconnexion SSH...",
+ "sshReconnectionFailed": "Échec de la reconnexion SSH",
+ "notConnected": "Non connecté",
+ "notePasteTooLarge": "Le contenu collé est trop volumineux pour le champ de note.",
+ "waitForSetupBeforeAgent": "Attendre la fin du setup avant de démarrer l'agent",
+ "waitForSetupBeforeAgentHelp": "Activez cette option quand le setup installe des dépendances, des serveurs MCP ou des fichiers de configuration dont l'agent a besoin au démarrage.",
+ "destroyDisabled": "destroy désactivé",
+ "destroyConfigured": "destroy configuré",
+ "noDestroyConfigured": "pas de destroy",
+ "ephemeralVm": "Environnement par espace de travail",
+ "chooseRunTarget": "Choisir la cible",
+ "noRunTargets": "Aucune cible d'exécution n'est prête pour ce projet.",
+ "perWorkspaceEnvHint": "Provisionner un environnement à la demande depuis une recette",
+ "branchName": "Nom de branche",
+ "branchNamePlaceholder": "feature/my-branch",
+ "connectTimedOut": "La connexion a expiré. Elle se poursuit peut-être en arrière-plan.",
+ "connectingHost": "Connexion…",
+ "connectHost": "Se connecter",
+ "setLocation": "Définir l'emplacement du projet",
+ "setLocationOnHost": "Définir l'emplacement du projet sur {{host}}",
+ "setLocationTooltip": "Choisissez un dossier ou clonez ce projet sur {{host}}.",
+ "addHost": "Ajouter un hôte",
+ "addHostHint": "Enregistrer une autre machine ou un serveur Orca",
+ "addSshHost": "Ajouter un hôte SSH",
+ "addSshHostHint": "Utiliser une machine existante via SSH",
+ "addRemoteOrcaServer": "Ajouter un serveur Orca distant",
+ "addRemoteOrcaServerHint": "Appairer un autre runtime Orca",
+ "hostConnectionFailed": "Échec de la connexion"
+ },
+ "NewWorkspaceComposerModal": {
+ "createWorktree": "Créer un worktree",
+ "createWorkspace": "Créer un espace de travail",
+ "fa90f739a5": "Choisissez le projet, le nom de l'espace de travail et l'agent avant de créer l'espace de travail."
+ },
+ "PullRequestPage": {
+ "2560588245": "Échec de la demande de reviewer",
+ "3450247584": "Commentaire",
+ "6ad2c1ab9c": "Aucun fichier modifié.",
+ "filesUnavailable": "Impossible de charger les fichiers modifiés.",
+ "filesRetry": "Réessayer",
+ "4d18310d55": "Fichiers modifiés",
+ "94d95cf1f7": "Vérifications",
+ "9e8d45700e": "Conversation",
+ "e6996f4024": "· mis à jour",
+ "dd5d9a4f17": "a mis à jour {{value0}}",
+ "71a3c0f9d2": "Démarrer l'espace de travail",
+ "00b7b82329": "branche source",
+ "e1f3641bfd": "de",
+ "c44b70352b": "branche cible",
+ "b0e80f083d": "veut merger dans",
+ "8ecda455a0": "Ouvrir sur GitHub",
+ "1a2570e18e": "Démarrer un nouveau espace de travail",
+ "57c13a5aa4": "Plus d'actions d'espace de travail pour la PR",
+ "25690a3855": "Démarrer un espace de travail à partir de la PR",
+ "a459866967": "Reprendre l'espace de travail attaché à la PR",
+ "347034903a": "Copier le lien GitHub",
+ "5a01ca7253": "Échec de la synchronisation de l'état consulté avec GitHub.",
+ "996a1897d2": "Impossible de synchroniser l'état consulté de cette pull request.",
+ "e0b15c793f": "Échec de la copie du lien GitHub",
+ "992e799227": "Lien GitHub copié",
+ "61bfc81ada": "Impossible d'ouvrir l'espace de travail attaché à cette pull request.",
+ "161d91ef02": "Envoyer le commentaire",
+ "d2030fc8cd": "Ajouter un commentaire…",
+ "1208347ac0": "Échec de l'ajout du commentaire",
+ "61452f2143": "Démarrer un espace de travail à partir de l'issue",
+ "14c9fc70ed": "+ Assigné",
+ "bc215fea4d": "+ Étiquette",
+ "b936cc51a4": "Fermé",
+ "7b8f6bf6d8": "Ouvert",
+ "a18d01cda3": "Aucune vérification signalée pour l'instant",
+ "3912daf310": "Cette pull request n'a aucune vérification signalée pour l'instant.",
+ "45877f5089": "Aucune vérification trouvée",
+ "85e62c5266": "Un agent IA a été démarré pour les vérifications cassées.",
+ "ddfd42f460": "Vérifiez le prompt avant de démarrer un agent.",
+ "a053bdd082": "Corriger les vérifications en échec avec l'IA",
+ "1b14d0a69c": "Ouvrir dans GitHub",
+ "1550675e5f": "Aucune sortie en ligne n'est disponible pour cette vérification.",
+ "7720c9c3f5": "Jobs",
+ "8432d17901": "Annotations",
+ "f01bf79a79": "vérification #",
+ "000f90afcf": "Terminé",
+ "76551b1161": "Démarré",
+ "662bc2998d": "Statut :",
+ "d8e82b7f15": "Chargement des détails de la vérification…",
+ "54cddd1858": "Relancer toutes les vérifications",
+ "68605516dd": "Relancer les vérifications échouées",
+ "522d9353e1": "Relancer",
+ "0fa8b8faec": "Démarrer l'agent IA par défaut sur ces vérifications",
+ "5d0f42766d": "Actualiser les vérifications",
+ "98583589c6": "Échec du démarrage d'un agent IA pour les vérifications cassées : {{value0}}",
+ "51c65c0265": "Aucune vérification cassée à corriger.",
+ "788a782bb0": "Échec de la relance des vérifications",
+ "18f2af42ac": "Relances des vérifications demandées",
+ "5963a6a852": "Relance de la vérification demandée",
+ "246b2c6456": "Échec de l'actualisation des vérifications",
+ "c057f2fcb0": "Impossible d'actualiser les vérifications sans chemin de dépôt.",
+ "6591b1fa82": "Annuler",
+ "42c36d9166": "{{value0}} {{value1}} réaction{{value2}}",
+ "7df8d5fc60": "Ouvrir la zone de merge GitHub",
+ "1939d0f663": "Pull request",
+ "973ef2fac9": "Échec de la désactivation de l'auto-merge",
+ "d31f4b508c": "Échec de l'activation de l'auto-merge",
+ "0f5821b035": "Auto-merge désactivé",
+ "5edbe7eefa": "Auto-merge activé",
+ "aae645d36d": "Échec de la fusion de la pull request",
+ "c57873d721": "Pull request fusionnée",
+ "a63b3c159c": "Cela mettra à jour la pull request sur GitHub.",
+ "eec3706a6a": "{{value0}} la PR #{{value1}} ?",
+ "710e47aa06": "Pull request rouverte",
+ "7aa3b5f706": "Pull request fermée",
+ "3d77438c92": "Cela rouvrira la pull request sur GitHub.",
+ "5a65651096": "Cela fermera la pull request sur GitHub.",
+ "d2d589556c": "Aucun commentaire pour le moment.",
+ "3463d10a63": "Commentaires",
+ "c8ea6c7c4c": "Aucune description fournie.",
+ "778683ec84": "Description",
+ "da9aaa8bcf": "Modifier la description",
+ "4a337ac05f": "Enregistrer",
+ "169a93b29a": "mis à jour",
+ "3c891789f6": "par",
+ "f4fe47c2bb": "Résolu",
+ "0ac19bb52e": "Ouvrir le commentaire sur GitHub",
+ "d6c6679de7": "Répondre au commentaire",
+ "76b2a0ac5b": "résolu",
+ "11505c7a71": "Réponse publiée.",
+ "6885c619e7": "Impossible de répondre sans chemin de dépôt.",
+ "d94810f652": "Échec de la mise à jour de la description.",
+ "9b4190dc98": "Description mise à jour.",
+ "51ed0cf38b": "Afficher plus de lignes en dessous",
+ "e295a78c11": "Afficher {{value0}} lignes supplémentaires au-dessus",
+ "c9de94b07a": "Afficher plus de lignes au-dessus",
+ "5f3e293517": "Réinitialiser le contexte de code",
+ "85d119be40": "Contrôles du contexte de code",
+ "791ddede19": "commentaire L",
+ "4b960e5978": "Chargement du contexte de code…",
+ "89e80af1c7": "fichiers consultés",
+ "319cf2d54b": "Afficher l'arborescence des fichiers",
+ "eff839f438": "Commentaire de review ajouté.",
+ "d8c3ba91c4": "Impossible de commenter sans le SHA head de la PR.",
+ "74660bd80b": "Diff indisponible car les SHA des commits de la PR manquent.",
+ "2e528e1c2d": "Consulté",
+ "ff84e1f54c": "{{value0}} {{value1}} comme consultés",
+ "5ad00c7a0e": "Aucun reviewer correspondant.",
+ "2760fa29a4": "Tous les autres",
+ "828f045847": "Suggestions",
+ "57750f4a8c": "Chargement...",
+ "805cb72cd4": "Demander jusqu'à 15 reviewers",
+ "a04c137bb7": "Reviewer",
+ "3bde131f49": "Saisissez ou choisissez un utilisateur",
+ "d10b6d5209": "Aucun reviewer demandé.",
+ "ae9a38fd4a": "Retirer le reviewer {{value0}}",
+ "acbd110867": "Chargement des reviewers",
+ "00d3be6bcd": "Reviewers",
+ "f4a4b3fd9f": "A récemment modifié ces fichiers",
+ "41d275d3ec": "Demander le reviewer {{value0}}",
+ "36b514a457": "Retirer la demande pour le reviewer {{value0}}",
+ "c798fa0ec7": "Échec du retrait du reviewer",
+ "1e6d089420": "Reviewers retirés",
+ "2c1d93da43": "Reviewer retiré",
+ "1ae11c905c": "Aucun contexte de dépôt disponible pour cette pull request.",
+ "102d3d177f": "Reviewers demandés",
+ "03282ff3b9": "Reviewer demandé",
+ "8f369a6b6b": "Vous pouvez demander jusqu'à 15 reviewers",
+ "dace0d1a9f": "Saisissez un reviewer",
+ "77d9388fb0": "unknown",
+ "c9e7094a7b": "Reprendre l'espace de travail",
+ "3b6886b2ee": "Copied",
+ "b6bda618cf": "compact",
+ "35a0573f41": "Annotation",
+ "61a8c69a33": "page",
+ "a4541fd3db": "Corriger les vérifications cassées",
+ "c808db1dd1": "Corriger les vérifications",
+ "c4c02ea23e": "Impossible de créer automatiquement un espace de travail de correction.",
+ "f119e5f5ef": "Répondre",
+ "894cfd884b": "Publication…",
+ "9d5425918e": "Rouvrir la PR",
+ "96d013ed28": "Fermer la pull request",
+ "d65f70786e": "closed",
+ "eca289e593": "Le merge exige un dépôt local enregistré",
+ "6568ae8ece": "default",
+ "19f19560d5": "destructive",
+ "aae99c6c04": "pr",
+ "e01e34f5fa": "comment",
+ "345b68254c": "thread",
+ "31a7b202f2": "Répondre à @{{value0}}",
+ "408e634fbb": "Répondre dans ce fil de review",
+ "34b9f7c264": ":L{{value0}}",
+ "5821aab360": "Échec de la publication de la réponse.",
+ "84fc40769a": "-L{{value0}}",
+ "1378d79e83": "Côte à côte",
+ "e5f4a24f78": "En ligne",
+ "dd94111c18": "Tout réduire",
+ "eb722a5a8c": "Tout développer",
+ "19628e058d": "Échec de l'ajout du commentaire de review.",
+ "50b8fb290f": "Marquer comme consulté",
+ "2b4fdb880c": "Ne plus marquer comme consulté",
+ "56ec6eafb7": "Ouvrez les détails de la PR pour voir les reviewers actuels.",
+ "7f964a365a": "Retirer le reviewer",
+ "commentTooLarge": "Le commentaire est trop volumineux pour être envoyé sans risque.",
+ "8ff5ae8866": "Assignés",
+ "82c87eceb9": "Modifier les assignés",
+ "1ff5d979df": "Personne n'est assigné",
+ "checkActionRequiredHint": "Cette vérification nécessite une action manuelle sur GitHub (par exemple, approuver l'exécution du workflow) avant que le merge soit débloqué.",
+ "6b1d5ee3e4": "Aucun détail en ligne n'est disponible pour cette vérification.",
+ "e04c027d98": "Échec du chargement des détails de la vérification.",
+ "5df7c41d2a": "Réessayer",
+ "77482513f8": "Fermer",
+ "2f5195c6a0": "Rouvrir",
+ "4b8ae7303f": "Échec du chargement du diff.",
+ "closePullRequestFailed": "Échec de la fermeture de la pull request",
+ "reopenPullRequestFailed": "Échec de la réouverture de la pull request",
+ "diffLoadTimedOut": "Délai dépassé lors du chargement de ce diff."
+ },
+ "QuickOpen": {
+ "1dbd3f59ff": "Déplacer",
+ "73b2c581f1": "Fermer",
+ "95fccbae88": "Esc",
+ "61b1c871a6": "Ouvert",
+ "250e5b2dfb": "Enter",
+ "74e2e1b3e4": "Aucun fichier correspondant.",
+ "722a21e1a8": "Chargement des fichiers...",
+ "1cb6ef47b7": "Accéder au fichier...",
+ "9e97f08d0f": "Rechercher un fichier à ouvrir",
+ "ec31e058f7": "Accéder au fichier",
+ "73b44e7bde": "Copier la commande d'installation",
+ "1cf8561ab4": "sur le remote pour activer un listage rapide respectant .gitignore :",
+ "5d80dc39bb": "ripgrep",
+ "2ca749c15d": "Installer",
+ "4725b0e931": "Scan Quick Open trop volumineux (",
+ "b227d88520": "{{value0}} fichiers trouvés",
+ "995be8ea22": "Copier",
+ "cf144856dc": "Copied",
+ "344f8a48dd": "sur l'hôte exécutant le scan Quick Open pour activer un listage rapide respectant .gitignore :"
+ },
+ "SelectedTextCopyMenu": {
+ "9b40d7b018": "Copier"
+ },
+ "StarNagCard": {
+ "92b0f9d921": "soit authentifiée et réessayez.",
+ "cd8c34aac1": "gh",
+ "cf82170065": "Impossible de mettre une étoile au dépôt. Vérifiez que",
+ "30c36231c1": "Orca est open source. Si ça vous a aidé aujourd'hui, une étoile GitHub aide d'autres développeurs à le découvrir.",
+ "b5e685e4d9": "Ignorer",
+ "5f6df21046": "Vous appréciez Orca ?",
+ "2d67b6c849": "Mettre une étoile sur GitHub",
+ "af3c9bbb37": "Ajout de l'étoile…",
+ "68a41bc3aa": "Impossible de mettre une étoile avec",
+ "996bf76e46": "Ouvrez GitHub pour terminer dans votre navigateur.",
+ "d32015fec7": "Ouverture...",
+ "157bb5ecbb": "Ouvrir GitHub",
+ "8c967b4d15": "Plus tard",
+ "73dfd4eb8d": "Ne plus demander"
+ },
+ "TaskPage": {
+ "513cddfa7a": "Vérification…",
+ "ff69a30681": "Annuler",
+ "2abe22ef76": "Votre token est chiffré via le trousseau du système d'exploitation et stocké localement.",
+ "246c2b3dd3": "Paramètres du compte Atlassian",
+ "59c14d34a2": "Créez un token dans",
+ "b95623e93f": "Token API Atlassian",
+ "68df347677": "you@example.com",
+ "163df31e0e": "https://example.atlassian.net",
+ "33fc2bcb30": "Utilisez l'URL d'un site Jira Cloud, un e-mail Atlassian et un token API pour parcourir les tickets.",
+ "60f806ce99": "Connecter le site Jira",
+ "8ff6fdc368": "Création…",
+ "fc0d8a1fa4": "pour valider.",
+ "919a20dd5b": "Saisissez {{value0}}",
+ "56cdb413a2": "Valeurs séparées par des virgules",
+ "1f0fce91e3": "Sélectionner {{value0}}",
+ "cbcdcbe244": "Chargement des champs Jira obligatoires…",
+ "34d97ca682": "Que se passe-t-il ?",
+ "f161bf9ede": "Description (facultatif)",
+ "578f730c16": "Résumé court",
+ "16cba35bee": "Titre",
+ "ae592fee62": "Type de ticket",
+ "7d63e2626e": "Chargement...",
+ "93c57f15e5": "Aucun projet trouvé.",
+ "cfb56a7868": "Rechercher des projets...",
+ "00022ec0ba": "Projet",
+ "0c11ca0b6d": "Nouveau ticket Jira",
+ "d0ca4aa1d0": "Étiquettes",
+ "1742eafc14": "Aucun projet",
+ "69591944e7": "Basse",
+ "7fd59c18d8": "Moyenne",
+ "345b169f1f": "Haute",
+ "f373ab1a4f": "Urgente",
+ "713179dfdc": "Aucune priorité",
+ "c8d5bec5f7": "Priorité",
+ "42a9160321": "Non assigné",
+ "d2a876ca53": "Assigné",
+ "154b0fa623": "Statut",
+ "9bc8aea407": "Ajouter une description...",
+ "d9151fd4e9": "Titre du ticket",
+ "4f3cb99f41": "Changer d'équipe",
+ "c11105dac5": "Nouveau ticket",
+ "1b59a07674": "Création...",
+ "cf72580c04": "Rédigez une description, une note de projet ou rassemblez des idées...",
+ "2ea1c701b6": "Date cible",
+ "7da41c9225": "Cible",
+ "09623359b9": "Date de début",
+ "7d08e8be0f": "Début",
+ "af9e877f30": "Aucun label",
+ "d6cda23ef1": "Membres",
+ "cfaadb6b22": "Aucun responsable",
+ "34da8ac06c": "Responsable",
+ "579f98afcd": "Ajoutez un résumé court...",
+ "ecbcc83140": "Nom du projet",
+ "b6795e65fd": "Fermer",
+ "a98cbe7664": "Équipe",
+ "02f67c0d09": "Nouveau projet",
+ "bdebffcbfe": "Créez un projet Linear pour l'équipe sélectionnée.",
+ "1361275ec3": "Nouveau projet Linear",
+ "7f3f7b4c18": "Description (facultatif, markdown)",
+ "9f2b4c03a6": "Création dans",
+ "d3d0998b7d": "Nouveau ticket GitHub",
+ "d1e243795c": "tickets",
+ "be8cf68d9f": "voir les tickets",
+ "67662ade50": "tickets du projet",
+ "6244a02f46": "Ouvrir dans Linear",
+ "606a85c774": "Ouvert",
+ "5e8061b088": "Démarrer un espace de travail depuis {{value0}} {{value1}}",
+ "592a55611b": "Essayez de sélectionner plus d'équipes ou d'actualiser ; les filtres d'équipe s'appliquent aux tickets déjà récupérés.",
+ "618107fab3": "Aucun ticket récupéré ne correspond aux équipes sélectionnées",
+ "903c7af49f": "Aucun ticket Linear trouvé",
+ "5ed38a49e5": "Consultez l'erreur d'espace de travail ci-dessous, puis actualisez.",
+ "cc8795e07c": "Impossible de charger les tickets Linear",
+ "f362667d55": "Mis à jour",
+ "b1eaa18ace": "Issue",
+ "37e7ee311e": "Clé",
+ "b7bae28b6a": "affichés",
+ "a26a48252e": "Propriétés affichées",
+ "5d2d835467": "Tri",
+ "5659da12fc": "Regroupement",
+ "9c57663908": "Affichage",
+ "af377b13b1": "Vue {{value0}}",
+ "d47248df4d": "Mode d'affichage Linear",
+ "f397d513e3": "Retour",
+ "b39fe6511d": "projets",
+ "8675cd6188": "Linear",
+ "733b8f2421": "Linear / Vues",
+ "bc06ed0fb0": "Retour aux vues",
+ "3cb855080f": "vues",
+ "b4e10f096e": "Propriétaire",
+ "a04fe7ba73": "Visibilité",
+ "0aa8525950": "Modèle",
+ "dfc0c79bd8": "Tickets",
+ "8a07f21e76": "Santé",
+ "851017590d": "Ajouter l'accès Linear",
+ "228b25028f": "Parcourez vos tickets Linear assignés et démarrez le travail directement depuis ici.",
+ "6d56559467": "Connectez votre compte Linear",
+ "eee68073b2": "Ouvrir dans Jira",
+ "9497f2787c": "Démarrer l'espace de travail",
+ "eba87f2edb": "Aucun ticket Jira trouvé",
+ "63b2abd3aa": "Tickets Jira",
+ "e7115334aa": "Masquer Jira",
+ "83bce6be5c": "Connecter Jira",
+ "b518ae6307": "Parcourez, modifiez, créez des tickets Jira et démarrez le travail directement depuis ici.",
+ "a150c59da7": "Connectez votre site Jira",
+ "bcdc1330b2": "Ouvrir dans GitLab",
+ "00b7ffb952": "Type / État",
+ "eb10c32872": "ID",
+ "e9b6955dcd": "Ticket #{{value0}}",
+ "a0544fb653": "MR !{{value0}}",
+ "8396825a14": "Action",
+ "c1d1600362": "Ouvrir dans le navigateur",
+ "b6329379ca": "Démarrer un nouveau espace de travail",
+ "054bf695cc": "Brouillon",
+ "285bc21dc5": "Modifiez la requête ou effacez-la.",
+ "d0e3c8f933": "Aucun travail GitHub correspondant",
+ "5b6b2af943": "Nouvelle tentative…",
+ "0c0de0fc0e": "Impossible de charger les tickets depuis",
+ "d1766fd62d": "projets n'ont pas pu être chargés",
+ "7762f4b03a": "sur",
+ "443f7dd928": "Merge",
+ "a7396b05c6": "Vérifications",
+ "f6fa3c97d0": "Reviewers",
+ "8aba10579d": "Assignés",
+ "5eccb3c841": "Titre / Contexte",
+ "d4c2830063": "Actualiser les éléments de travail GitLab",
+ "c679af7ad9": "Actualiser mes todos",
+ "dfd72673e7": "Échec de l'enregistrement de la sélection de projets.",
+ "b797bdd7c3": "Effacer la recherche",
+ "99c2755218": "JQL Jira, ex. project = ABC AND statusCategory != Done",
+ "2ff9fd71fd": "Actualiser les tickets Jira",
+ "0b65d3fb2c": "Rechercher des projets Linear...",
+ "eec0c5c079": "Rechercher des tickets Linear...",
+ "8964184a8b": "Actualiser Linear",
+ "3feb524d42": "Nouveau ticket Linear",
+ "0cbf7e5cf3": "Mode tâches Linear",
+ "ff53631e6f": "Actualiser le travail GitHub",
+ "6ffa6be99f": "Actualisation du travail GitHub",
+ "b15ceb409d": "Rechercher des tickets GitHub...",
+ "eee4df4c66": "Rechercher des PR GitHub...",
+ "e592d99051": "Tous les sites Jira",
+ "d09b7631b7": "Échec du changement de site Jira.",
+ "8029e2bd4d": "Sélectionnez une équipe Linear à ouvrir dans Linear",
+ "609532fae7": "Échec de l'enregistrement de la source de tâches par défaut.",
+ "4826fd1ad8": "Fermer · Esc",
+ "1a06219d5c": "Fermer les tâches",
+ "3f594861a5": "Échec de l'enregistrement de la sélection d'équipe.",
+ "d0d570b306": "Échec du changement d'espace de travail Linear.",
+ "cb98f0350c": "{{value0}} créé",
+ "1e1b2ad8f2": "Sélectionnez une équipe depuis l'espace de travail du projet avant de créer ce ticket.",
+ "3ca9b424a3": "Échec de la création du projet.",
+ "3f9604efc7": "Ticket #{{value0}} ouvert",
+ "585dba2989": "Impossible d'ouvrir l'espace de travail associé à cette issue.",
+ "534a9c6017": "Impossible d'ouvrir l'espace de travail attaché à cette pull request.",
+ "fe380f306c": "Échec de l'enregistrement de la vue de tâches par défaut.",
+ "af2a8371de": "Échec du chargement des types de tickets Jira.",
+ "6775c05483": "Échec de la mise à jour de l'état Linear",
+ "745ae567d4": "« {{value0}} » n'est pas disponible pour {{value1}}",
+ "669e419d65": "Contexte d'espace de travail manquant pour la vue Linear.",
+ "cba2a2b7fb": "Contexte d'espace de travail manquant pour le projet Linear.",
+ "f4374519ae": "Votre source de tickets préférée (upstream) n'est plus configurée pour {{value0}}. Utilisation d'origin.",
+ "e9139db03f": "Échec du masquage de {{value0}}.",
+ "b73717af92": "Suivant",
+ "0c8df28045": "Page suivante",
+ "ae859c816b": "Page {{value0}}",
+ "cd171f3391": "...",
+ "297a805b64": "Précédent",
+ "6cd6b3ae6a": "Page précédente",
+ "e65757a338": "Pagination",
+ "37d60046e3": "Ouvrir la zone de merge GitHub",
+ "1a9ea003dc": "Échec de la désactivation de l'auto-merge",
+ "a3318684bc": "Échec de l'activation de l'auto-merge",
+ "a5bf86defe": "Auto-merge désactivé",
+ "fed317634c": "Auto-merge activé",
+ "88f478cdef": "Échec de la fusion de la pull request",
+ "a161925adc": "Pull request fusionnée",
+ "0506a78337": "Cela mettra à jour la pull request sur GitHub.",
+ "844dc193c7": "{{value0}} la PR #{{value1}} ?",
+ "995dd6af9b": "Ouvrir les vérifications de la PR",
+ "8a22eb3f7b": "Aucun reviewer correspondant.",
+ "67755a83a1": "Tous les autres",
+ "3ace2e6bcf": "Suggestions",
+ "0eacf48491": "Chargement…",
+ "0b9b04f4b5": "Saisissez ou choisissez un utilisateur",
+ "62c7bd789f": "Demander jusqu'à 15 reviewers",
+ "5d4fd69a6a": "Actifs récemment dans cette pull request",
+ "ed1daeb49a": "Échec du retrait du reviewer",
+ "837bb901ec": "Reviewers retirés",
+ "f9191d1714": "Reviewer retiré",
+ "dc67f69962": "Échec de la demande de reviewer",
+ "8f06dbb9e5": "Reviewer demandé",
+ "969e26577c": "Vous pouvez demander jusqu'à 15 reviewers",
+ "d00571d9b1": "Saisissez un reviewer",
+ "edf4bc4135": "Aucun utilisateur assignable.",
+ "53e002d895": "Le ticket n'a pas de slug de dépôt.",
+ "7f94eb6395": "Assigner le ticket",
+ "bb63046423": "Assigné à {{value0}}",
+ "ca63694b4c": "Échec de la mise à jour des assignés.",
+ "b36f4bf9de": "Aucun label.",
+ "5ebff3a0aa": "Aucun",
+ "d09bf34db7": "Fermé",
+ "1c893195ac": "Échec de la mise à jour de l'état",
+ "afc68824ff": "Aucun état trouvé",
+ "cc13109b5d": "Chargement des états",
+ "d45a910c4a": "Changer l'état Linear depuis {{value0}}",
+ "d8a517ad89": "Identifiant",
+ "50387522d7": "Aucun regroupement",
+ "d747aed72f": "Tableau",
+ "a6f7e93d7f": "Liste",
+ "e78ec261ed": "Vues",
+ "727069bee5": "Projets",
+ "137e2a8a01": "PRs",
+ "18451e99df": "Terminé",
+ "4b6e40e42c": "Tous les ouverts",
+ "bd9965df51": "Signalés",
+ "1301d376f1": "Assignés",
+ "9cd11ba218": "Jira",
+ "11a828abf8": "GitLab",
+ "acef77f7ca": "GitHub",
+ "524f095d55": "En attente de relecture",
+ "7698af5263": "À moi",
+ "94f0339621": "Assignés à moi",
+ "c2268a9982": "Tous",
+ "37a82eaaf8": "Fusionnés",
+ "887efe9140": "Se connecter",
+ "a70153f583": "connexion en cours",
+ "6459faa8b3": "error",
+ "e15ba2d2eb": "Créer un ticket",
+ "3b11c8e8fc": "tableau",
+ "e178c0a953": "Choisissez un projet Jira avant de créer le ticket.",
+ "0f7b0d964a": "Crée un nouveau ticket dans {{value0}}.",
+ "eff9800d4b": "{{value0}} label{{value1}}",
+ "d7f16d0e32": "Sélectionner l'équipe",
+ "5301ca0f20": "Créer le projet",
+ "7719d8daa9": "{{value0}} membre{{value1}}",
+ "5af6f0ae5b": "Sélectionner l'équipe",
+ "cfb730b73e": "issue",
+ "3659f9792a": "tableau",
+ "d079be2dc8": "Aucun ticket assigné. Essayez de rechercher autre chose.",
+ "2bdefbcac3": "Essayez une autre requête de recherche.",
+ "25ff84769a": "Aucun ticket ne correspond à ce contexte Linear.",
+ "cbce2bc9cd": "aucun",
+ "51411113df": "liste",
+ "60f68a2ef4": "Issues Linear",
+ "b2007ba885": "projet",
+ "6edf402e11": "vue d'ensemble",
+ "9ae151b26b": "linear",
+ "94d900518d": "Aucune issue ne correspond au préréglage sélectionné.",
+ "f51e254d35": "Essayez une requête JQL différente.",
+ "4645a7814f": "jira",
+ "e224d76876": "MR",
+ "bbec4717ee": "mr",
+ "d6d08c1650": "Sélectionnez un projet pour voir les work items GitLab.",
+ "f294c500ef": "Aucun work GitLab ne correspond à ce filtre.",
+ "cd7dc432a3": "Aucune MR GitLab ne correspond à ce filtre.",
+ "171b7739d8": "mrs",
+ "a9f256ecea": "Aucune issue GitLab ne correspond à ce filtre.",
+ "2007e14d95": "gitlab",
+ "456b8512da": "MergeRequest",
+ "03da966159": "Sélectionnez un projet afin que nous puissions nous authentifier auprès de GitLab.",
+ "d591aac6ae": "Aucun todo en attente. Vous êtes à jour !",
+ "33a4bd7f5c": "todos",
+ "66ae7330f6": "Plus d'actions",
+ "93d5f21fc1": "pr",
+ "e104fa3d3d": "Démarrer un espace de travail à partir de l'issue",
+ "2193a99ec1": "Ouvrir l'espace de travail associé à l'issue",
+ "7deb9e59a5": "Plus d'actions sur la PR",
+ "7753652524": "Reprendre",
+ "e4b29c5bcf": "Démarrer un espace de travail à partir de la PR",
+ "67d881244c": "Reprendre l'espace de travail attaché à la PR",
+ "6430594b18": "auteur inconnu",
+ "7799ad9ab6": "brouillon",
+ "0bfbf62f75": "Réessayer",
+ "38139edb52": "github",
+ "31f81cc334": "Actualisation des works GitHub…",
+ "3d93316bb0": "prs",
+ "937b29fa35": "éléments",
+ "bc46d8204e": "Sélectionnez un seul projet à ouvrir dans GitHub",
+ "d1132848f8": "Sélectionnez un seul projet GitHub à ouvrir dans GitHub",
+ "2af3ab5c58": "Sélectionnez une seule équipe à ouvrir dans Linear",
+ "aec5feeb69": "Échec de la création de l'issue Jira.",
+ "7437e340b4": "Échec de la création de l'issue.",
+ "9e03c17847": "Ouvrez les détails de la PR pour voir les reviewers actuels.",
+ "3b7f34282f": "closed",
+ "246bd64aed": "Ouvrir {{value0}} dans Linear",
+ "ff90d0abc7": "Démarrer un espace de travail à partir de {{value0}}",
+ "fe28c9821f": "vue",
+ "8d1e17a3ef": "Ouvrir {{value0}} dans GitHub",
+ "4ac8ff2275": "Ouvrir {{value0}} dans Jira",
+ "40eaf2c27c": "Détails",
+ "closeAsCompleted": "Fermer comme terminée",
+ "closeAsCompletedDescription": "Terminée, fermée, corrigée, résolue",
+ "closeAsNotPlanned": "Fermer comme non planifiée",
+ "closeAsNotPlannedDescription": "Ne sera pas corrigée, non reproductible, obsolète",
+ "closeAsDuplicate": "Fermer comme doublon",
+ "closeAsDuplicateDescription": "Doublon d'une autre issue de ce dépôt",
+ "duplicateIssueNumberPlaceholder": "Numéro d'issue",
+ "closeAsDuplicateSubmit": "Fermer le doublon",
+ "duplicateIssueMissing": "Saisissez un numéro d'issue de ce dépôt.",
+ "duplicateIssueNotInteger": "Utilisez un numéro d'issue entier.",
+ "duplicateIssueNotPositive": "Utilisez un numéro d'issue positif.",
+ "duplicateIssueSameIssue": "Choisissez une autre issue.",
+ "repository": "Dépôt",
+ "backToCloseReasons": "Retour",
+ "searchIssues": "Rechercher des issues",
+ "useIssueNumber": "Utiliser l'issue #{{value0}}",
+ "noMatchingIssuesLoaded": "Aucune issue correspondante n'a été chargée.",
+ "editReviewersWithCurrent": "Modifier les reviewers : {{value0}}",
+ "linearEmptyAttributeFilter": "Aucune issue ne correspond aux filtres sélectionnés. Effacez un filtre ou essayez d'autres critères.",
+ "linearEmptyUnfilteredScope": "Aucune issue dans le périmètre de cet espace de travail. Essayez de rechercher ou d'ajuster les équipes.",
+ "linearFetchMore": "Charger plus",
+ "jiraSortAscending": "croissant",
+ "jiraSortDescending": "décroissant",
+ "jiraSortBy": "Trier par",
+ "jiraToggleSortDirection": "Trier : {{value0}}",
+ "75a38d7df8": "Les données GitHub sont temporairement indisponibles. Son API est peut-être en panne, limitée en débit ou injoignable. Réessayez dans quelques instants.",
+ "noGithubSourceDetected": "Aucune source GitHub détectée pour",
+ "noGithubSourceDetectedHint": "il n'a peut-être aucun remote GitHub, ou la source n'a pas pu être résolue.",
+ "jiraLinkSourceUnavailable": "Impossible de lier cette issue Jira. Reconnectez Jira ou sélectionnez le site correspondant, puis réessayez.",
+ "loadPageUnreachable": "La page {{value0}} dépasse ce que la recherche GitHub peut renvoyer.",
+ "loadPageFailed": "La page {{value0}} n'a pas pu être chargée depuis GitHub.",
+ "loadPageNoMoreResults": "Aucun autre résultat sur la page {{value0}}.",
+ "linearHasWorktreeLoadFailed": "Impossible de charger les issues Linear liées à un espace de travail Orca.",
+ "linearHasWorktreePartialLoadFailed": "Certaines issues Linear liées à un espace de travail Orca n'ont pas pu être chargées. Actualisez pour réessayer.",
+ "linearHasWorktreeSearchPlaceholder": "Filtrer les issues liées à un espace de travail Orca...",
+ "linearModeHasWorktree": "Avec espace de travail",
+ "linearModeHasWorktreeTooltip": "Tickets Linear liés à un espace de travail Orca",
+ "linearEmptyHasWorktree": "Aucun ticket Linear n'est encore lié à un espace de travail Orca. Démarrez le travail depuis une issue Linear pour le voir ici.",
+ "linearOpenAttachedWorkspace": "Ouvrir l'espace de travail attaché à {{value0}}",
+ "linearWorktreesColumn": "Espaces de travail"
+ },
+ "Terminal": {
+ "73768427cf": "Fermer",
+ "f82e9f02df": "Annuler",
+ "7958465754": "Des terminaux locaux exécutent des processus. Fermer quand même la fenêtre ?",
+ "2fa9c69ff3": "Fermer la fenêtre ?",
+ "cd51e28d8b": "Enregistrer",
+ "0037b21794": "Ne pas enregistrer",
+ "21295c6b8c": "Modifications non enregistrées",
+ "5c1d2a32bb": "Chargement de l'éditeur...",
+ "f0600556b3": "Échec de la création du fichier markdown sans titre.",
+ "37da0d736f": "Nouvel onglet de navigateur",
+ "a2a279b32a": "L'enregistrement a expiré ou échoué. Corrigez les erreurs avant de fermer.",
+ "46e08bc5c8": "Ce fichier contient des modifications non enregistrées.",
+ "61ed600d29": "« {{value0}} » contient des modifications non enregistrées. Voulez-vous enregistrer avant de fermer ?",
+ "cdc9ac4b2d": "éditeur",
+ "e57db40c11": "Impossible de construire la commande de lancement pour {{value0}}.",
+ "5b2c1a9e44": "Aucun CLI d'agent détecté — installez-en un ou choisissez un agent par défaut dans les paramètres."
+ },
+ "TerminalSearch": {
+ "db234b7519": "Fermer",
+ "7cb40c04eb": "Correspondance suivante",
+ "0f3066256e": "Correspondance précédente",
+ "42e466b9f1": "Regex",
+ "90c61387d9": "Respecter la casse",
+ "e07012f26e": "Rechercher..."
+ },
+ "UpdateCard": {
+ "68b235d264": "Redémarrer pour mettre à jour",
+ "6714206e5a": "Orca v{{value0}} est téléchargée. Redémarrez quand vous êtes prêt.",
+ "93794ea932": "Orca v{{value0}} est en cours de téléchargement.",
+ "8acbdd3961": "Réduire dans la barre d'état",
+ "17412483da": "Prêt à installer",
+ "47126bcf57": "Télécharger manuellement",
+ "3553a8672f": "Dernière erreur",
+ "90559b14e3": "Active un commutateur réseau Electron à l'échelle du processus après redémarrage. Utilisez-le pour les VPN d'entreprise ou les proxys qui rejettent les téléchargements de mises à jour en HTTP/2.",
+ "6e45bfa2e0": "Téléchargement...",
+ "558842597d": "Téléchargement de la mise à jour",
+ "f58b5c57a6": "Nouveau :",
+ "ec8fe71cfc": "Mettre à jour",
+ "44324ef542": "Notes de version",
+ "fdd4a364fa": "Les sessions ne seront pas interrompues.",
+ "05ad78a6d1": "Orca v{{value0}} est prête.",
+ "318d3b4bc7": "Ignorer la mise à jour",
+ "9abc59f814": "Mise à jour disponible",
+ "aad383aecc": "Lire toutes les notes de version",
+ "ccd8b0a793": "en plus depuis votre dernière mise à jour",
+ "b1d867f4fb": "Vos sessions de terminal ne seront pas interrompues pendant la mise à jour.",
+ "09a55c39b5": "Installation...",
+ "ea2a41adbe": "Vous utilisez déjà la dernière version.",
+ "ba5ffc949c": "Recherche de mises à jour...",
+ "2c2d3e03ca": "Réessayer",
+ "4cf109845a": "Erreur de mise à jour",
+ "6b0085010d": "Revérifier",
+ "48565a32bc": "Réessayer le téléchargement",
+ "933c6fdf5b": "Activer et redémarrer",
+ "1339b82cee": "Téléchargement HTTP/2 bloqué",
+ "7274ef6e59": "Ignorer l'astuce",
+ "a726967bd3": "Ignorer",
+ "d5253b54af": "error",
+ "7ffc08506e": "check",
+ "522df222b9": "spinner",
+ "e944c2de43": "Vérification de la mise à jour bloquée",
+ "5b309b19f3": "La mise à jour n'a pas été installée",
+ "092f09fc14": "L'éditeur de l'installateur ne correspond pas à Orca ; la mise à jour a donc été stoppée. N'installez pas ce téléchargement ; consultez les versions officielles pour obtenir une version corrigée.",
+ "c9ff9b9ec2": "Consulter les versions officielles",
+ "a05992a26b": "La vérification de signature n'a pas pu s'exécuter — généralement parce qu'un logiciel antivirus l'a bloquée. Relancez le téléchargement ou récupérez l'installateur depuis nos versions officielles.",
+ "5194358929": "Masquer les détails",
+ "8bc9e17d8f": "Afficher les détails",
+ "8cf17b10af": "Erreur de build local",
+ "a4650b0dc4": "Impossible d'utiliser le build local",
+ "b1e390250d": "Impossible de finaliser le basculement vers le build local.",
+ "d29740d175": "Le build sélectionné n'a pas pu être utilisé.",
+ "37d45c9ec1": "Choisir un autre build"
+ },
+ "WorktreeJumpPalette": {
+ "ac037cfac2": "Déplacer",
+ "75499e01d9": "Fermer",
+ "66b5a67bee": "Esc",
+ "45def60329": "Ouvert",
+ "f65d992a11": "Enter",
+ "c5081f2814": "Worktree actuel",
+ "52404f8096": "Onglet actuel",
+ "739bda980c": "principal",
+ "556e7232ca": "Actuel",
+ "684e8d7bc2": "Collecte de vos worktrees récents et de vos onglets ouverts.",
+ "ff908adfe9": "Chargement des cibles de navigation",
+ "4ee378034d": "Aller à...",
+ "f7fda8d562": "Créez un worktree ou ouvrez un onglet dans Orca pour commencer.",
+ "1628fd7dfa": "Aucun worktree actif, paramètre, action ni onglet ouvert",
+ "b781ae05e3": "Saisissez pour rechercher parmi les worktrees, paramètres, onglets et actions.",
+ "f60f8730be": "Aucun autre worktree vers lequel basculer",
+ "c4afa68159": "Essayez un worktree, un paramètre, une action, un titre d'onglet, un prompt d'agent, une URL, une PR ou un port.",
+ "dbd9d87eec": "Aucun résultat ne correspond à votre recherche",
+ "2c38630a01": "L'espace de travail n'existe plus",
+ "7726ce9970": "L'onglet émulateur mobile n'existe plus",
+ "d7d496a451": "La page du navigateur n'existe plus",
+ "50a1d11d5b": "Onglets ouverts",
+ "088d66d980": "Actions et paramètres",
+ "dabd819ca1": "Saisissez pour voir les {{value0}} worktrees",
+ "20af998bff": "{{value0}} éléments disponibles{{value1}}",
+ "bb72c08e63": "{{value0}} résultats trouvés{{value1}}",
+ "34c8fbb46e": "Remote SSH",
+ "63c2be1914": "SSH déconnecté",
+ "95be6587d3": "Créer le worktree « {{value0}} »",
+ "worktreesHeader": "Worktrees",
+ "recentWorktreesHeader": "Worktrees récents",
+ "settingsBadge": "Paramètres",
+ "actionBadge": "Action",
+ "paletteHostBadge": "Hôte : {{value0}}",
+ "workspaceTabMissing": "L'onglet n'existe plus",
+ "projectsGroupsHeader": "Projets et groupes",
+ "projectBadge": "Projet",
+ "repoGroupBadge": "Groupe de dépôts",
+ "pluginCommandFailed": "Impossible d'exécuter la commande du plugin.",
+ "recentChatsTerminalsHeader": "Chats et terminaux récents",
+ "27f10cca63": "Rechercher chats, terminaux, worktrees, paramètres et actions...",
+ "2770f02910": "Rechercher chats, terminaux, worktrees, paramètres et actions",
+ "paletteOpenTabBranch": "Nom de branche",
+ "paletteOpenTabWorkspace": "Nom de l'espace de travail",
+ "lastActiveTime": "Dernière activité il y a {{value0}}"
+ },
+ "github": {
+ "pr": {
+ "merge": {
+ "state": {
+ "a80132573b": "GitHub calcule encore le statut de merge de cette pull request",
+ "f958920f3a": "Vérification",
+ "9bd983ce8f": "GitHub indique que cette PR peut merger, mais des checks sont encore en cours",
+ "4e2507176b": "Vérifications en attente",
+ "1432ecff30": "GitHub indique que cette PR peut merger, mais certains checks ont échoué",
+ "87fa36ac83": "Vérifications échouées",
+ "1766eb46ba": "GitHub signale que cette pull request est bloquée",
+ "bf5e4c6c92": "Bloquée",
+ "c614e2660a": "Mettez à jour la branche avant de merger",
+ "039c072f94": "En retard",
+ "b37d45bca9": "GitHub signale des conflits de merge",
+ "7e8bbe3cd7": "Conflits",
+ "09896aad26": "Statut de merge indisponible pour cette PR",
+ "bd4f27b50e": "Merge",
+ "35ec24bc43": "Cette branche de base utilise la merge queue GitHub",
+ "b289646bcd": "GitHub signale des changements demandés sur cette pull request",
+ "c606463dc2": "Modifications demandées",
+ "a20db875ed": "GitHub exige une review approuvée avant que cette pull request puisse merger",
+ "1f8eb81c0e": "Approbation requise",
+ "f03028e055": "Cette pull request est toujours en brouillon",
+ "ec8e2cebaa": "Brouillon",
+ "820fd21663": "Cette pull request est fermée",
+ "4f976d3450": "Fermé",
+ "62eb8d39da": "Cette pull request est déjà mergée",
+ "83ecdbb4a6": "Fusionnés",
+ "331ebe1170": "Ajouter cette pull request à la merge queue GitHub",
+ "b169f943e1": "Merger quand prêt",
+ "62703b1dc4": "L'auto-merge GitHub est activé pour cette pull request",
+ "48d75ae118": "Désactiver l'auto-merge",
+ "a5b66afb58": "Checks réussis",
+ "fbd4f57f0a": "Checks réussis. L'éligibilité au merge sera revérifiée avant le merge.",
+ "4ab19a62ef": "Activer l'auto-merge",
+ "8f6cb3772f": "Merger automatiquement cette pull request une fois les conditions remplies"
+ }
+ }
+ },
+ "project": {
+ "ColumnResizeHandle": {
+ "1304289353": "Redimensionner la colonne"
+ },
+ "GhAuthErrorHelp": {
+ "7e800068d8": "Recharger",
+ "baa006f9af": "Docs",
+ "3fefeebde4": "Copier la commande d'actualisation",
+ "9c2da6353b": "Copier la commande de connexion",
+ "b436c586d1": "Copier la commande",
+ "8a7f6bf5dc": "Échec de la copie",
+ "224c9d0ae8": "Copié dans le presse-papiers",
+ "891a7d4616": "Retirer pour ce shell",
+ "fd17b3019f": "Retirer (PowerShell, persistant)",
+ "ae43542893": "Trouver où elle est définie",
+ "df636f5886": "Vérifier si elle est définie (PowerShell)"
+ },
+ "ProjectCell": {
+ "4b5b871da8": "Aucun label dans ce dépôt.",
+ "2219e945ef": "Chargement…",
+ "54cac64427": "La ligne n'a pas de slug de dépôt.",
+ "8ae56a88a6": "Étiquettes",
+ "f7cdb78efb": "Assignés",
+ "ebde486e3c": "Effacer",
+ "191905e20e": "Actuels et à venir",
+ "e17bb96881": "Terminé",
+ "943b3dadc9": "Ce dépôt n'a aucun type d'issue.",
+ "c7b059cf07": "Type de ticket",
+ "c5f949e489": "Issue",
+ "8d669084f6": "Restreint",
+ "6efdc0d920": "Brouillon",
+ "d0d0e13a5a": "PR",
+ "af5d8c912a": "Élément restreint",
+ "bb7ebc11e3": "Ajouter un nombre",
+ "9cb1a0c984": "Ajouter du texte",
+ "2e26a06c70": "Ajouter un label",
+ "36341ffc66": "Assigner",
+ "e369bf4fec": "Sélectionner",
+ "ffeff79861": "PULL_REQUEST"
+ },
+ "ProjectGroupHeader": {
+ "82a22d2079": "Actuel",
+ "244c9e7d06": "Tous"
+ },
+ "ProjectItemSlugDialog": {
+ "e55a5c4e68": "Aperçu de la ligne de projet.",
+ "4450efea9c": "Élément GitHub"
+ },
+ "ProjectPicker": {
+ "96739284c3": "Collez ci-dessous l'URL d'un projet pour accéder à ceux qui manquent.",
+ "9b36829267": "Aucune vue trouvée.",
+ "72a05c04a6": "Chargement des vues…",
+ "9bf55fa1e8": "Choisir une vue",
+ "a51b3337ab": "← Retour",
+ "8ab5447c64": "Épingler",
+ "5009ffc2f3": "Retirer l'épingle",
+ "fce99a24a7": "Ajouter",
+ "5113ecc298": "Ajouter par URL ou propriétaire/numéro",
+ "7b6d39627e": "Chargement…",
+ "b787682111": "Tout parcourir",
+ "ba0ab9a117": "Tout parcourir (chargement…)",
+ "b3044b7a25": "Récents",
+ "707843206c": "Épinglés",
+ "f492e1b539": "Rechercher des projets",
+ "44b2c6326b": "Échec du chargement des vues : {{value0}}",
+ "ab1a2c357d": "Roadmap (non prise en charge)",
+ "d34ef9b554": "Board (non pris en charge)",
+ "43a88ae574": "BOARD_LAYOUT",
+ "1a2b8e512e": "Tableau",
+ "cafb908f34": "TABLE_LAYOUT"
+ },
+ "ProjectRow": {
+ "75b5d816e3": "Démarrer le travail",
+ "e12be8b4d4": "Ouvrir dans GitHub",
+ "c3b81ddea2": "DRAFT_ISSUE"
+ },
+ "ProjectViewList": {
+ "989f81dc2a": "Colonnes",
+ "f949f5b2b7": "Configurer les colonnes",
+ "eddfc7a794": "Trier par {{value0}}",
+ "4f57d2e0b1": "Aucun élément ne correspond au filtre de cette vue."
+ },
+ "ProjectViewWrapper": {
+ "463f1205c0": "Chargement de la vue du projet",
+ "23b87ba9f7": "Ouvrir dans GitHub",
+ "4d2a77a119": "Soumettre une demande de fonctionnalité",
+ "1bf8c01c8b": "Passez en vue Tableau pour travailler sur ce projet dans Orca.",
+ "55de4fb57a": "{{value0}}. {{value1}} Soumettez une demande de fonctionnalité sur {{value2}}.",
+ "2edf5e7e77": "{{value0}} — Orca ne prend pas encore en charge les vues de projet {{value1}}. Soumettez une demande de fonctionnalité sur {{value2}}.",
+ "7245c3d7ac": "Effacer la recherche",
+ "c5bc7ec007": "Filtre de la vue : {{value0}}",
+ "840c268665": "Ajouter un dépôt",
+ "dffa899f36": "Annuler",
+ "7037c8f5f1": "Dépôt absent d'Orca",
+ "512fc171d6": "Choisissez un projet pour commencer.",
+ "71fb69926c": "Actualiser",
+ "a8fa0d2bf5": "Actualisation",
+ "fd15491034": "Ouvrir la vue dans GitHub",
+ "22df63c393": "Les données de sub-issues ne sont pas disponibles avec votre token.",
+ "067119985c": "Recherche GitHub, ex. assignee:@me is:open",
+ "1850fceac8": "{{value0}}/{{value1}} n'est pas ajouté à Orca. Ajoutez-le pour démarrer le travail, ou ouvrez-le dans GitHub.",
+ "1aa7c952b9": "Vue de projet",
+ "f352abf7c3": "La liste des dépôts est en cours de mise à jour.",
+ "1ce21b8cff": "Cet élément est hors des dépôts sélectionnés.",
+ "030de75bc5": "Cet élément correspond à plusieurs dépôts sélectionnés."
+ },
+ "slug": {
+ "dialog": {
+ "AssigneesEditor": {
+ "529fec247b": "Chargement…",
+ "98914e6b36": "Assignés :",
+ "94a4e6e4fa": "aucun"
+ },
+ "Comments": {
+ "fd5cccd138": "Commentaire",
+ "1c95937c8b": "Écrire un commentaire…",
+ "c0e576e96b": "Annuler",
+ "c3e829b4d9": "Enregistrer",
+ "463d030ae4": "Supprimer",
+ "8564f58542": "Édition",
+ "5f104bf855": "Aucun commentaire pour le moment.",
+ "commentTooLarge": "Le commentaire est trop volumineux pour être envoyé sans risque."
+ },
+ "LabelsEditor": {
+ "34dd57d6c8": "Chargement…",
+ "a7b182fcda": "Labels :",
+ "1a5366b5be": "aucun"
+ },
+ "SlugDialogBody": {
+ "598ad6a517": "Commentaires",
+ "41169e41fb": "Ajouter une description…",
+ "a91735d19f": "Annuler",
+ "e64f6c3eff": "Enregistrer",
+ "e4ef8281e9": "Chargement…",
+ "ae98897edf": "Fermer",
+ "69caf40ae8": "Ouvrir dans GitHub",
+ "7c302f8174": "Sans titre"
+ }
+ }
+ }
+ },
+ "GitHubMarkdownComposer": {
+ "015b4e607d": "Annuler",
+ "e3bd59143c": "Insérer",
+ "f24783f470": "https://...",
+ "ec6310b731": "Utilisez une URL d'image en http:// ou https://.",
+ "b7e4a1c902": "Collez, déposez ou cliquez pour ajouter des fichiers",
+ "8f1c2d4e6a": "Rien à prévisualiser",
+ "c91f0a2b14": "Écrire",
+ "d82b1e3f05": "Aperçu",
+ "imageUrlTooLarge": "L'URL de l'image est trop volumineuse."
+ },
+ "IssueSourceSelector": {
+ "d6aeb2012b": "Affichage des issues de",
+ "787c970baf": "Source des issues",
+ "643d7e9496": "upstream",
+ "51d1608920": "Origine",
+ "cdc9bd64fa": "compact",
+ "30b2c9df91": "Upstream"
+ },
+ "PRFilterDropdowns": {
+ "979be3cf6b": "Assigné",
+ "b27b7e526c": "Review de",
+ "7f1ba66c3e": "Review effectuée par",
+ "9d0f2eda6d": "Label",
+ "01f3f3d161": "Auteur",
+ "13b3ac0a84": "Statut",
+ "79c54552f7": "Filtres",
+ "8a2ffbf9b3": "Retirer le filtre {{value0}}",
+ "19bb6f115f": "reviewed-by"
+ },
+ "PRFilterPickers": {
+ "fdf387297c": "Effacer (",
+ "472c12ae03": "Effacer",
+ "2d1f58eda6": "Utiliser"
+ },
+ "PRFilterSections": {
+ "a00830d3f7": "Aucun utilisateur",
+ "0103e1cb18": "Review effectuée par",
+ "94b42b0edf": "Review demandée",
+ "de26e2eb06": "Aucun label",
+ "458ea3602b": "Aucun auteur",
+ "b69fa4fa20": "Retour",
+ "30ebb6ca44": "Effacer tous les filtres",
+ "8177eda37e": "Filtre",
+ "ea3416d646": "Assigné",
+ "b1d9fdea08": "Label",
+ "24754c44ad": "Auteur",
+ "764a0b4ce1": "Statut",
+ "f0cf6dd591": "désactivé",
+ "1e9b5244f2": "activé",
+ "b930de7194": "Brouillons uniquement",
+ "e0002f1eba": "sélectionnés",
+ "2b2f019091": "Tout état",
+ "0fd3249e2e": "Fermé",
+ "d78b60b5c2": "Ouvert",
+ "bd162b7d5a": "Fusionnés",
+ "2e639b84fa": "reviewer",
+ "712c5abdbf": "label",
+ "4e50c7bc03": "assigné",
+ "7bf3a6e5ac": "auteur",
+ "66256a73b3": "statut",
+ "3ce4d5e96e": "prs"
+ },
+ "github": {
+ "rate": {
+ "limit": {
+ "display": {
+ "5509443543": "Chargement du quota d'API GitHub…",
+ "34973d4695": "Le quota d'API GitHub est indisponible.",
+ "d12d3d6f33": "Actualiser le quota d'API GitHub",
+ "d5e5de9070": "Orca utilise REST, Search et GraphQL via GitHub CLI.",
+ "58c5f88216": "Quota d'API GitHub",
+ "6da1858354": "restant · réinitialisation dans",
+ "f42790d150": "sur",
+ "01f7323e58": "API GraphQL",
+ "1daf0f22a9": "GraphQL",
+ "1f2f28a4de": "API Search",
+ "c377a4f06a": "Recherche",
+ "c392c749a6": "API REST",
+ "bb227706a6": "REST",
+ "budget_scope_prefix": "Périmètre du quota"
+ }
+ }
+ }
+ },
+ "CloseReasonDropdown": {
+ "e1f2a3b4c5": "Choisir le motif de clôture"
+ },
+ "GitHubIssueCommentComposer": {
+ "082515176a": "Échec de l'ajout du commentaire",
+ "9f88657c4e": "Issue fermée",
+ "e9b7cb7d17": "Échec de la fermeture de l'issue",
+ "bd3b4492a0": "Issue rouverte",
+ "f2a8c1d903": "Échec de la réouverture de l'issue",
+ "a1b2c3d4e5": "Ajouter un commentaire",
+ "c5c117270e": "Ajoutez votre commentaire ici, soyez bienveillant",
+ "f6a7b8c9d0": "Fermer l'issue",
+ "b1c2d3e4f5": "Rouvrir l'issue",
+ "0a73f59e85": "Envoyer le commentaire",
+ "bf43425540": "Commentaire",
+ "commentTooLarge": "Le commentaire est trop volumineux pour être envoyé sans risque."
+ },
+ "GitHubWorkItemAssigneePopoverContent": {
+ "cddd9b04a7": "Chargement des assignés",
+ "a00830d3f7": "Aucun utilisateur",
+ "4f8b6f2c1d": "Filtrer les assignés..."
+ },
+ "GitHubWorkItemLabelPopoverContent": {
+ "2aa9acdf34": "Modifier les étiquettes sur GitHub",
+ "cddd9b04a7": "Chargement des étiquettes",
+ "de26e2eb06": "Aucun label",
+ "8b0d52ee3a": "Filtrer les labels..."
+ },
+ "githubIssueCloseReasons": {
+ "completed": {
+ "label": "Fermer comme terminée",
+ "description": "Terminée, fermée, corrigée, résolue"
+ },
+ "notPlanned": {
+ "label": "Fermer comme non planifiée",
+ "description": "Ne sera pas corrigée, non reproductible, obsolète"
+ },
+ "duplicate": {
+ "label": "Fermer comme doublon",
+ "description": "Doublon d'une autre issue"
+ }
+ },
+ "CommentReactions": {
+ "addReaction": "Ajouter une réaction",
+ "removeNamedReaction": "Retirer la réaction {{value0}}",
+ "addNamedReaction": "Ajouter la réaction {{value0}}"
+ }
+ },
+ "linear": {
+ "api": {
+ "key": {
+ "dialog": {
+ "834a52c084": "Vérification...",
+ "f8f704a019": "Annuler",
+ "e603ee9156": "Paramètres d'API de l'espace de travail",
+ "dc7ccb0f7c": "Clés d'API personnelles",
+ "e3100b36b9": "Si les clés d'API des membres sont bloquées, demandez à un administrateur de l'espace de travail de les autoriser depuis les paramètres d'API de l'espace de travail.",
+ "d56d3629f4": "Privilégiez l'accès complet lorsqu'Orca doit afficher toutes les équipes accessibles au compte dans cet espace de travail. Les clés restreintes n'exposent que les équipes autorisées, et les équipes privées exigent que le propriétaire de la clé y ait accès.",
+ "af52a6227f": "Créez une clé d'API personnelle depuis Account > Security & Access.",
+ "edec49dfae": "lin_api_...",
+ "7d498f653c": "Clé d'API personnelle",
+ "57a66522c8": "connexion en cours",
+ "c9889a09f8": "Utilisez Linear pour choisir l'espace de travail souhaité avant de créer la clé.",
+ "e689a4d0a6": "error"
+ }
+ }
+ },
+ "priority": {
+ "icon": {
+ "c43d3e065b": "Priorité :"
+ }
+ },
+ "project": {
+ "view": {
+ "surfaces": {
+ "8bbecb2510": "Aucun",
+ "e1fa97d21d": "Sélectionnez un projet pour afficher sa vue d'ensemble.",
+ "1748d3b9af": "Étiquettes",
+ "65bda65159": "Membres",
+ "c5f79616c3": "Équipes",
+ "25a2196732": "Cible",
+ "3fb6473111": "Début",
+ "111bef9aa8": "Responsable",
+ "3be47aed6f": "Priorité",
+ "f5ef24cf46": "Santé",
+ "9ddb58edbd": "Statut",
+ "0a6a5a7dd6": "Dernière mise à jour",
+ "c8db98b73b": "Ressources",
+ "bb1405eff8": "Jalons",
+ "5d99315fb8": "Planification",
+ "3ad562bdf4": "issues du périmètre",
+ "563501f191": "Progression",
+ "bb5664d456": "Aucune description de projet.",
+ "7b147907dc": "Linear",
+ "a9785c7158": "Actualiser",
+ "ee3d2caabd": "Tickets",
+ "5f79bc76b0": "Retour aux projets",
+ "aac9a4afc6": "Ouvrir dans Linear",
+ "7616c986c6": "Ouvrir {{value0}} issues",
+ "93e1f6bfca": "Chargement",
+ "98730088a6": ". Recherchez ou ouvrez Linear pour voir l'ensemble complet.",
+ "06b887d622": "Affichage des premières",
+ "2c4b1c2c08": "nombre",
+ "f2cc1e0ff6": "Linear / Projets",
+ "906b5e4cb8": "Linear / Projets / {{value0}}",
+ "85607ff793": "Projet",
+ "20b9d09b7d": "Inconnu",
+ "f059181bd9": "Privé",
+ "27d91cb1a6": "Partagé",
+ "9f0f51fd9e": "Créez ou enregistrez des vues dans Linear, puis actualisez.",
+ "f4c79cff5f": "Consultez l'erreur d'espace de travail ci-dessous, puis actualisez.",
+ "ef90b21366": "Aucune vue trouvée",
+ "c0a50f96a4": "Impossible de charger les vues",
+ "df4bd63c1d": "Non assigné",
+ "30402d2c6e": "Essayez la recherche ou actualisez.",
+ "a2f31c4cd6": "Aucun projet Linear trouvé",
+ "c9b6e9f90d": "Impossible de charger les projets Linear"
+ }
+ }
+ },
+ "scope": {
+ "selector": {
+ "91c8871dad": "Ajouter un accès d'équipe",
+ "7783361266": "Toutes les équipes",
+ "e1ae6bebb0": "Équipes",
+ "a14ce4df2b": "Tous les espaces de travail",
+ "05baa5ae90": "Espace de travail",
+ "89f6580dbf": "Rechercher des équipes...",
+ "b3488fad3c": "Aucune équipe n'a été récupérée. L'accès peut dépendre du périmètre de la clé, de l'appartenance à des équipes privées, d'équipes archivées, des permissions ou d'un échec de récupération.",
+ "405b33c378": "Aucune équipe récupérée ne correspond à votre recherche."
+ }
+ }
+ },
+ "notification": {
+ "sound": {
+ "options": {
+ "e38b0a2e68": "Beep",
+ "0acd3d384e": "Clack",
+ "79919c832d": "Ding",
+ "2b44847d8d": "Blop",
+ "020826ef17": "Sonar",
+ "588c90487d": "Blip",
+ "1e4b81d892": "Thump",
+ "86af8d938c": "Bong",
+ "80f7cc95b3": "Two Tone",
+ "017abebfa6": "Son système par défaut"
+ }
+ }
+ },
+ "worktree": {
+ "creation": {
+ "WorktreeCreationPanel": {
+ "dabd226118": "Ignorer",
+ "34dd5ee38b": "Réessayer",
+ "ed2a664f8b": "Impossible de créer le worktree",
+ "a3346fc6ed": "Annuler la création du worktree",
+ "532aea14ce": "Annuler",
+ "767951265d": "Un problème est survenu lors de la création du worktree.",
+ "vmProvisioningTitle": "Provisionnement de la VM",
+ "cancelProvisioning": "Annuler",
+ "vmProvisioningLogEmpty": "En attente de la sortie de la recette…"
+ }
+ }
+ },
+ "workspace": {
+ "space": {
+ "WorkspaceSpacePage": {
+ "8d0048e1cb": "Utilisation du disque de l'espace de travail et stockage worktree récupérable.",
+ "e8d6ba11ab": "Beta",
+ "45f6302dbc": "Espace",
+ "ecf72fdc3b": "Retour"
+ }
+ },
+ "cleanup": {
+ "WorkspaceCleanupDialog": {
+ "3828408538": "Supprimer {{value0}}",
+ "352f15d6fc": "Dernière activité",
+ "cbf2f664e2": "Supprimer",
+ "b6bae1eed1": "Annuler",
+ "592fbab446": "Trié par activité la plus ancienne",
+ "dba753e94f": "à supprimer",
+ "38ca0b1400": "Cette opération supprime définitivement leurs fichiers locaux. Vous ne pourrez pas annuler.",
+ "c4f4782c02": "Non suggéré",
+ "0a2e3c7cba": "À examiner",
+ "e97e4580c7": "Modifié",
+ "9623a5107d": "Commits non poussés",
+ "e8b3741ff7": "Ignoré",
+ "a9957007eb": "Ignorer {{value0}}",
+ "1bffc07ba7": "Voir {{value0}}",
+ "bef0adef9b": "Branche",
+ "0b1766738a": "Dépôt",
+ "bbb1ab6a6f": "Sélectionner {{value0}}",
+ "d1094dd529": "En attente de relecture",
+ "4b93a235d8": "Suggéré",
+ "f68d538c63": "Aucun espace de travail dans cet ensemble de nettoyage.",
+ "4719327c9c": "Toutes les suggestions de nettoyage sont ignorées.",
+ "a19040cd67": "Aucun espace de travail inactif ne correspond aux dépôts sélectionnés.",
+ "97c772c4fe": "Aucun espace de travail inactif trouvé dans les dépôts cochés.",
+ "d3eef9463d": "Aucun espace de travail inactif à supprimer.",
+ "aaee139eab": "Restaurer les suggestions ignorées",
+ "06cf78521e": "Tout sélectionner dans {{value0}}",
+ "73690b0031": "Tout désélectionner dans {{value0}}",
+ "b771c92598": "Supprimer la sélection",
+ "37ab28277e": "non suggéré",
+ "1b18868569": "à vérifier",
+ "b299f201b9": "suppression sans risque",
+ "2b31bf68de": "inactif",
+ "ac5ba84cc1": "sélectionnés",
+ "8b74d4ea6e": "Analyse des worktrees et de l'état Git, puis combinaison des signaux d'onglets ouverts, de terminaux, d'agents actifs et de disponibilité distante avant de suggérer les suppressions.",
+ "7eee951968": "Analyse des espaces de travail",
+ "191f0bc98e": "Fermer",
+ "7ae2ad30f4": "Actualiser",
+ "e0b5a4deaa": "Vérifiez les espaces de travail inactifs avant de supprimer leurs fichiers locaux et leur état Orca.",
+ "b2c1331844": "Supprimer les espaces de travail inactifs",
+ "41d594d01e": "Impossible de supprimer {{value0}} espace de travail{{value1}}",
+ "0f00612b6d": "Suppression de {{value0}} espace de travail{{value1}} effectuée",
+ "7f451a3e2c": "Impossible d'ignorer la suggestion de nettoyage",
+ "662b8ec3f8": "Échec du scan de nettoyage des espaces de travail",
+ "bc43c37faf": "masqué",
+ "0c6672f5e3": "Suggestions de nettoyage ignorées",
+ "fc49f79434": "mixte",
+ "2ddbd6fe8a": "coché",
+ "ee81adfcef": "Affichage",
+ "4d0b72481c": "Ignorer",
+ "9cc26c019d": "Supprimer",
+ "0e2d235c63": "Analyse des espaces de travail prête",
+ "4a35c08764": "À examiner",
+ "47123d0108": "Collecte des informations sur les espaces de travail. Vous pouvez fermer cette fenêtre et revenir plus tard.",
+ "9a3be9f2df": "Analyse des espaces de travail en cours. Les nouvelles lignes apparaissent ici au fur et à mesure. Vous pouvez fermer cette fenêtre et revenir plus tard.",
+ "3d957ff117": "Aucun espace de travail ne correspond à ces filtres.",
+ "e94b1f8bb4": "Réinitialiser les filtres",
+ "efb3843e75": "Filtrer et trier les espaces de travail",
+ "93b7381d50": "Filtres",
+ "a615e24679": "Trier",
+ "4cc5b73efe": "Recherche des espaces de travail...",
+ "5bf2e88480": "{{value0}}/{{value1}} {{value2}} analysés",
+ "searchPlaceholder": "Rechercher des espaces de travail",
+ "ageFilter": "Âge",
+ "reviewFilter": "À examiner",
+ "gitFilter": "Git",
+ "contextFilter": "Contexte",
+ "sortBy": "Trier par",
+ "sortDirection": "Direction",
+ "1d3503357d": "Vous pouvez fermer cette fenêtre et revenir pendant que la suppression continue.",
+ "4c2990886e": "{{value0}}/{{value1}} supprimés",
+ "86ba852118": "{{value0}}, {{value1}} en échec",
+ "7b7bde5181": "Espaces de travail vérifiés jusqu'à présent : {{value0}}",
+ "deletingCount": "Suppression des espaces de travail : {{value0}}",
+ "deleteCount": "Supprimer les espaces de travail : {{value0}} ?",
+ "selectedForDeletionCount": "Sélectionnés pour suppression : {{value0}}",
+ "deleteButtonCount": "Supprimer {{value0}}",
+ "archivedStatus": "Archivé",
+ "readyStatus": "Prêt",
+ "f637f63882": "Le Resource Manager compte {{value0}} ; cette liste en a trouvé {{value1}}. Ce compteur repose uniquement sur le journal d'activité d'Orca, tandis que cette analyse vérifie aussi l'historique Git de chaque espace de travail et ignore les remotes déconnectés.",
+ "74f6c16279": "Retour"
+ },
+ "backgroundRemoval": {
+ "removed": "Espaces de travail supprimés : {{value0}}",
+ "failed": "Espaces de travail non supprimés : {{value0}}",
+ "error": "Échec du nettoyage des espaces de travail",
+ "skippedAncestor": "Ignoré car un espace de travail imbriqué n'a pas pu être supprimé.",
+ "timedOut": "La suppression de {{value0}} prend plus de temps que prévu. Elle continuera en arrière-plan.",
+ "stillRemoving": "Espaces de travail encore en cours de suppression : {{value0}}",
+ "skippedPendingAncestor": "Ignoré car un espace de travail imbriqué n'a pas terminé sa suppression."
+ },
+ "candidateRow": {
+ "gitLabel": "Git",
+ "commitsLabel": "Commits",
+ "contextLabel": "Contexte",
+ "flagsLabel": "Indicateurs",
+ "collapseDetails": "Réduire les détails",
+ "expandDetails": "Développer les détails",
+ "cleanGit": "Git propre",
+ "dirtyGit": "Git modifié",
+ "unpushedCommits": "Commits non poussés",
+ "gitUnknown": "Git inconnu",
+ "gitStatusUnknown": "État Git inconnu",
+ "noUnpushedCommits": "Aucun commit non poussé",
+ "unpushedCommitsCount": "Commits non poussés : {{value0}}",
+ "uncommittedChanges": "Modifications non validées",
+ "terminalTabsCount": "Onglets de terminal : {{value0}}",
+ "editorTabsCount": "Onglets d'éditeur : {{value0}}",
+ "browserTabsCount": "Onglets de navigateur : {{value0}}",
+ "diffNotesCount": "Notes de diff : {{value0}}",
+ "completedAgentsCount": "Agents terminés : {{value0}}",
+ "contextCount": "Contexte : {{value0}}",
+ "mainWorkspaceBlocker": "Espace de travail principal",
+ "folderProjectBlocker": "Projet de dossier",
+ "pinnedBlocker": "Épinglés",
+ "activeWorkspaceBlocker": "Espace de travail actif",
+ "runningTerminalBlocker": "Processus de terminal en cours",
+ "terminalLivenessUnknownBlocker": "Activité du terminal inconnue",
+ "dirtyEditorBufferBlocker": "Tampon d'éditeur non enregistré",
+ "volatileLocalContextBlocker": "Contexte local volatil",
+ "recentVisibleContextBlocker": "Onglets visités récemment",
+ "liveAgentBlocker": "Agent actif",
+ "sshDisconnectedBlocker": "Distant indisponible",
+ "gitStatusErrorBlocker": "État Git indisponible",
+ "dirtyFilesBlocker": "Fichiers modifiés",
+ "unknownBaseBlocker": "Impossible de vérifier les commits non poussés",
+ "dismissedBlocker": "Ignoré"
+ },
+ "workspace": {
+ "cleanup": {
+ "candidate": {
+ "row": {
+ "b5d2b33e47": "Suppression…",
+ "e1135728e3": "En attente de suppression"
+ }
+ }
+ }
+ }
+ }
+ },
+ "ui": {
+ "color": {
+ "picker": {
+ "ebcf6ba29e": "Couleur hexadécimale invalide.",
+ "faa855a582": "Hex",
+ "1cec618bcc": "Sélecteur {{value0}}"
+ }
+ },
+ "dialog": {
+ "f26c4baeda": "Fermer"
+ },
+ "repo": {
+ "multi": {
+ "combobox": {
+ "286ce70256": "SSH",
+ "4471d4a1c0": "Aucun projet ne correspond à votre recherche.",
+ "bfd8ce21c6": "Tous les projets",
+ "a58a0cd100": "Rechercher des projets...",
+ "65a3dae41d": "Aucun projet"
+ }
+ }
+ },
+ "sheet": {
+ "1189e9fe0a": "Fermer"
+ }
+ },
+ "terminal": {
+ "quick": {
+ "commands": {
+ "TerminalQuickCommandActionToggle": {
+ "b0d58e37ed": "Prompt d'agent",
+ "b5ea4d64f6": "Commande de terminal",
+ "terminal_short": "Terminal",
+ "agent_short": "Agent"
+ },
+ "TerminalQuickCommandAppendEnterSwitch": {
+ "e4e5fed3b3": "Activer/désactiver « Ajouter Entrée »",
+ "c936c2d6d2": "Envoie immédiatement au lieu de simplement insérer le texte.",
+ "5fa607d807": "Ajouter Entrée",
+ "767e4be3e3": "Ajouter Entrée — exécution immédiate"
+ },
+ "TerminalQuickCommandDialog": {
+ "925b8e0f6e": "Avancé",
+ "97e96cc027": "/goal",
+ "e604bd40d6": "Prend en charge les skills, les chemins de fichiers et les commandes intégrées comme",
+ "79af0c0841": "npm run dev",
+ "577a342c7d": "Demander à l'agent d'examiner cet espace de travail",
+ "026cfb232a": "Ne prend pas en charge les commandes de prompt",
+ "346d409ab2": "Choisir un agent",
+ "0adba8fa0c": "Agent",
+ "ec8f081919": "Action",
+ "ed04233b3e": "Les éléments enregistrés apparaissent dans le menu de la barre d'onglets pour un lancement en un clic.",
+ "ca414324ee": "Texte de la commande",
+ "dc921c17ee": "Prompt",
+ "5b3f634a55": "Ajouter une commande rapide",
+ "f9b184fc16": "Modifier la commande rapide",
+ "6751598542": "modifier",
+ "command_label": "Commande",
+ "agent_toolbar_hint": "Prend en charge /goal, les skills et les chemins",
+ "agent_footer_hint": "Les prompts multi-lignes conviennent — restez concis.",
+ "resize_hint": "Glissez le coin pour redimensionner"
+ },
+ "TerminalQuickCommandDialogFooter": {
+ "2e2b958dfc": "Enregistrer",
+ "8dff838dea": "Enregistrer ({{value0}})",
+ "28370f16b9": "Annuler"
+ },
+ "TerminalQuickCommandLabelField": {
+ "66ea254301": "Démarrer le serveur de dev",
+ "db17f1e41e": "Label"
+ },
+ "TerminalQuickCommandScopeField": {
+ "2db6edede7": "L'enregistrement conserve la portée de projet actuelle, sauf si vous en choisissez une autre.",
+ "2496523a6f": "Choisir le projet",
+ "2264edd5d3": "Projet absent de la liste",
+ "3834d24243": "Projet",
+ "b83efc79e2": "Global",
+ "c25cf350ef": "Portée",
+ "f0631e4999": "dépôt"
+ }
+ }
+ },
+ "pane": {
+ "CloseTerminalDialog": {
+ "ebd2fa844d": "Fermer",
+ "1d1a7a9c1f": "Annuler",
+ "6b9a6975f8": "Le terminal exécute toujours un processus. Si vous fermez le terminal, le processus sera interrompu.",
+ "78b79d854d": "Fermer le terminal ?",
+ "stop_agent_title": "Arrêter cet agent ?",
+ "stop_command_title": "Arrêter la commande en cours ?",
+ "stop_agent_description": "Fermer ce terminal interrompra le travail en cours de l'agent.",
+ "stop_command_description": "Fermer ce terminal interrompra la commande qui s'y exécute.",
+ "dont_ask_again": "Ne plus demander pour les terminaux en cours d'exécution",
+ "stop_agent_confirm": "Arrêter l'agent",
+ "stop_command_confirm": "Arrêter et fermer"
+ },
+ "MobileDriverOverlay": {
+ "c6460cf584": "Reprendre",
+ "c8f2e1a4b9": "Reprendre ce terminal",
+ "c44659e09f": "Pilotage par téléphone",
+ "7cffad954c": "Réduire",
+ "3eed73394f": "Votre clavier est en pause",
+ "faa367dc74": "Votre téléphone a laissé ce terminal en taille téléphone",
+ "54f7d6f69d": "Reprendre tous les terminaux",
+ "b3d8e1f42a": "Restaurer ce terminal",
+ "e8c4f2a91b": "Restaurer tous les terminaux",
+ "f2a8b9c1d3": "Depuis votre téléphone",
+ "c7e4a2b8f1": "Votre téléphone a le contrôle",
+ "d9f3c6e2a4": "Le clavier de l'ordinateur est en pause. Reprenez ce terminal pour taper ici, reprenez tous les terminaux contrôlés par votre téléphone, ou réduisez pour continuer à observer.",
+ "a6b1d8f3e2": "Votre session téléphone est terminée. Restaurez ce terminal à la taille bureau, ou tous les terminaux que votre téléphone a laissés en taille téléphone."
+ },
+ "TerminalAgentSessionForkDialog": {
+ "17fc841e59": "Copier le contexte",
+ "0c8a8629b1": "Le fork apparaît comme son propre espace de travail, et non comme un enfant imbriqué. Le nouvel agent reçoit une transcription limitée sous forme de brouillon modifiable.",
+ "620461df22": "Fork de premier niveau",
+ "619b5a35d2": "Créer un fork de espace de travail au premier niveau et démarrer un nouvel onglet d'agent avec le contexte capturé.",
+ "64e292e8e3": "Forker la session d'agent",
+ "9d25de2920": "Créer un fork",
+ "2b10412cfc": "Création..."
+ },
+ "TerminalContextMenu": {
+ "b4cdd9314e": "Effacer l'écran",
+ "8c17d6786d": "Fermer le volet",
+ "copyTerminalId": "Copier l'ID du terminal",
+ "2cf85a6a55": "Copier l'ID du volet",
+ "39809d152f": "Définir le titre…",
+ "clearPaneTitle": "Effacer le titre du volet",
+ "06c2b0f043": "Égaliser les tailles des volets",
+ "98bccf4fa2": "Scinder le terminal vers le bas",
+ "20e565d865": "Scinder le terminal vers la droite",
+ "8a7ddb8b8a": "Forker la session d'agent…",
+ "0a82b0608c": "Ajouter une commande rapide…",
+ "9528a65ef8": "Aucune commande rapide",
+ "3ce594a4a0": "Global",
+ "ec85df5914": "Commandes rapides",
+ "0a917b591a": "Coller",
+ "f3eeb1de13": "Copier",
+ "selectAll": "Tout sélectionner",
+ "c2f0b72b8d": "Insérer",
+ "925f49f210": "Agrandir le volet",
+ "df766809e0": "Réduire le volet",
+ "cff67afad1": "Copier le contexte",
+ "15dd899676": "Ajouter à {{value0}}…"
+ },
+ "TerminalErrorToast": {
+ "e4aa243f8c": "Redémarrer le daemon",
+ "a7e2fd2699": "signaler un problème",
+ "5c8ce20be6": "Si le problème persiste, veuillez",
+ "cc6d997c65": "Redémarrez le daemon de terminal depuis ici pour effacer un état de daemon obsolète.",
+ "7ee11bc0db": "Orca n'a pas pu confirmer si la session précédente de ce terminal tourne encore ; il a donc laissé la session intacte. Rouvrez ce volet pour réessayer.",
+ "e16012e31e": "Le daemon de terminal propriétaire de cette session s'est arrêté ; la session et son historique de défilement n'ont pas pu être récupérés. Ouvrez un nouveau terminal pour continuer.",
+ "sessionUnavailable": "Orca n'a pas pu se rattacher à la session de terminal de ce volet sur l'hôte. Ouvrez un nouveau terminal pour continuer."
+ },
+ "TerminalProcessExitOverlay": {
+ "capacityTitle": "Limite de consoles Git Bash atteinte",
+ "capacityDetail": "Git Bash a atteint sa limite de 128 consoles. Fermez les terminaux Git Bash inutilisés, puis redémarrez ce terminal.",
+ "failedTitle": "Terminal terminé",
+ "failedDetail": "Le processus shell s'est terminé avec le code de sortie {{code}}. Sa sortie est conservée.",
+ "close": "Fermer",
+ "restart": "Redémarrer"
+ },
+ "TerminalPane": {
+ "ac112e9036": "Retirer le titre",
+ "f984ab2a30": "Retirer le titre du volet : {{value0}}",
+ "cc5a2dc706": "Modifier le titre du volet : {{value0}}",
+ "7dbbfcbecc": "Titre du volet"
+ },
+ "TerminalLinkActionPopover": {
+ "openLink": "Ouvrir le lien",
+ "openFile": "Ouvrir le fichier",
+ "switchWorkspace": "Changer de espace de travail",
+ "openInFinder": "Afficher dans le Finder",
+ "openFolder": "Ouvrir le dossier",
+ "openWithDefaultApp": "Ouvrir avec l'application par défaut",
+ "switchTerminal": "Changer de terminal",
+ "openTaskTerminal": "Ouvrir le terminal de tâches",
+ "systemBrowser": "Navigateur système",
+ "orcaBrowser": "Navigateur Orca",
+ "terminalLinkSettings": "Paramètres des liens du terminal",
+ "copyLink": "Copier le lien",
+ "copied": "Copied",
+ "copiedLink": "Lien copié",
+ "copyLinkFailed": "Échec de la copie du lien"
+ },
+ "TerminalSessionStateSaveFailureDialog": {
+ "6bee0c8f17": "Ouvrir l'Analyseur d'espace disque",
+ "ae20d0ffc2": "Ignorer",
+ "38c282a2c4": "L'analyseur s'ouvre directement depuis ici. Vous pouvez aussi l'ouvrir plus tard depuis le menu d'outils en bas à gauche, en choisissant Analyseur d'espace.",
+ "e2fcf07c0d": "Orca n'a pas pu enregistrer cette session de terminal car le stockage local est plein ou inaccessible en écriture. Ouvrez l'analyseur d'espace disque pour trouver du stockage de espace de travail à nettoyer.",
+ "678c780a2c": "Espace disque indisponible"
+ },
+ "osc52": {
+ "clipboard": {
+ "blocked": {
+ "toast": {
+ "97c98f1afe": "Ouvrir le paramètre",
+ "7cf51f74fd": "Activez l'écriture du presse-papiers par les TUI dans les paramètres du terminal pour copier depuis SSH, Zellij, tmux, Neovim, fzf ou Grok.",
+ "89eaa3e80b": "Écriture du presse-papiers du terminal bloquée"
+ }
+ },
+ "default": {
+ "on": {
+ "notice": {
+ "title": "L'écriture du presse-papiers par les TUI est désormais activée par défaut",
+ "description": "Zellij, tmux, Neovim et les autres programmes de terminal peuvent désormais copier vers votre presse-papiers. Désactivez l'option dans les paramètres du terminal.",
+ "action": "Ouvrir le paramètre"
+ }
+ }
+ },
+ "failed": {
+ "toast": {
+ "62a0af2cb4": "La copie du presse-papiers du terminal n'a pas pu être confirmée",
+ "fdd3e7e977": "L'application de terminal a demandé une copie, mais Orca n'a pas pu confirmer qu'elle a bien atteint le presse-papiers système."
+ }
+ }
+ }
+ },
+ "stale": {
+ "agent": {
+ "row": {
+ "ad991ece5c": "Le volet de l'agent n'est plus disponible.",
+ "090d607412": "stale-agent-row-{{value0}}"
+ }
+ }
+ },
+ "terminal": {
+ "agent": {
+ "session": {
+ "fork": {
+ "2317900211": "Échec de la copie du contexte du fork.",
+ "88e34d00eb": "Fork de session de premier niveau ouvert dans un nouveau espace de travail",
+ "fd3d12a1e1": "Échec de la création de l'espace de travail fork.",
+ "38e41edc6e": "Cet espace de travail ne peut pas être forké en worktree Git.",
+ "f867385bb5": "Impossible de trouver l'espace de travail source de ce fork.",
+ "046e8d853c": "Aucun contexte de terminal à forker",
+ "c00421d320": "Contexte du fork copié. Lancez un agent et collez-le pour démarrer le fork.",
+ "f62b40e2c7": "Aucun contexte de terminal à copier",
+ "373a3103e7": "Contexte copié",
+ "3fc568a49d": "Échec de la copie du contexte."
+ }
+ }
+ },
+ "drop": {
+ "handler": {
+ "1e072f611e": "Échec de l'envoi de {{value0}} {{value1}}.",
+ "53f015fd85": "Ignoré : {{value0}} symlink{{value1}}.",
+ "29c031b49a": "Envoi de {{value0}} fichier{{value1}} vers le runtime…",
+ "0c77693641": "Worktree pas encore prêt — réessayez dans un instant.",
+ "ce8248b835": "Chemin du worktree indisponible.",
+ "internalTooManyPaths": "Le contenu déposé contient trop de chemins pour un collage sûr dans le terminal.",
+ "internalPathsTooLarge": "La liste de chemins déposée est trop volumineuse pour un collage sûr dans le terminal.",
+ "b4cf68e889": "Ignoré : {{value0}} {{value1}}.",
+ "writeTimeout": "Glisser-déposer de fichiers annulé : le terminal n'a pas accepté le chemin avant le délai de sécurité.",
+ "writeRejected": "Glisser-déposer de fichiers annulé : le terminal n'a pas pu accepter le chemin."
+ }
+ }
+ },
+ "use": {
+ "terminal": {
+ "pane": {
+ "context": {
+ "menu": {
+ "a29b9faa01": "ID du volet copié",
+ "pane": {
+ "id": {
+ "copy": {
+ "failed": "Impossible de copier l'ID du volet"
+ }
+ }
+ },
+ "terminal": {
+ "id": {
+ "copied": "ID du terminal copié",
+ "copy": {
+ "failed": "Impossible de copier l'ID du terminal"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "PinnedTabCloseDialog": {
+ "6c190f295a": "Fermer l'onglet épinglé ?",
+ "0d1963f4a6": "Cet onglet est épinglé. Voulez-vous vraiment le fermer ?",
+ "dont_ask_again": "Ne plus demander pour les onglets épinglés",
+ "0b38ee2f86": "Annuler",
+ "c337c9d75c": "Fermer"
+ },
+ "TerminalSshReconnectOverlay": {
+ "authFailed": "Échec de l'authentification pour {{value0}}. Reconnectez-vous pour poursuivre cette session de terminal.",
+ "reconnectFailed": "La connexion SSH à {{value0}} a échoué. Reconnectez-vous pour poursuivre cette session de terminal.",
+ "connecting": "Connexion à {{value0}} en cours. Ce terminal reprendra dès que l'hôte sera disponible.",
+ "connected": "SSH est connecté.",
+ "disconnected": "Ce terminal attend {{value0}}. Connectez-vous pour poursuivre cette session SSH.",
+ "connectFailed": "Échec de la connexion SSH",
+ "title": "Connexion SSH requise",
+ "connectingButton": "Connexion...",
+ "connectButton": "Se connecter",
+ "removedTitle": "Hôte SSH supprimé",
+ "removedBody": "L'hôte SSH de cet espace de travail a été supprimé ; il ne peut plus se connecter. Supprimez l'espace de travail pour l'effacer — les fichiers distants restent intacts.",
+ "removeWorkspaceButton": "Supprimer l'espace de travail"
+ },
+ "TerminalRemoteRuntimeReconnectBanner": {
+ "retryingTitle": "Reconnexion au runtime distant",
+ "disconnectedTitle": "Runtime distant déconnecté",
+ "retryingBody": "Orca réessaiera pendant une minute au maximum. Ce terminal reprendra si la connexion revient.",
+ "disconnectedBody": "Les tentatives automatiques sont interrompues. Reconnectez-vous pour reprendre cette session de terminal.",
+ "reconnectButton": "Reconnecter"
+ },
+ "TerminalQuickCommandsSubmenu": {
+ "3ccc7981bb": "Hôte indisponible",
+ "54f29b7c0d": "Chargement de l'hôte…"
+ }
+ }
+ },
+ "tab": {
+ "group": {
+ "TabGroupPanel": {
+ "814fb04c43": "Chargement de l'éditeur...",
+ "f7d6ce445e": "Fermer le groupe",
+ "0db2081805": "Scinder vers le haut",
+ "30137df7d0": "Scinder vers la gauche",
+ "4df2a06d36": "Scinder vers le bas",
+ "ab1e2bff04": "Scinder vers la droite",
+ "9acaf92093": "Actions du volet",
+ "1bce81dba6": "simulateur",
+ "1ff1c77616": "navigateur",
+ "586d2ac445": "terminal",
+ "addSplitPane": "Ajouter un volet scindé",
+ "closePaneColumn": "Fermer le volet scindé"
+ },
+ "AiVaultSessionDropLayer": {
+ "dropOntoTerminalPane": "Déposez sur un volet de terminal pour reprendre cette session.",
+ "couldNotReadPayload": "Impossible de lire les données de glisser-déposer de la session.",
+ "localWorkspacesOnly": "La reprise depuis l'historique n'est disponible que dans les espaces de travail locaux.",
+ "openLocalWorkspace": "Ouvrez un espace de travail local avant de reprendre une session.",
+ "sessionQueued": "Session en attente",
+ "openSupportedWorkspace": "Ouvrez un espace de travail avant de reprendre une session.",
+ "sessionHostMismatchUnsupported": "Cette session appartient à un autre hôte. Déposez-la sur un espace de travail du même hôte.",
+ "localSessionSshWorkspaceUnsupported": "L'historique de cette session est stocké sur cette machine ; la reprise est impossible dans un espace de travail SSH. Déposez-la plutôt sur un espace de travail local."
+ },
+ "TabGroupDropOverlay": {
+ "paneColumnLabel": "Nouvelle scission"
+ }
+ },
+ "bar": {
+ "BrowserTab": {
+ "6e0bc8f3a8": "Ouvrir dans le navigateur",
+ "9dd880bd56": "Fermer les onglets à droite",
+ "1611a1324b": "Fermer",
+ "5d6e89891f": "Dupliquer l'onglet",
+ "966feb9ad5": "Scinder vers la droite",
+ "7e8106899f": "Scinder vers la gauche",
+ "2186a8407c": "Scinder vers le bas",
+ "96354ed249": "Scinder vers le haut",
+ "911542656f": "Épingler l'onglet",
+ "c5aaee8c39": "Détacher l'onglet"
+ },
+ "EditorFileTab": {
+ "3da7445c84": "Renommer le fichier {{value0}}"
+ },
+ "EditorFileTabContextMenu": {
+ "52ce4f4605": "Copier le chemin relatif",
+ "5b85754786": "Copier le chemin",
+ "bfd5797ef4": "Ouvrir l'aperçu Markdown",
+ "e5ff31ccaf": "Fermer les onglets à droite",
+ "ba1369dd24": "Fermer tous les onglets de l'éditeur",
+ "1ba8492c5b": "Fermer",
+ "68cc610e7f": "Renommer",
+ "f7c3d7d5af": "Scinder vers la droite",
+ "e3ff145b98": "Scinder vers la gauche",
+ "1d04b1630b": "Scinder vers le bas",
+ "6b3efb106e": "Scinder vers le haut",
+ "fdd29eb669": "Épingler l'onglet",
+ "8e9d603a09": "Détacher l'onglet"
+ },
+ "QuickLaunchButton": {
+ "348a04c1ad": "Paramètres de l'agent…",
+ "ec2adf093e": "Lancer {{value0}} dans un nouveau terminal",
+ "465e432ef1": "Impossible de construire la commande de lancement pour {{value0}}.",
+ "e518f544b1": "Aucun agent détecté",
+ "8dea9b5cdf": "Aucun agent activé"
+ },
+ "RecentTabSwitcher": {
+ "329638ff6f": "Changer d'onglet",
+ "07ad4cd0b7": "Basculer entre les onglets"
+ },
+ "SortableTab": {
+ "ab19f603eb": "Renommer l'onglet {{value0}}",
+ "6df69d9388": "Fermer l'onglet {{value0}}",
+ "fdb2691425": "Réduire le volet",
+ "95db5f2f7d": "Fermer l'onglet"
+ },
+ "SortableTabContextMenu": {
+ "35e8892fd0": "Couleur de l'onglet",
+ "2f697b3c31": "Modifier le titre",
+ "c1ee099c7e": "Fermer les onglets à droite",
+ "8d16f9cd30": "Fermer les autres",
+ "89359a36f7": "Fermer",
+ "21132389e9": "Scinder vers la droite",
+ "0ce4bae39d": "Scinder vers la gauche",
+ "af80ed83c1": "Scinder vers le bas",
+ "591f9b12c1": "Scinder vers le haut",
+ "7703990447": "Gris",
+ "845576bed1": "Sarcelle",
+ "be905e9b0a": "Vert",
+ "69682e2ce4": "Jaune",
+ "a47629b3cf": "Orange",
+ "620aec6729": "Rouge",
+ "03cf6dab1a": "Rose",
+ "c2d8b0991f": "Violet",
+ "cb3eadefd2": "Bleu",
+ "20baa43c05": "Aucun",
+ "60f958ec75": "Épingler l'onglet",
+ "417722e9c2": "Détacher l'onglet",
+ "splitTerminalRight": "Scinder le terminal vers la droite",
+ "splitTerminalDown": "Scinder le terminal vers le bas"
+ },
+ "TabBar": {
+ "b1a132357f": "Nouvel onglet",
+ "4f327c8b3d": "Ouvrir un Markdown...",
+ "3d5d6c960d": "Nouveau Markdown",
+ "fd2b42aaa3": "Nouvel émulateur mobile",
+ "aea43b5748": "Ouvrir l'onglet émulateur existant.",
+ "b426bb2615": "Aller à l'émulateur mobile",
+ "4833fb2cbe": "Nouvel onglet de navigateur",
+ "d364f3c8d4": "Nouveau terminal",
+ "7c1313d237": "Nouveau terminal :",
+ "d1afac112b": "WSL",
+ "efb33546ff": "Git Bash",
+ "1a8af49530": "Invite CMD",
+ "2148f65e04": "PowerShell",
+ "ab589350e5": "Impossible de construire la commande de lancement pour {{value0}}.",
+ "7a9b4af2af": "Faire défiler les onglets vers la gauche",
+ "232e075b07": "Faire défiler les onglets vers la droite"
+ },
+ "TabBarCreateEntry": {
+ "d62d63b807": "Créer un fichier",
+ "25dc1cd653": "Ouvrir le fichier",
+ "7cdf8ee0c8": "Ouvrir une URL",
+ "b27864279e": "Lancer un agent",
+ "0e5b7a3f16": "Rechercher parmi les onglets ouverts, fichiers, URL et agents…",
+ "8f0a1c4d92": "Basculer vers l'onglet",
+ "2c38630a01": "L'espace de travail n'existe plus",
+ "4f0d9a71c2": "L'onglet n'existe plus",
+ "d7d496a451": "La page du navigateur n'existe plus",
+ "7726ce9970": "L'onglet émulateur mobile n'existe plus",
+ "chooseAction": "Choisissez une action.",
+ "searchProvider": "Rechercher {{value0}}"
+ },
+ "TabBarQuickCommandsButton": {
+ "a2c7a33831": "Commande",
+ "20bbd75896": "Aucune commande",
+ "b82e237a4b": "Plus de commandes rapides",
+ "85482c57bc": "Exécuter la commande rapide",
+ "b775303755": "Exécuter la commande rapide : {{value0}}",
+ "196593b6a9": "Supprimer {{value0}}",
+ "15529ede69": "Modifier {{value0}}",
+ "1d411fb6a5": "Enregistrer une commande rapide pour ce dépôt",
+ "8f1e971966": "Ajouter une commande rapide",
+ "3220e2da27": "Cette commande rapide sera retirée de votre liste enregistrée.",
+ "e8e1a52edb": "Supprimer « {{value0}} » ?",
+ "37e1bb90ce": "Exécuter : {{value0}}",
+ "77ac113df0": "Démarrer {{value0}} : {{value1}}",
+ "7b1c9d6ae1": "Exécution",
+ "c781f992e4": "destructive",
+ "be8f0ff166": "Supprimer",
+ "f3a8c2d1e7": "Rechercher des commandes rapides...",
+ "b4e7f9a2c1": "Aucune commande ne correspond",
+ "8d525e5f15": "Copied",
+ "53b17a4b1b": "Impossible de copier",
+ "a9a564b7e7": "Copier {{value0}}",
+ "69a1441a21": "Rien à copier",
+ "192a4616a5": "Actions de la commande rapide"
+ },
+ "shell": {
+ "icons": {
+ "d4ceaa227c": "Git",
+ "e9b2e70613": "WSL"
+ }
+ },
+ "tab": {
+ "create": {
+ "entry": {
+ "classifier": {
+ "42e6262ae9": "Aucune action disponible.",
+ "097a982ee0": "Chargement des fichiers...",
+ "c41f8d20b7": "Rechercher parmi les onglets ouverts, fichiers, URL et agents…",
+ "90eb94dc48": "Saisissez une URL http:// ou https://.",
+ "5553b283ce": "Saisissez une URL ou un chemin de fichier.",
+ "queryTooLarge": "Le texte recherché est trop long.",
+ "absolutePathRemoteBlocked": "Les chemins absolus nécessitent un espace de travail local."
+ }
+ },
+ "menu": {
+ "options": {
+ "5501c2fb7a": "terminal",
+ "9630dd5494": "shell",
+ "a094576900": "nouveau terminal",
+ "4f23f4d01d": "nouveau shell",
+ "4f2a91e15b": "navigateur",
+ "6d0e6a4b7a": "nouveau navigateur",
+ "c87ad57785": "onglet de navigateur",
+ "cce7ef1d2c": "web",
+ "5f17fb9d0c": "markdown",
+ "44caaf7b36": "md",
+ "fb50e3d874": "nouveau markdown",
+ "6d8b6b4117": "nouveau fichier",
+ "b330f72434": "mark",
+ "37ff3ddca1": "ouvrir un markdown",
+ "164c394bab": "ouvrir un fichier",
+ "bbaf4f85a4": "émulateur mobile",
+ "3784b83bd4": "émulateur",
+ "a63847a742": "simulateur",
+ "1baeb07c17": "simulateur iOS",
+ "8a580f88cf": "iPhone",
+ "7ecdc5ef08": "iPad",
+ "14965cc123": "mobile"
+ }
+ }
+ }
+ },
+ "TabWorkspaceLayoutMenuSection": {
+ "right": "Droite",
+ "left": "Gauche",
+ "down": "Bas",
+ "up": "Haut",
+ "moveToPaneColumn": "Déplacer l'onglet vers la scission"
+ },
+ "EditorFileTabCloseButton": {
+ "4655cf570e": "Fermer l'onglet",
+ "a768f428f1": "Fermer l'onglet"
+ },
+ "TerminalTabSplitMenuSection": {
+ "splitTerminal": "Scinder le terminal"
+ },
+ "TerminalTabLeadingIcon": {
+ "7ab2964bea": "Achèvement d'agent non lu"
+ },
+ "TabBarQuickCommandAddActions": {
+ "45a2f36d51": "Commande",
+ "b856c833ae": "Commande sur {{value0}}"
+ },
+ "TabBarQuickCommandHostLoadStatus": {
+ "82e294f3ca": "Hôte indisponible",
+ "7c129b08ff": "Chargement de l'hôte…"
+ }
+ }
+ },
+ "status": {
+ "bar": {
+ "PetStatusSegment": {
+ "3668339495": "Supprimer {{value0}}",
+ "cd8c6c654c": "Paramètres du compagnon…",
+ "ed176ad68f": "Importer un bundle .codex-pet…",
+ "59b5955621": "Téléverser le vôtre…",
+ "0608ad02a2": "Choisir un compagnon",
+ "b75484a01a": "Taille du compagnon",
+ "c6aa805b1b": "px",
+ "2f7bbaa457": "Taille",
+ "34c25dfe9c": "Compagnon",
+ "aec479308a": "Menu du compagnon",
+ "cef0ab4636": "Échec de l'importation du bundle de compagnon",
+ "2021d4f6db": "L'importation d'un bundle de compagnon nécessite un redémarrage complet de l'app (pas seulement un rechargement).",
+ "f395c9a685": "Échec de l'importation du fichier",
+ "e6234bcc17": "L'ajout d'un compagnon personnalisé nécessite un redémarrage complet de l'app (pas seulement un rechargement).",
+ "6d0a8cd179": "Afficher le compagnon",
+ "1fbc51cc77": "Masquer le compagnon"
+ },
+ "PortsStatusSegment": {
+ "4ebf90c12e": "Aucun port externe détecté",
+ "7dac3ecc9d": "Ports externes",
+ "95495019ed": "Scan des ports indisponible sur {{value0}} : {{value1}}",
+ "a8e4bdb412": " · {{value0}} externe(s)",
+ "9aa11005bf": "workspace ·",
+ "c22ea609fd": "Ports",
+ "a11ed266ce": "espace de travail",
+ "ca41be2802": "Ports — espace de travail {{value0}} {{value1}}{{value2}}",
+ "b8bc3e420a": "Ports, espace de travail {{value0}} {{value1}}",
+ "3a87d54dfb": "Aucun port de espace de travail détecté",
+ "c174bbbfed": "Recherche des ports de espace de travail...",
+ "8caaa86e9a": "ports",
+ "45834a9ace": "port",
+ "4ae65d871a": "externes",
+ "2b84c4d11f": "{{value0}} espace de travail · {{value1}} externes"
+ },
+ "ResourceUsageStatusSegment": {
+ "946d9f94d0": "Annuler",
+ "67c4ecda49": "Force la fermeture de ce terminal. Tout travail non enregistré dans le volet est perdu. Action irréversible.",
+ "4bb076fa89": "Tuer",
+ "996295bff2": "terminal orphelin",
+ "92924a14e3": "Nettoyer les espaces de travail",
+ "27a74f91f0": "Rien en cours d'exécution pour le moment",
+ "1b24a32d3a": "Mémoire",
+ "298f4be7f2": "CPU",
+ "2aa2de6cb9": "Nom",
+ "30ff2c3c31": "{{value0}} orphelin",
+ "6449a95c78": "Part de la RAM physique de cette machine occupée par les processus suivis par Orca.",
+ "e7ccce7e87": "de la RAM système",
+ "9e2525c89f": "Mémoire résidente utilisée par Orca ainsi que par les processus des terminaux de chaque worktree.",
+ "1fedf94eae": "Charge CPU cumulée. Une valeur supérieure à 100 % signifie que plusieurs cœurs travaillent simultanément.",
+ "e7cf14ec78": "Sessions de terminal indisponibles. La liste est peut-être obsolète.",
+ "93b0de3c21": "Redémarrer",
+ "f85af9cda6": "Instantanés de ressources et sessions de terminal indisponibles.",
+ "f8e0d794b4": "Le daemon ne répond pas",
+ "bd19fd7a59": "Tuer toutes les sessions",
+ "c9382662bb": "Redémarrer le daemon",
+ "59f178fe11": "{{value0}}, daemon injoignable",
+ "21cacb16d1": "· distant",
+ "73a3fd68a9": "Réduire le dépôt",
+ "b12e31dfcb": "Développer le dépôt",
+ "d659d71d2d": "Reprendre l'espace de travail {{value0}}",
+ "bbcd9b7b85": "Réduire l'espace de travail",
+ "c4a8968bdd": "Développer l'espace de travail",
+ "b10695d6ce": "Tuer la session",
+ "288a4dd177": "Orca",
+ "53dd5560ae": "Réduire Orca",
+ "e419d27083": "Développer Orca",
+ "41ae4fa725": "Arrêt forcé…",
+ "138b99bd80": "cette session",
+ "888dad8c55": "Chargement…",
+ "6d9793d4bc": "Gestionnaire de ressources - Terminaux",
+ "ca95d077db": "Daemon injoignable",
+ "a82253b458": "Supprimer l'espace de travail.",
+ "946724a70a": "L'espace de travail principal ne peut pas être supprimé.",
+ "16bc3c998a": "Supprimer l'espace de travail {{value0}}",
+ "0f9e50eb07": "Autre",
+ "d406915b78": "Renderer",
+ "81cd37af99": "Main",
+ "fa6d36758d": "Tuer la session {{value0}}",
+ "b8f4a2c1d0e3": "{{value0}} orphelins",
+ "c7e3b1a0d9f2": "Tuer {{value0}} terminal orphelin",
+ "d8f4c2b1e0a3": "Tuer {{value0}} terminaux orphelins",
+ "e9a5d3c2b1f0": "Tuer {{value0}} ?"
+ },
+ "SshStatusSegment": {
+ "3ad70e0365": "Gérer les hôtes distants…",
+ "6e8a9a4242": "Hôtes distants",
+ "d09ec41831": "Hôtes distants",
+ "fdc57e9970": "État de connexion de l'hôte distant",
+ "59b553e2aa": "Déconnecter",
+ "63f36455cc": "Se connecter",
+ "bf07aee59e": "Échec de la déconnexion",
+ "2c29e2de68": "Échec de la connexion",
+ "bc5a3fd41a": "partiel",
+ "3d0128b105": "connecté",
+ "fd9a3c600e": "error",
+ "fbb3f9f05e": "conflit",
+ "95e4ff5b4b": "push en cours",
+ "63a2b965f6": "pull en cours",
+ "remote_server": "Serveur distant",
+ "runtime_checking": "Vérification",
+ "runtime_online": "Connecté",
+ "runtime_unavailable": "Déconnecté",
+ "runtime_connect_unavailable": "Hôte distant injoignable",
+ "runtime_disconnect_failed": "Échec de la déconnexion",
+ "runtime_reconnecting": "Reconnexion",
+ "runtime_last_close_reason": "Fermé : {{value0}}",
+ "runtime_reconnect_attempt": "Tentative {{value0}}",
+ "runtime_workspace_window_closed": "Fenêtre de l'espace de travail fermée",
+ "connectedHostCount_one": "{{count}} hôte",
+ "connectedHostCount_other": "{{count}} hôtes",
+ "connecting": "Connexion…",
+ "workspaceConflict": "Conflit de espace de travail",
+ "workspaceSyncError": "Erreur de synchronisation de l'espace de travail"
+ },
+ "StatusBar": {
+ "9659e38343": "Ports",
+ "d1e1a7a6bf": "Gestionnaire de ressources",
+ "24ac89df1a": "Hôtes distants",
+ "5e59007df4": "Utilisation Kimi",
+ "8c86cd77b0": "Utilisation OpenCode Go",
+ "c1df0d67ec": "Utilisation Gemini",
+ "c0909c686e": "Utilisation Codex",
+ "3885eb74d8": "Utilisation Claude",
+ "c8857b40f7": "Actualiser les données d'utilisation",
+ "75ded02687": "Gérer les comptes…",
+ "ff0fbe9311": "Actif",
+ "7657e3db9c": "Compte Codex",
+ "38b5647724": "Runtime d'utilisation Codex",
+ "ba55303942": "Ouvrir les détails Codex et le sélecteur de compte",
+ "4dff061aab": "·",
+ "2483c60695": "···",
+ "c35af53b73": "Se connecter",
+ "f19a63e7cd": "Se connecter pour voir l'utilisation",
+ "5c938d39ac": "sem.",
+ "54e8d6bb2d": "Fable",
+ "d79c3362c4": "5 h",
+ "a79c64f87e": "Fable",
+ "8295903d17": "Redémarrez les terminaux Claude actifs avant de reprendre d'anciennes conversations après un changement de compte.",
+ "c98ea88392": "Aucun autre compte",
+ "9332ba8684": "Basculer vers",
+ "d450654fa2": "Compte Claude",
+ "11e2354daf": "Runtime d'utilisation Claude",
+ "3dd7ddfae1": "Ouvrir les détails Claude et le sélecteur de compte",
+ "59c6e7b4e0": "Les sessions visibles redémarrent maintenant. Les autres redémarrent quand leur worktree devient actif.",
+ "c676918adc": "Valeur par défaut du système",
+ "3325d996cb": "Actualiser les limites de débit",
+ "fda8146810": "Ouvrir les détails d'utilisation Kimi",
+ "629251f4b6": "Ouvrir les détails d'utilisation OpenCode Go",
+ "d2375976eb": "Ouvrir les détails d'utilisation Gemini",
+ "3d79122c3f": "kimi",
+ "d7a0668acc": "opencode-go",
+ "68efc0345c": "gemini",
+ "76b06d4da5": "claude",
+ "a28a5dd9b1": "error",
+ "cd9d7b40ff": "Redémarrer {{value0}} sessions",
+ "6cd6650b4c": "Redémarrer la session",
+ "1446d0d8a0": "{{value0}} sessions Codex sont toujours sur l'ancien compte.",
+ "605901a495": "1 session Codex est encore sur l'ancien compte",
+ "5e5f9f5160": "1 réinitialisation de limite de débit disponible",
+ "5ecae9197c": "{{value0}} réinitialisations de limite de débit disponibles",
+ "25d8bbde69": "Utilisation de la réinitialisation…",
+ "e159fc1fd7": "Réinitialiser maintenant",
+ "972a1ff497": "Réinitialiser les limites Codex ?",
+ "6d1042aa6f": "Cela consomme un crédit de réinitialisation de limite de débit Codex pour le compte actif et réinitialise immédiatement toute fenêtre d'utilisation éligible.",
+ "f077f586db": "Ne plus demander",
+ "c0e972d726": "Annuler",
+ "06741a2f3d": "Ouvrir les détails d'utilisation MiniMax",
+ "3bbf140864": "Utilisation MiniMax",
+ "remoteServerLabel": "Serveur distant",
+ "antigravityUsage": "Utilisation Antigravity",
+ "antigravityUsageDetails": "Ouvrir les détails d'utilisation Antigravity",
+ "grokUsageAria": "Ouvrir les détails d'utilisation Grok",
+ "grokUsageMenu": "Utilisation Grok",
+ "floatingTerminalNewActivity": "{{label}}, nouvelle activité",
+ "codexSignInSuccess": "Connecté à Codex",
+ "codexSignInError": "Échec de la connexion à Codex. Veuillez réessayer."
+ },
+ "StatusBarUsageEmptyCta": {
+ "828c764a79": "Connecter un compte",
+ "caa0f39811": "Prend en charge :",
+ "97957ad3a3": "Connectez vos comptes de fournisseurs d'IA pour voir leur utilisation en temps réel et basculer facilement entre les comptes.",
+ "9a542f46c7": "Masquer de la barre d'état",
+ "84c3b15dca": "Limites d'utilisation des agents",
+ "d663430cf9": "Configurer le suivi d'utilisation"
+ },
+ "SkillUpdateStatusSegment": {
+ "runningLabel": "Mise à jour des skills",
+ "runningOne": "Mise à jour de {{value0}}…",
+ "runningMany": "Mise à jour des skills de {{value0}}…",
+ "runningAria": "Mise à jour des skills en cours. Cliquez pour ouvrir les détails.",
+ "successLabel": "Skills mis à jour",
+ "successOne": "{{value0}} mis à jour",
+ "successMany": "Skills de {{value0}} mis à jour",
+ "successAria": "Skills mis à jour. Cliquez pour ouvrir les détails.",
+ "errorLabel": "Échec de la mise à jour",
+ "errorTooltip": "Échec de la mise à jour des skills — cliquez pour voir les détails",
+ "errorAria": "Échec de la mise à jour des skills. Cliquez pour ouvrir les détails.",
+ "stoppingLabel": "Arrêt de la mise à jour",
+ "stoppingTooltip": "Arrêt de la mise à jour des skills…",
+ "stoppingAria": "Arrêt de la mise à jour des skills. Cliquez pour ouvrir les détails."
+ },
+ "UpdateStatusSegment": {
+ "5cd13105a3": "Échec de la mise à jour. Cliquez pour déplier.",
+ "2201df6987": "Échec de la mise à jour — cliquez pour voir les détails",
+ "8533c12c3c": "Échec de la mise à jour",
+ "962404f68e": "Mise à jour prête à installer. Cliquez pour déplier.",
+ "248ee5d8ef": "Téléchargement d'Orca v{{value0}}… {{value1}} %",
+ "57a29c3b0e": "Mise à jour prête",
+ "fd1d3b3a1d": "Téléchargement de la mise à jour, {{value0}} pour cent. Cliquez pour déplier.",
+ "9d13213a56": "Orca v{{value0}} prête à installer"
+ },
+ "WorkspaceSpaceCompactPanel": {
+ "a471aa9c24": "Mis à jour",
+ "9be86c46a0": "Libérable",
+ "f4d2651498": "Analysé",
+ "6a5dc3c61a": "À examiner",
+ "c361440dc0": "Beta",
+ "8ff597593d": "Espace",
+ "0582df6d2e": "Analyser",
+ "f5e1a84d79": "Actualiser",
+ "2af2174d6d": "Annuler",
+ "5691353a21": "Arrêt en cours",
+ "2837dc7c72": "annulation",
+ "0583c806ac": "L'espace disque des espaces de travail n'a pas été analysé.",
+ "39786e3b73": "Analyse de la taille des espaces de travail.",
+ "bef4dc0457": "{{value0}} récupérables · {{value1}} indisponibles",
+ "3d8d47ce77": "{{value0}} · dernier résultat conservé"
+ },
+ "WorkspaceSpaceManagerPanel": {
+ "2965415393": "Échec de la suppression forcée",
+ "e031e93219": "Aucun espace de travail correspondant.",
+ "a02d84d2d2": "Analyse des espaces de travail. Vous pouvez quitter cette page.",
+ "be37293b10": "État",
+ "33aef3e9cc": "Taille",
+ "81f14d9924": "Dépôt",
+ "e4aebea158": "Espace de travail",
+ "1d0f8300d1": "Sélectionner les espaces de travail supprimables visibles",
+ "697d60c456": "Effacer la sélection visible",
+ "81aaf1de65": "Afficher uniquement les espaces de travail supprimables",
+ "d7ac56452e": "Activité",
+ "243287ac60": "Nom",
+ "6f8f6a6b04": "Filtrer les espaces de travail",
+ "5caccea440": "Supprimer la sélection",
+ "e4a12c455b": "Effacer",
+ "0cb1501ccf": "récupérable",
+ "65402b7192": "sélectionnés",
+ "43171f3e60": "Espaces de travail",
+ "83f1a0a932": "Récupérable",
+ "09960d86bd": "Analysé",
+ "1cc6cd4c0f": "espaces de travail",
+ "02b27c2230": "espace de travail",
+ "63efebe0e6": "{{value0}} {{value1}} supprimés de Space.",
+ "eee5240810": "Espaces de travail supprimés",
+ "9afc97f9a3": "Espace de travail supprimé",
+ "792a214457": "Supprimer l'espace de travail",
+ "a998501630": "Forcer",
+ "9155381019": "Sparse",
+ "f39d291997": "Sélectionner",
+ "16988df079": "Aucun fichier trouvé.",
+ "b25c2c1086": "éléments de premier niveau",
+ "d3f9c69ddc": "Zoom",
+ "ef890d31b9": "Tous",
+ "c28643d3da": "Aller à l'espace de travail",
+ "66870929fb": "Issue",
+ "fb2069acb7": "À examiner",
+ "b9b4a3a25d": "Branche",
+ "c432278ec7": "Buffers de l'éditeur",
+ "0bc756efaf": "Modifications Git",
+ "e9528a89b3": "Terminaux",
+ "a8d9e0de79": "Agents",
+ "d384a4ce9f": "Décision de suppression",
+ "7d7745bb8f": "Supprimable",
+ "720870a18e": "Conserver : lié",
+ "cbc343a7a8": "Conserver : en cours d'utilisation",
+ "2055bc6a5a": "Conserver : modifications non enregistrées",
+ "ec7b076a75": "Conserver : état Git non vérifié",
+ "7ab8d7e2d7": "Conserver : fichiers modifiés",
+ "7f7895514e": "Conserver : actif",
+ "2b501ee391": "Conserver : principal",
+ "39801484e0": "Échec",
+ "33653dbac2": "Suppression en cours",
+ "52b629eb84": "Mis à jour",
+ "e91dd2a9ae": "Lancez une analyse pour inspecter la taille des espaces de travail.",
+ "61e25239da": "Aucune ligne de espace de travail n'a été fournie par l'analyse.",
+ "8194a4fb29": "L'analyse a échoué avant la collecte de la moindre taille de espace de travail.",
+ "b2f82ed5ae": "Supprimable",
+ "20a4204dce": "Les derniers résultats valides restent visibles.",
+ "8c7c57fbf8": "Analyser",
+ "508673bac0": "Actualiser",
+ "8dc9ddac8a": "Annuler",
+ "1fce91d1b9": "Arrêt en cours",
+ "d254f04097": "annulation",
+ "265d956765": "{{value0}}. Vous pouvez quitter cette page.",
+ "d595295d7d": "{{value0}} peuvent être récupérés depuis les worktrees liés.",
+ "34174bd83d": "{{value0}}. Vous pouvez quitter cette page ; le dernier résultat reste visible.",
+ "433bb7f595": "ok",
+ "0ba046fbc5": "Échec de l'analyse.",
+ "5c6d25720c": "Sélectionnez un espace de travail à inspecter.",
+ "c5135e7e4a": "Analyse de la taille des espaces de travail. Vous pouvez quitter cette page.",
+ "0990a63160": "Aucune taille de espace de travail analysée pour le moment.",
+ "977bdf9a36": "Aucun élément de premier niveau à afficher.",
+ "131662ac65": "{{value0}} ouvert(s)",
+ "0d1c78d749": "Sélectionner {{value0}}"
+ },
+ "ports": {
+ "status": {
+ "popover": {
+ "rows": {
+ "a49ea79246": "Aller au worktree",
+ "f2b813345f": "Espace de travail indisponible",
+ "0e72c8d9fb": "Arrêter le processus",
+ "536d48a5dc": "Copier {{value0}}",
+ "085f4f0334": "Ouvrir dans le navigateur",
+ "e4a709548c": "Échec de l'actualisation des ports",
+ "acdb6df590": "Processus arrêté sur {{value0}}",
+ "480d8f2347": "{{value0}} copié",
+ "b854ec9ff5": "Échec de l'ouverture du navigateur"
+ }
+ }
+ }
+ },
+ "tooltip": {
+ "cedb7b99e3": "% utilisé",
+ "6d6df77f41": "Aucune donnée disponible",
+ "7f7f208060": "Mensuel",
+ "252c096536": "Hebdomadaire",
+ "a79c64f87e": "Fable",
+ "94038ad2fa": "Session",
+ "2c35eca8d4": "Impossible de récupérer l'utilisation",
+ "1292d4f2ee": "Indisponible",
+ "7567cd1c6b": "Utilisation indisponible",
+ "a9a318b7a3": "Échec de l'actualisation — affichage des données en cache",
+ "7ad719c4bf": "Limité",
+ "e740f92596": "Échec de l'actualisation",
+ "8418ec448d": "L'utilisation de {{value0}} n'a pas pu être actualisée. Des sessions d'agent sont peut-être encore connectées.",
+ "45198c7d95": "1 réinitialisation de limite de débit disponible",
+ "bce421cba3": "{{value0}} réinitialisations de limite de débit disponibles",
+ "7ec6e030a0": "La prochaine expire maintenant",
+ "d1e442a9e5": "Expire maintenant",
+ "6cf9eaed10": "La prochaine expire dans {{value0}}",
+ "20ad66aed1": "Expire dans {{value0}}",
+ "0d8d7cfe15": "En attente d'une session Claude",
+ "1804cd8c3f": "Actualisation de la connexion",
+ "f8f0f9d8cc": "Problème de réseau",
+ "bf2e739f18": "Connexion indisponible",
+ "f8b8dbed85": "Utilisation indisponible",
+ "3d3c9c0c1f": "L'utilisation de Claude s'actualisera une fois que le terminal Claude actif aura renouvelé ses identifiants.",
+ "42fdd4da1d": "La connexion Claude est en cours d'actualisation. Des sessions d'agent sont peut-être encore connectées.",
+ "c06c1d215d": "L'utilisation de Claude n'a pas pu être actualisée car la requête réseau a échoué.",
+ "cabdc2a9e0": "Les identifiants de connexion Claude n'ont pas pu être lus.",
+ "a7517cccb6": "L'utilisation de Claude est indisponible pour le moment.",
+ "e2c6a4f917": "Exécutez Grok pour actualiser",
+ "d1b7f509ac": "Exécutez grok dans un terminal sur l'ordinateur qui fait tourner Orca et attendez son démarrage. Si demandé, terminez la connexion, puis réessayez l'utilisation. Inutile d'envoyer un message.",
+ "f90b3d7a16": "Exécutez Kimi pour actualiser",
+ "a37e8c15d4": "Exécutez kimi dans un terminal sur l'ordinateur qui fait tourner Orca et attendez son démarrage, puis réessayez l'utilisation."
+ },
+ "SshTargetStatusRow": {
+ "sshHost": "Hôte SSH"
+ },
+ "usagePercentageLabel": {
+ "used": "{{value0}} % utilisé",
+ "remaining": "{{value0}} % restants"
+ },
+ "UsagePercentageDisplayChangeNotice": {
+ "title": "L'utilisation affiche désormais le pourcentage consommé",
+ "body": "Vous préférez le restant ? Changez ce choix dans les paramètres.",
+ "dismiss": "Ignorer",
+ "openSettings": "Ouvrir les paramètres",
+ "gotIt": "Compris"
+ },
+ "UsageRosterPanel": {
+ "title": "Utilisation",
+ "openDetails": "Ouvrir les détails d'utilisation",
+ "notSignedIn": "non connecté",
+ "allAgents": "tous les agents",
+ "usageDetails": "Détails et historique d'utilisation",
+ "loadingUsage": "Chargement de l'utilisation…",
+ "usageUnavailable": "Utilisation indisponible",
+ "noUsageData": "Aucune donnée d'utilisation",
+ "detailed": "Détaillé",
+ "compact": "Compact",
+ "detailedTooltip": "Utilisation complète avec barres, libellés et pourcentages",
+ "compactTooltip": "Utilisation condensée : uniquement la fenêtre la plus contrainte",
+ "footerDetailAria": "Détail du pied de page d'utilisation"
+ },
+ "RemoteServerUpdateStatusSegment": {
+ "updating": "Mise à jour de {{value0}}/{{value1}}",
+ "updatingTooltip": "Des mises à jour de serveurs Orca distants sont en cours",
+ "failed": "Échec de la mise à jour de {{value0}} serveurs",
+ "failedTooltip": "Ouvrez les mises à jour des serveurs Orca distants pour vérifier et relancer",
+ "updated": "{{value0}} serveurs mis à jour",
+ "updatedTooltip": "Mises à jour des serveurs Orca distants terminées",
+ "failedOne": "Échec de la mise à jour d'un serveur",
+ "updatedOne": "1 serveur mis à jour"
+ },
+ "resource": {
+ "memory": {
+ "metric": {
+ "workingSetDescription": "Somme des working sets (WS). Les pages partagées peuvent apparaître dans plusieurs processus.",
+ "rssDescription": "Somme des tailles résidentes (RSS). Les pages partagées ou aliasées peuvent apparaître dans plusieurs processus."
+ }
+ },
+ "manager": {
+ "terminal": {
+ "copy": {
+ "terminalSessionCount_one": "{{count}} session de terminal",
+ "terminalSessionCount_other": "{{count}} sessions de terminal",
+ "memoryUnavailable": "mémoire indisponible",
+ "tooltipSummary": "Gestionnaire de ressources - {{memory}} - {{sessions}}",
+ "spaceScanReady": "Analyse Space prête",
+ "sessionsGroupedByWorkspace": "Les sessions de terminal sont regroupées par espace de travail.",
+ "noTerminalSessions": "Aucune session de terminal pour le moment.",
+ "ariaLabel": "Gestionnaire de ressources, {{sessions}}",
+ "ariaLabelWithSpaceScan": "Gestionnaire de ressources, {{sessions}}, {{spaceScan}}"
+ }
+ }
+ }
+ },
+ "CaffeinateStatusSegment": {
+ "active": "Actif",
+ "inactive": "Inactif",
+ "ariaLabel": "{{title}}, {{status}}",
+ "onDescription": "Maintenir cet ordinateur éveillé en permanence",
+ "autoDescription": "Rester éveillé pendant qu'un agent travaille",
+ "offDescription": "Autoriser le comportement de veille normal du système"
+ }
+ }
+ },
+ "stats": {
+ "ClaudeUsageDailyChart": {
+ "2a6360c7cb": "Écriture en cache",
+ "61c58f8976": "Lecture du cache",
+ "7d2efeff5e": "Sortie",
+ "d7fb787e6b": "Entrée",
+ "a7902d3c1d": "tokens",
+ "059945f71d": "Totaux quotidiens d'entrées, de sorties, de lectures et d'écritures en cache.",
+ "c9f7cd30e9": "Utilisation quotidienne"
+ },
+ "ClaudeUsagePane": {
+ "21ea00bfa8": "Cache",
+ "a8b7487ff7": "Sortie",
+ "faf3444859": "Entrée",
+ "0f03975d59": "Tours",
+ "1afc25eb06": "Modèle",
+ "c17bed0416": "Projet",
+ "01476891c7": "Dernière activité",
+ "abfc4a4943": "Taux de réutilisation du cache :",
+ "7e76c84153": "Sessions récentes",
+ "32176e1d44": "tours",
+ "02a046792e": "sessions •",
+ "f97435845c": "Projet principal :",
+ "7dc9e5613b": "Par projet",
+ "c3fdbc5474": "Modèle principal :",
+ "0f394c24e3": "Par modèle",
+ "51ae85fa00": "Le taux de réutilisation du cache est calculé ainsi : tokens de lecture du cache / (tokens d'entrée + tokens de lecture du cache).",
+ "b26d4ddb58": "Coût estimé équivalent API",
+ "0f3e696ca9": "Sessions / Tours",
+ "8cc23be4a3": "Tours sans lecture du cache",
+ "1634c4f404": "Taux de réutilisation du cache",
+ "b786fb4a70": "Écriture en cache",
+ "268cf0af51": "Lecture du cache",
+ "2b8a2f14aa": "Tokens de sortie",
+ "ea71fae8fc": "Tokens d'entrée",
+ "7dde9331fd": "Aucune utilisation locale de Claude trouvée pour l'instant dans ce périmètre.",
+ "424cd50412": "Activer les statistiques d'utilisation Claude",
+ "8d18bbb771": "Actualiser",
+ "c5b9b344d0": "Actualiser l'utilisation Claude",
+ "505be9aac4": "Période",
+ "f61cffb9c8": "Portée",
+ "dd29209b21": "Filtres",
+ "e9bf9fce0e": "Options d'utilisation Claude",
+ "6afacbee37": "Suivi de l'utilisation Claude",
+ "0cb1a36d7d": "Lit les journaux locaux d'utilisation de Claude pour afficher les statistiques de tokens, de modèles et de sessions.",
+ "5ce4842c2c": "Toute l'utilisation locale de Claude",
+ "4f8368c272": "Worktrees Orca uniquement",
+ "cfe2282ffa": "Inconnu",
+ "7765a4c3e1": "n/d",
+ "2d41fd45c6": " • Dernière erreur d'analyse : {{value0}}",
+ "rangeLast7Days": "7 derniers jours",
+ "rangeLast30Days": "30 derniers jours",
+ "rangeLast90Days": "90 derniers jours",
+ "rangeAllTime": "Depuis toujours"
+ },
+ "CodexUsageDailyChart": {
+ "1e6f62d7e3": "Raisonnement",
+ "c646e1783c": "Entrées en cache",
+ "7b596a88b2": "Sortie",
+ "99a91d3143": "Entrée",
+ "e4bdcf0071": "tokens",
+ "c756cda6a8": "Totaux quotidiens d'entrées, d'entrées en cache, de sorties et de raisonnement.",
+ "609aa96e8b": "Utilisation quotidienne"
+ },
+ "CodexUsagePane": {
+ "e0b988599d": "Total",
+ "bbd20344b8": "Sortie",
+ "3acc582214": "Entrée",
+ "bd0822ca47": "Événements",
+ "c2478bcc3c": "Modèle",
+ "1a65900aea": "Projet",
+ "0c36b100be": "Dernière activité",
+ "0bd8655475": "Sessions locales Codex les plus récentes dans ce périmètre.",
+ "0cb0983c07": "Sessions récentes",
+ "79a69522a5": "événements",
+ "bf1bf2f674": "sessions •",
+ "829ee743f2": "Projet principal :",
+ "b98718aaab": "Par projet",
+ "95d2d89285": "Modèle principal :",
+ "5a0d1d69cd": "Par modèle",
+ "94ac1f1ee7": "Les tokens de raisonnement sont affichés à titre indicatif, mais le coût est calculé uniquement à partir des entrées hors cache, des entrées en cache et des sorties.",
+ "1a18fbd56b": "Coût estimé équivalent API",
+ "907b31865f": "Sessions / Événements",
+ "6e18146e9b": "Sortie de raisonnement",
+ "a9ac0f423a": "Entrées en cache",
+ "5d8eba87bd": "Tokens de sortie",
+ "e365eaa6fd": "Tokens d'entrée",
+ "4c865393b4": "Aucune utilisation locale de Codex trouvée pour l'instant dans ce périmètre.",
+ "f7c1affbd5": "Activer les statistiques d'utilisation Codex",
+ "3022cda443": "Actualiser",
+ "ec4d270e2c": "Actualiser l'utilisation Codex",
+ "89162e019b": "Période",
+ "6d68e8399a": "Portée",
+ "1af1a39b2f": "Filtres",
+ "70b5b8581f": "Options d'utilisation Codex",
+ "408210470c": "Suivi de l'utilisation Codex",
+ "13badcd8f2": "Lit les journaux locaux d'utilisation de Codex pour afficher les statistiques de tokens, de modèles et de sessions.",
+ "4fe8820098": "Toute l'utilisation locale de Codex",
+ "201766b754": "Worktrees Orca uniquement",
+ "bf6cf2d4dd": "Inconnu",
+ "ae255c3dba": "n/d",
+ "247c93ca92": "• tarification déduite",
+ "8a6655f7a2": " • Dernière erreur d'analyse : {{value0}}",
+ "rangeLast7Days": "7 derniers jours",
+ "rangeLast30Days": "30 derniers jours",
+ "rangeLast90Days": "90 derniers jours",
+ "rangeAllTime": "Depuis toujours"
+ },
+ "OpenCodeUsagePane": {
+ "349f7c3f5c": "Total",
+ "dfc4513657": "Sortie",
+ "0f2f266c9d": "Entrée",
+ "d416f5cf92": "Événements",
+ "08c78441b7": "Modèle",
+ "a4738de041": "Projet",
+ "d97bdf6e27": "Dernière activité",
+ "81817a641a": "Sessions locales OpenCode les plus récentes dans ce périmètre.",
+ "4799177b1c": "Sessions récentes",
+ "1e5d410df0": "événements",
+ "bc0cb89901": "sessions •",
+ "048ffe4d65": "Projet principal :",
+ "0f0a1684bb": "Par projet",
+ "a15206a63a": "Modèle principal :",
+ "040c044d39": "Par modèle",
+ "e5bb23d85e": "Le coût provient de la base de données locale d'OpenCode lorsque le message de l'assistant en enregistre un.",
+ "15c34d4b08": "Coût enregistré",
+ "7e9433469a": "Sessions / Événements",
+ "5a65d68b77": "Sortie de raisonnement",
+ "603504ee3b": "Entrées en cache",
+ "7aa4d8ce35": "Tokens de sortie",
+ "d637a892ed": "Tokens d'entrée",
+ "bb6363e08c": "Aucune utilisation locale d'OpenCode trouvée pour l'instant dans ce périmètre.",
+ "f04131b3be": "Activer les statistiques d'utilisation OpenCode",
+ "603cd138dc": "Actualiser",
+ "bed558df0b": "Actualiser l'utilisation OpenCode",
+ "b5ed5c9fd0": "Période",
+ "40d283c837": "Portée",
+ "01583b30aa": "Filtres",
+ "230d6de108": "Options d'utilisation OpenCode",
+ "bea80ceae0": "Suivi de l'utilisation OpenCode",
+ "b8b3522436": "Lit les journaux locaux d'utilisation d'OpenCode pour afficher les statistiques de tokens, de modèles et de sessions.",
+ "144a6050e9": "Toute l'utilisation locale d'OpenCode",
+ "e04c58327c": "Worktrees Orca uniquement",
+ "362231082f": "Inconnu",
+ "8095a63426": "n/d",
+ "6cc7782458": " • Dernière erreur d'analyse : {{value0}}",
+ "rangeLast7Days": "7 derniers jours",
+ "rangeLast30Days": "30 derniers jours",
+ "rangeLast90Days": "90 derniers jours",
+ "rangeAllTime": "Depuis toujours"
+ },
+ "ShareUsageButton": {
+ "7d6b25323d": "Partager sur X",
+ "b295c1c75d": "Copier l'image",
+ "bd82c76a70": "Copied",
+ "bce08eccb9": "Partager l'utilisation",
+ "cecefa7c32": "Partager"
+ },
+ "ShareUsageCard": {
+ "4a4c6c79a3": "sessions ·",
+ "66c83284cf": "Tokens quotidiens",
+ "b760c0b622": "Modèle principal",
+ "2d9eb39264": "Total de tokens",
+ "beb6f24f37": "Coût estimé",
+ "da62578d9d": "Utilisation",
+ "0eb31e79ee": "IDE Orca",
+ "960324e9b8": "événements",
+ "6adac63cfe": "tours"
+ },
+ "StatsPane": {
+ "42d3e0bdf7": "Fournisseur de statistiques d'utilisation : {{value0}}",
+ "c79f073d4c": "Statistiques d'utilisation",
+ "a58aba506f": "PRs créées",
+ "1c96f433e2": "Temps de travail des agents",
+ "9dbec9e675": "Agents lancés",
+ "73ed07859c": "Lancez votre premier agent pour commencer le suivi",
+ "1e696db2f6": "OpenCode",
+ "7d26110cea": "Codex",
+ "85457c02fe": "Claude",
+ "b2cf4310ce": "Vue d'ensemble",
+ "908c470587": "codex",
+ "eb6a066185": "claude",
+ "eee19cfade": "vue d'ensemble",
+ "grokUsageTab": "Grok",
+ "trackingSince": "Suivi depuis {{value0}}"
+ },
+ "UsageOverviewPane": {
+ "22ed1b7669": "sessions",
+ "444585cb41": "avec données",
+ "ecb0cd8a4c": "activés -",
+ "33f7b043d2": "Fournisseurs",
+ "60002bb22f": "Aucune utilisation locale de Claude, Codex ou OpenCode trouvée pour l'instant. La vue d'ensemble se remplira après la prochaine session d'agent qui écrira des journaux de tokens.",
+ "70f36452d4": "Part du cache",
+ "327603fe8b": "Jours actifs",
+ "0eaf937335": "Coût estimé",
+ "3887b94ce5": "Total de tokens",
+ "2d13e57f72": "Activer OpenCode",
+ "2f1ee2878b": "Activer Codex",
+ "0ea0cae435": "Activer Claude",
+ "6c00c46815": "Activez un fournisseur pour analyser les journaux locaux des agents et construire le registre combiné de tokens.",
+ "49405ccc8d": "Commencer le suivi des tokens",
+ "ca6bc5fded": "Actualiser",
+ "e06d1baf5c": "Actualiser la vue d'ensemble de l'utilisation",
+ "c760c481c5": "Vue d'ensemble de l'utilisation",
+ "55c910f4f1": "- certains prix de modèles sont indisponibles"
+ },
+ "share": {
+ "card": {
+ "utils": {
+ "19f4b4dc75": "github.com/stablyai/orca",
+ "d864fc5f98": "sortie",
+ "5d66fdd7c2": "entrée",
+ "7080aeaebb": "Raisonnement",
+ "4ee864629a": "Entrées en cache",
+ "33d38e2177": "Sortie",
+ "c2d7b23d57": "Entrée",
+ "9d166247ee": "Écriture en cache",
+ "cc28cb965e": "Lecture du cache"
+ }
+ }
+ },
+ "stats": {
+ "search": {
+ "cb6a9f0334": "cache",
+ "eaf251e183": "tokens",
+ "6953af58e6": "opencode",
+ "b77826fca3": "codex",
+ "e9dc37d889": "claude",
+ "8efeae0b22": "suivi",
+ "5acbe1fdf2": "temps",
+ "ef8bbf7739": "prs",
+ "ce8533f02e": "agents",
+ "0bba8ca244": "statistiques",
+ "0e2a0b6431": "utilisation",
+ "372debfac0": "stats",
+ "26bb901fcd": "Statistiques Orca, plus analyses de tokens Claude, Codex, OpenCode et utilisation d'abonnement Grok.",
+ "cb2430ae6a": "Statistiques & usage",
+ "f8a1b2c3d4": "grok",
+ "e7f0a1b2c3": "abonnement",
+ "d6e9f0a1b2": "crédits",
+ "a3b6c7d8e9": "utilisation grok",
+ "9f2a3b4c5d": "xai"
+ }
+ },
+ "usage": {
+ "overview": {
+ "model": {
+ "bc474051e5": "OpenCode",
+ "eb220d193b": "Codex",
+ "544d6d4c16": "Claude"
+ },
+ "sections": {
+ "9564a3b21b": "sessions -",
+ "32330a6e66": "{{value0}} : {{value1}} tokens",
+ "57d1448ef8": "Activer",
+ "f6df0d7d6d": "Plus",
+ "1dd166c920": "Moins",
+ "52d9221dc0": "Heatmap d'activité récente des tokens",
+ "c424eb3f8e": "Meilleur :",
+ "f28ff1f852": "Activité combinée récente des tokens Claude, Codex et OpenCode.",
+ "69e2b50427": "Intensité quotidienne",
+ "3a795542fa": "Répartition combinée des tokens",
+ "e65084cb4b": "raisonnement",
+ "3bc4a01b24": "Tokens d'entrée, de sortie et de cache cumulés sur les fournisseurs activés.",
+ "4ff104da47": "Répartition des tokens",
+ "0015facc1f": "Cache",
+ "7f270458af": "Sortie",
+ "9365b14a4e": "Nouvelle entrée",
+ "3de9bf87fc": "Aucun modèle pour l'instant",
+ "6762f6a682": "tokens",
+ "a7f937fb29": "{{value0}} sessions - {{value1}} {{value2}}",
+ "c8f3a2d1e0b4": "tours",
+ "d9a4b3e2f1c5": "événements",
+ "statusScanning": "Analyse en cours",
+ "statusEnabled": "Activé",
+ "statusOff": "Désactivé"
+ }
+ }
+ },
+ "UsageBreakdownSection": {
+ "7765a4c3e1": "n/d",
+ "247c93ca92": "• tarification déduite",
+ "32176e1d44": "tours",
+ "79a69522a5": "événements",
+ "02a046792e": "sessions •"
+ },
+ "UsageSessionsTable": {
+ "1afc25eb06": "Tours",
+ "0f03975d59": "Événements",
+ "21ea00bfa8": "Cache",
+ "e0b988599d": "Total",
+ "01476891c7": "Dernière activité",
+ "c17bed0416": "Projet",
+ "f6a2c8d019": "Modèle",
+ "faf3444859": "Entrée",
+ "a8b7487ff7": "Sortie",
+ "cfe2282ffa": "Inconnu"
+ },
+ "GrokUsagePane": {
+ "g8h9i0j1k2": "Utilisation Grok",
+ "b2d3e4f5c6": "Crédits hebdomadaires de l'abonnement via OAuth du Grok CLI (~/.grok/auth.json). Même source que la barre d'état.",
+ "c3e4f5a6b7": "Configurer dans Comptes",
+ "h9i0j1k2l3": " • {{value0}}",
+ "i0j1k2l3m4": "Actualiser l'utilisation Grok",
+ "d4f5a6b7c8": "Actualiser",
+ "e5a6b7c8d9": "Crédits hebdomadaires utilisés",
+ "f6b7c8d9e0": "Réinitialisation de la période de facturation",
+ "a7b8c9d0e1": "Paramètres du compte Grok"
+ }
+ },
+ "sparse": {
+ "SparseCheckoutPresetSelect": {
+ "c4ac80151d": "Nouveau préréglage",
+ "7c3275d307": "Modifier {{value0}}",
+ "c7f9b3f0c1": "Désactivé",
+ "8b12c0850a": "Enregistrer",
+ "de8fce5854": "Annuler",
+ "ddbcaef7be": "src/renderer packages/ui",
+ "0e9ad9c798": "Répertoires",
+ "064c1e2d12": "UI du renderer",
+ "b3a500c623": "Nom",
+ "16223dde6a": "Charger les préréglages",
+ "a683a4bc8e": "Réessayer le chargement des préréglages",
+ "14952d451e": "{{value0}} répertoires",
+ "e9283eb171": "1 répertoire",
+ "69c020eddc": "Modifier le préréglage",
+ "bd6cec2056": "nouveau"
+ }
+ },
+ "source": {
+ "control": {
+ "SourceControlActionVariableChips": {
+ "1b77798d5f": "Variables",
+ "6b921a0ac2": "Exemple",
+ "4bf6d88039": "(vide)",
+ "7377483644": "Cet espace de travail"
+ }
+ }
+ },
+ "skills": {
+ "SkillsPage": {
+ "cb142070b4": "Actualiser",
+ "984405683f": "Plugin",
+ "4d177feabd": "Intégré",
+ "aa59462502": "Dépôt",
+ "571c5818c1": "Home",
+ "0bc1379f4c": "Toutes les sources",
+ "38e0951c3a": "Skills d'agent",
+ "fb6bf60b52": "Claude",
+ "426be2aac6": "Codex",
+ "39b6998ddb": "Tous les fournisseurs",
+ "a68dee6a32": "Rechercher des skills",
+ "e46e162e2e": "de",
+ "b088e0785d": "Beta",
+ "f43ad6edf3": "Skills",
+ "7e828fb2c6": "Retour",
+ "ea72d6185b": "Impossible d'analyser les skills",
+ "dc4c3328ee": "Afficher le fichier",
+ "9963dff6d3": "Aucune description trouvée.",
+ "995fde8337": "Impossible d'afficher le fichier du skill",
+ "ab5b777350": "Dossiers de skills home, dépôt, intégrés et plugins passés en revue.",
+ "08a321a984": "Ajustez la recherche ou les filtres.",
+ "4acd6d68ec": "Aucun skill trouvé",
+ "6a62a0168c": "Aucun résultat",
+ "cd7893fbc1": "Analyse des skills",
+ "35b9a724a0": "Disponibles",
+ "0c74e7ff34": "Installés",
+ "c13b82793c": "Gérer les installations",
+ "aee7b99cc6": "Installer depuis un lien",
+ "filterProvider": "Filtrer par agent",
+ "filterSource": "Filtrer par source",
+ "allSources": "Tous",
+ "clearFilters": "Réinitialiser les filtres",
+ "closeSkills": "Fermer les skills",
+ "closeTooltip": "Fermer · Esc",
+ "moreActions": "Plus d'actions",
+ "sharedLinks": "Liens partagés",
+ "emptyCopy": "Les dossiers de skills analysés sont vides. Installez un bundle partagé, ou actualisez après avoir ajouté un skill.",
+ "retry": "Réessayer",
+ "remoteShareNotice": "Ces skills se trouvent sur {{host}}. Ouvrez Skills sur cette machine pour les partager.",
+ "viewSwitch": "Afficher",
+ "searchLinks": "Rechercher des liens",
+ "deleteSkills": "Supprimer des skills…"
+ },
+ "SkillFreshnessNudge": {
+ "titleOne": "Un skill Orca installé n'est pas à jour",
+ "titleMany": "{{value0}} skills Orca installés ne sont pas à jour",
+ "description": "Mettez à jour {{value0}} pour que les agents suivent les instructions actuelles de cette version d'Orca.",
+ "updateOne": "Mettre à jour le skill",
+ "updateMany": "Mettre à jour les skills"
+ },
+ "SkillFreshnessRow": {
+ "statusUpdateAvailable": "Mise à jour disponible",
+ "statusCantUpdate": "Ignoré",
+ "cantUpdateReason": "Orca a exclu ce skill de la commande de mise à jour.",
+ "skippedReasonUnrecognized": "La copie ici ne correspond pas à la version officielle — elle a peut-être été modifiée, ou il s'agit d'un autre skill portant le même nom. Orca l'a exclue de la mise à jour pour ne pas l'écraser. Supprimez-la si vous voulez qu'Orca mette à jour ce skill.",
+ "skippedReasonReadOnly": "Cette copie se trouve dans un emplacement en lecture seule, donc Orca l'a exclue de la mise à jour. Modifiez ses permissions pour permettre à Orca de la mettre à jour.",
+ "skippedReasonInaccessible": "Orca n'a pas pu lire cette copie, il a donc exclu le skill de la mise à jour.",
+ "skippedReasonInRepo": "C'est un skill de projet, pas un skill global — Orca ne met à jour que vos skills globaux, il a donc exclu celui-ci de la mise à jour.",
+ "skippedReasonPluginCache": "Un plugin gère ce skill, donc Orca l'a exclu de la mise à jour — mettez plutôt à jour le plugin.",
+ "skippedReasonExternalLink": "Cette copie est un raccourci pointant hors des dossiers de skills d'Orca, donc Orca l'a exclue de la mise à jour.",
+ "skippedReasonBrokenLink": "Cette copie est un raccourci vers quelque chose qui n'existe plus, donc Orca l'a exclue — vous pouvez la supprimer sans risque.",
+ "chipCurrent": "Actuel",
+ "chipUnrecognized": "Non reconnu",
+ "chipInaccessible": "Inaccessible",
+ "chipDuplicate": "Doublon",
+ "chipExternalLink": "Lien externe",
+ "chipBrokenLink": "Lien cassé",
+ "chipReadOnly": "Lecture seule",
+ "chipInRepo": "Dans un dépôt",
+ "chipPluginCache": "Cache de plugins",
+ "tipCurrent": "Cette copie correspond à la version officielle actuelle.",
+ "tipUnrecognized": "Cette copie ne correspond à aucune version officielle — elle a peut-être été modifiée, ou il s'agit d'un autre skill portant le même nom.",
+ "tipInaccessible": "Orca n'a pas pu lire cette copie (erreur de permissions ou de fichier).",
+ "tipDuplicate": "Une copie séparée de ce skill, installée à part de la principale.",
+ "tipExternalLink": "Un raccourci pointant hors des dossiers de skills d'Orca.",
+ "tipBrokenLink": "Un raccourci vers quelque chose qui n'existe plus.",
+ "tipReadOnly": "Cette copie se trouve dans un emplacement en lecture seule.",
+ "tipInRepo": "Cette copie vit dans un projet, pas dans vos skills globaux.",
+ "tipPluginCache": "Cette copie est gérée par un plugin.",
+ "skippedReasonDuplicate": "Il s'agit d'une copie séparée : la mise à jour ne l'atteindra pas — la commande ne rafraîchit que la copie principale. Supprimez cette copie puis réinstallez le skill pour que cet emplacement suive le principal.",
+ "skippedReasonNewer": "Cette copie est plus récente que celle fournie avec cette build d'Orca ; Orca l'a donc laissée telle quelle plutôt que de la rétrograder. La mise à jour d'Orca remettra les deux en phase.",
+ "skippedReasonStaleRecord": "Le programme de mise à jour des skills n'a aucun enregistrement exploitable pour cette copie ; il signale donc le skill comme déjà à jour sans rien modifier. Réinstallez-le pour remettre l'enregistrement en cohérence : {{value0}}",
+ "chipNewer": "Plus récent",
+ "tipNewer": "Cette copie est plus récente que celle fournie avec cette build d'Orca."
+ },
+ "SkillFreshnessUpdateDialog": {
+ "title": "Mettre à jour les skills",
+ "checking": "Vérification des skills Orca installés…",
+ "none": "Aucun skill Orca installé trouvé.",
+ "updateOne": "1 mise à jour disponible",
+ "updateMany": "{{value0}} mises à jour disponibles",
+ "blockedOne": "1 skill ne peut pas être mis à jour automatiquement.",
+ "blockedMany": "{{value0}} skills ne peuvent pas être mis à jour automatiquement.",
+ "success": "Tous les skills Orca installés sont à jour.",
+ "attention": "Certains skills Orca installés ont été exclus de la mise à jour.",
+ "runningOne": "Mise à jour d'1 skill…",
+ "runningMany": "Mise à jour des skills de {{value0}}…",
+ "runningDescription": "Vous pouvez fermer cette fenêtre — le traitement continue en arrière-plan.",
+ "progressAria": "Mise à jour des skills",
+ "updatedOne": "1 skill mis à jour",
+ "updatedMany": "Skills de {{value0}} mis à jour",
+ "updatedPartial": "{{value0}} skills mis à jour sur {{value1}}",
+ "errorTitle": "La mise à jour ne s'est pas terminée",
+ "retry": "Réessayer",
+ "copyCommand": "Copier la commande",
+ "copied": "Copied",
+ "showLog": "Afficher le journal",
+ "updating": "Mise à jour…",
+ "updateActionOne": "Mettre à jour 1 skill",
+ "updateActionMany": "Mettre à jour {{value0}} skills",
+ "checkNow": "Revérifier",
+ "done": "Terminé",
+ "close": "Fermer",
+ "stop": "Arrêter",
+ "stopping": "Arrêt…",
+ "stoppingHeadline": "Arrêt de la mise à jour…",
+ "scanIncomplete": "Orca n'a pas pu terminer la vérification des skills gérés par plugin.",
+ "scanDepthLimit": "Orca a atteint sa limite de profondeur d'analyse des plugins avant de vérifier ce dossier.",
+ "scanEntryLimit": "Orca a atteint sa limite d'entrées d'analyse des plugins avant de vérifier le reste de ce cache.",
+ "scanCandidateLimit": "Orca a trouvé plus de dossiers de skills homonymes qu'il ne peut en inspecter sans risque.",
+ "scanManifestLimit": "Orca a ignoré ce manifeste de plugin car il dépassait une limite de sécurité.",
+ "scanOutsideRoot": "Orca a ignoré ce chemin de plugin car il pointe hors du cache de plugins.",
+ "scanIoErrorWithCode": "Orca n'a pas pu lire ce chemin de plugin ({{value0}}).",
+ "scanIoError": "Orca n'a pas pu lire ce chemin de plugin.",
+ "scanIssueLimit": "Orca a trouvé trop de dossiers de plugins ignorés pour pouvoir les lister un par un."
+ },
+ "SkillUpdateResultRows": {
+ "stillOutdated": "Toujours pas à jour après l'exécution de la mise à jour."
+ },
+ "SkillUpdateRow": {
+ "oneLocation": "1 emplacement",
+ "manyLocations": "{{value0}} emplacements"
+ },
+ "SkillFreshnessStatusPill": {
+ "updateAvailable": "Mise à jour disponible",
+ "upToDate": "À jour",
+ "installed": "Installés",
+ "details": "Détails",
+ "needsAttention": "Examiner le skill",
+ "checking": "Vérification...",
+ "checkFailed": "Échec de la vérification"
+ },
+ "SkillShareDialog": {
+ "reconnect": "Reconnectez votre compte Orca avant de partager.",
+ "unconfigured": "Connectez un compte Orca Cloud avant de partager.",
+ "prepareFailed": "Impossible de préparer ce skill au partage.",
+ "peopleRequired": "Sélectionnez au moins un membre de l'équipe.",
+ "publishCancelled": "Téléversement annulé. La copie préparée reste disponible pour une nouvelle tentative.",
+ "publishFailed": "Impossible de publier ce skill. La copie préparée reste disponible pour une nouvelle tentative.",
+ "copied": "Lien de partage copié",
+ "preparing": "Préparation de l'aperçu…",
+ "ready": "Lien du skill prêt",
+ "title": "Partager le skill",
+ "readyDescription": "Les destinataires s'authentifient auprès d'Orca avant de pouvoir l'inspecter ou l'installer.",
+ "newVersionDescription": "Vérifiez les fichiers exacts, puis publiez une version immuable dans le package Cloud existant.",
+ "description": "Vérifiez les fichiers exacts, choisissez qui peut y accéder, puis publiez une version immuable.",
+ "0f07fa2a79": "Publier le skill",
+ "7aa4ba0dba": "Publier une nouvelle version",
+ "3a51d0f34f": "Annuler le téléversement",
+ "e9d652ae3d": "Annulation…",
+ "30985d4fc0": "Annuler",
+ "3af85f6add": "Terminé",
+ "readyDescriptionV2": "Toute personne disposant de ce lien non répertorié peut inspecter et installer les skills.",
+ "descriptionV2": "Vérifiez les fichiers exacts, puis publiez une version immuable protégée par un lien non répertorié.",
+ "publishBundle": "Publier le bundle"
+ },
+ "SkillCard": {
+ "d25a1b8ae6": "Partager le skill",
+ "01c5a16e01": "Sélectionner {{value0}}"
+ },
+ "SkillCloudManagementActions": {
+ "ed753624c0": "Supprimer le package Cloud",
+ "640bc6b92e": "Confirmer la suppression du package",
+ "6b6adf68c1": "Supprimer la version Cloud sélectionnée",
+ "bbec37d8f8": "Confirmer la suppression de la version",
+ "7a80285dad": "Ne plus partager",
+ "0ac5c175fd": "Confirmer l'arrêt du partage",
+ "9e6bd31487": "Aucun lien de partage actif.",
+ "c7f2b69122": "L'arrêt du partage bloque les installations futures. Les copies déjà installées sur une machine y restent.",
+ "8cac3b9362": "Liens actifs",
+ "cc8e4ef6ba": "Enregistrer les accès",
+ "eb603f7888": "en dehors de la liste actuelle seront conservés.",
+ "3d346ddce8": "destinataire existant",
+ "2b8c569ea7": "Organisation actuelle",
+ "2552c12fea": "Accès",
+ "505cd6105c": "Contrôles de partage Cloud",
+ "linkCopied": "Lien de partage copié",
+ "activeLinkBearerDescription": "Toute personne disposant d'un lien actif peut inspecter et installer les skills. La révocation bloque les accès futurs mais laisse inchangées les copies installées.",
+ "copyLink": "Copier le lien"
+ },
+ "SkillInstallDialog": {
+ "39acb9e8f4": "Installer le skill",
+ "59c3b76cdd": "Réessayer l'installation",
+ "241e72f9d6": "Installation…",
+ "05588076a9": "Annuler l'installation",
+ "d198ec91e5": "Fermer",
+ "4a00d133c5": "Orca vérifie l'accès et l'identité du package avant de modifier la machine sélectionnée.",
+ "fcbec627cc": "Installer le skill partagé",
+ "01c5a14e01": "Installer les skills partagés",
+ "opening": "Ouverture de ce lien…"
+ },
+ "SkillInstallManagementDialog": {
+ "8095927ff3": "Fermer",
+ "e91af0079f": "Supprimer",
+ "470d8d2476": "Confirmer la suppression",
+ "c1d03ee50d": "Annuler l'installation",
+ "561e49ccd1": "Installer la version sélectionnée",
+ "2ae587d39c": "Réessayer les installations incomplètes",
+ "f7c5075e77": "Abandonner les modifications et supprimer",
+ "e1884c812e": "Abandonner les modifications et installer la version",
+ "070654c6d1": "Orca les conservera, sauf si vous abandonnez explicitement les modifications locales.",
+ "74b70892ea": "Fichiers locaux modifiés",
+ "86ed219b55": "Choisir une version",
+ "9c04bd0120": "Version installée",
+ "64c71cf7b9": "Aucune installation de skill gérée par Orca n'a été trouvée sur cette machine.",
+ "0900db719a": "— déconnecté",
+ "176fef9516": "· SSH",
+ "6cb1fbe039": "Cet ordinateur",
+ "3677ae58e7": "Mettez à jour, revenez en arrière ou supprimez sans risque les versions installées par Orca.",
+ "44d118a8f7": "Gérer les skills installés",
+ "1c9e7f420a": "Skills de bundle installés",
+ "34c2ef9e71": "Installer {{count}} skills",
+ "a0cab67c2f": "Supprimer {{count}} skills",
+ "f970d8088d": "Confirmer la suppression de {{count}} skills",
+ "dab29e4b54": "{{installed}} installés · {{updated}} mis à jour · {{keptLocal}} conservés en local",
+ "installAnotherMachine": "Installer sur une autre machine"
+ },
+ "SkillInstallReviewContent": {
+ "89e2601162": "Abandonner et remplacer",
+ "a5675fb371": "de contenu et l'a laissée intacte. Conservez-la, ou abandonnez explicitement et remplacez-la par cette version.",
+ "37d990b94c": "modifié",
+ "2a31912f14": "Orca a détecté",
+ "651b7d8a57": "La copie locale nécessite une décision",
+ "98ed90e523": "Un skill contient des instructions et peut inclure des scripts. Considérez-le comme du code provenant de son auteur.",
+ "f72ee4022a": "SHA-256",
+ "3d8421ca2f": "exécutable",
+ "87137bcb8d": "scripts",
+ "fab8fce842": "fichiers",
+ "8f9833d509": "Version immuable",
+ "66270286ac": "· Vous pouvez réessayer sans risque.",
+ "1b6ad2ca5c": "vérifié(s).",
+ "3fc62a61eb": "emplacement",
+ "157de228b4": "Inspecter le skill",
+ "69236de8d6": "Vérification…",
+ "27672470d9": "Ouvrir le lien n'installe rien. Examinez d'abord la version immuable.",
+ "66cff7a804": "https://app.orca.dev/skills/share/…",
+ "93eb0fe8c7": "Lien de skill Orca",
+ "releaseNotes": "Notes de version :",
+ "targetHeader": "Cible d'installation"
+ },
+ "SkillInstallTargetFields": {
+ "8e6a972229": "Aucun espace de travail connu sur cette machine.",
+ "7a366323e7": "Dossier",
+ "d628c416a2": "Worktree Git",
+ "5845cfe543": "Choisir un worktree ou un dossier",
+ "0e5b43a9e3": "Espace de travail",
+ "0c10a406fb": "WSL ·",
+ "e5b0d15e64": "Système d'exploitation hôte",
+ "cb47652227": "Environnement d'exécution",
+ "a4dfd33095": "Un seul espace de travail",
+ "c779621aa0": "Skills globaux",
+ "63cc9e31fe": "Destination",
+ "85d85880df": "· SSH",
+ "71eefd7660": "— déconnecté",
+ "0785d0a503": "— mise à jour requise",
+ "8562dd1e6e": "Cet ordinateur",
+ "b8a0b706ad": "Machine"
+ },
+ "SkillInstallWorkspaceCombobox": {
+ "search": "Rechercher des espaces de travail...",
+ "empty": "Aucun espace de travail trouvé."
+ },
+ "SkillShareReviewContent": {
+ "e3caf6baeb": "La révocation de ce lien bloque les accès futurs. Elle ne retire pas les copies déjà installées sur les machines des destinataires.",
+ "6d6233a3a4": "Copier le lien",
+ "0142581727": "Téléversement…",
+ "ad940367a5": "Validation et publication…",
+ "bf02d6ed9e": "Quoi de neuf dans cette version ?",
+ "f0c0411549": "Notes de version",
+ "4895d3e0ee": "Aucun membre de l'équipe disponible.",
+ "8188f5d765": "peuvent accéder au lien.",
+ "fe204e06f0": "l'organisation",
+ "0cd4b3e396": "Tous les membres actuels de",
+ "f25429e266": "Personnes sélectionnées",
+ "0a49d901ab": "Organisation",
+ "6817a7f6f8": "Accès aux skills",
+ "c15d90c10b": "Un compte Orca Cloud connecté est requis.",
+ "266527d295": "Publication en tant que {{value0}}{{value1}}.",
+ "78d863235c": "Accès",
+ "b3b1d4b911": "SHA-256",
+ "77f636eac3": "exécutable",
+ "8edd32622f": "scripts",
+ "3121f44358": "fichiers",
+ "2dca0b720b": "Publier une nouvelle version du skill",
+ "01c5a17e01": "{{value0}} skills",
+ "01c5a17e02": "Examiner les skills inclus",
+ "01c5a17e03": "{{value0}} fichiers · {{value1}}",
+ "unlistedLinkTitle": "Lien non répertorié",
+ "unlistedPublishingAs": "Publication en tant que {{value0}}. Toute personne disposant du lien peut inspecter et installer ces skills.",
+ "unlistedLinkDetails": "Le lien n'est ni recherchable ni listé publiquement. Révoquez-le pour bloquer les accès futurs ; les copies installées restent en place.",
+ "bundleReady": "Lien du bundle de skills prêt",
+ "publishBundleVersion": "Publier une nouvelle version du bundle de skills",
+ "shareBundle": "Partager le bundle de skills",
+ "publishingLink": "Publication du lien…",
+ "verifyingPackage": "Vérification du package…"
+ },
+ "SkillBundleInstallFlow": {
+ "01c5a13e01": "Fermer",
+ "01c5a13e02": "Annuler l'installation",
+ "01c5a13e03": "Installation…",
+ "01c5a13e04": "Réessayer {{value0}} skills",
+ "01c5a13e05": "Installer {{value0}} skills"
+ },
+ "SkillBundleInstallOutcome": {
+ "01c5a12e01": "Installés",
+ "01c5a12e02": "Mis à jour",
+ "01c5a12e03": "Inchangé",
+ "01c5a12e04": "Conservé en local",
+ "01c5a12e05": "Échec",
+ "01c5a12e06": "Annulé",
+ "01c5a12e07": "L'installation du bundle nécessite votre attention.",
+ "01c5a12e08": "Skills installés et vérifiés.",
+ "01c5a12e09": "{{value0}} skills sélectionnés vérifiés.",
+ "01c5a12e10": "Réessai nécessaire"
+ },
+ "SkillBundleInstallReview": {
+ "01c5a11e01": "Version immuable",
+ "01c5a11e02": "{{value0}} skills",
+ "01c5a11e03": "{{value0}} fichiers",
+ "01c5a11e04": "{{value0}} scripts",
+ "01c5a11e05": "{{value0}} exécutable",
+ "01c5a11e06": "SHA-256",
+ "01c5a11e09": "Notes de version :",
+ "01c5a11e0a": "Les skills contiennent des instructions et peuvent inclure des scripts. Considérez-les comme du code provenant de leur auteur.",
+ "01c5a11e0b": "Skills à installer",
+ "01c5a11e0c": "Choisissez n'importe quel sous-ensemble de ce bundle.",
+ "01c5a11e0d": "Tout sélectionner",
+ "01c5a11e0e": "Aucune description",
+ "01c5a11e0f": "{{value0}} fichiers · {{value1}} scripts",
+ "01c5a11e10": "{{value0}} exécutable",
+ "01c5a11e11": "Les copies locales nécessitent une décision",
+ "01c5a11e12": "Par défaut, Orca conserve ces copies locales. Sélectionnez uniquement celles que vous voulez abandonner et remplacer.",
+ "01c5a11e13": "Remplacer {{value0}} ({{value1}})",
+ "01c5a11e14": "{{value0}} nouveaux",
+ "01c5a11e15": "{{value0}} inchangés",
+ "01c5a11e16": "{{value0}} mises à jour",
+ "01c5a11e17": "{{value0}} conflits"
+ },
+ "SkillShareSelectionControls": {
+ "01c5a15e05": "Partager {{value0}} skills",
+ "01c5a15e01": "Annuler le partage",
+ "01c5a15e02": "Partager des skills",
+ "01c5a15e03": "{{value0}} sélectionnés",
+ "01c5a15e04": "Sélectionner tous les résultats",
+ "01c5a15e06": "Ouvrez ce skill sur la machine qui l'héberge pour le partager.",
+ "01c5a15e07": "Installez ce skill avant de le partager.",
+ "01c5a15e08": "Seuls les skills home et espace de travail peuvent être partagés.",
+ "01c5a15e09": "Un skill portant ce nom est déjà sélectionné depuis une autre source."
+ },
+ "SkillManagedInstallList": {
+ "86c76cb262": "{{count}} bundle de skills"
+ },
+ "skill-install-progress-state": {
+ "currentSkill": "Installation de {{value0}} sur {{value1}} : {{value2}}…"
+ },
+ "SkillRow": {
+ "updatedUnknown": "Aucune date",
+ "pathCopied": "Chemin copié",
+ "copyPath": "Copier le chemin",
+ "notShareable": "Non partageable",
+ "detailPath": "Chemin",
+ "detailSource": "Source",
+ "detailContents": "Contenu",
+ "skillActions": "Actions pour {{value0}}",
+ "viewDetails": "Voir les détails",
+ "deleteSkill": "Supprimer…",
+ "notDeletable": "Non supprimable"
+ },
+ "SkillSelectionBar": {
+ "selectAll": "Sélectionner les {{count}} éligibles",
+ "clear": "Effacer"
+ },
+ "SkillsList": {
+ "listLabel": "Skills"
+ },
+ "sourceStatus": {
+ "missing": "Dossier introuvable",
+ "remoteRepo": "Dépôt distant — non analysé",
+ "unavailable": "Non analysé"
+ },
+ "sources": {
+ "heading": "Dossiers de skills"
+ },
+ "sourceKind": {
+ "home": "Home",
+ "workspace": "Espace de travail",
+ "bundled": "Intégré",
+ "plugin": "Plugin"
+ },
+ "count": {
+ "skillOne": "{{count}} skill",
+ "skillOther": "{{count}} skills",
+ "sourceOne": "{{count}} source",
+ "sourceOther": "{{count}} sources",
+ "fileOne": "{{count}} fichier",
+ "fileOther": "{{count}} fichiers",
+ "resultOne": "{{count}} résultat",
+ "resultOther": "{{count}} résultats",
+ "selected": "{{count}} sélectionnés",
+ "shareOne": "Partager {{count}} skill",
+ "shareOther": "Partager {{count}} skills",
+ "installOne": "Installer {{count}} skill",
+ "installOther": "Installer {{count}} skills",
+ "retryOne": "Réessayer {{count}} skill",
+ "retryOther": "Réessayer {{count}} skills",
+ "linkOne": "{{count}} lien",
+ "linkOther": "{{count}} liens",
+ "deleteOne": "Supprimer {{count}} skill",
+ "deleteOther": "Supprimer {{count}} skills",
+ "deletedOne": "{{count}} skill supprimé",
+ "deletedOther": "{{count}} skills supprimés",
+ "deleteFolderOne": "{{count}} dossier",
+ "deleteFolderOther": "{{count}} dossiers",
+ "deleteLinkOne": "{{count}} lien",
+ "deleteLinkOther": "{{count}} liens"
+ },
+ "host": {
+ "local": "Cette machine",
+ "remote": "Runtime connecté"
+ },
+ "share": {
+ "fileExecutable": "exécutable",
+ "fileScript": "script",
+ "reviewFiles": "Examiner les fichiers exécutables",
+ "reviewSkills": "Examiner les skills inclus",
+ "releaseNotesVersion": "Notes de version",
+ "releaseNotesOptional": "Ajouter des notes de version (facultatif)",
+ "releaseNotesFirst": "Décrivez cette version",
+ "readyDescription": "Copiez le lien pour le partager.",
+ "newVersionDescription": "Publie une version immuable dans le package existant.",
+ "description": "Publie une version immuable protégée par un lien non répertorié.",
+ "accessSummary": "Toute personne disposant du lien peut l'installer. Il n'est listé nulle part, et le révoquer bloque les nouvelles installations — pas les copies déjà installées. Publication en tant que {{value0}}.",
+ "manageLinks": "Gérer ou révoquer ce lien dans les Paramètres",
+ "scriptOne": "{{count}} script",
+ "scriptOther": "{{count}} scripts",
+ "executableOne": "{{count}} exécutable",
+ "executableOther": "{{count}} exécutables",
+ "noExecutableContent": "Aucun script ni exécutable",
+ "newVersionDescriptionPlain": "Ajoute une nouvelle version au lien existant.",
+ "accessSummaryShort": "Lien non répertorié — toute personne qui le possède peut l'installer. Publication en tant que {{value0}}.",
+ "accessSummaryPlain": "Lien non répertorié — toute personne qui le possède peut l'installer."
+ },
+ "description": {
+ "showLess": "Afficher moins",
+ "showMore": "Afficher plus"
+ },
+ "install": {
+ "agentsLabel": "Agents",
+ "agentsCanonical": "Toujours installé pour {{value0}}, qui lisent {{value1}}.",
+ "agentsNoneChosen": "Aucun agent supplémentaire",
+ "agentsSummary": "Également : {{value0}}",
+ "agentNotInstalled": "Non installé",
+ "bundleDestinationSummary": "{{value0}} nouveaux · {{value1}} déjà installés · {{value2}} nécessitent une décision",
+ "trustNote": "Les skills sont des instructions et du code provenant de leur auteur. N'installez que ce en quoi vous avez confiance.",
+ "agentsNone": "Aucun agent sélectionné",
+ "agentsSelected": "Installation pour : {{value0}}",
+ "agentAlways": "Toujours",
+ "agentsCanonicalNote": "{{value0}} lisent {{value1}}, que chaque installation écrit.",
+ "linkHint": "Ouvrir un lien n'installe jamais rien — vous l'examinez d'abord.",
+ "rowNeedsDecision": "Nécessite une décision",
+ "rowInstalled": "Déjà installé",
+ "rowUpdate": "Mettre à jour",
+ "rowNew": "Nouveau",
+ "chooseSkills": "Choisissez ce que vous voulez installer depuis ce lien.",
+ "singleRunnableWarning": "Ce skill inclut des scripts ou des fichiers binaires.",
+ "runnableWarning": "{{affectedCount}} des {{selectedCount}} skills sélectionnés incluent des scripts ou des fichiers binaires : {{affected}}.",
+ "reviewRunnableFiles": "Inclut des scripts ou des fichiers binaires",
+ "reviewSupportingFiles": "Inclut des fichiers annexes",
+ "reviewInstructions": "À propos de ce skill",
+ "supportingFileWarning": "Les skills sélectionnés incluent {{fileCount}} fichiers en plus de SKILL.md.",
+ "agentAccessWarning": "Les skills contiennent des instructions que votre agent pourrait suivre. Ne continuez que si vous faites confiance à la source de ce lien de partage.",
+ "fileBinary": "binaire",
+ "fileRunnable": "exécutable",
+ "agentsCountBadge": "{{count}} sélectionnés",
+ "agentsCountLabel": "{{count}} {{label}}",
+ "targetAgentsHeader": "Agents ciblés",
+ "deselectAll": "Désélectionner les facultatifs",
+ "selectAll": "Tout sélectionner",
+ "canonicalHeader": "Agents standard (toujours inclus)",
+ "canonicalExplanation": "Ces agents lisent nativement {{root}}, où Orca installe par défaut :",
+ "additionalAgentsHeader": "Répertoires d'agents supplémentaires"
+ },
+ "filter": {
+ "allAgents": "Tous les agents",
+ "sharedAgent": "Partagé (.agents)"
+ },
+ "SkillsSelectionHeader": {
+ "exit": "Quitter la sélection",
+ "exitTooltip": "Quitter la sélection · Esc",
+ "title": "Sélectionner les skills à partager",
+ "selectAll": "Sélectionner les {{count}} éligibles",
+ "clear": "Effacer",
+ "deleteTitle": "Sélectionner les skills à supprimer"
+ },
+ "SkillDetailDialog": {
+ "agents": "Agents",
+ "updated": "Mis à jour",
+ "copy": "Copier"
+ },
+ "SkillSharedLinkRow": {
+ "contentsUnavailable": "Impossible de charger le contenu de ce lien.",
+ "loading": "Chargement du contenu…",
+ "deleteFailed": "Orca n'a pas pu supprimer cet élément du Cloud.",
+ "deleted": "Supprimé du Cloud",
+ "confirmDelete": "Confirmer la suppression",
+ "moreActions": "Autres actions pour {{name}}",
+ "deletePackage": "Supprimer du Cloud"
+ },
+ "SkillSharedLinksView": {
+ "loading": "Chargement des liens partagés…",
+ "noMatches": "Aucun lien ne correspond à cette recherche."
+ },
+ "SkillsSharedLinksHeader": {
+ "back": "Retour aux skills",
+ "backTooltip": "Retour aux skills · Esc",
+ "unlisted": "Non répertorié — seules les personnes disposant d'un lien peuvent l'ouvrir."
+ },
+ "managedInstall": {
+ "versionLabel": "Version",
+ "editedWarning": "Vos modifications sont conservées, sauf si vous choisissez de les abandonner.",
+ "finishInstall": "Terminer l'installation",
+ "useVersion": "Utiliser cette version",
+ "reinstall": "Réinstaller",
+ "cancel": "Annuler",
+ "sendToMachine": "Envoyer vers une autre machine",
+ "confirmRemove": "Confirmer la suppression",
+ "remove": "Supprimer",
+ "discardEditsRemove": "Abandonner mes modifications et supprimer",
+ "discardEdits": "Abandonner mes modifications et réinstaller",
+ "titleMore": "{{name}} +{{count}}",
+ "scopeWorkspace": "Cet espace de travail",
+ "scopeGlobal": "Partout",
+ "installedOn": "Installé le {{date}}",
+ "stateEdited": "Modifié après l'installation",
+ "stateMissing": "Des fichiers sont manquants",
+ "skillEdited": "Modifié",
+ "skillMissing": "Manquant",
+ "versionCurrent": "{{date}} (installé)",
+ "installElsewhere": "Installer ailleurs…"
+ },
+ "skillWarningPreview": {
+ "bundleDescription": "Un bundle d'aperçu couvrant tous les niveaux d'avertissement.",
+ "instructionsOnlyDescription": "Instructions entièrement contenues dans SKILL.md.",
+ "supportingFilesDescription": "Inclut des fichiers de référence lisibles en plus des instructions principales.",
+ "runnableFilesDescription": "Inclut des scripts qu'un agent peut exécuter avec vos accès.",
+ "binaryFilesDescription": "Inclut des ressources opaques et un binaire exécutable."
+ },
+ "SkillDelete": {
+ "dismissResults": "Fermer",
+ "reasonBundled": "Intégré à Orca — il serait restauré",
+ "reasonPlugin": "Installé par un plugin — supprimez plutôt le plugin",
+ "reasonUnowned": "Ce skill vit hors des dossiers de skills d'Orca — supprimez-le là où il est stocké",
+ "reasonMissing": "Ce skill n'est plus présent sur le disque",
+ "reasonStale": "Ce skill a changé depuis le chargement de la liste — actualisez puis réessayez",
+ "blockedLine": "{{count}} × {{reason}}",
+ "resultLine": "{{count}} × {{label}}",
+ "statusSkipped": "Ignoré",
+ "statusBusy": "Occupé — une autre opération de skill est en cours",
+ "statusPartial": "Partiellement supprimé — certains fichiers restent sur le disque sous un nom caché",
+ "statusFailed": "Échec — rien n'a changé",
+ "statusDeleted": "Supprimé",
+ "confirmHost": "Sur {{host}}.",
+ "confirmPermanent": "Action irréversible.",
+ "failed": "Impossible de supprimer les skills",
+ "hostUnavailable": "Impossible de joindre la machine sélectionnée. Actualisez puis réessayez.",
+ "hostUnresolved": "La machine qui héberge ces skills est encore en cours d'identification.",
+ "hostUpdateRequired": "Mettez à jour Orca sur la machine sélectionnée pour supprimer des skills.",
+ "nothingOne": "Ce skill ne peut pas être supprimé depuis ici.",
+ "nothingOther": "Aucun des {{count}} skills sélectionnés ne peut être supprimé depuis ici.",
+ "placementSummaryParts": "Supprime {{parts}} répartis sur {{roots}}.",
+ "placementJoin": " et ",
+ "retainedSource": "Le skill lui-même reste à {{path}}."
+ }
+ },
+ "sidebar": {
+ "local": {
+ "base": {
+ "ref": {
+ "suggestion": {
+ "toast": {
+ "670864ab52": "Mise à jour du {{value0}} local en cours",
+ "84c62e4d7f": "Impossible d'activer {{value0}}",
+ "442552c656": "Ouvrez les paramètres et réessayez.",
+ "f15fd80989": "Votre nouveau worktree est à jour, mais le {{value0}} local est en retard de {{value1}} {{value2}}, les diffs IA peuvent donc se comparer à un historique obsolète. Laissez Orca le maintenir à jour automatiquement. Modifiable à tout moment dans",
+ "3d260e1a5d": "Paramètres › {{value0}}",
+ "34a03a6565": "Garder {{value0}} à jour",
+ "4a18052018": "Le {{value0}} local est en retard sur {{value1}}",
+ "commit": "commit",
+ "commits": "commits"
+ }
+ }
+ }
+ }
+ },
+ "AddProjectFromFolderDialog": {
+ "7d1f51678c": "Ajouter un projet",
+ "7726a16374": "Annuler",
+ "046751dbfb": "Ajoutez ce dossier comme projet Orca distinct.",
+ "e643b30398": "Projet ajouté sur l'hôte SSH"
+ },
+ "AddRepoCreateStep": {
+ "0ae45b8238": "my-project",
+ "a8149a3a5a": "Nom",
+ "11fd2a7db8": "Dépôt Git",
+ "c7b9f94456": "Créer un nouveau projet",
+ "b100311784": "Nommez-le et Orca créera un vrai projet avec des valeurs par défaut sensées.",
+ "685b5eefe1": "{{kind}} dans {{parent}}",
+ "2a762f3b19": "Vérification de Git sur cet hôte...",
+ "fe1e616c5b": "Git est requis pour créer un projet.",
+ "c234df77f7": "Choisissez ou saisissez un dossier parent sur l'hôte avant de créer.",
+ "3a13f6e88b": "emplacement non sélectionné",
+ "6ed14c0281": "dossier hôte non sélectionné",
+ "5e97f0c4b9": "Projet créé",
+ "2c12db1511": "Projet déjà ajouté",
+ "875dda0995": "Saisissez un chemin parent sur l'hôte.",
+ "ssh_parent_manual": "Saisissez un chemin parent SSH.",
+ "45b7c26034": "Créer le projet",
+ "85085d74d2": "Création…"
+ },
+ "AddRepoHostSelector": {
+ "host": "Hôte",
+ "local": "Local",
+ "runtime": "Serveur",
+ "ssh": "SSH",
+ "connecting": "Connexion",
+ "connect": "Se connecter",
+ "addSshHost": "Ajouter un hôte SSH",
+ "addRemoteServer": "Ajouter un serveur distant",
+ "addRemoteHost": "Ajouter un hôte distant",
+ "addRemoteHostTooltip": "Choisissez SSH pour une machine sur laquelle vous pouvez vous connecter, ou serveur distant pour un serveur Orca.",
+ "addSshHostDetail": "Utiliser une machine existante via SSH.",
+ "addRemoteServerDetail": "Se jumeler avec Orca exécuté sur un autre ordinateur.",
+ "addRemoteHostDetail": "Hôte SSH ou serveur Orca"
+ },
+ "AddRepoNestedImportStep": {
+ "496f68cf8c": "Analyse des dépôts en cours. Cliquez pour arrêter.",
+ "a32bef9516": "Arrêter l'analyse",
+ "2f8298f3c3": "Interrompre l'analyse",
+ "5f857ba8e6": "dans",
+ "4df0d08cc5": "Trouvé(s)",
+ "8db50afe1a": "Importer des dépôts depuis un dossier",
+ "5b2e6fe3c8": "Importer séparément",
+ "cf9d382ca1": "Importer",
+ "220dd32d83": "Analyse...",
+ "fb33359f69": "Regrouper ces dépôts ?",
+ "d75170194e": "Choisissez ceci si ces projets vont ensemble — un monorepo, ou juste un ensemble de dépôts liés. Orca les regroupera et vous permettra de travailler depuis le dossier parent.",
+ "39d51212cc": "Nom du groupe",
+ "aa0247680d": "Non, importer séparément",
+ "a0bc4d1f8e": "Oui, importer comme groupe",
+ "8401a7a0d0": "1 dépôt",
+ "d4f1df62ef": "{{value0}} dépôts",
+ "b4263a2ac4": "{{value0}} trouvé(s) dans {{value1}}.",
+ "24eda6c8b2": "Analyse... {{value0}}",
+ "6149d5203f": "Aucun dépôt sélectionné. Ouvrez plutôt le dossier parent pour utiliser l'éditeur, le terminal et la recherche sans les fonctionnalités Git.",
+ "e52454b7f6": "Ouvrir comme dossier"
+ },
+ "AddRepoRemoteStep": {
+ "5b205b5281": "Interrompre l'analyse",
+ "6680289908": "/home/user/project",
+ "ef410aa881": "Chemin sur l'hôte",
+ "0416bde073": "Ajouter dans les paramètres",
+ "df6fbcf880": "Aucune cible SSH configurée.",
+ "44637f43bd": "Cible SSH",
+ "80557be85a": "Choisissez une cible SSH connectée et saisissez le chemin vers un dépôt Git.",
+ "91b93a90a4": "Ouvrir un projet sur l'hôte SSH",
+ "007651bdf9": "Naviguez jusqu'à un répertoire et cliquez sur Sélectionner pour le choisir.",
+ "dd3ff65486": "Parcourir le système de fichiers distant",
+ "36d427bb66": "Ajouter un projet sur l'hôte SSH",
+ "35831a7312": "Ajout...",
+ "lockedDescription": "Saisissez le chemin vers un dépôt Git sur {{value0}}.",
+ "lockedDisconnected": "{{value0}} est déconnecté.",
+ "93e0221434": "Se connecter"
+ },
+ "AddRepoServerStartStep": {
+ "ae990c86a0": "Retour aux options d'ajout",
+ "e1710bf831": "Ouvrir comme dossier",
+ "8da4d1a5be": "Ajouter un projet Git",
+ "ac66a3ed2d": "Parcourir le système de fichiers de l'hôte",
+ "92d25420a0": "/home/user/project",
+ "867692f505": "Chemin sur l'hôte",
+ "423b5d3d31": "Ajoutez un dépôt Git ou un dossier déjà présent sur l'hôte sélectionné.",
+ "3d0c035483": "Ouvrir un projet de l'hôte",
+ "438493f214": "Ou saisissez manuellement un chemin sur l'hôte",
+ "6b9958492a": "Vous voulez importer plusieurs dépôts d'un coup ? Parcourez jusqu'au dossier parent.",
+ "d40d751517": "Nouveau dépôt ou dossier",
+ "a81ffa0a99": "Créer sur l'hôte",
+ "a2ea37d549": "Dépôt Git distant",
+ "47759c9491": "Cloner depuis une URL",
+ "516187414c": "Projet ou dossier existant",
+ "0adf083af7": "Parcourir l'hôte",
+ "8efa930eb5": "Ajoutez un autre projet depuis l'hôte sélectionné.",
+ "39bd249b3a": "Ajouter un projet",
+ "0f8aba944c": "Naviguez jusqu'à un répertoire et cliquez sur Sélectionner pour le choisir."
+ },
+ "AddRepoStartSteps": {
+ "87596c1446": "Autres moyens d'ajout",
+ "acf895cb42": "Ajoutez un projet pour démarrer avec Orca.",
+ "d13757911c": "Ajouter un projet",
+ "d301db1c9a": "Analyse des dépôts en cours. Cliquez pour arrêter.",
+ "69ea7f8dc4": "Arrêter l'analyse",
+ "9906cae183": "Interrompre l'analyse"
+ },
+ "AddRepoStepIndicator": {
+ "3bb655c117": "Retour"
+ },
+ "AddRepoSteps": {
+ "569326d9cc": "Choisir un dossier",
+ "a93ef169b5": "Parcourir le système de fichiers de l'hôte",
+ "2ce3f6edf8": "/path/to/destination",
+ "04a4c4e84a": "Emplacement du clone",
+ "b698a4a29d": "https://github.com/user/repo.git",
+ "3d4acbe693": "URL Git",
+ "5b2ea674b1": "Saisissez l'URL Git et choisissez où le cloner.",
+ "c05f88a31f": "Cloner depuis une URL",
+ "fe8e629fe3": "Naviguez jusqu'à un répertoire et cliquez sur Sélectionner pour le choisir.",
+ "df8b0e6c22": "Projet ajouté sur l'hôte SSH",
+ "3e64e8a70d": "Échec de la connexion",
+ "32a7256d85": "Cloner",
+ "69f5b5380d": "Clonage...",
+ "cloneOnHostDescription": "Saisissez l'URL Git et choisissez où le cloner sur {{value0}}.",
+ "cloneParentFolder": "Dossier parent",
+ "remoteCloneParentPlaceholder": "/home/user/projects"
+ },
+ "AutoRenameFailedDialog": {
+ "aed1623b1e": "Fermer",
+ "eab8b45238": "Copier l'erreur",
+ "a23b22d16f": "Copied",
+ "74fc00776f": "Détails de l'erreur",
+ "3afcad0497": "à partir du premier message de l'agent.",
+ "ff62a18580": "Orca n'a pas pu générer un nom de branche pour",
+ "ca3b225195": "Échec du nommage automatique de la branche"
+ },
+ "CreateProjectLocationField": {
+ "95548e33bf": "Choisir un dossier parent...",
+ "632b456b1b": "Modifier",
+ "afaf54f245": "Modifier le dossier parent",
+ "f520f83a97": "Parcourir le système de fichiers de l'hôte",
+ "2a20a603a3": "/home/user/projects",
+ "134e37f711": "Emplacement",
+ "b589b77997": "Naviguez jusqu'à un répertoire et cliquez sur Sélectionner pour le choisir."
+ },
+ "DeleteWorktreeDialog": {
+ "ff2a74ac0e": "et",
+ "91492c9ad6": "Supprimer",
+ "4f6750ca7b": "Échec de la suppression de l'espace de travail",
+ "42e610d6cf": "Échec de la suppression forcée",
+ "5cc1a6701c": "Ouvrir les paramètres",
+ "2b56b35f53": "Vous pouvez changer cela dans les paramètres.",
+ "dd3a45bbbd": "Cette confirmation sera ignorée la prochaine fois.",
+ "fc23c4cbdf": "Supprimer l'espace de travail",
+ "86f0ae1257": "Supprimer les espaces de travail"
+ },
+ "DeleteWorktreeDirtyChangeHint": {
+ "8e2994ce28": "Supprimer cet espace de travail efface définitivement ces modifications du disque."
+ },
+ "DeleteWorktreeLineageNotice": {
+ "ad407c2d55": "de plus",
+ "a940f3c96e": "Les espaces de travail enfants seront supprimés",
+ "29b98bf9cd": "La suppression de cet espace de travail supprime aussi {{value0}} espaces de travail enfants.",
+ "66798cc6a2": "La suppression de cet espace de travail supprime aussi 1 espace de travail enfant."
+ },
+ "DeleteWorktreeSkipConfirmOption": {
+ "29aefb7e52": "Ne plus demander"
+ },
+ "DeleteWorktreeWarningPanels": {
+ "026738155a": "(le répertoire de clone d'origine).",
+ "c4f96a6e18": "worktree principal",
+ "e3be9eba15": "Ceci est le"
+ },
+ "ImportedWorktreesVisibilityLine": {
+ "b7a87dc32f": "Afficher dans la liste des worktrees",
+ "ad99f4eea9": "Garder masqué",
+ "9f4f14e821": "Modifiable plus tard depuis le menu du projet.",
+ "b2bc47c080": "autres emplacements",
+ "b47ba1a9d2": "Aperçu de {{value0}}",
+ "2251d41ebb": "Groupes de worktrees masqués",
+ "f54f2bec5d": "{{value0}} worktrees masqués pour {{value1}}",
+ "5a9688802a": "Afficher {{value0}} de plus",
+ "294de4aeb2": "Afficher moins"
+ },
+ "NonGitFolderDialog": {
+ "e52454b7f6": "Ouvrir comme dossier",
+ "05b33a17a9": "Annuler",
+ "8fba4b8cbb": "Ce dossier n'est pas un dépôt Git. Vous aurez l'éditeur, le terminal et la recherche, mais les fonctionnalités basées sur Git ne seront pas disponibles.",
+ "c49fb13492": "Échec de l'ajout du dossier sur cet hôte",
+ "9a766f33ac": "Ce chemin a été vérifié sur l'hôte SSH.",
+ "79fd02cf5f": "Ce chemin a été vérifié sur {{hostName}}.",
+ "8851b77327": "Ce chemin a été vérifié localement."
+ },
+ "OrcaYamlTrustDialog": {
+ "f3e2b868fb": "Exécuter les hooks",
+ "43b7bec4cd": "Ne pas exécuter",
+ "c494b3ccb1": "dans",
+ "79afc6772b": "orca.yaml",
+ "531689199b": "Toujours faire confiance",
+ "bf800b7e04": ". N'exécutez que si vous faites confiance",
+ "831f2cd9f0": "s'exécute sur votre machine",
+ "aa3ffb33fb": "De ce dépôt, le",
+ "c55beddbf8": "a changé depuis votre dernière approbation. Revérifiez avant son exécution",
+ "95bf974a1a": "script {{value0}}",
+ "9e52effffd": "Nouveau script {{value0}}",
+ "e4a51dc4b3": "Exécuter {{value0}} de {{value1}} ?",
+ "02b0ede5ad": "Le {{value1}} de {{value0}} a changé — exécuter la nouvelle version ?"
+ },
+ "PendingWorktreeRow": {
+ "af21e953d1": "Annuler la création du worktree",
+ "188f6922a0": "Annuler"
+ },
+ "ProjectGroupDeleteDialog": {
+ "ca65b78f78": "Annuler",
+ "9be10d49ea": "et dissocier ses projets.",
+ "69f5cb97d0": "Supprimer",
+ "591f330288": "Supprimer le groupe de projets",
+ "2c14ce677a": "Suppression...",
+ "0e0e6764af": "Projets contenus",
+ "ad407c2d55": "de plus",
+ "removeContainedProjectSingular": "Retirer 1 projet contenu",
+ "removeContainedProjectPlural": "Retirer {{value0}} projets contenus",
+ "eeabb8e8e4": "Retirer {{value0}} {{value1}} d'Orca",
+ "55f75628c0": "Les dossiers de projets sur le disque ne sont pas supprimés.",
+ "897e5d3d4c": "Supprimer le groupe et retirer les projets",
+ "fec7e9c8ae": "Supprimer le groupe"
+ },
+ "ProjectGroupNameDialog": {
+ "d99a034073": "Annuler",
+ "83dfbc5313": "Nom du groupe",
+ "4a64e78822": "Enregistrement..."
+ },
+ "RemoteFileBrowser": {
+ "2300612806": "Tapez pour filtrer ou saisissez un chemin…",
+ "9e060f5815": "Sélectionner un dossier",
+ "f8b1deb1a4": "Annuler",
+ "51001182e3": "Répertoire vide",
+ "971d85cc84": "S'ouvre comme projet sur cet hôte · {{value0}}",
+ "00c4235c10": "Aucun résultat pour « {{value0}} »",
+ "largeInputNoMatches": "Aucun résultat pour cette saisie longue"
+ },
+ "RemoveFolderDialog": {
+ "4dc5b5065b": "Supprimer",
+ "d36883e046": "Annuler",
+ "b79b39d865": "Retirer le projet",
+ "removeDescriptionSsh": "Ceci retire uniquement {{name}} d'Orca. Ses fichiers restent sur {{host}} — rajoutez cet hôte SSH pour le récupérer.",
+ "removeDescriptionLocal": "Ceci retire uniquement {{name}} d'Orca. Il est toujours sur votre disque.",
+ "removeDescriptionVmRecipe": "Ceci retire {{name}} d'Orca. Sa recette de VM détermine si l'environnement et ses fichiers sont définitivement supprimés."
+ },
+ "ScrollToCurrentWorkspaceToolbarButton": {
+ "23989bb663": "Révéler l'espace de travail actif"
+ },
+ "SetupGuideSidebarEntry": {
+ "b0a7bfc34c": "Masquer de la barre latérale",
+ "88d402b71d": "Checklist d'intégration"
+ },
+ "SetupScriptPromptCard": {
+ "ff1e819a11": "Ajouter un script de setup",
+ "70715947fb": "Le script de setup ne peut pas être vide",
+ "888b83bf78": "Échec de l'enregistrement du script de setup",
+ "a49196d538": "S'exécute quand Orca crée un nouveau worktree.",
+ "d9f2db2738": "paramètres du projet",
+ "a5bb8c5135": "Enregistré dans les",
+ "dcaa645da5": "ok"
+ },
+ "SetupScriptPromptCardViews": {
+ "96a7f4198c": "Enregistrer le setup local",
+ "3933401d28": "Configurer",
+ "31b8b01a45": "Paramètres",
+ "4a98f907ae": "Réessayer",
+ "0a98169776": "Ajoutez une commande de setup à exécuter quand Orca crée de nouveaux worktrees.",
+ "8349e3fa4c": ". Enregistrez-la pour l'exécuter sur les nouveaux worktrees.",
+ "b56d1322f7": "Une commande de setup a été trouvée dans",
+ "aef6c0a213": "Enregistrez la commande détectée pour l'exécuter chaque fois qu'Orca crée un worktree.",
+ "660cdc17f8": "scripts de setup partagés. Ajoutez une commande locale, ou changez la source dans les paramètres.",
+ "8f6be51aa1": "orca.yaml",
+ "bb879db364": "Ce dépôt ignore les",
+ "0155fb9ed3": "Impossible de vérifier le script de setup de ce dépôt pour le moment.",
+ "eefa756190": "Configurer manuellement",
+ "ca4efcbc25": "Enregistrer",
+ "d02e6a42b1": "Détecté depuis",
+ "fdbc6cb064": "Script de setup détecté",
+ "7275f674cc": "Setup détecté",
+ "822ff300ad": "Ignorer",
+ "5bfd5c8779": "Ignorer les scripts de setup"
+ },
+ "SidebarFeedbackDialog": {
+ "8bf619e4cf": "Annuler",
+ "8de03e23c5": "Envoyez avec votre commentaire texte uniquement, ou connectez `gh` pour inclure votre identité GitHub.",
+ "d20439c560": "Vérification de l'identité GitHub…",
+ "5b120b9634": "Envoyer anonymement",
+ "c9e5ea0791": "GitHub :",
+ "d46ddd66fc": "Que pouvons-nous améliorer ?",
+ "3460258a54": "Suivre sur X",
+ "26108d3699": "Rejoindre Discord",
+ "d245c4ef6c": "Issues GitHub",
+ "9b33530b3d": "Autres moyens de nous contacter",
+ "a828fa4aee": "Partagez ce qui fonctionne, ce qui est cassé, ou ce qu'Orca devrait faire ensuite.",
+ "0eb643f07f": "Envoyer un commentaire",
+ "60b721e857": "Échec de l'envoi du commentaire. Réessayez.",
+ "7a46c228b8": "Merci pour votre commentaire.",
+ "a2fd890d9e": "Saisissez un commentaire avant d'envoyer.",
+ "f2e42e1307": "Envoyer",
+ "69969ba364": "Envoi…",
+ "imageReadFailed": "Impossible de lire les images jointes. Essayez de les joindre à nouveau.",
+ "attachWhileSending": "Attendez la fin de l'envoi du commentaire en cours avant de joindre d'autres images.",
+ "imagesNotDelivered": "Commentaire envoyé, mais la livraison des images n'a pas pu être confirmée."
+ },
+ "SidebarFilter": {
+ "e3b3898218": "Ajouter un projet",
+ "92a23e6d07": "Réinitialiser les filtres",
+ "81ded53722": "SSH",
+ "b9e8802e73": "Aucun projet ne correspond",
+ "779b7ba05d": "Effacer",
+ "139877b384": "Tout sélectionner",
+ "5f7085a077": "Projets",
+ "e5cb32a898": "Masquer la branche par défaut",
+ "638a2d221d": "Masquer les espaces en veille",
+ "f506a1262a": "Filtrer les espaces de travail",
+ "75405270ed": "Modifier les filtres ({{value0}} actifs)",
+ "489d1c8c9f": "Rechercher des projets...",
+ "ee240a39eb": "Modifier les filtres",
+ "automationCreated": "Masquer ceux créés par l'automatisation",
+ "cliCreated": "Masquer ceux créés par le CLI",
+ "detachedHead": "Masquer les HEAD détachés",
+ "keepDefaultBranch": "Sauf la branche par défaut",
+ "keepDefaultBranchAria": "Garder la branche par défaut visible tout en masquant les espaces de travail en veille"
+ },
+ "SidebarHeader": {
+ "25a95899c9": "Ajouter un projet",
+ "92154beb7e": "Nouvel espace de travail",
+ "49f62c5665": "Tableau des espaces de travail",
+ "5c9c7c16aa": "Ajoutez un projet pour créer des espaces de travail",
+ "ca6f729da2": "Nouvel espace de travail ({{value0}})",
+ "a30e34eb5c": "Fermer le tableau des espaces de travail"
+ },
+ "SidebarNav": {
+ "80611a8b10": "Recherche",
+ "0c3395fd32": "Rechercher parmi les worktrees et les onglets du navigateur",
+ "c86d83b5c3": "Nouveau",
+ "1b5c41caee": "Orca Mobile",
+ "9c95e1ce91": "Agents",
+ "f323383e9a": "Automatisations",
+ "e7ad3c540d": "Ouvrir les tâches Jira",
+ "c39ab10000": "Ouvrir les tâches Linear",
+ "196c1b5362": "Ouvrir les tâches GitLab",
+ "0ccba862b8": "Ouvrir les tâches GitHub",
+ "fee535205b": "Tâches",
+ "d599269755": "Masquer de la barre latérale",
+ "artifacts": "Artefacts",
+ "skills": "Skills"
+ },
+ "SidebarRepositoryFilterSection": {
+ "d3a9c4cea1": "Effacer",
+ "7679f0c268": "Projets",
+ "f10ca29601": "Retirer le filtre {{value0}}",
+ "2656053db4": "SSH",
+ "83a820fa71": "Filtrer les projets...",
+ "5a273fbfce": "Ajouter un projet...",
+ "4815c70605": "Aucun projet ne correspond",
+ "bbbc6e8e3b": "Aucun projet non sélectionné ne correspond",
+ "allProjects": "Tous les projets",
+ "selectedProjectsCount": "{{value0}} projets"
+ },
+ "SidebarSettingsHelpMenu": {
+ "ad3d3ed7f1": "Redémarrer Orca",
+ "29c56f30ee": "Rechercher des mises à jour",
+ "eb9884e55b": "Discord",
+ "5687ab246a": "GitHub",
+ "5f83d86d92": "Journal des modifications",
+ "cdc87f897e": "Docs",
+ "e565171a7c": "Raccourcis clavier",
+ "4cf5b868d7": "Envoyer un commentaire",
+ "a428c25998": "Paramètres",
+ "2991a0106c": "Aide",
+ "4e8f5710d3": "Impossible de redémarrer Orca.",
+ "5161eef55d": "Redémarrage d'Orca…",
+ "d396773ef0": "vérification",
+ "f8a2c91d4e": "Jalons",
+ "b7e4d2a19c": "Prise en main",
+ "c4f8e1b72a": "X"
+ },
+ "SidebarToolbar": {
+ "87d0064026": "Tableau des espaces de travail déplacé dans la barre inférieure",
+ "a30e34eb5c": "Fermer le tableau des espaces de travail",
+ "49f62c5665": "Tableau des espaces de travail",
+ "19e32d0e5f": "Ouvrir le sélecteur de dossiers pour ajouter un projet",
+ "abc62b6328": "Ajouter un projet"
+ },
+ "SidebarWorkspaceFilterSection": {
+ "c3fa13dc2e": "Masquer la branche par défaut",
+ "ed1611b65b": "Masquer les espaces en veille",
+ "82594419ba": "Filtres",
+ "automationCreated": "Masquer ceux créés par l'automatisation",
+ "cliCreated": "Masquer ceux créés par le CLI",
+ "detachedHead": "Masquer les HEAD détachés",
+ "keepDefaultBranch": "Sauf la branche par défaut",
+ "keepDefaultBranchAria": "Garder la branche par défaut visible tout en masquant les espaces de travail en veille",
+ "otherClients": "Masquer les espaces de travail d'autres clients",
+ "otherClientsAria": "Masquer les espaces de travail créés depuis d'autres clients Orca sur des serveurs distants partagés"
+ },
+ "sidebarHostOptions": {
+ "3e102f111c": "Tous les hôtes",
+ "visibleHostsCount": "{{value0}} hôtes"
+ },
+ "SidebarHostScopeStrip": {
+ "scopedTo": "{{value0}} visible(s)",
+ "backToAll": "Tous les hôtes"
+ },
+ "SidebarWorkspaceOptionsMenu": {
+ "hosts": "Hôtes",
+ "showSection": "Afficher",
+ "allHostsDetail": "Afficher tous les hôtes",
+ "configuredSshHost": "SSH configuré",
+ "projectSshHost": "SSH du projet",
+ "activeRuntimeHost": "Serveur actif",
+ "projectRuntimeHost": "Serveur du projet",
+ "95c9754653": "Disposition de l'activité des agents",
+ "3d4b9c4997": "Survol",
+ "ba87080fb7": "Afficher les propriétés",
+ "320b675c9a": "Disposition en cartes",
+ "newCardDisplay": {
+ "title": "Affichage des cartes"
+ },
+ "09faabd875": "Ordre des projets",
+ "7bada3b1ab": "Trier par",
+ "dc0bb670bc": "Grouper par",
+ "631b97eea9": "Portée des hôtes",
+ "9919ae1082": "Options des espaces de travail",
+ "bc96dbd041": "Options des espaces de travail ({{value0}})",
+ "af9249c505": "Activité d'espace de travail la plus récente",
+ "b451c8b162": "Récents",
+ "6664282a7b": "Glissez les projets pour les ranger",
+ "7b316bdd51": "Manuel",
+ "7153d07485": "Glissez les espaces de travail pour les ranger au sein de chaque groupe.",
+ "2170d553cf": "Projet",
+ "b759bb87ee": "Agents nécessitant une attention, puis activité la plus récente.",
+ "503462f2b4": "Activité des agents",
+ "3728165cdd": "Nom",
+ "2a81e07366": "Liste complète",
+ "25105b28cb": "Compact",
+ "d7084e8bc8": "Activité des agents",
+ "automation": "Automatisation",
+ "b64d8bcca0": "Ports",
+ "26c71e536c": "Notes",
+ "b8dcc6f321": "Lien PR/MR",
+ "ca4d3c522e": "Ticket Linear",
+ "jiraIssue": "Ticket Jira",
+ "91dfc653e8": "Ticket GitHub",
+ "bdd23b4e07": "Issues GitHub",
+ "44713a5d04": "Issues Linear",
+ "jiraIssues": "Tickets Jira",
+ "cc17bd443b": "Détaillé",
+ "0f9b959b31": "PR",
+ "e029a2d775": "Statut",
+ "c2c7a45cda": "Aucun",
+ "680043342f": "compact",
+ "c7591b6014": "dépôt",
+ "2d4f0eb933": "Par défaut",
+ "1a0eec0d35": "Statut",
+ "b5536d5a88": "Tâches",
+ "8d62c68b35": "Notes",
+ "2d74665a56": "Ports",
+ "65a9820bd1": "Statuts des agents",
+ "219ebf1961": "Nom de branche",
+ "folderPathIdentity": "Branche / chemin de dossier",
+ "cli": "Orca CLI"
+ },
+ "SshTargetRow": {
+ "4677394048": "Connexion…",
+ "75ad429b5d": "Se connecter"
+ },
+ "WorkspaceKanbanCard": {
+ "cefae8983e": "Épinglés"
+ },
+ "WorkspaceKanbanDrawerHeader": {
+ "f369f5c5a3": "Fermer",
+ "e1a34450fc": "Organisez les espaces de travail par statut et ouvrez les cartes d'espace de travail.",
+ "81870af08f": "sélectionnés",
+ "c6a77ab0f4": "Tableau des espaces de travail"
+ },
+ "WorkspaceKanbanPinDropTarget": {
+ "c30151c5ee": "Déposez ici pour épingler sans changer de statut.",
+ "8fae2d0862": "Épinglés"
+ },
+ "WorkspaceKanbanSettingsMenu": {
+ "79eb990aa4": "Ajouter un statut",
+ "054cb50df7": "Supprimer {{value0}}",
+ "b45b350eb0": "Déplacer {{value0}} vers la gauche",
+ "8ce44af9a8": "Renommer {{value0}}",
+ "395e541d5d": "Statuts",
+ "34f03eb0de": "Paramètres du tableau",
+ "26cbc92150": "Paramètres du tableau des espaces de travail",
+ "87d24a0c2f": "Synchroniser le tableau et le statut des issues",
+ "4c2eaa78cc": "Déplacer un espace de travail lié met à jour le statut de son issue Linear quand un état de workflow correspondant existe."
+ },
+ "WorkspaceKanbanStatusLane": {
+ "8ad104642b": "Vide",
+ "3611d1ae7f": "Redimensionner les colonnes du tableau des espaces de travail",
+ "2df01a03ff": "Aucun résultat"
+ },
+ "WorkspaceStatusAppearancePopover": {
+ "514be2f569": "Définir la couleur de {{value0}} sur {{value1}}",
+ "8be427206b": "Icône",
+ "2ac106f6b2": "Couleur",
+ "74b1413279": "Apparence",
+ "ccbd1e2c69": "Personnaliser l'apparence de {{value0}}"
+ },
+ "WorktreeCard": {
+ "a88c92d0e3": "{{value0}}/{{value1}} existe déjà.",
+ "6f09f58541": "Supprimer l'espace de travail",
+ "0777de5970": "Worktree principal (répertoire de clone d'origine)",
+ "0f33af979b": "Checkout partiel. Les fichiers hors de ces chemins ne sont pas sur le disque.",
+ "4f964d5e8c": "sparse",
+ "7d517f82e2": "principal",
+ "4eba2ea99e": "Échec du nommage automatique. Cliquez pour voir les détails.",
+ "74522ee457": "échec du renommage",
+ "02e19349f4": "Échec du renommage auto : voir l'erreur",
+ "691ccfd622": "Suppression…",
+ "35ccfe2475": "Projet {{value0}}",
+ "1d66d84f0b": "string",
+ "57eaa61b55": "Masquer les espaces de travail enfants",
+ "8cb634cda6": "Afficher les espaces de travail enfants",
+ "01f45d3d8a": "barre latérale",
+ "93aebe4529": "Dossier",
+ "0d224eff10": "Worktree principal",
+ "ca74db7550": "Projet sur hôte SSH",
+ "021538e1d1": "SSH déconnecté",
+ "runtimeHostDisconnected": "Serveur déconnecté",
+ "runtimeHostDisconnectedNamed": "{{hostName}} déconnecté",
+ "runtimeHostProject": "Projet sur serveur Orca",
+ "runtimeHostProjectNamed": "Projet sur {{hostName}}",
+ "automationCreated": "Créé par automatisation",
+ "branchIdentity": "Branche",
+ "branchFolderPathIdentity": "Branche ou chemin de dossier",
+ "ef18787206": "En attente de suppression"
+ },
+ "WorktreeCardAgents": {
+ "1b0a156717": "Agents"
+ },
+ "WorktreeCardReviewDetailSection": {
+ "copyLink": "Copier le lien",
+ "reviewHeader": "{{value0}} #{{value1}}"
+ },
+ "WorktreeCardMeta": {
+ "3e65e11cc6": "Métadonnées de l'espace de travail",
+ "c7fa72ead0": "Modifier les notes",
+ "93cbea12c2": "Notes",
+ "eace1d2cf6": "neutre",
+ "dbe2d18972": "Plus d'actions {{value0}}",
+ "ae76907ca6": "Délier {{value0}}",
+ "ad25c3ff05": "Voir sur {{value0}}",
+ "2c67730e07": "Ouvrir dans Orca",
+ "e42941631a": "Voir sur Linear",
+ "5e982e6128": "Linear {{value0}}",
+ "807b13b9ec": "Modifier l'issue",
+ "b22f058067": "Voir sur GitHub",
+ "e97d8f2876": "Ticket #{{value0}}",
+ "moreIssueActions": "Plus d'actions d'issue",
+ "copyLink": "Copier le lien",
+ "copyLinkSuccess": "{{value0}} copié",
+ "copyLinkFailure": "Échec de la copie du lien",
+ "issueLinkLabel": "Lien d'issue",
+ "reviewLinkLabel": "Lien {{value0}}",
+ "3ea2702e62": "Lié à {{value0}} #{{value1}}",
+ "b105fd3057": "Lié à Linear {{value0}}",
+ "3f2649eeb8": "Issue #{{value0}} liée",
+ "fe075cb851": "Notes de l'espace de travail",
+ "automationHeader": "Automatisation",
+ "openAutomation": "Ouvrir l'automatisation",
+ "openAutomationRun": "Ouvrir l'exécution",
+ "automationCreated": "Créé par automatisation",
+ "checkingAutomationAvailability": "Vérification de la disponibilité de l'automatisation...",
+ "automationMissing": "Cette automatisation n'est plus disponible.",
+ "automationRunMissing": "L'historique d'exécution n'est plus disponible.",
+ "automationAvailabilityUnavailable": "La disponibilité de l'automatisation n'a pas pu être vérifiée.",
+ "cliHeader": "Orca CLI",
+ "cliCreatedFromAgent": "Créé par un agent via `orca worktree create`",
+ "cliCreatedFromShell": "Créé via `orca worktree create`",
+ "cliStartupAgent": "Démarré avec {{value0}}",
+ "cliCreated": "Créé par Orca CLI",
+ "jiraIssue": "Jira {{value0}}",
+ "viewOnJira": "Voir sur Jira",
+ "linkedJira": "Lié à Jira {{value0}}"
+ },
+ "WorktreeCardMetadataStatusBadges": {
+ "fe188062a1": "État : ouvert",
+ "2931b42b09": "État : brouillon {{value0}}",
+ "e888362def": "État : fermé",
+ "f394b3e86e": "État : fusionné",
+ "af2b07bda5": "État : {{value0}}",
+ "29df45afa2": "MR"
+ },
+ "WorktreeCardPorts": {
+ "34f733dda2": "Aller au worktree",
+ "3240f320d7": "Ports actifs",
+ "3e5f66564e": "Espace de travail indisponible",
+ "2f854442ff": "Arrêter le processus",
+ "c8067a829a": "Copier {{value0}}",
+ "33bc7d7495": "Ouvrir dans le navigateur",
+ "9950fe2d20": "Échec de l'actualisation des ports",
+ "5d1a5d51bb": "Processus arrêté sur {{value0}}",
+ "c89f290e25": "{{value0}} copié",
+ "d1113f4660": "Échec de l'ouverture du navigateur",
+ "fed49903c9": "{{value0}} {{value1}} actif(s)"
+ },
+ "WorktreeContextMenu": {
+ "c39c37676a": "Créez un groupe et déplacez-y ce projet.",
+ "6664418e98": "Nouveau groupe de projets",
+ "e091caab15": "Le projet est introuvable",
+ "439fa94d53": "Mettre à jour",
+ "579b1a8e61": "Retirer du parent",
+ "8d9cd19d09": "Ouvrir le worktree parent",
+ "d35dfeae58": "Retirer du groupe",
+ "76865d827f": "Déplacer vers un groupe",
+ "503ec0f8e6": "Nouveau groupe à partir du projet",
+ "3350101edb": "Copier le chemin",
+ "f4475537d8": "Supprimer",
+ "f5ac91531d": "Retirer le projet d'Orca",
+ "b42391d8bf": "Suppression…",
+ "0918b35e4f": "Fermez tous les panneaux actifs de cet espace de travail pour libérer de la mémoire et du CPU.",
+ "7d190f7d2b": "Fermez tous les panneaux actifs des espaces de travail sélectionnés pour libérer de la mémoire et du CPU.",
+ "84cdbb7e30": "Déplacer vers le statut",
+ "56cde9e8e6": "Déplacer les statuts vers",
+ "f50603c6b2": "Marquer comme non lu",
+ "8dacff1fe0": "Marquer comme lu",
+ "3baa7d6507": "Épingler",
+ "697d0f6e1b": "Désépingler",
+ "250de158fd": "Retirer l'espace de travail",
+ "changeParentWorkspace": "Changer le worktree parent...",
+ "setParentWorkspace": "Définir le worktree parent...",
+ "workspaceSection": "Espace de travail",
+ "primaryDeleteDisabled": "Worktree principal — impossible à supprimer. Retirez plutôt le projet.",
+ "deleteWorktree": "Supprimer le worktree",
+ "sleepWithDescendants": "Mettre en veille avec les descendants ({{value0}})",
+ "sleepWithDescendantsDescription": "Fermez les panneaux actifs de cet espace de travail et de chaque descendant imbriqué pour libérer de la mémoire et du CPU.",
+ "deleteWithDescendants": "Supprimer avec les descendants…"
+ },
+ "WorktreeList": {
+ "7a8b9c0d1e": "Mise à jour requise",
+ "hostAuthNeeded": "Authentification requise",
+ "hostDisconnected": "Déconnecté",
+ "d880ea0744": "Créez un groupe et déplacez-y ce projet.",
+ "bc1460beb3": "Modifiez le nom du groupe affiché dans la barre latérale.",
+ "13757c053c": "Nouveau groupe de projets",
+ "f9dc6cc5d3": "Renommer le groupe de projets",
+ "370c6a55dd": "Réinitialiser les filtres",
+ "b7acbf038b": "Aucun espace de travail trouvé",
+ "0c6ee14f23": "enfant",
+ "5fc9d1891b": "Suppression…",
+ "c83968f87f": "Retirer le projet",
+ "64e55f7f01": "Retirer du groupe",
+ "4a08fb55f2": "Déplacer vers un groupe",
+ "cbfd565f83": "Nouveau groupe à partir du projet",
+ "e82d3589a1": "Changer l'icône du projet",
+ "2cdffbc728": "Paramètres du projet",
+ "2ef41bf9a7": "Actions du projet",
+ "609633a9e6": "Actions du projet pour {{value0}}",
+ "902115cdbe": "Supprimer le groupe",
+ "4d7b73658c": "Renommer le groupe",
+ "79465e9034": "Actions du groupe pour {{value0}}",
+ "bfbedc547b": "Worktrees",
+ "45fbfe0335": "renommer",
+ "ebc5c7dcef": "Masquer les espaces de travail enfants",
+ "84a2238242": "Afficher les espaces de travail enfants",
+ "045a8aed48": "enfants",
+ "2ca6e29a3c": "dépôt",
+ "bb85cd86ba": "Créer un espace de travail pour {{value0}}",
+ "ebadb7eadb": "{{value0}} {{value1}} enfant {{value2}}",
+ "20bebf9c7f": "Afficher {{value0}} espace de travail enfant",
+ "c1f4a31623": "Afficher {{value0}} espaces de travail enfants",
+ "e97297cb75": "Masquer {{value0}} espace de travail enfant",
+ "0cd15956d4": "Masquer {{value0}} espaces de travail enfants",
+ "bd37a57ac8": "Créer un espace de travail pour {{value0}}",
+ "b667b59632": "Certains projets n'ont pas pu être retirés d'Orca",
+ "f94466bc39": "{{value0}} projet{{value2}} sur {{value1}} est resté après la suppression du groupe.",
+ "groupDeleteFailed": "Échec de la suppression du groupe",
+ "groupDeleteFailedDesc": "Une erreur est survenue lors de la suppression du groupe. Aucun projet n'a été retiré.",
+ "groupRenameFailed": "Échec du renommage du groupe",
+ "groupRenameFailedDesc": "Orca n'a pas pu confirmer le nouveau nom auprès de l'hôte du groupe. Revérifiez le groupe après reconnexion.",
+ "failedNestWorkspace": "Échec de l'imbrication de l'espace de travail",
+ "sidebarRowMissing": "La cible n'existe plus",
+ "failedUnnestWorkspace": "Échec de la désimbrication de l'espace de travail"
+ },
+ "WorktreeMetaDialog": {
+ "3db0a2a593": "Annuler",
+ "b48c271d39": "pour enregistrer, Maj+Entrée pour un retour à la ligne.",
+ "7f0be5e9a6": "Prend en charge **markdown** — gras, listes, `code`, liens. Appuyez sur Entrée ou",
+ "030d484fc0": "Notes sur ce worktree...",
+ "9c1d1e9b71": "Commentaire",
+ "5ae06f40fd": "Collez une URL de pull request ou saisissez un numéro. Laissez vide pour supprimer le lien.",
+ "077a4f7b5c": "N° de PR ou URL GitHub",
+ "1b91db7e14": "PR GH",
+ "7c454be4c5": "Collez une URL d'issue ou saisissez un numéro. Laissez vide pour supprimer le lien.",
+ "029ea5ec57": "Ouvrir l'issue GitHub",
+ "741279e7b7": "N° d'issue ou URL GitHub",
+ "645fa4a0fd": "Issue GH",
+ "459ad7f650": "Change uniquement le nom affiché dans la barre latérale — le dossier sur disque reste identique. Laissez vide pour utiliser le nom de branche ou de dossier.",
+ "7f21e0464f": "Nom d'affichage personnalisé...",
+ "ad5e4e514f": "Nom d'affichage",
+ "65770ad0f0": "Modifiez les liens GitHub et les notes de cet espace de travail.",
+ "382fd11a3e": "Modifier les détails du worktree",
+ "2174f17011": "Enregistrer",
+ "61d6f612cf": "Enregistrement...",
+ "a0d191b7a7": "Modifiez les liens d'issues, de pull requests et les notes de cet espace de travail."
+ },
+ "WorktreeOpenInMenu": {
+ "localOnly": "Local uniquement",
+ "remoteSsh": "SSH distant",
+ "remoteRuntimeUnsupported": "L'ouverture de ce chemin dans une application locale n'est pas disponible.",
+ "remoteRuntimeUnsupportedDetail": "Passez à un espace de travail local ou SSH, puis réessayez.",
+ "sshTargetNotFound": "L'hôte SSH n'est plus disponible.",
+ "sshTargetNotFoundDetail": "Actualisez les espaces de travail ou reconnectez l'hôte, puis réessayez.",
+ "sshTargetInvalid": "La configuration de l'hôte SSH est incomplète.",
+ "sshTargetInvalidDetail": "Modifiez l'hôte SSH ou reconnectez-le, puis réessayez.",
+ "sshAliasRequired": "VS Code a besoin d'un alias de config SSH pour cet hôte.",
+ "sshAliasRequiredDetail": "Ajoutez un alias Host pour {{host}}:{{port}} dans votre config SSH locale, reconnectez l'espace de travail, puis réessayez.",
+ "remoteEditorUnsupported": "Cette application ne peut pas ouvrir les espaces de travail SSH.",
+ "remoteEditorUnsupportedDetail": "Choisissez VS Code ou utilisez l'application localement.",
+ "remotePathInvalid": "Le chemin n'est pas valide pour l'hôte SSH.",
+ "remotePathInvalidDetail": "Actualisez l'espace de travail avant de réessayer.",
+ "remoteLaunchFailed": "Impossible d'ouvrir le chemin dans VS Code.",
+ "remoteLaunchFailedDetail": "Vérifiez la commande VS Code configurée sur cette machine.",
+ "1417fd8380": "Personnaliser les applications...",
+ "8009ab69a6": "Ouvrir dans",
+ "bd0e8159f8": "Vérifiez la commande de l'éditeur ou la configuration du gestionnaire de fichiers sur cette machine.",
+ "9a5381eb09": "Impossible d'ouvrir le dossier de l'espace de travail.",
+ "0bed8727db": "Il a peut-être été déplacé ou supprimé. Actualisez les espaces de travail ou retirez-le d'Orca.",
+ "3921d3d9a5": "Le dossier de l'espace de travail est introuvable.",
+ "f387af445b": "Le chemin de l'espace de travail n'est pas un chemin local valide.",
+ "3ec372b664": "file-manager"
+ },
+ "WorktreeTitleInlineRename": {
+ "2f42ae024f": "Non lus :",
+ "bff3bdd00c": "Renommer l'espace de travail",
+ "8df295a78d": "Échec du renommage de l'espace de travail."
+ },
+ "WorktreeVisibilityHelpPopover": {
+ "c41f2d7e90": "Quels worktrees sont masqués par défaut ?",
+ "8db4e19a26": "Ce paramètre ne masque jamais les worktrees créés via Orca.",
+ "ec1e6a10fb": "Les autres worktrees démarrent masqués pour éviter un encombrement inattendu de la barre latérale.",
+ "1c68c9cf77": "Activez une source pour tous les worktrees actuels et futurs, ou affichez des worktrees individuels ci-dessous."
+ },
+ "HiddenWorktreeRecoveryList": {
+ "64e6f53f05": "Affichez-en un sans activer sa source.",
+ "showWorktree": "Afficher {{value0}} à {{value1}}"
+ },
+ "WorktreeVisibilityDialog": {
+ "83a5ba8dd1": "Worktrees non Orca",
+ "f1f71b9f02": "Toujours afficher",
+ "759371df43": "Masquer",
+ "25ddf19920": "{{value0}} actuellement masqués",
+ "8372e4bbd9": "{{value0}} actuellement affichés",
+ "5d02a5647f": "Masqué dans la barre latérale",
+ "3e045d4cb8": "Affiché dans la barre latérale",
+ "a3f19c07d2": "Vérification…",
+ "b8d24e61f5": "Impossible de lister les worktrees de ce dépôt.",
+ "c5e70a93b1": "Réessayer",
+ "7d21c5e848": "Worktrees masqués ({{value0}})",
+ "2f80cd4b97": "Affichage…",
+ "e64b81d3a9": "Afficher",
+ "d40d436fc2": "Impossible de mettre à jour la visibilité du worktree. Réessayez.",
+ "unsupportedHost": "Cet hôte ne prend pas en charge la visibilité des worktrees par source. Mettez à jour Orca sur l'hôte pour modifier ce paramètre.",
+ "search": "Rechercher des worktrees masqués",
+ "searchPlaceholder": "Rechercher parmi {{value0}} worktrees…",
+ "noMatches": "Aucun worktree correspondant",
+ "allShown": "Tous les worktrees détectés sont affichés",
+ "noneFound": "Aucun worktree non Orca trouvé",
+ "tryDifferentSearch": "Essayez un autre nom ou chemin.",
+ "disableSource": "Désactivez une source pour gérer ses worktrees individuellement.",
+ "appearWhenDetected": "Les nouveaux worktrees apparaîtront ici quand Orca les détectera.",
+ "openGlobalSettings": "Gérer dans les paramètres globaux",
+ "globalSettingsSources": "Ces sources ont un paramètre global que vous pouvez remplacer ici :"
+ },
+ "add": {
+ "repo": {
+ "local": {
+ "start": {
+ "actions": {
+ "d72789705e": "Démarrer depuis un dossier vide",
+ "c709860596": "Créer un nouveau projet",
+ "5f9ffac036": "Cloner un dépôt Git distant",
+ "7edb8ebe24": "Cloner depuis une URL",
+ "a6c20dca96": "Ouvrir un projet depuis une cible SSH",
+ "sshCreateUnavailable": "Pas encore disponible pour les hôtes SSH",
+ "3d162cc76f": "Projet sur hôte SSH",
+ "fb4fc5380e": "Projet local, dépôt Git ou dossier contenant plusieurs dépôts",
+ "2281fdc8c7": "Parcourir le dossier",
+ "sshBrowseTitle": "Ouvrir un projet sur l'hôte SSH",
+ "sshBrowseDescription": "Dépôt Git existant ou dossier sur cet hôte SSH",
+ "runtimeBrowseDescription": "Dépôt Git existant ou dossier sur cette machine"
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "worktree": {
+ "flow": {
+ "b81b4e40ca": "Actualisez l'espace et réessayez si la liste des espaces de travail semble obsolète.",
+ "7243145cd6": "Aucun espace de travail supprimable sélectionné",
+ "ae57cbf6e4": "Échec de la suppression de l'espace de travail",
+ "7488ed8711": "Affichage",
+ "2b20ce87b3": "Forcer la suppression",
+ "4f3876c0f5": "Échec de la suppression forcée",
+ "workspaceListChanged": "Liste des espaces de travail modifiée"
+ },
+ "toast": {
+ "1d0fa5c0a5": "Échec de la suppression de l'espace de travail {{value0}}",
+ "ead7b8ee15": "Il contient des fichiers modifiés. Utilisez Forcer la suppression pour le supprimer quand même.",
+ "905fc8efac": "Git a déjà supprimé cet espace de travail. Utilisez Forcer la suppression pour le nettoyer d'Orca.",
+ "0899ebdb28": "Git a déjà oublié cet espace de travail, mais son dossier est toujours sur le disque. Utilisez Forcer la suppression pour retirer le dossier orphelin.",
+ "locked": "Cet espace de travail est verrouillé par Git. Exécutez git worktree unlock depuis son dépôt, puis relancez la suppression.",
+ "lockedReason": "Cet espace de travail est verrouillé par Git. Git a signalé : {{value0}}. Exécutez git worktree unlock depuis son dépôt, puis relancez la suppression.",
+ "unstoppedPty": "Orca n'a pas pu confirmer que tous les terminaux de cet espace de travail sont fermés, il s'est donc arrêté avant de supprimer des fichiers. Utilisez Forcer la suppression pour le retirer quand même.",
+ "unstoppedPtyLive": "Cet espace de travail a encore des terminaux actifs, Orca s'est donc arrêté avant de supprimer des fichiers. Forcer la suppression les arrêtera et abandonnera tout travail non commité qu'ils contiennent."
+ }
+ }
+ },
+ "remote": {
+ "file": {
+ "browser": {
+ "helpers": {
+ "4dbd72a7d7": "{{value0}} n'est pas un dossier dans {{value1}}",
+ "be266af66c": "{{value0}} correspond à plusieurs dossiers dans {{value1}}"
+ }
+ }
+ }
+ },
+ "repo": {
+ "header": {
+ "create": {
+ "state": {
+ "992cfbc44b": "Créer un nouveau worktree pour {{value0}}",
+ "3a70acd808": "Reconnectez la cible SSH avant de créer des espaces de travail pour {{value0}}",
+ "6d022563a8": "Reconnectez la cible SSH avant de créer des espaces de travail",
+ "62e71f2d5d": "Créer un espace de travail pour {{value0}}"
+ }
+ }
+ }
+ },
+ "sidebar": {
+ "project": {
+ "drop": {
+ "669e12dd97": "Dossiers locaux et dépôts Git",
+ "ffc769ca29": "Déposez un dossier pour ajouter un projet",
+ "740e8d0d46": "Utilisez « Ajouter un projet » pour les chemins d'hôte",
+ "e344666fb8": "Runtime serveur actif",
+ "d0f8943f8b": "Préparation du flux d'ajout de projet",
+ "18d3cf40e9": "Vérification du dossier"
+ }
+ }
+ },
+ "sleep": {
+ "worktree": {
+ "flow": {
+ "c460fecc4a": "Échec de mise en veille de certains espaces de travail",
+ "8bc3fc0671": "Échec de mise en veille de l'espace de travail",
+ "legacy": {
+ "unverified": "L'ancien runtime d'hôte n'a pas pu confirmer l'arrêt des terminaux. L'espace de travail est resté ouvert ; mettez à jour l'hôte et réessayez."
+ },
+ "host": {
+ "unverified": "L'hôte n'a pas pu confirmer l'arrêt des terminaux. L'espace de travail est resté ouvert ; vérifiez la connexion et réessayez."
+ },
+ "retry": "L'espace de travail est resté ouvert. Réessayez ; si le problème persiste, vérifiez la connexion de l'hôte."
+ }
+ }
+ },
+ "useAddRepoCloneFlow": {
+ "4d0013cc93": "Dépôt cloné",
+ "0dc4d1b657": "Saisissez un chemin d'hôte pour la destination du clone."
+ },
+ "useAddRepoLocalFolderFlow": {
+ "7ab10e4974": "Utilisez un chemin d'hôte pour ajouter des projets depuis un hôte distant.",
+ "skippedBatchFolders": "Certains dossiers ont été ignorés",
+ "skippedBatchFoldersDescription": "Ajoutez les dossiers ignorés un par un pour les examiner ou les confirmer."
+ },
+ "useAddRepoNestedImportFlow": {
+ "680cac2c82": "{{value0}} a échoué",
+ "cbfbc7a797": "Certains dépôts n'ont pas pu être importés",
+ "1b33c5f090": "Aucun dépôt importé"
+ },
+ "useSidebarProjectDrop": {
+ "f34a286c0d": "Impossible d'ajouter le dossier déposé.",
+ "451a4638db": "Déposez un dossier pour l'ajouter comme projet.",
+ "5ccb56c7be": "Utilisez « Ajouter un projet » pour saisir un chemin d'hôte.",
+ "849ef13dc0": "Le dépôt de dossiers locaux n'est pas disponible pour les runtimes serveur.",
+ "c0315153d1": "Déposez un seul dossier à la fois."
+ },
+ "workspace": {
+ "status": {
+ "cb387159f6": "En cours",
+ "6c1efa2cf8": "En revue",
+ "6b8285b8dd": "Terminé",
+ "93ac840dcb": "Bloquée",
+ "2c19d1db33": "Lecture",
+ "111db162bf": "En pause",
+ "642da473f2": "Alerte",
+ "6380517b10": "Drapeau",
+ "251c817bdd": "Minuteur",
+ "409528031f": "À examiner",
+ "5f9ca31a84": "En attente",
+ "821d156f54": "Tirets",
+ "226d1e7773": "Progression",
+ "a702bc08d4": "Point",
+ "b4a7101fe1": "Cercle",
+ "1a9383112b": "Progression Conductor",
+ "caebe3c10f": "Revue Conductor",
+ "895f381714": "Conductor terminé",
+ "caabd5ca85": "Zinc",
+ "7adb43ecf0": "Rose",
+ "ddf25b6262": "Émeraude",
+ "7cebab6d4a": "Ambre",
+ "1b81da243a": "Violet",
+ "6437a8c253": "Azur",
+ "fc3b92756c": "Bleu",
+ "52e3c6e2a4": "Neutre"
+ }
+ },
+ "worktree": {
+ "card": {
+ "compact": {
+ "agents": {
+ "a128d7006b": "{{value0}} {{value1}} enfant {{value2}}",
+ "289a1d2ca7": "Développer {{value0}}. {{value1}}",
+ "0c1debfe84": "Réduire {{value0}}"
+ }
+ }
+ },
+ "list": {
+ "groups": {
+ "0ed04075b8": "Tous",
+ "4aeefc5996": "Épinglés",
+ "682ed5d551": "Fermé",
+ "7c2f009786": "En cours",
+ "6798dc7c94": "En revue",
+ "5076efc3d2": "Terminé"
+ }
+ }
+ },
+ "CacheTimer": {
+ "07729cc155": "expiré"
+ },
+ "DeleteWorktreeDialogFooter": {
+ "c0e972d726": "Annuler",
+ "cf95e3b5bb": "Fermer"
+ },
+ "index": {
+ "b826a98b6f": "occupé"
+ },
+ "HostRemoveDialog": {
+ "1a2b3c4d5e": "{{value0}} retiré",
+ "2b3c4d5e6f": "Échec du retrait de l'hôte",
+ "3c4d5e6f7a": "Retirer {{value0}} ?",
+ "4d5e6f7a8b": "Ceci ouvre les paramètres des serveurs d'Orca où vous pouvez retirer ce serveur.",
+ "5e6f7a8b9c": "Ceci retire l'hôte SSH enregistré et ses identifiants de cet ordinateur. Les fichiers distants ne sont pas supprimés.",
+ "6f7a8b9c0d": "Annuler",
+ "7a8b9c0d1e": "Ouvrir les paramètres",
+ "8b9c0d1e2f": "Retirer l'hôte",
+ "workspacesFailed": "Impossible de retirer {{count}} espaces de travail de cet hôte. L'hôte a été conservé pour vous permettre de réessayer.",
+ "oneWorkspace": "1 espace de travail",
+ "manyWorkspaces": "{{count}} espaces de travail",
+ "hostHasWorkspacesDefault": "Retire {{value0}} et ses identifiants de cet ordinateur. Ses {{value1}} restent dans Orca — les fichiers distants ne sont pas touchés.",
+ "alsoDeleteRemote": "Supprimer aussi ces {{value0}} sur {{value1}}",
+ "alsoForgetLocal": "Retirer aussi ces {{value0}} d'Orca",
+ "advanced": "Avancé",
+ "alsoDeleteRemoteHint": "Supprime définitivement les worktrees Git distants et leurs branches. Irréversible.",
+ "alsoForgetLocalHint": "Les retire uniquement d'Orca. Les fichiers, worktrees et branches distants restent intacts."
+ },
+ "HostRenameDialog": {
+ "1a2b3c4d5e": "Renommer l'hôte",
+ "2b3c4d5e6f": "Ce libellé n'est affiché que sur cet ordinateur. Laissez vide pour utiliser le nom par défaut.",
+ "3c4d5e6f7a": "Nom d'affichage",
+ "4d5e6f7a8b": "Rétablir la valeur par défaut",
+ "5e6f7a8b9c": "Annuler",
+ "6f7a8b9c0d": "Enregistrer"
+ },
+ "HostSectionHeaderMenu": {
+ "5b8b4b6a01": "Mise à jour du serveur requise",
+ "9b3c1d2e44": "Mise à jour du client requise",
+ "2c29e2de68": "Échec de la connexion",
+ "bf07aee59e": "Échec de la déconnexion",
+ "7f1a2b3c4d": "{{value0}} est joignable",
+ "4f2c8a9b10": "Actions d'hôte pour {{value0}}",
+ "6b7c8d9e10": "Actions d'hôte",
+ "8d1e2f3a4b": "Renommer…",
+ "63f36455cc": "Reconnecter",
+ "59b553e2aa": "Déconnecter",
+ "2d3e4f5a6b": "Vérifier la connexion",
+ "3c4d5e6f7a": "Gérer l'hôte…",
+ "6e7f8a9b0c": "Retirer l'hôte…"
+ },
+ "LinearAgentSkillSetupPrompt": {
+ "missingCliAndSkill": "La CLI Orca et le skill d'agent Linear sont manquants.",
+ "modalTitle": "Activer l'accès aux tickets Linear",
+ "modalDescription": "Installez le skill Linear depuis un terminal.",
+ "modalPrompt": "Permet aux agents de lire et de modifier le ticket Linear attaché.",
+ "dontShowAgain": "Ne plus afficher",
+ "missingBoth": "La CLI Orca et le skill d'agent Linear sont manquants.",
+ "missingCli": "La CLI Orca est manquante.",
+ "missingSkill": "Le skill d'agent Linear est manquant.",
+ "title": "Configurer le skill d'agent Linear",
+ "remoteCopy": "Ceci installe la configuration côté hôte ; les environnements d'agents distants peuvent nécessiter une configuration séparée.",
+ "hostCopy": "Installez-le pour les passations d'agents hôtes depuis le travail Linear lié.",
+ "dismiss": "Ignorer la configuration du skill d'agent Linear",
+ "setup": "Configurer",
+ "recheck": "Revérifier",
+ "panelTitle": "Skill d'agent Linear",
+ "panelDescription": "Installez le skill d'agent hôte pour les passations de tâches Linear liées.",
+ "terminalTitle": "Installer le skill d'agent Linear",
+ "terminalAria": "Terminal d'installation du skill d'agent Linear",
+ "install": "Installer la CLI et le skill",
+ "successTitle": "L'accès aux tickets Linear est prêt",
+ "successDescription": "Les agents peuvent désormais lire et mettre à jour les tickets Linear liés depuis cet espace de travail.",
+ "successDescriptionWsl": "Les agents WSL peuvent désormais utiliser les tickets Linear liés depuis cet espace de travail.",
+ "successDescriptionRemote": "Les agents de l'hôte peuvent désormais utiliser les tickets Linear liés. Les environnements d'agents distants peuvent encore nécessiter leur propre configuration.",
+ "successStatus": "Accès aux tickets Linear prêt",
+ "done": "Terminé",
+ "wslCopy": "Installez-le pour les passations d'agents WSL depuis le travail Linear lié.",
+ "wslLabel": "WSL par défaut",
+ "toastMissingCliAndSkill": "La CLI Orca et le skill Linear sont manquants",
+ "toastMissingCli": "La CLI Orca est manquante",
+ "toastMissingSkill": "Le skill Linear est manquant",
+ "toastInstallCliAndSkillDescription": "Installez la CLI Orca et le skill Linear pour permettre à vos agents de lire et de modifier les tâches Linear.",
+ "toastInstallCliDescription": "Installez la CLI Orca pour permettre à vos agents de lire et de modifier les tâches Linear.",
+ "toastInstallSkillDescription": "Installez le skill Linear pour permettre à vos agents de lire et de modifier les tâches Linear via la CLI Orca.",
+ "toastRemoteDescription": "{{value0}} Les environnements d'agents distants peuvent nécessiter leur propre configuration.",
+ "toastWslDescription": "{{value0}} Cette configuration s'exécute dans le runtime d'agent WSL sélectionné."
+ },
+ "FolderWorkspaceComposerDialog": {
+ "connectFailed": "Échec de la connexion au projet.",
+ "noRepos": "Ajoutez un projet Git sous ce dossier pour attacher des tâches GitHub ou GitLab.",
+ "title": "Créer un espace de travail dossier",
+ "create": "Créer un espace de travail",
+ "sourceProject": "Source des tâches",
+ "chooseSourceProject": "Choisir la source des tâches",
+ "createFailed": "Échec de la création de l'espace de travail dossier."
+ },
+ "ProjectOrderManualDefaultNotice": {
+ "a1f4c2d8e0": "L'ordre manuel des projets est désormais le réglage par défaut",
+ "822ff300ad": "Ignorer",
+ "b7e3a91c4f": "Faites glisser les en-têtes de projets pour les réordonner, ou passez à",
+ "e8c1f4a2b9": "dans les options de l'espace de travail."
+ },
+ "WorkspaceKanbanDrawer": {
+ "1975a4e480": "Échec de la synchronisation du statut des tâches",
+ "e02b0d92ff": "Synchronisation du statut des tâches ignorée",
+ "c1d2e3f4a5": "L'issue Linear {{value0}} n'a pas pu être lue.",
+ "d2e3f4a5b6": "Aucun état de workflow Linear ne correspond à {{value0}}.",
+ "e3f4a5b6c7": "Plusieurs états de workflow Linear correspondent à {{value0}}.",
+ "f4a5b6c7d8": "Impossible de mettre à jour l'issue Linear {{value0}}.",
+ "a5b6c7d8e9": "Impossible de synchroniser l'issue Linear {{value0}}.",
+ "b6c7d8e9f0": "La synchronisation du statut des tâches n'a pas pu aboutir.",
+ "c7d8e9f0a1": "{{value0}} mis à jour",
+ "d8e9f0a1b2": "{{value0}} ignoré",
+ "e9f0a1b2c3": "{{value0}} a échoué"
+ },
+ "WorktreeParentPickerPopover": {
+ "failedSetParent": "Échec de la définition du worktree parent",
+ "current": "Actuel",
+ "searchPlaceholder": "Rechercher des worktrees...",
+ "empty": "Aucun worktree éligible correspondant.",
+ "setParentFor": "Définir le parent pour"
+ },
+ "WorktreeCardStatusSlot": {
+ "branchIdentity": "Branche"
+ },
+ "NewExternalWorktreesInboxLine": {
+ "9f2d4c8b17": "Masquer définitivement les worktrees externes pour {{value0}}",
+ "5e1b8d3f62": "Masquer définitivement les worktrees externes",
+ "2a6f31d8c7": "worktree masqué",
+ "5b90e4a2f6": "worktrees masqués",
+ "7f18c5b0d3": "Examiner {{value0}} worktree masqué dans {{value1}}",
+ "4e2b7a9c05": "Examiner {{value0}} worktrees masqués dans {{value1}}",
+ "c3e8a1f4b2": "Ne plus afficher",
+ "6c07f3a91e": "{{value0}} sur {{value1}}"
+ },
+ "NoticeHostGlyph": {
+ "hostDisconnected": "{{hostName}} déconnecté",
+ "sshHostProject": "Projet sur l'hôte SSH {{hostName}}",
+ "localHostProject": "Projet sur cette machine",
+ "runtimeHostProject": "Projet sur {{hostName}}"
+ },
+ "newExternalWorktreesInboxActions": {
+ "a11c2f6d89": "Impossible de garder les worktrees externes masqués. Réessayez.",
+ "b7e4d1a062": "Impossible d'importer les worktrees externes. Réessayez.",
+ "c94f0b3a15": "Impossible de masquer définitivement les worktrees externes. Réessayez."
+ },
+ "SuppressExternalWorktreeInboxDialog": {
+ "a4c2d8f1b0": "Masquer les worktrees externes ?",
+ "6e91b3c4d2": "Les worktrees externes ne seront plus affichés dans la barre latérale ni dans cette liste pour {{value0}}, y compris ceux créés ultérieurement.",
+ "1f8a5d9e73": "Vous pourrez réactiver cela plus tard depuis les paramètres du projet.",
+ "8c0b2e7a41": "Ouvrir les paramètres des worktrees non Orca",
+ "5d1c9f0a82": "Annuler",
+ "3b7e4a1c96": "Masquer les worktrees externes"
+ },
+ "useAddRepoHostSelection": {
+ "connectionFailed": "Échec de la connexion SSH."
+ },
+ "AddRemoteHostDialog": {
+ "sshHostRequired": "Un hôte ou un alias de config SSH est requis.",
+ "sshPortInvalid": "Le port doit être compris entre 1 et 65535.",
+ "sshRelayGraceInvalid": "Le délai d'expiration du terminal doit être compris entre 60 et {{value0}} secondes.",
+ "sshSaved": "Hôte SSH ajouté.",
+ "sshSaveFailed": "Échec de l'ajout de l'hôte SSH.",
+ "serverFieldsRequired": "Le nom du serveur et le code d'appairage sont requis.",
+ "serverSaved": "Serveur distant ajouté.",
+ "serverSaveFailed": "Échec de l'ajout du serveur distant.",
+ "serverTitle": "Ajouter un serveur distant",
+ "sshTitle": "Ajouter un hôte SSH",
+ "serverDescription": "Se jumeler avec Orca exécuté sur un autre ordinateur.",
+ "sshDescription": "Ajoutez une machine permanente sur laquelle vous pouvez vous connecter en SSH.",
+ "cancel": "Annuler",
+ "saving": "Enregistrement...",
+ "save": "Enregistrer",
+ "label": "Label",
+ "sshLabelPlaceholder": "Machine de dev",
+ "sshHost": "Hôte ou alias",
+ "sshHostPlaceholder": "deploy@server:22",
+ "username": "Nom d'utilisateur",
+ "usernamePlaceholder": "deploy",
+ "port": "Port",
+ "identityFile": "Fichier d'identité",
+ "identityFilePlaceholder": "~/.ssh/id_ed25519 (facultatif)",
+ "sshPersistenceDefault": "Les terminaux distants de cet hôte restent actifs jusqu'à ce que vous les fermiez ou réinitialisiez le relais.",
+ "serverName": "Nom du serveur",
+ "serverNamePlaceholder": "Machine de dev",
+ "pairingCode": "Code d'appairage",
+ "pairingCodePlaceholder": "orca://pair?code=...",
+ "pairingHelpPrefix": "Exécution",
+ "pairingCommand": "orca serve --pairing-address ",
+ "pairingHelpSuffix": "sur le serveur et collez l'URL d'appairage affichée.",
+ "sshImportAlreadySynced": "~/.ssh/config est déjà à jour.",
+ "sshImportSynced": "Ajout de {{value0}} hôte{{value1}} à Orca.",
+ "sshImportFailed": "Échec de l'import de la config SSH.",
+ "sshAlreadyExists": "Cet hôte SSH est déjà dans Orca.",
+ "advanced": "Avancé",
+ "linkDestination": "Destination du lien",
+ "sshTunnel": "J'utilise un tunnel SSH",
+ "sshTunnelHelp": "Sinon, ce lien pointe vers cet appareil et ne peut pas identifier l'autre ordinateur.",
+ "loopbackBlocked": "Activez l'option de tunnel SSH ou créez un nouveau lien utilisant l'adresse Tailscale ou LAN de l'autre hôte.",
+ "sshConfigPickerLoadFailed": "Échec de la lecture de ~/.ssh/config.",
+ "sshConfigPickerResolveFailed": "Échec de la résolution de cet hôte de la config SSH.",
+ "sshConfigPickerRestartRequired": "Redémarrez Orca pour finaliser l'application de la mise à jour du sélecteur de config SSH.",
+ "sshConfigPickerFilled": "Rempli depuis {{value0}}. Vérifiez puis enregistrez.",
+ "sshConfigPickerTitle": "Choisir depuis ~/.ssh/config",
+ "sshConfigPickerDescription": "Choisissez un hôte pour remplir le formulaire. Rien n'est enregistré tant que vous n'avez pas cliqué sur Enregistrer.",
+ "sshConfigPickerFilter": "Filtrer les hôtes…",
+ "sshConfigPickerHostsLabel": "Hôtes de la config SSH",
+ "sshConfigPickerLoading": "Lecture de ~/.ssh/config…",
+ "sshConfigPickerEmpty": "Aucun hôte dans ~/.ssh/config",
+ "sshConfigPickerNoMatch": "Aucun hôte correspondant",
+ "sshConfigPickerEmptyHint": "Ajoutez-y une entrée Host, ou revenez en arrière pour saisir les détails manuellement.",
+ "sshConfigPickerNoMatchHint": "Essayez un autre filtre, ou revenez en arrière pour saisir manuellement.",
+ "sshConfigPickerMoreResults": "Affichage des {{value0}} premières correspondances. Affinez le filtre pour en trouver davantage.",
+ "sshConfigPickerInOrca": "Dans Orca",
+ "sshConfigPickerPreviouslyRemoved": "Retiré d'Orca",
+ "sshConfigPickerBulkHint": "La synchronisation groupée reste dans Paramètres → SSH",
+ "sshConfigPickerBack": "Retour",
+ "fillFromSshConfig": "Remplir depuis ~/.ssh/config…",
+ "sshConfigPickerAddingAll": "Ajout des hôtes…",
+ "sshConfigPickerAddAll": "Ajouter tous les {{value0}} à Orca",
+ "sshConfigPickerNoNewHosts": "Aucun nouvel hôte à ajouter",
+ "sshConfigPickerAddAllEmpty": "Tout ajouter à Orca",
+ "identityFileFromConfigHint": "Volontairement laissé vide : Orca utilise toutes les clés résolues par ~/.ssh/config pour {{value0}}. Saisissez un chemin pour n'utiliser que cette clé.",
+ "sshConfigPickerRetry": "Réessayer",
+ "sshConfigPickerResolving": "Lecture…"
+ },
+ "ForgetSshWorkspaceDialog": {
+ "reconnectFailed": "Échec de la reconnexion",
+ "forgetBody": "Retire uniquement cet espace de travail d'Orca. Les fichiers, le worktree Git et les branches sur {{host}} restent intacts.",
+ "title": "Supprimer « {{name}} » ?",
+ "disconnectedBody": "L'hôte SSH de cet espace de travail n'est pas connecté. Reconnectez-le pour le supprimer aussi à distance, ou retirez-le seulement d'Orca.",
+ "ghostBody": "{{host}} n'est plus un hôte SSH enregistré ; cet espace de travail n'est donc plus connecté à un hôte actif. Il ne peut être que retiré d'Orca — les fichiers et branches distants restent intacts.",
+ "cancel": "Annuler",
+ "forget": "Retirer d'Orca",
+ "reconnectAndDelete": "Reconnecter et supprimer"
+ },
+ "MarkdownImageLightbox": {
+ "image": "Image",
+ "expand": "Développer l'image",
+ "close": "Fermer"
+ },
+ "WorktreeDeveloperMenu": {
+ "developer": "Développeur",
+ "parkTerminal": "Parquer le terminal"
+ },
+ "SidebarFeedbackImageAttachments": {
+ "screenshotsHint": "Joignez jusqu'à {count} captures d'écran",
+ "attachImages": "Joindre",
+ "removeImage": "Supprimer {{fileName}}"
+ },
+ "WorkspaceKanbanSearchField": {
+ "bdb753c78d": "Aucun espace de travail correspondant",
+ "4d96c209d6": "{{value0}} espaces de travail sur {{value1}} correspondent",
+ "c0cd6bdf6c": "Rechercher des espaces de travail",
+ "3b7ea51793": "Effacer la recherche",
+ "7f1c2e94a5": "Texte de recherche trop long — le tableau n'est pas filtré",
+ "9a4d0f6b21": "Trop long"
+ },
+ "WorktreeIssueLinkField": {
+ "25852bfc59": "Linear",
+ "5b440069e6": "GitHub",
+ "161b2d053a": "Ouvrir l'issue liée",
+ "d4785f9954": "Les liens d'issue sont définis à la création d'un espace de travail dossier et ne peuvent pas encore être modifiés ici.",
+ "964d9bc00a": "Ni une clé d'issue Linear ni une URL d'issue linear.app.",
+ "0a7a2c6efd": "Ni un numéro d'issue GitHub ni une URL d'issue.",
+ "72486800ff": "Enregistrer dissociera {{first}} et {{second}} — un espace de travail ne suit qu'une seule issue.",
+ "2c245ac134": "Enregistrer dissociera {{link}} — un espace de travail ne suit qu'une seule issue.",
+ "d8c8a30d1f": "Impossible d'ouvrir cette issue. Vérifiez l'identifiant et votre connexion Linear.",
+ "269198eeda": "Impossible d'ouvrir cette issue. Vérifiez le numéro et votre connexion GitHub.",
+ "f047887705": "Collez une URL GitHub ou Linear, ou saisissez un numéro. Laissez vide pour supprimer le lien.",
+ "ad78f9bee2": "Issue",
+ "662ae142f8": "N° d'issue, ou URL GitHub ou Linear",
+ "929c98d05a": "Fournisseur d'issues"
+ },
+ "worktreeIssueDisplacement": {
+ "3f61c0a8d2": "Linear {{value}}",
+ "9c4b7e1f60": "GitHub #{{value}}"
+ },
+ "WorktreeCardSshHostControl": {
+ "connectFailed": "Échec de la connexion SSH",
+ "removedTooltip": "Hôte SSH retiré — reconnexion impossible",
+ "removedName": "L'hôte SSH {{value0}} a été retiré",
+ "connectedTooltip": "Projet sur hôte SSH",
+ "connectedName": "Projet sur l'hôte SSH {{value0}}",
+ "connectingName": "Connexion à l'hôte SSH {{value0}}",
+ "authFailedName": "Reconnexion à l'hôte SSH {{value0}} — échec d'authentification",
+ "retryName": "Relancer la connexion SSH vers {{value0}}",
+ "connectName": "Se connecter à l'hôte SSH {{value0}}",
+ "authFailedTooltip": "{{value0}} · échec d'authentification",
+ "failedTooltip": "{{value0}} · échec de connexion"
+ },
+ "preserved": {
+ "branch": {
+ "batch": {
+ "toast": {
+ "a3cdd9d9e6": "Git a conservé {{count}} branches locales car elles peuvent contenir des commits non fusionnés. Les branches conservées ne gardent pas les dossiers des espaces de travail ; leurs commits restent dans le dépôt. Orca peut continuer à libérer de l'espace disque en arrière-plan.",
+ "a3cdd9d9e6_one": "Git a conservé {{count}} branche locale car elle peut contenir des commits non fusionnés. Les branches conservées ne gardent pas les dossiers des espaces de travail ; leurs commits restent dans le dépôt. Orca peut continuer à libérer de l'espace disque en arrière-plan.",
+ "a3cdd9d9e6_other": "Git a conservé {{count}} branches locales car elles peuvent contenir des commits non fusionnés. Les branches conservées ne gardent pas les dossiers des espaces de travail ; leurs commits restent dans le dépôt. Orca peut continuer à libérer de l'espace disque en arrière-plan.",
+ "6310412304": "Examiner {{count}} branches",
+ "6310412304_one": "Examiner {{count}} branche",
+ "6310412304_other": "Examiner {{count}} branches",
+ "cea24c2b7d": "{{count}} espaces de travail supprimés",
+ "cea24c2b7d_one": "{{count}} espace de travail supprimé",
+ "cea24c2b7d_other": "{{count}} espaces de travail supprimés",
+ "0e0379f24a": "{{count}} branches conservées",
+ "0e0379f24a_one": "{{count}} branche conservée",
+ "0e0379f24a_other": "{{count}} branches conservées",
+ "4cf75caab7": "{{value0}}, {{value1}}",
+ "e61d78054f": "Suppression des branches locales : {{value0}}",
+ "1e1a5f6763": "Branches locales supprimées : {{value0}}",
+ "43d9395605": "{{value0}} supprimées, {{value1}} non supprimées",
+ "d42f1f14e0": "Réessayer {{count}} branches",
+ "d42f1f14e0_one": "Réessayer {{count}} branche",
+ "d42f1f14e0_other": "Réessayer {{count}} branches"
+ }
+ }
+ }
+ },
+ "PreservedBranchBatchReviewDialog": {
+ "c4bf8e7eaf": "Examiner les branches conservées",
+ "f21976c9a8": "Sélectionnez les branches locales à supprimer de force. Les branches non sélectionnées restent dans leurs dépôts.",
+ "38c947f7c5": "Tout sélectionner",
+ "9602129d38": "{{value0}} sur {{value1}} sélectionnées",
+ "ee39e872d5": "Possiblement non fusionnée",
+ "676db406fd": "Head indisponible",
+ "285e1e4882": "Annuler",
+ "a0f9863597": "Forcer la suppression de {{count}} branches",
+ "a0f9863597_one": "Forcer la suppression de {{count}} branche",
+ "a0f9863597_other": "Forcer la suppression de {{count}} branches"
+ },
+ "WorktreeListScrollToTopButton": {
+ "jumpToTop": "Remonter en haut"
+ },
+ "WorktreeVisibilitySourceList": {
+ "claude": "Claude Code",
+ "gsd": "GSD",
+ "other": "Autres emplacements",
+ "custom": "Emplacement personnalisé",
+ "otherPath": "Hors des sources listées",
+ "invalidPath": "Saisissez un chemin absolu pour cet hôte.",
+ "duplicatePath": "Cet emplacement est déjà listé.",
+ "limit": "Retirez un emplacement personnalisé avant d'en ajouter un autre.",
+ "sources": "Sources",
+ "sourcesDescription": "Les sources affichées incluent les worktrees actuels et futurs dans la barre latérale.",
+ "found": "{{value0}} trouvés",
+ "remove": "Supprimer {{value0}}",
+ "removeLocation": "Retirer l'emplacement personnalisé",
+ "addLocation": "Ajouter un emplacement",
+ "worktreeRoot": "Racine des worktrees",
+ "add": "Ajouter",
+ "rootHelp": "Orca reconnaîtra les worktrees sous ce dossier.",
+ "visibility": "Visibilité pour {{value0}}",
+ "show": "Afficher",
+ "hide": "Masquer",
+ "overridingGlobal": "Remplace le paramètre global : {{value0}}",
+ "projectOnly": "Ajouté dans ce projet uniquement.",
+ "useGlobalFor": "Utiliser la valeur globale pour {{value0}}",
+ "useGlobal": "Utiliser la valeur globale"
+ }
+ },
+ "shared": {
+ "useDaemonActions": {
+ "01af244097": "Annuler",
+ "28c8e53176": "Force l'arrêt de tous les volets de terminal en cours d'exécution dans tous les espaces de travail. Tout travail non enregistré de ces sessions est perdu. Le daemon continue de tourner et de nouveaux terminaux peuvent être ouverts immédiatement. Irréversible.",
+ "1bbea41a77": "Forcer l'arrêt de toutes les sessions de terminal ?",
+ "01d6b7c64e": "Tue tous les volets de terminal en cours d'exécution et redémarre le processus daemon. Les volets affichent \"Process exited\" et peuvent être rouverts immédiatement. Les sessions au protocole hérité d'une version précédente de l'application sont préservées. Irréversible.",
+ "922548bc66": "Redémarrer le daemon de terminal ?",
+ "2b4efdc162": "Impossible de tuer les sessions.",
+ "d18f3005c2": "Fermeture refusée pour {{value0}} session{{value1}}.",
+ "baad8cd651": "Aucune session en cours.",
+ "fe2ab66d45": "{{value0}} sessions sur {{value1}} tuées. Fermeture refusée pour {{value2}}.",
+ "d762b41f41": "Échec du redémarrage.",
+ "b5954e12d3": "Échec du redémarrage — consultez les logs.",
+ "0e9da1b98e": "Daemon redémarré.",
+ "d6372cc797": "Arrêt forcé de {{value0}} session{{value1}}.",
+ "87412c2a68": "Arrêt forcé de {{value0}} session.",
+ "a2f040ac1c": "{{value0}} sessions terminées.",
+ "63520148e2": "La session {{value0}} a refusé de se terminer.",
+ "cc0a26cb14": "{{value0}} sessions ont refusé de se terminer.",
+ "71a8d342b0": "Onglets de terminal absents : {{value0}}/{{value1}}. Tentatives de fermeture échouées : {{value2}}. Demandes d'arrêt PTY exactes acceptées : {{value3}} ; échecs : {{value4}}.",
+ "2e57c1a940": "Le résultat de l'arrêt du daemon est non vérifié car sa requête de gestion a échoué.",
+ "993af6052c": "Le gestionnaire de daemons signale : arrêtés : {{value0}}/{{value1}} ; encore présents avant le nettoyage précis : {{value2}}.",
+ "1f0d8ac762": "Le nettoyage des terminaux s'est terminé avec des erreurs.",
+ "80b6ea14cf": "Le nettoyage des terminaux s'est terminé avec des avertissements.",
+ "47cd2a50e9": "Aucune session ni onglet de terminal signalé.",
+ "c34fb1098d": "Onglets de terminal fermés et arrêt demandé.",
+ "d9657ac204": "Arrêt de la session de terminal demandé.",
+ "e8f25bd903": "Impossible de terminer le nettoyage des terminaux.",
+ "a702d4196e": "Ceci ferme tous les onglets de terminal de tous les espaces de travail et demande l'arrêt des sessions de terminal en cours. Tout travail non enregistré dans un terminal est perdu. Le daemon continue de tourner et de nouveaux terminaux peuvent être ouverts immédiatement. Cette action est irréversible."
+ }
+ },
+ "setup": {
+ "guide": {
+ "SetupGuideModal": {
+ "3598a3ca0c": "Terminez les workflows essentiels qui rendent Orca utile au travail parallèle avec des agents.",
+ "48a9e5ef2d": "Premiers pas",
+ "28cf59fcb4": "Cela masquera la checklist de la barre latérale",
+ "f3b5ffb2a6": "Masquer la checklist de la barre latérale"
+ },
+ "SetupGuideProgressRing": {
+ "dac3a4724a": "{{value0}} étapes de configuration sur {{value1}} terminées"
+ }
+ }
+ },
+ "settings": {
+ "keep": {
+ "local": {
+ "main": {
+ "up": {
+ "to": {
+ "date": {
+ "setting": {
+ "f8bda25f29": "Garder la branche main locale à jour"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "AccountsPane": {
+ "c2d2751587": "Supprimer le compte",
+ "dbb9626ed1": "Annuler",
+ "854ebbcc45": "Orca supprimera l'authentification Claude gérée pour ce compte enregistré. S'il est actuellement actif, Orca revient à la connexion Claude par défaut du système.",
+ "63843e37e2": "Supprimer le compte Claude ?",
+ "380a7736cc": "La suppression de ce compte efface définitivement son home Codex géré, y compris tout l'historique de sessions Codex et les connexions MCP stockés dedans. Cette action est irréversible. Si le compte est actuellement actif, Orca revient à la connexion Codex par défaut du système.",
+ "0d47394635": "Supprimer le compte Codex ?",
+ "ae3b21eb6c": "opencode.ai/workspace/wrk_…/go",
+ "51c9104e13": "Trouvez-le dans l'URL après connexion à opencode.ai (par ex.",
+ "b398b834c9": "Effacer",
+ "316ca4e610": "Oublier le cookie",
+ "a122332371": "wrk_… (laisser vide pour une recherche automatique)",
+ "dbdb0b0bd8": "Remplacement de l'ID de espace de travail",
+ "d70a5287a4": "Remplacement facultatif de l'ID de espace de travail si la recherche automatique échoue.",
+ "02cb127710": "ID de espace de travail OpenCode Go",
+ "7ce0e1907c": "). Trouvez-le dans les DevTools de votre navigateur → Réseau → une requête opencode.ai → en-tête Cookie. L'authentification OpenCode Go est web et partagée entre les terminaux Windows et WSL.",
+ "8951c5309f": "auth=Fe26.2**…",
+ "338820326a": ") ou l'en-tête cookie complet (par ex.",
+ "922b51e02d": "Fe26.2**…",
+ "0023cc336e": "Collez soit la valeur brute du jeton (par ex.",
+ "a7e38affcd": "jeton Fe26.2**… ou en-tête auth=Fe26.2**…",
+ "67e3c33670": "Cookie de session OpenCode Go",
+ "b2b1aa936d": "Collez votre cookie de session opencode.ai pour récupérer les rate limits.",
+ "36223200ac": "Cookie de session OpenCode Go",
+ "ea631977b5": "Configurer les paramètres du fournisseur OpenCode Go.",
+ "4ac10b4d08": "OpenCode Go",
+ "c2aee76420": "Extrait les identifiants OAuth de votre installation locale de Gemini CLI pour vous authentifier auprès de Google pour {{value0}}. Utilise les identifiants émis pour l'app Gemini CLI, pas pour Orca. Peut cesser de fonctionner si Google met à jour la CLI. À utiliser à vos risques et périls.",
+ "96f3649526": "Utiliser les identifiants Gemini CLI (expérimental)",
+ "d676c41fc6": "Extrait les identifiants OAuth de votre installation locale de Gemini CLI pour vous authentifier auprès de Google. Utilise les identifiants émis pour l'app Gemini CLI, pas pour Orca. Peut cesser de fonctionner si Google met à jour la CLI. À utiliser à vos risques et périls.",
+ "0c7f915b01": "Utiliser les identifiants Gemini CLI",
+ "973741a871": "Configurer les paramètres du fournisseur Gemini.",
+ "0c64dc2a64": "Gemini",
+ "db209ee572": "Supprimer",
+ "8a0f870153": "Réauthentifier",
+ "3d245ef7d9": "Codex signale que cette connexion est périmée",
+ "589eba1eee": "Réauthentification requise",
+ "e74831fb6b": "Actif",
+ "b4c9450319": "Aucun compte Codex géré pour {{value0}}. Orca utilisera la connexion Codex par défaut du système de cet environnement jusqu'à ce que vous en ajoutiez un ici.",
+ "93c47b333a": "Connexion requise",
+ "f2a265f8c7": "Valeur par défaut du système",
+ "b0e948a4f9": "Ajouter un compte",
+ "c0a52abfc5": "Affiche les comptes pour {{value0}}. Les nouveaux comptes y sont ajoutés.",
+ "94d351af4a": "Comptes",
+ "d0d53b7eb0": "Gérer le compte Codex qu'Orca utilise pour récupérer les rate limits en direct.",
+ "3180536c7a": "Comptes Codex",
+ "340d6f7a85": "Chaque compte garde son propre contexte de connexion local dans Orca. L'authentification des comptes reste sur cet appareil.",
+ "cedfab35ab": "Facultatif. Orca peut utiliser votre connexion Codex habituelle ; ajoutez des comptes seulement si vous voulez changer rapidement de compte dans Orca.",
+ "ef91cfa06b": "Codex",
+ "3fe7862418": "Aucun compte Claude géré pour {{value0}}. Orca utilisera la connexion Claude par défaut du système de cet environnement jusqu'à ce que vous en ajoutiez un ici.",
+ "3455cf43fa": " Claude.",
+ "fcc4093fc1": "Utilisez votre connexion Codex {{value0}} actuelle.",
+ "79e484c3b2": "Sélecteur de comptes facultatif pour les fichiers d'authentification Claude partagés.",
+ "8bbfd74556": "Comptes Claude",
+ "72b36ea174": "Facultatif. Orca peut utiliser votre connexion Claude habituelle ; ajoutez des comptes seulement si vous voulez changer rapidement sans déplacer les sessions de chat.",
+ "26ef4b55be": "Claude",
+ "2743cdc0af": "Échec de la mise à jour du compte Claude.",
+ "b15ce90870": "{{value0}} -> {{value1}}. Redémarrez les terminaux Claude actifs avant de poursuivre les anciennes sessions.",
+ "f921d32606": "Compte Claude mis à jour.",
+ "5bf8764953": "Échec de la mise à jour du compte Codex.",
+ "9baf45d071": "Cet appareil",
+ "2358ac71d2": "WSL par défaut",
+ "ad47a33f72": "Chargement de WSL",
+ "8619f9afa9": "WSL",
+ "46cf7e7495": "Emplacement des comptes",
+ "0b4591ff93": "Choisissez l'environnement local à inspecter et l'emplacement où les nouveaux comptes Claude et Codex gérés sont ajoutés.",
+ "0c67a2a1aa": "WSL n'est pas disponible sur cette machine.",
+ "2cd197025c": "Choisissez si les comptes des fournisseurs sont inspectés et ajoutés dans {{value0}} ou WSL.",
+ "f54b4fbd71": "Emplacement des comptes",
+ "9107406589": "Impossible de charger les comptes Claude.",
+ "b8c2905c2b": "Impossible de charger les comptes Codex.",
+ "fd62f37c24": "Codex signale que cette connexion {{value0}} est périmée.",
+ "b10cb4f696": "ajout",
+ "e4a28e8894": "Codex signale que la connexion {{value0}} doit être refaite. Reconnectez-vous avant de démarrer de nouvelles sessions Codex.",
+ "75ca9b718e": "Codex signale que le compte actif doit être reconnecté. Réauthentifiez-le avant de démarrer de nouvelles sessions Codex.",
+ "b11078a9c2": "wsl",
+ "350b2a1aa7": "Utilisez votre connexion ",
+ "e05d0ff737": "Utilisez votre connexion Claude {{value0}} actuelle.",
+ "2f24f244a4": "Le cookie MiniMax est requis.",
+ "8e6f0cb1d8": "Le cookie MiniMax n'a pas été enregistré.",
+ "8d61637a77": "Cookie MiniMax enregistré.",
+ "b43e761fe5": "Échec de la mise à jour du cookie MiniMax.",
+ "5d63bbfbec": "MiniMax",
+ "15e831350e": "Configurer le suivi de consommation MiniMax depuis platform.minimax.io.",
+ "21d6eb141e": "Cookie de session MiniMax",
+ "33bba5ad83": "Collez votre cookie de session MiniMax pour récupérer les rate limits localement.",
+ "73ea15f24b": "Enregistré",
+ "23afe8f226": "Non enregistré",
+ "566d9a99ab": "_token=…; minimax_group_id_v2=…",
+ "f38b9cc4bd": "Remplacer",
+ "590a3130f9": "Enregistrer",
+ "79418c782a": "Ouvrez platform.minimax.io/console/usage dans votre navigateur, connectez-vous, puis copiez l'en-tête de requête Cookie depuis les DevTools (Réseau → une requête remains → Cookie).",
+ "9dd50d3f75": "Avancé",
+ "174fb408f9": "Ne touchez pas à ces valeurs par défaut, sauf si l'actualisation de consommation MiniMax vise le mauvais espace de travail ou modèle.",
+ "bf160bb6c0": "Remplacement du group ID",
+ "b1e2743313": "Facultatif. Laissez vide pour utiliser minimax_group_id_v2 du cookie.",
+ "0747d6391a": "Utiliser le group ID du cookie",
+ "4ff2af7524": "Noms de modèles pour la consommation",
+ "5cf4b0f85f": "Noms de modèles facultatifs, séparés par des virgules. Laissez general sauf si MiniMax renvoie une erreur propre à un modèle.",
+ "3c92b0d31c": "general",
+ "0d8e77bc40": "Ouvrir la console",
+ "0b8c1c7e02": "Stocké localement",
+ "1fd1b1b6b4": "Cookie non défini",
+ "5e08b0fe57": "Stocké localement et envoyé uniquement à platform.minimax.io pour les actualisations de consommation.",
+ "43d7a45b97": "Comment copier",
+ "b8a4f21c3e": "Collez l'en-tête Cookie depuis les DevTools",
+ "53f7b8c7a2": "Dernière actualisation : {{value0}}",
+ "31d24a4e87": "Le cookie expire quand vous vous déconnectez dans le navigateur.",
+ "3a30aaf526": "à l'instant",
+ "f5d8d2a6a1": "Ouvrez platform.minimax.io/console/usage dans votre navigateur et connectez-vous.",
+ "24560fe830": "Ouvrez les DevTools.",
+ "4cab0fa42d": "Allez dans l'onglet Network et activez Preserve log.",
+ "bee4e63e1c": "Rechargez la page.",
+ "87f814af6f": "Filtrez sur remains et sélectionnez la requête coding_plan/remains.",
+ "435df0ee51": "Sous Request Headers, copiez la valeur Cookie.",
+ "7492fb3bba": "Collez-la ici et cliquez sur Enregistrer.",
+ "9fec52de4b": "Comment copier le cookie",
+ "4e32e030b2": "Stocké localement. Orca ne l'envoie qu'à platform.minimax.io pour les actualisations de consommation.",
+ "remoteServerFallback": "le serveur distant",
+ "loadAccountsFailed": "Impossible de charger les comptes du fournisseur.",
+ "remoteScopeAccounts": "Affiche les comptes gérés par {{value0}}. Ajoutez ou réauthentifiez les comptes sur ce serveur.",
+ "accountScopePrefix": "Portée des comptes",
+ "accountScopeRemoteServerUnnamed": "Serveur distant",
+ "remoteScopeLocalAccountsKept": "Les comptes gérés sur ce bureau restent inchangés. Repassez le runtime par défaut sur Local desktop pour les voir.",
+ "remoteScopeAuthContext": "Chaque compte garde son propre contexte de connexion sur {{value0}}.",
+ "remoteEmptyClaudeAccounts": "Aucun compte Claude géré sur {{value0}}. Il utilise sa connexion Claude par défaut du système ; ajoutez des comptes sur ce serveur.",
+ "remoteEmptyCodexAccounts": "Aucun compte Codex géré sur {{value0}}. Il utilise sa connexion Codex par défaut du système ; ajoutez des comptes sur ce serveur.",
+ "codexSystemDefaultCustomProvider": "Fournisseur personnalisé — aucune consommation suivie.",
+ "codexSystemDefaultNeedsSignIn": "Aucune connexion Codex trouvée pour {{value0}}.",
+ "codexConfigSyncMissingSource": "Codex utilise toujours les derniers paramètres synchronisés car {{value0}} est absent. Restaurez ce fichier pour reprendre la synchronisation.",
+ "codexConfigSyncBlankSource": "Codex utilise toujours les derniers paramètres synchronisés car {{value0}} est vide. C'est attendu tant qu'un dossier synchronisé finit de se télécharger.",
+ "codexConfigSyncManagedHomeUnavailable": "Orca n'a pas réussi à lire les fichiers Codex de ce compte à l'instant ; les paramètres peuvent donc ne plus se synchroniser. Cela se résout généralement tout seul — un antivirus ou un outil de sauvegarde les verrouille brièvement.",
+ "codexConfigSyncUnreadableSource": "Codex utilise toujours les derniers paramètres synchronisés car {{value0}} n'a pas pu être lu. Vérifiez les permissions de ce fichier."
+ },
+ "AdvancedPane": {
+ "40b29e0bf3": "Redémarrer",
+ "87a2cb2ac8": "Orca applique ce mode réseau au démarrage.",
+ "89958d7edf": "Redémarrage requis",
+ "b3ad629640": "À utiliser uniquement quand un VPN d'entreprise ou un proxy fait échouer les téléchargements de mises à jour avec des erreurs de protocole HTTP/2. Affecte tout le réseau d'Electron après redémarrage.",
+ "6627e75c92": "Expliquer la compatibilité HTTP/1.1",
+ "e9506d3377": "Compatibilité HTTP/1.1",
+ "8b7a8df299": "Contournements de bas niveau pour le diagnostic du support.",
+ "8d8d8ac599": "Compatibilité",
+ "network": "Réseau",
+ "networkDescription": "Routage réseau au niveau de l'app pour proxys et environnements d'entreprise."
+ },
+ "AgentLocationSetting": {
+ "92f4238f1a": "WSL par défaut",
+ "fc806485ae": "Chargement de WSL",
+ "43663b5e69": "WSL",
+ "9bccf48906": "Emplacement des agents",
+ "d00949e59b": "Affiche les agents installés depuis {{value0}}. L'actualisation revérifie PATH dans cet environnement.",
+ "c7c516946f": "WSL n'est pas disponible sur cette machine.",
+ "f97b986b7f": "wsl"
+ },
+ "AgentSkillSetupPanel": {
+ "0b810ec59f": "Appuyez sur Entrée pour lancer la commande.",
+ "ed197f59a2": "Copier la commande",
+ "817d3f9f18": "Copier la commande",
+ "5289300939": "Non installé",
+ "9fcebceb2a": "Installés",
+ "68a468752e": "Vérification...",
+ "c689392435": "Revérifier",
+ "a31e2aa302": "Échec de la copie de la commande.",
+ "378ad26865": "Commande copiée.",
+ "copiedCommand": "Commande copiée.",
+ "failedToCopyCommand": "Échec de la copie de la commande.",
+ "copyCommandAria": "Copier la commande",
+ "runCommandDescription": "Appuyez sur Entrée pour lancer la commande.",
+ "5f818f12ab": "Préparation...",
+ "4c05b9d7cb": "Préparation du terminal de configuration.",
+ "installLabel": "Installer",
+ "updateLabel": "Mettre à jour",
+ "setupCommandFailed": "La commande de configuration s'est terminée avec le code {{value0}}. Cette erreur disparaîtra après une nouvelle tentative réussie.",
+ "setupFailed": "Échec de la configuration",
+ "retrySetup": "Réessayer"
+ },
+ "AgentsPane": {
+ "d83834f5e6": "Détection des agents installés…",
+ "024bd95089": "agents",
+ "e8da2af684": "Disponibles à l'installation",
+ "ed3e110e61": "détecté",
+ "03e1a5081a": "sur {{value0}}",
+ "25a41a9aad": "Redétecter les agents installés sur le serveur actif",
+ "remoteDetectionFailed": "Impossible de détecter les agents installés. Vérifiez la connexion à l'hôte puis réessayez.",
+ "retryDetection": "Réessayer",
+ "02e0143be5": "Installés",
+ "110b74b022": "Aucun agent (terminal vierge)",
+ "92033495ff": "Auto",
+ "9b175d0f5e": "Agent présélectionné à l'ouverture d'un nouveau espace de travail.",
+ "385212c7a1": "Agent par défaut",
+ "f9f127d664": "Remplacez le chemin ou le nom du binaire, et modifiez les arguments de lancement ou l'environnement par défaut de cet agent.",
+ "f95b5c79b8": "Installer",
+ "fe4d630c94": "Docs",
+ "8dc0192e48": "Désactivé",
+ "df123171d1": "Non installé",
+ "c8794e622e": "Détecté",
+ "5200dac9da": "Réinitialiser",
+ "2e45ca29b6": "Commande",
+ "d4d2a45d63": "Activé",
+ "1c9a9679ec": "Disponibilité de {{value0}}",
+ "0d9e293a02": "Actualiser",
+ "c9b33eb5c0": "Actualisation…",
+ "13647f9f80": "Relire le PATH de votre shell et redétecter les agents installés",
+ "dc4a2ffdc0": "Déplier le remplacement de commande",
+ "cea7d97be1": "Replier le remplacement de commande",
+ "db9e9e5887": "Personnaliser la commande",
+ "959b67385b": "Définir par défaut",
+ "24e032fa34": "Par défaut",
+ "5f986a9b92": "Définir par défaut",
+ "d7625cf8b2": "Agent par défaut",
+ "cfb3f35775": "Arguments",
+ "6f99bf5dd0": "Aucun argument par défaut",
+ "8fbe1f37c1": "Environnement",
+ "2d133152fa": "Aucun environnement par défaut",
+ "agentPermissions": "Permissions des agents",
+ "agentPermissionsInfo": "Infos permissions des agents",
+ "agentPermissionsTooltip": "Ne s'applique pas aux agents dont vous avez remplacé les arguments de lancement.",
+ "agentPermissionsDescription": "Choisissez si Orca lance les agents avec moins de demandes de permission ou avec des vérifications manuelles.",
+ "agentPermissionsYolo": "Yolo",
+ "agentPermissionsManual": "Manuel",
+ "3f1bdf3cb4": "Le texte d'environnement est trop volumineux pour être analysé sans risque.",
+ "codexSessionSource": "Home Codex d'où importer",
+ "codexSessionSourceInfo": "À propos de l'import de l'historique Codex",
+ "codexSessionSourceTooltip": "Orca exécute Codex dans un home isolé. Pointez ceci vers votre home Codex existant pour en importer l'historique de sessions. Si vide, ~/.codex est utilisé.",
+ "storedDefaultUndetected": "Enregistré par défaut, mais non détecté actuellement",
+ "noAgentsDetected": "Aucun agent détecté. Si un agent est installé, la détection a peut-être expiré."
+ },
+ "AppIconSelector": {
+ "d5a112dc9b": "Icône suivante",
+ "415fa76f64": "Icône d'app sélectionnée",
+ "5f5142a62a": "Icône précédente"
+ },
+ "AppearancePane": {
+ "3057983501": "Non assigné",
+ "872af9556e": "Zone de notification",
+ "2edf606c46": "Réduire dans la zone de notification à la fermeture",
+ "b707773a0d": "Quand activé, fermer la fenêtre laisse Orca tourner dans la zone de notification au lieu de quitter.",
+ "0cd9b8228f": "Choisissez l'icône d'app affichée dans le Dock et le sélecteur de fenêtres.",
+ "ca1590d42f": "Icône de l'app",
+ "61d842eca0": "Afficher le raccourci Orca Mobile dans la barre latérale. Il reste accessible depuis la Toolbox.",
+ "9da1020447": "Afficher le bouton Orca Mobile",
+ "5db6ba961f": "Afficher le bouton Orca Mobile en haut de la barre latérale gauche.",
+ "fa882a3e6b": "Afficher le bouton Automatisations en haut de la barre latérale gauche.",
+ "511f270ebb": "Afficher le bouton Automatisations",
+ "661942ab7f": "Afficher le bouton Tâches en haut de la barre latérale gauche.",
+ "cf81907069": "Afficher le bouton Tâches",
+ "dc29f3cc0d": "Barre latérale",
+ "ea943d0db0": "Choisissez les indicateurs affichés en bas de la fenêtre. Un clic droit sur la barre d'état donne accès aux mêmes options.",
+ "3e4175e5c6": "Barre d'état",
+ "2df8f79aa5": "Afficher Orca dans la barre de titre.",
+ "9868f39007": "Nom de l'app dans la barre de titre",
+ "4de76f6902": "Contrôler ce qui apparaît dans la barre de titre de l'application.",
+ "6a272ca553": "Barre de titre",
+ "e9f2ca5582": "Désactivez pour masquer les fichiers correspondant à .gitignore dans l'explorateur de fichiers.",
+ "0fafabcf35": "Afficher les fichiers ignorés par Git",
+ "75f07ab60c": "Afficher les fichiers correspondant à .gitignore dans l'explorateur de fichiers.",
+ "d496901cd0": "Explorateur de fichiers",
+ "42554f615f": "Choisissez la police utilisée par l'interface d'Orca.",
+ "102d6b5f9b": "Police de l'IDE",
+ "ef89200c1f": "en dehors d'un panneau de terminal.",
+ "f687711a9b": "Mettez toute l'interface de l'application à l'échelle. Utilisez",
+ "5e6d7aba8d": "Zoom de l'interface",
+ "622e1c3465": "Met toute l'interface de l'application à l'échelle.",
+ "fd89b5487c": "Clair",
+ "7d26ccabe8": "Sombre",
+ "fb0e0b4453": "Système",
+ "932ff1fbff": "Thème",
+ "0f28e7b30c": "Choisissez l'apparence d'Orca dans la fenêtre de l'app.",
+ "leftSidebarAppearance": {
+ "title": "Apparence de la barre latérale gauche",
+ "rowDescription": "Accordez la barre latérale gauche à votre terminal, gardez-la par défaut ou appliquez une teinte.",
+ "default": "Par défaut",
+ "matchTerminal": "Comme le terminal",
+ "tinted": "Teintée",
+ "tintColor": "Teinte de la barre latérale",
+ "tintColorDescription": "Couleur mélangée à la surface de la barre latérale gauche.",
+ "tintOpacity": "Intensité de la teinte",
+ "tintOpacityDescription": "Contrôle l'intensité avec laquelle la teinte est mélangée à la barre latérale."
+ },
+ "workspaceCardLayoutGuidance": "Géré depuis la barre latérale des espaces de travail.",
+ "interfaceDefaultFont": "Police par défaut",
+ "terminalDefaultFont": "Police par défaut",
+ "interfaceTitle": "Interface",
+ "terminalTitle": "Terminal",
+ "windowSidebarTitle": "Fenêtre & barre latérale",
+ "windowSidebarSummary": "Barre latérale, barre d'état et explorateur de fichiers",
+ "statusBarCount": "{{value0}} indicateurs visibles.",
+ "gitIgnoredGlossary": "Fichiers correspondant à .gitignore.",
+ "statusBarDescription": "Choisissez les indicateurs affichés dans la barre d'état.",
+ "showPinnedWorktreesInGroups": {
+ "title": "Afficher aussi les worktrees épinglés dans leurs listes d'origine",
+ "description": "Les worktrees épinglés restent dans Épinglés et apparaissent aussi dans Tous, Projet, Statut et PR."
+ }
+ },
+ "AutoRenameBranchFromWorkSetting": {
+ "1626524572": "Nautilus",
+ "0de9fda203": "Abandonner",
+ "c71770c455": "{basePrompt}",
+ "f19a56498d": "; votre paramètre de préfixe de branche s'applique toujours.",
+ "800edb1e54": "fix-login-flow",
+ "5d569f5199": ". Orca ne génère que le dernier segment, comme",
+ "570817d126": "et",
+ "56580dcf60": ". Vous pouvez aussi référencer",
+ "9c9b54e4ea": "l'invite intégrée de nom de branche d'Orca",
+ "69bf4830c2": "pour inclure",
+ "9241b59bf5": "Utiliser",
+ "a869d0edd8": "Modèle de commande de nom de branche",
+ "e784ea62dc": "Avancé",
+ "d9b65054ef": ") en un nom court résumant la tâche. Seules les branches qu'Orca a lui-même nommées sont renommées, et jamais après avoir été poussées.",
+ "12ea4a408d": "Quand un agent commence à travailler dans un nouveau espace de travail, Orca renomme sa branche générée automatiquement (par ex.",
+ "ef787db0e3": "Renommage auto de la branche",
+ "6a051586d2": "Renommer la branche générée automatiquement d'après le travail dès qu'un agent démarre.",
+ "ec3e0c388e": "Enregistrer",
+ "cfd82406dd": "Enregistrement...",
+ "40e7be7850": "Enregistré",
+ "7c7e34a66d": "Modifications non enregistrées",
+ "a4fa380b67": "{assistantMessage}",
+ "2ee2779c05": "{firstPrompt}"
+ },
+ "AutoRenameBranchPromptEditor": {
+ "63121132c0": "Abandonner",
+ "4416b25d29": "Privilégiez les noms du domaine de la tâche, évitez les IDs de ticket et gardez des noms faciles à relire.",
+ "39278f4411": "; votre paramètre de préfixe de branche s'applique toujours.",
+ "ebb942a2ec": "fix-login-flow",
+ "af2d9a2cc6": ". Orca ne génère que le dernier segment, comme",
+ "182d419b97": "l'invite intégrée de nom de branche d'Orca",
+ "2f5dc661fe": "Ajouté à",
+ "7d6176f506": "Prompt",
+ "5968112152": "Enregistrer",
+ "54ac229ad4": "Enregistrement...",
+ "af0831a590": "Enregistré",
+ "0691753cf2": "Modifications non enregistrées"
+ },
+ "BaseRefPicker": {
+ "1b8e54151f": "Aucune branche correspondante.",
+ "d166ff883d": "Actuel",
+ "a4a9372eb2": "Recherche des branches...",
+ "7db7fb87e5": "Rechercher des branches par nom...",
+ "773a5687a3": "Utiliser la principale",
+ "ade9a5bb03": ") pour restreindre les résultats.",
+ "b468f46726": "upstream/main",
+ "80f7c82303": ") ou un ref complet (par ex.",
+ "915ad97875": "upstream",
+ "a5c16712c1": "Plusieurs remotes détectés. Tapez un nom de remote (par ex.",
+ "9a14ec7400": "Choisissez une branche de base ci-dessous",
+ "086ce7f369": "Suit la branche principale ({{value0}})",
+ "2f3cda96f5": "Épinglé pour ce dépôt",
+ "ee110e1830": "Aucun ref de base par défaut"
+ },
+ "BrowserDefaultZoomSetting": {
+ "2622126877": "Niveau de zoom appliqué aux nouveaux onglets du navigateur.",
+ "bbeec087d3": "Appliqué aux nouveaux onglets du navigateur.",
+ "265597101f": "Zoom par défaut"
+ },
+ "BrowserHomePageSetting": {
+ "d4ddcd0056": "Enregistrer",
+ "37a30c5bfd": "https://google.com",
+ "c6cbd1c105": "Page d'accueil enregistrée.",
+ "6a37540f4b": "URL ouverte à la création d'un nouvel onglet du navigateur. Laissez vide pour un onglet vierge.",
+ "70224e37b1": "Page d'accueil par défaut"
+ },
+ "BrowserPane": {
+ "81ff774667": "Annuler",
+ "7d4c0a2aa4": "Nom du profil",
+ "612f7f6861": "Échec de la création du profil.",
+ "8f22b7580d": "Profil « {{value0}} » créé.",
+ "8481ee0331": "Nouveau profil de navigateur",
+ "c0f85056d9": "Profils de navigateur sur ce serveur Orca.",
+ "86b7c83fee": "Cet ordinateur",
+ "6480776a03": "Profils de navigateur pour l'hôte sélectionné.",
+ "5e19a692f7": "Hôte",
+ "6f2584b39e": "Ajouter un profil",
+ "e4aaf8051b": "menu de la barre d'outils.",
+ "cd47bc9622": "Sélectionnez un profil par défaut pour les nouveaux onglets du navigateur. Importez des cookies et changez de profil par onglet via le ",
+ "2d66a6efb5": "Session & cookies",
+ "aa1074bfe9": "Gérez les profils de navigateur et importez les cookies depuis Chrome, Edge, Comet ou d'autres navigateurs.",
+ "113cd2dc9b": "Session & cookies",
+ "d3eb69c0aa": "Routage des liens",
+ "3e46903ad4": "Utilisé quand on saisit du texte autre qu'une URL dans la barre d'adresse.",
+ "0d9c987f21": "Moteur de recherche par défaut",
+ "7b225c78f5": "Moteur de recherche utilisé quand on saisit du texte autre qu'une URL dans la barre d'adresse.",
+ "64898ecdab": "Créer",
+ "7b649a578a": "Création…",
+ "4399c77caa": "Par défaut",
+ "4af9a17947": "kagi"
+ },
+ "BrowserProfileRow": {
+ "8e636cae25": "Profil « {{value0}} » supprimé.",
+ "2d4bea7f35": "Cookies par défaut effacés.",
+ "ebb78dfd6f": "Depuis un fichier…",
+ "7df818977e": "Depuis",
+ "cdec84552f": "Importer des cookies",
+ "796d846483": "Aucun cookie importé",
+ "c29648fe5b": "Actif",
+ "d420c43729": "{{value0}} cookies importés depuis {{value1}}{{value2}} vers {{value3}}.",
+ "b4c167764d": "{{value0}} cookies importés depuis un fichier vers {{value1}}.",
+ "a3f8c2d1e0b4": "{{value0}} cookies importés depuis {{value1}} ({{value2}}) vers {{value3}}.",
+ "b4e9d3f2a1c5": "{{value0}} cookies importés depuis {{value1}} vers {{value2}}.",
+ "c5a273a809": "Depuis {{value0}}",
+ "b5c0479e21": "User agent non modifié"
+ },
+ "BrowserUseComputerUseNotice": {
+ "15b5e680ba": "Ouvrir Computer Use",
+ "79209b37b9": "Si l'import de cookies ne convient pas, Computer Use peut contrôler des apps locales et, le cas échéant, réutiliser des sessions de navigateur déjà connectées. Installez la skill Computer Use ; macOS exige aussi des autorisations de confidentialité.",
+ "333984cf90": "Utiliser une session de navigateur existante"
+ },
+ "BrowserUseEnableSwitch": {
+ "aea3f45349": "Activer Agent Browser Use"
+ },
+ "BrowserUseExamples": {
+ "1199258ace": "Copier",
+ "1188e56af4": "Copier l'exemple de prompt",
+ "b84807f228": "\"",
+ "59722f31b4": "\"",
+ "c5325e91f6": "Collez l'un de ces exemples dans Claude Code, Codex ou un autre agent, dans un projet où la skill est installée.",
+ "2a180694f7": "Essayez — exemples de prompts",
+ "5ec620ccc4": "Échec de la copie.",
+ "a602d43069": "{{value0}} copié."
+ },
+ "BrowserUsePane": {
+ "be6df68384": "Depuis un fichier…",
+ "e44c5d681e": "Depuis",
+ "67d9a53f47": "Gérer les profils pour des connexions distinctes",
+ "112f70adc4": "Dernier import depuis {{value0}}",
+ "72d4815523": "Importez vos connexions existantes dans Orca pour que les agents accèdent aux pages authentifiées. Importe dans le profil par défaut.",
+ "2eb906706c": "Importer les cookies du navigateur",
+ "af8c83ed61": "Importez les cookies depuis Chrome, Edge ou d'autres navigateurs pour que les agents réutilisent vos connexions.",
+ "68ea76eb71": "Installez la skill Browser Use pour que les agents pilotent le navigateur d'Orca.",
+ "2d6ead9ab2": "Installer la skill Browser Use",
+ "e9f3f3b488": "Installé dans",
+ "9fca1f7f5d": "Enregistre la commande CLI d'Orca pour que les agents orchestrent le navigateur depuis leur shell.",
+ "c6065d205d": "Activer Orca CLI",
+ "c79eff0213": "Enregistrer Orca CLI pour que les agents pilotent le navigateur.",
+ "702488a5f7": "Permettez aux agents de code de piloter ce navigateur avec vos connexions. Terminez les trois étapes ci-dessous.",
+ "b8a1f2d84d": "Agent Browser Use",
+ "96b91c6349": "Permettez aux agents de code de piloter ce navigateur avec vos connexions.",
+ "2ea4617e3a": "{{value0}} cookies importés depuis {{value1}}{{value2}}.",
+ "721aee31b4": "Orca CLI enregistré dans PATH.",
+ "180a9abf3a": "Échec du chargement de l'état de la CLI.",
+ "2ccfc9cff8": "Importer",
+ "0462565413": "Réimporter",
+ "de9b2f32f3": "Activer",
+ "ad8cb0ee22": "Corriger PATH",
+ "0289434ed6": "Activé",
+ "8b3054dac7": "Enregistrement...",
+ "8f2675c2f3": "{{value0}} cookies importés depuis un fichier.",
+ "5301857d88": "Depuis {{value0}}"
+ },
+ "BrowserUseSkillStep": {
+ "0871b6998d": "Permet aux agents de naviguer et de vérifier des pages dans le navigateur d'Orca.",
+ "459e24eebc": "Skill Browser Use"
+ },
+ "CliSection": {
+ "8671e406f0": "Annuler",
+ "a4aafe46e3": "Chemin cible :",
+ "e8012c03a1": "Permet aux agents d'utiliser les commandes espace de travail, terminal et progression d'Orca.",
+ "6053cf736c": "Skill CLI",
+ "36a6f919ba": "Donne aux agents des workflows espace de travail, terminal et progression propres à Orca.",
+ "04873eea3e": "Skills des agents",
+ "7f2747f7dd": "n'est pas actuellement visible dans PATH pour ce shell.",
+ "b0c310ab46": "Cible du lanceur existante :",
+ "15eaad0d31": "Chemin de la commande :",
+ "5dae812f50": "Actualiser",
+ "52e640f3a0": "Actualiser l'état de la CLI",
+ "38edbb5721": "Commande shell",
+ "6930feda9e": "Utilisez Orca depuis votre terminal pour ouvrir l'app, gérer les worktrees et interagir avec les terminaux Orca.",
+ "c5c0f2641d": "Orca CLI",
+ "d77352f2df": "Échec de la suppression de `{{value0}}` de PATH.",
+ "af5540930c": "`{{value0}}` supprimé de PATH.",
+ "a2b13efa94": "Échec de l'enregistrement de `{{value0}}` dans PATH.",
+ "9cbcd31338": "`{{value0}}` enregistré dans PATH.",
+ "7baec27029": "Échec du chargement de l'état de la CLI.",
+ "d00df2e397": "Enregistrer",
+ "9a5f8a4568": "Supprimer",
+ "b0fca411a0": "Enregistrement…",
+ "4c7e3e4c5f": "installer",
+ "068552b191": "Suppression…",
+ "8d96213669": "supprimer",
+ "aa6536977e": "Orca va enregistrer {{value0}} pour que la commande fonctionne depuis votre terminal.",
+ "a030816e3e": "Ceci supprime le lien symbolique de la commande shell. Orca reste installé.",
+ "fa87db3d6e": "Enregistrer `{{value0}}` dans PATH ?",
+ "14444243ba": "Supprimer `{{value0}}` de PATH ?",
+ "5d432fe44d": "installé",
+ "8a9b784c60": "périmé",
+ "d363e5929b": "Vérification de l'enregistrement de la CLI…",
+ "cliSkillTerminalTitle": "Configuration de la skill CLI",
+ "cliSkillTerminalAria": "Terminal d'installation de la skill CLI"
+ },
+ "CliSkillRuntimeSetup": {
+ "04325573f8": "WSL",
+ "a58ba464ad": "Emplacement de la skill",
+ "0ed08febc5": "Échec de l'enregistrement de la commande shell WSL.",
+ "3728a94fb6": "La commande shell WSL demande votre attention",
+ "775a4cfbb8": "L'enregistrement de la commande shell WSL est indisponible",
+ "c47127f222": "WSL par défaut",
+ "0c9f3cf9da": "Choisissez où Orca vérifie et installe les skills globales des agents.",
+ "f00d6aa9b5": "WSL n'est pas disponible sur cette machine.",
+ "7c776ff9d8": "wsl",
+ "fc0fcf72fd": "Enregistrez la commande shell WSL avant la configuration des skills.",
+ "windowsPathUnknown": "Le PATH de la commande shell WSL n'a pas pu être vérifié",
+ "refreshCliRegistration": "Actualisez le statut d'enregistrement du CLI et réessayez."
+ },
+ "CommitMessageAiPane": {
+ "841ed9884a": "Utilisé par les dépôts qui n'ont pas personnalisé Source Control AI.",
+ "ad66ff886d": "Valeurs par défaut de Source Control AI",
+ "347094560b": "Utilisé par les dépôts qui héritent des valeurs par défaut globales de hosted review.",
+ "2dafc7646e": "Valeurs par défaut de création de hosted review",
+ "e9d46a544d": "Valeurs par défaut utilisées à l'ouverture du composer de hosted review.",
+ "b125eabffa": "Ouvrir la hosted review créée dans votre navigateur après l'envoi.",
+ "7662715213": "Ouvrir la hosted review après création",
+ "b27b0809f3": "Lance la génération des détails de hosted review une fois, à l'ouverture du composer.",
+ "d5f0de6309": "Générer les détails à l'ouverture de Create PR",
+ "6278c0ce43": "Préférer les templates de pull request du dépôt quand aucune description n'est définie.",
+ "d8b6764d79": "Utiliser le template de review quand disponible",
+ "e001734396": "Créer les hosted reviews en brouillon, sauf changement dans le composer.",
+ "6ba48f07a4": "Brouillon par défaut",
+ "15b60d54b2": "par ex. ollama run llama3.1 {prompt}",
+ "3f1b26cc91": "pour passer l'entrée de la commande en argument ; sinon Orca la redirige sur stdin.",
+ "4f722a5f53": "Utilisé par les recettes de message de commit, de pull request et de nom de branche qui sélectionnent Commande personnalisée. Utilisez",
+ "47e45cbd5a": "Commande personnalisée",
+ "1ef29f8c29": "Ligne de commande qu'Orca exécute quand une recette texte utilise Commande personnalisée.",
+ "2339a89104": "Ajoute des boutons IA qui exécutent l'agent sélectionné avec le modèle de commande de cette action.",
+ "d5b45a3628": "Afficher les actions Source Control AI",
+ "7bcad2b200": "Ajoute des recettes d'actions pour les actions commit, pull request, nom de branche et correction de Source Control.",
+ "d54c64163d": "activé",
+ "4ec89c319e": "agent",
+ "34d0348e34": "générer",
+ "8cd2be0948": "message",
+ "ca433708cb": "commit",
+ "0b7eafe55f": "ai",
+ "2c5436c018": "ouvrir",
+ "6c84ba6de3": "template",
+ "ebed4d2a29": "brouillon",
+ "02bab6542c": "pr",
+ "fdee745b87": "merge request",
+ "b388463881": "pull request",
+ "19e10a12bb": "hosted review",
+ "b8b6fd55b4": "{prompt}",
+ "fc1a525fa5": "placeholder",
+ "a69e1fe91a": "prompt",
+ "1df7d71313": "binaire",
+ "407d28bde6": "cli",
+ "54038660e0": "command",
+ "25350d670f": "custom"
+ },
+ "ComputerUsePane": {
+ "1735461723": "Permet aux agents d'inspecter et de piloter des applications de bureau locales.",
+ "93255aaf18": "Compétence Computer Use",
+ "45f8e22c2e": "Ouvert",
+ "d95d1cfab8": "Actualiser",
+ "0c29da5805": "Prêt",
+ "3383ea1aab": "Impossible de réinitialiser les autorisations Computer Use",
+ "f189f448a3": "Réinitialiser l'accès Computer Use",
+ "5c45349665": "Impossible d'ouvrir les autorisations Computer Use",
+ "7801ac08ec": "Les autorisations Computer Use ne sont requises que sur macOS",
+ "740766c291": "La configuration de Computer Use est déjà terminée",
+ "697005758f": "Panneau Confidentialité et sécurité de macOS ouvert",
+ "2168fa5ab0": "Impossible de charger les autorisations Computer Use",
+ "0c9a33f468": "Capture les fenêtres des applications pour que les agents puissent inspecter leur état visuel.",
+ "07bbe4c4cb": "Captures d'écran",
+ "4d03dec2d0": "Lit les arborescences d'interface des applications et effectue les actions demandées.",
+ "6b5a2cd3a5": "Accessibilité",
+ "6b17602073": "Réinitialiser l'accès",
+ "506f2acf7a": "Réinitialisation de l'accès...",
+ "4b65070096": "darwin",
+ "statusGranted": "Accordée",
+ "statusUnsupported": "macOS uniquement",
+ "statusNotEnabled": "Non activé"
+ },
+ "DeveloperPermissionsPane": {
+ "4c17304beb": "Actualiser",
+ "6326a4c5cc": "Utilisez ces contrôles quand une CLI, une application locale ou un outil d'automatisation a besoin d'une autorisation de confidentialité macOS. Orca ne le demande pas au démarrage.",
+ "6f011b9bf6": "Les outils de terminal héritent du périmètre de confidentialité macOS d'Orca.",
+ "bfa3402305": "Impossible de demander l'autorisation",
+ "66e94d6cf3": "Demande d'autorisation envoyée",
+ "fa809e8ada": "Panneau Confidentialité et sécurité de macOS ouvert",
+ "48d87edcd2": "Autorisation accordée",
+ "a552887288": "Impossible de charger les autorisations développeur",
+ "4cfaa7e98a": "Outils pour appareils Bluetooth et expérimentations matérielles locales.",
+ "b2210b1b4f": "Bluetooth",
+ "dfbc12c8c8": "Débogage matériel et outils d'appareils communiquant avec des périphériques USB.",
+ "bf51e4a542": "Périphériques USB",
+ "f903bf20b5": "Permet aux terminaux et outils de développement de se connecter aux services de votre réseau local. macOS ne signale pas à Orca l'état actuel de cette autorisation.",
+ "e7bb06007c": "Réseau local",
+ "4a73f5217a": "Événements Apple pour les scripts qui contrôlent d'autres applications locales.",
+ "e119f0d66b": "Automatisation",
+ "7ca17b62c8": "macOS désigne Orca quand les agents qu'il exécute lisent les données d'autres applications, car Orca est le processus responsable des commandes du terminal. Accordez cette autorisation à Orca pour réduire ces invites. Puis quittez et rouvrez Orca.",
+ "c566bca278": "Accès complet au disque",
+ "9f35980756": "Outils d'injection de frappes clavier, de contrôle de fenêtres et d'automatisation d'UI.",
+ "5b2f22ca2d": "Accessibilité",
+ "0639db5496": "Outils de capture d'écran, d'automatisation visuelle et d'inspection d'UI.",
+ "f24f31a884": "Enregistrement de l'écran",
+ "550cfa3750": "Capture webcam et applications de test locales utilisant la caméra.",
+ "e5b5f3d6b9": "Caméra",
+ "cc8151d9fa": "Saisie vocale, transcription, enregistrement audio, CLI sox, ffmpeg et Whisper.",
+ "16381e040a": "Microphone",
+ "dac08ec03e": "Traitement...",
+ "actionRequest": "Demander",
+ "actionOpenSettings": "Ouvrir les paramètres",
+ "actionTriggerPrompt": "Déclencher l'invite",
+ "statusGranted": "Accordée",
+ "statusDenied": "Refusée",
+ "statusNotRequested": "Non demandée",
+ "statusRestricted": "Restreint",
+ "statusUnsupported": "macOS uniquement",
+ "statusEntitled": "Habilitée",
+ "statusCheckManually": "Vérifier manuellement",
+ "actionRequestAccess": "Demander l'accès",
+ "localNetworkPromptCheck": "Vérifiez la présence d'une invite macOS",
+ "localNetworkPromptGuidance": "Si une invite apparaît, choisissez Autoriser. Si aucune invite n'apparaît, ouvrez Réglages Système et activez Orca sous Confidentialité et sécurité → Réseau local.",
+ "localNetworkOpenSettings": "Ouvrir Réglages Système",
+ "openSettingsFailed": "Impossible d'ouvrir Réglages Système",
+ "localNetworkOpenSystemSettings": "Ouvrir Réglages Système",
+ "statusManagedByMacOS": "Géré par macOS",
+ "connectionTestInvalidTarget": "Saisissez un nom d'hôte ou une adresse IP privée du réseau local, et un port compris entre 1 et 65535.",
+ "connectionTestTimeout": "Délai de connexion dépassé. Vérifiez la cible, le service et les réglages Réseau local de macOS.",
+ "connectionTestRefused": "L'hôte a répondu, mais le port a refusé la connexion.",
+ "connectionTestUnreachable": "Impossible d'atteindre la cible.",
+ "connectionTestUnresolved": "Le nom d'hôte n'a pas pu être résolu.",
+ "connectionTestUnsupported": "Le test de connexion est disponible dans l'application de bureau macOS.",
+ "connectionTestFailed": "Le test de connexion n'a pas pu être effectué.",
+ "connectionTestTitle": "Tester la connexion",
+ "connectionTestDescription": "Saisissez un service situé sur un autre appareil de votre réseau local. Orca teste le même chemin réseau que celui utilisé par les outils de terminal.",
+ "connectionTestLastVerified": "Dernière vérification",
+ "connectionTestNotYetVerified": "Aucun test réussi enregistré.",
+ "connectionTestHost": "Hôte",
+ "connectionTestPort": "Port",
+ "connectionTestRunning": "Test en cours...",
+ "connectionTestAction": "Tester la connexion"
+ },
+ "ExperimentalPane": {
+ "9762364929": "Utilise le clonage APFS sur macOS quand c'est possible, sinon crée des liens symboliques vers les dossiers ou fichiers configurés dans les worktrees créés.",
+ "24416f42cd": "Chemins partagés sur les worktrees",
+ "fb82ea1d7a": "Matérialise automatiquement les fichiers ou dossiers configurés dans les worktrees nouvellement créés.",
+ "a20d5ea365": "Maintient un surlignage au niveau du volet après une sonnerie de terminal ou la fin d'un agent, jusqu'à ce que vous interagissiez avec ce volet. Expérimental pendant que nous ajustons le signal.",
+ "ec897e8d89": "Attention du terminal",
+ "88b7613afb": "Surlignage persistant du volet pour les sonneries de terminal et les fins d'agents.",
+ "0277901cf7": "Ajoute une entrée Agents à la barre latérale gauche avec un flux groupé par worktree pour les agents terminés, les questions bloquantes, l'état non lu et les événements de création de worktree. Expérimental — le modèle d'événements et l'interface peuvent changer.",
+ "a05bcdaf57": "Vue Agents",
+ "f63ea281e3": "Flux groupé dans la barre latérale gauche pour les achèvements d'agents et les états bloquants.",
+ "agentHibernation": {
+ "copy": "Arrête les terminaux d'agents en arrière-plan inactifs après la fenêtre d'inactivité configurée et reprend les sessions prises en charge quand vous les rouvrez. La mise en veille des agents conserve les options de lancement pour les agents démarrés par Orca. Les agents démarrés manuellement peuvent reprendre avec vos valeurs par défaut Orca actuelles. Expérimental pendant que nous ajustons le modèle de sécurité.",
+ "description": "Arrête les terminaux d'agents en arrière-plan inactifs après la fenêtre d'inactivité configurée et reprend les sessions prises en charge quand vous les rouvrez.",
+ "idleMinutesDescription": "Nombre de minutes d'inactivité qu'un agent d'arrière-plan terminé doit attendre avant qu'Orca puisse le mettre en veille.",
+ "idleMinutesLabel": "Mise en veille après",
+ "idleMinutesSuffix": "minutes",
+ "title": "Mise en veille des agents",
+ "toggleLabel": "Activer la mise en veille des agents"
+ },
+ "newWorktreeCardStyle": {
+ "copy": "Prévisualise la nouvelle mise en page des cartes de worktree, l'emplacement des métadonnées, les options du menu d'affichage des cartes et la présentation des statuts.",
+ "description": "Prévisualise la nouvelle mise en page des cartes de worktree, l'emplacement des métadonnées, les options du menu d'affichage des cartes et la présentation des statuts.",
+ "title": "Nouveau style de carte",
+ "toggleLabel": "Activer le nouveau style de carte"
+ },
+ "ca2219fe5e": "Affiche un petit compagnon animé épinglé dans le coin inférieur droit. Choisissez un personnage (Claudino, OpenCode, Gremlin) ou importez votre propre PNG, APNG, GIF, WebP, JPG ou SVG depuis le menu compagnon de la barre d'état. Masquez-le à tout moment depuis le même menu sans désactiver ce réglage.",
+ "dd6f0a1d45": "Compagnon",
+ "0e89a574ae": "Compagnon animé flottant dans le coin inférieur droit.",
+ "nativeChat": {
+ "title": "Chat UI",
+ "description": "Prévisualisez la surface de chat de bureau pour les sessions de terminal d'agents prises en charge.",
+ "copy": "Ajoute une vue Chat UI accessible depuis les volets de terminal d'agents pris en charge. Expérimental pendant que nous peaufinons la fidélité de la transcription, le streaming et la parité avec le terminal.",
+ "toggleLabel": "Activer Chat UI",
+ "defaultTitle": "Vue par défaut",
+ "defaultCopy": "Choisissez comment s'ouvrent les nouveaux onglets de terminal d'agents pris en charge.",
+ "defaultViewLabel": "Vue Chat UI par défaut",
+ "defaultViewTerminal": "Chat dans le terminal",
+ "defaultViewNative": "Chat UI"
+ },
+ "agentDashboard": {
+ "title": "Tableau de bord des agents",
+ "description": "Tableau Kanban pour surveiller les agents de tous les worktrees, dans la fenêtre ou en fenêtre indépendante.",
+ "copy": "Ajoute une entrée Tableau de bord des agents à la barre latérale gauche. Surveillez les agents qui ont besoin de vous, qui travaillent ou qui ont terminé, avec en option les agents inactifs.",
+ "toggleLabel": "Activer le tableau de bord des agents",
+ "modeLabel": "Mode d'affichage",
+ "modeCopy": "Affiche le tableau de bord dans la fenêtre, à côté de la barre latérale, ou dans une fenêtre indépendante.",
+ "modeAriaLabel": "Mode d'ouverture du tableau de bord des agents",
+ "modeInWindow": "Dans la fenêtre",
+ "modePopout": "Fenêtre indépendante"
+ }
+ },
+ "FloatingWorkspacePane": {
+ "aeaf76fda9": "Barre d'état",
+ "9fb225f2d7": "Bouton flottant",
+ "3c900e26e5": "Le raccourci clavier fonctionne quel que soit l'emplacement du bouton d'activation.",
+ "5e5a8da236": "Emplacement du bouton d'activation",
+ "505001823e": "Choisir le dossier de l'espace de travail flottant",
+ "81afb79785": "C'est ici que démarrent les nouveaux onglets de terminal flottants. Les notes Markdown sont enregistrées dans l'espace de travail flottant propre à l'application Orca.",
+ "12aa09f10c": "Dossier du terminal",
+ "41eb95f7f0": "Affiche le bouton et le panneau de l'espace de travail flottant.",
+ "5136813663": "Activer l'espace de travail flottant",
+ "37df688d6f": "Activez l'espace de travail flottant et choisissez où démarrent les nouveaux onglets.",
+ "1f67f39384": "Espace de travail flottant"
+ },
+ "AgentCacheTimerSection": {
+ "05de84a104": "1 heure",
+ "54395ecd7c": "5 minutes",
+ "8b9e202e0a": "Alignez cette durée sur le TTL de cache de votre fournisseur. Par défaut : 5 minutes.",
+ "a2a8962138": "Durée du minuteur",
+ "b4e7302944": "Minuteur de cache",
+ "487b176240": "Afficher un compte à rebours dans la barre latérale quand un agent Claude devient inactif.",
+ "9c20253679": "Afficher un compte à rebours quand un agent Claude devient inactif.",
+ "fe590653c1": "Claude met votre conversation en cache pour réduire les coûts. Après une trop longue inactivité, le cache expire et le message suivant renvoie tout le contexte, à un coût plus élevé. Ce compte à rebours vous indique quand reprendre.",
+ "a137f8854d": "Minuteur de cache de prompt",
+ "80c454e8a6": "Alignez cette durée sur le TTL de cache de votre fournisseur."
+ },
+ "GeneralEditorSettingsSection": {
+ "f80603d293": "Afficher les contrôles des notes markdown locales en mode éditeur enrichi et dans les actions de passation aux agents.",
+ "4edc104f0f": "Notes de revue Markdown",
+ "5f02e6fb21": "Afficher les contrôles des notes de revue markdown locales en mode éditeur enrichi.",
+ "51161d1647": "Afficher la minimap lors de l'édition d'un fichier.",
+ "6690b1ffb9": "Minimap",
+ "5a1ea6eaa2": "Masquée",
+ "73a09aad63": "Affichée",
+ "1de48ad940": "Arborescence de fichiers des diffs par défaut",
+ "1b87897af9": "Afficher ou masquer l'arborescence de fichiers à l'ouverture des vues de diff combinées.",
+ "12cbc0d0d6": "Côte à côte",
+ "05b6df93b3": "En ligne",
+ "7311f67ee7": "Vue de diff par défaut",
+ "b492397d34": "Format de présentation préféré pour l'affichage par défaut des diffs git.",
+ "a5db1d3975": "ms",
+ "fc5c5306ff": "ms.",
+ "8112cd6dcf": "Délai d'attente d'Orca après votre dernière modification avant l'enregistrement automatique. Au premier lancement, la valeur par défaut est de",
+ "d6cf227ca0": "Délai d'enregistrement automatique",
+ "1bec6d8318": "Délai d'attente d'Orca après votre dernière modification avant l'enregistrement automatique.",
+ "70bb30feb1": "Enregistre automatiquement les modifications de l'éditeur et des diffs éditables après une courte pause.",
+ "0df2e4fd12": "Enregistrement automatique des fichiers",
+ "d21136d9ef": "Configure la façon dont Orca enregistre les modifications de fichiers.",
+ "45c6e85c4d": "Éditeur",
+ "8f1afdfbd8": "Retour à la ligne dans les diffs",
+ "4aa4d9fb73": "Renvoie à la ligne les lignes longues dans les éditeurs de diff au lieu d'imposer un défilement horizontal.",
+ "bf16ef0af2": "Désactivé",
+ "3f6892f307": "Activé",
+ "b82f86d7d2": "Vérification orthographique du Markdown enrichi",
+ "5195f0b9ef": "Affiche les soulignements d'orthographe et les suggestions du navigateur pendant l'édition du Markdown enrichi.",
+ "7ddd66fede": "Retour à la ligne dans l'éditeur",
+ "9b18de6eea": "Renvoie à la ligne les lignes longues dans les éditeurs de fichiers au lieu d'imposer un défilement horizontal."
+ },
+ "AdvancedNetworkSettingsSection": {
+ "3e431564b5": "localhost, 127.0.0.1, *.internal",
+ "33ee3ca3af": "Facultatif. Séparez les hôtes par des virgules, des points-virgules ou des sauts de ligne.",
+ "f6d76cc8f4": "Règles de contournement du proxy",
+ "fb7130dcb9": "Hôtes qui doivent contourner le proxy HTTP configuré.",
+ "0adfce9fa7": "Prend en charge les URL http, https, socks, socks4 et socks5.",
+ "476f302aca": "http://proxy.example.com:8080",
+ "1e214e265a": "Laissez vide pour utiliser les réglages de proxy du système et les variables d'environnement de proxy héritées.",
+ "f00daf6324": "Proxy HTTP",
+ "823e0f15b1": "URL de proxy pour les requêtes réseau d'Orca et les processus enfants des terminaux locaux.",
+ "d93c7cd531": "Configure le routage réseau au niveau de l'application.",
+ "c46cdbbd4e": "Réseau",
+ "configureProxy": "Configurer le proxy"
+ },
+ "GeneralPane": {
+ "d58fccfd84": "Navigation",
+ "5cb5475664": "Confirmer avant de fermer les onglets épinglés",
+ "36b2a5dc6d": "Afficher une boîte de dialogue de confirmation avant de fermer un onglet épinglé.",
+ "projectRuntime": "Runtime des projets",
+ "projectRuntimeDescription": "Runtime par défaut pour les projets Windows locaux qui ne le remplacent pas."
+ },
+ "GeneralSupportSection": {
+ "af7d9f4396": "Merci pour votre soutien !",
+ "6922c1fa2b": "Ajouter une étoile à Orca sur GitHub",
+ "511782265b": "Soutenez le projet avec une étoile GitHub via la CLI gh.",
+ "55a87e5fd1": "Soutenir Orca",
+ "964acc6bb4": "Ajouter une étoile",
+ "73b327e793": "Réessayer",
+ "c9f96d4234": "error",
+ "397719bee5": "Ajout de l'étoile...",
+ "1e29570462": "ajout de l'étoile",
+ "9d181300e3": "favori",
+ "5c49f02662": "masqué",
+ "b3f0584f5d": "chargement",
+ "cb65c75b11": "Ouverture...",
+ "f2d4f877b2": "Ouvrir GitHub"
+ },
+ "GeneralUpdateSettingsSection": {
+ "8a52ca1d02": "Notes de version",
+ "d89806cc89": "est prête à être installée.",
+ "a6b37929dc": "Version",
+ "8311da27ba": "est disponible. Cliquez sur « Installer la mise à jour » pour la télécharger et l'installer.",
+ "f44299636f": "Redémarrer pour mettre à jour (",
+ "42717918f4": "Installer la mise à jour (",
+ "02dc082e70": "Impossible de démarrer le téléchargement de la mise à jour.",
+ "e1a647adc5": "Rechercher des mises à jour",
+ "ceb579abaf": "Recherche les mises à jour de l'application et installe une version plus récente d'Orca.",
+ "d91ebfb87e": "Version actuelle : {{value0}}",
+ "f2b1ccc12a": "Mises à jour",
+ "bd79d412f0": "Échec de la vérification des mises à jour. {{value0}}",
+ "b9ad70c30d": "Erreur de mise à jour. {{value0}}",
+ "6405510b92": "error",
+ "a0832ccdb1": "téléchargée",
+ "2a48034c4c": "Téléchargement v{{value0}}... {{value1}} %",
+ "4c1c001813": "téléchargement",
+ "f40d88390d": "Vous utilisez la dernière version.",
+ "90eb7309d7": "not-available",
+ "82465b2444": "disponible",
+ "31fd7150cf": "Recherche de mises à jour...",
+ "3394d1f663": "vérification",
+ "d69a09b672": "Les mises à jour sont vérifiées automatiquement au lancement.",
+ "7173352632": "idle"
+ },
+ "GeneralWorkspaceSettingsSection": {
+ "3d538a98f7": "Choisissez les applications proposées dans le menu « Ouvrir dans » d'un espace de travail.",
+ "008f92085f": "Applications « Ouvrir dans »",
+ "824b98a0d9": "Demander confirmation avant de supprimer des automatisations et leur historique d'exécution.",
+ "ea98373cd8": "Confirmer avant de supprimer les automatisations",
+ "d2dd2ca2e3": "Afficher une boîte de dialogue de confirmation avant de supprimer une automatisation et son historique d'exécution.",
+ "28bc3d085e": "Demander confirmation avant de supprimer un espace de travail depuis le menu contextuel. En cas d'échec, l'option « Forcer la suppression » reste disponible.",
+ "9f380934cf": "Confirmer avant de supprimer les espaces de travail",
+ "5734db82af": "Afficher une boîte de dialogue de confirmation avant de supprimer un espace de travail.",
+ "4fbf910ded": "Créer les espaces de travail dans un sous-dossier nommé d'après le dépôt.",
+ "ba3480642f": "Imbriquer les espaces de travail",
+ "a246f5ce6f": "Dossier racine où sont créés les dossiers des espaces de travail.",
+ "5567191a6e": "Parcourir",
+ "0e9fc0eadc": "Dossier des espaces de travail",
+ "e2955d9ccb": "Configure où sont créés les nouveaux espaces de travail.",
+ "7511097c5d": "Espace de travail",
+ "31e300af1c": "Confirmer avant de supprimer les artefacts",
+ "fb29a73a17": "Afficher une boîte de dialogue de confirmation avant de supprimer un artefact partagé et de rompre son lien public.",
+ "bf46474e33": "Demander confirmation avant de supprimer un artefact partagé. Quiconque détient son lien public perd l'accès.",
+ "externalWorktrees": "Worktrees externes",
+ "externalWorktreesDescription": "Choisissez si les worktrees créés en dehors d'Orca apparaissent par défaut.",
+ "show": "Afficher",
+ "hide": "Masquer"
+ },
+ "GhosttyImportModal": {
+ "9d3e56ca36": "Appliquer les modifications",
+ "f96688b6bc": "Annuler",
+ "b7ddae600c": "Terminé",
+ "e4bda7ce6f": "Aucune configuration Ghostty trouvée sur ce système.",
+ "b58d4c9051": "Clés non prises en charge",
+ "674b5ccd6b": "Aucun nouveau réglage à importer — vos réglages actuels correspondent déjà.",
+ "a4c5dec640": "Réglages à mettre à jour",
+ "4466f4cdaa": "Importation terminée",
+ "023a52c1f7": "Chargement de l'aperçu…",
+ "2763b0c045": "Consultez les réglages qui seront importés depuis votre configuration Ghostty.",
+ "d2f33670a9": "Importer depuis Ghostty",
+ "273e7e81fe": "Configurations",
+ "1f744a72f4": "Configuration"
+ },
+ "WarpThemeImportModal": {
+ "title": "Importer depuis Warp",
+ "description": "Importez des thèmes Warp comme thèmes de terminal Orca.",
+ "yaml_title": "Importer un thème YAML",
+ "yaml_description": "Importez des fichiers YAML de thèmes (format Warp) comme thèmes de terminal Orca.",
+ "yaml_no_themes_found": "Aucun thème trouvé dans les fichiers sélectionnés.",
+ "choose_file": "Choisir un fichier",
+ "choose_folder": "Choisir un dossier",
+ "loading": "Chargement des thèmes Warp...",
+ "found_theme_one": "1 thème trouvé",
+ "found_theme_other": "{{value0}} thèmes trouvés",
+ "found_in_source": " dans {{value0}}",
+ "clear_all": "Tout effacer",
+ "select_all": "Tout sélectionner",
+ "colors_only": "Couleurs uniquement",
+ "no_themes_found": "Aucun thème Warp personnalisé trouvé.",
+ "builtin_themes_hint": "Les thèmes préinstallés de Warp font partie de l'application Warp et ne peuvent pas être lus depuis le disque. Orca inclut déjà la plupart d'entre eux, comme Dracula, Gruvbox, Solarized et Tokyo Night.",
+ "custom_theme_yaml_hint": "Les thèmes personnalisés et communautaires doivent exister sous forme de fichiers YAML dans un dossier de thèmes Warp pour que l'import automatique puisse les détecter. Si vous avez cloné le dépôt public de thèmes de Warp, utilisez « Choisir un dossier » pour importer ce clone.",
+ "choose_manually": "Choisissez un fichier ou un dossier YAML de thèmes à importer manuellement.",
+ "skipped_files": "Fichiers ignorés",
+ "more_skipped_files": "{{value0}} autres fichiers ignorés.",
+ "cancel": "Annuler",
+ "import_theme_one": "Importer 1 thème",
+ "import_theme_other": "Importer {{value0}} thèmes",
+ "import_themes": "Importer des thèmes"
+ },
+ "useWarpThemeImport": {
+ "unknown_error": "Erreur inconnue",
+ "imported_one": "1 thème importé",
+ "imported_other": "{{value0}} thèmes importés",
+ "import_failed": "Échec de l'importation des thèmes",
+ "over_limit_one": "Importer ces thèmes dépasserait la limite de {{value0}} thèmes de terminal personnalisés. Désélectionnez 1 nouveau thème et réessayez.",
+ "over_limit_other": "Importer ces thèmes dépasserait la limite de {{value0}} thèmes de terminal personnalisés. Désélectionnez {{value1}} nouveaux thèmes et réessayez."
+ },
+ "YamlThemeImportButton": {
+ "label": "Importer depuis YAML"
+ },
+ "BranchPrefixFeedback": {
+ "6c40c0908f": "Le préfixe ne peut pas contenir d'espaces ni de caractères spéciaux comme ~ ^ : ? * [ \\",
+ "64d70b156a": "Les branches seront nommées {{example}}",
+ "808f9a726e": "Aucun préfixe ne sera appliqué"
+ },
+ "GitPane": {
+ "895d3f70b8": "gh",
+ "32dca11189": "github",
+ "c4f610d057": "En-têtes de limite de débit REST actuels de la CLI GitLab, lorsqu'ils sont disponibles.",
+ "0de4ae556c": "Budget API GitLab",
+ "cdd793134e": "budget api",
+ "b9c011fbc2": "limite de débit",
+ "3072428ac7": "glab",
+ "8a527d48e3": "gitlab",
+ "aa204f185f": "Limites de débit REST, Search et GraphQL actuelles de la CLI GitHub.",
+ "612a440e57": "Quota d'API GitHub",
+ "2cde9044a8": "graphql",
+ "36e3de3619": "de comparer avec un historique périmé. Orca ignore la mise à jour si cette branche contient des modifications non commitées ou des commits uniquement locaux.",
+ "d072a12995": "git diff main...HEAD",
+ "db3a127eb1": ". Cela permet de garder des commandes comme",
+ "3ae3de8898": "master",
+ "5bf885be48": "ou",
+ "ffba483bae": "main",
+ "976afc6b3e": "Quand vous créez un espace de travail, Orca actualise la base distante et fait avancer en toute sécurité votre branche locale correspondante (fast-forward), telle que",
+ "1ec5c91e1d": "Choisissez si les noms de branches utilisent votre nom d'utilisateur Git, un préfixe personnalisé ou aucun préfixe.",
+ "330f584b50": "Préfixe de branche",
+ "1ffaadf0a0": "Préfixe ajouté aux noms de branches à la création des worktrees.",
+ "813e15b346": "custom",
+ "2351aa5a31": "nom d'utilisateur git",
+ "cc63fce906": "nommage des branches",
+ "b559bf9899": "p. ex. feature",
+ "aefa1ecb59": "Aucun nom d'utilisateur git configuré",
+ "f35007e6e8": "git-username",
+ "3d172725cc": "Aucun",
+ "1f32ba27a6": "Personnalisé",
+ "a182c5125e": "Nom d'utilisateur Git",
+ "sourceControlGroupOrderTitle": "Ordre des groupes du contrôle de code source",
+ "sourceControlGroupOrderDescription": "Choisissez si Modifications, Modifications indexées ou Fichiers non suivis apparaissent en premier dans le contrôle de code source.",
+ "changesFirst": "Modifications d'abord",
+ "stagedFirst": "Indexées d'abord",
+ "untrackedFirst": "Non suivis d'abord",
+ "compareAgainstUpstreamTitle": "Base de comparaison par défaut",
+ "compareAgainstUpstreamDescription": "Choisissez la base que le contrôle de code source utilise par défaut pour comparer les changements commités. L'amont de la branche suit automatiquement la branche courante et revient à la branche par défaut du dépôt en l'absence d'amont. Vous pouvez toujours changer la base de comparaison d'un worktree depuis son panneau Git. Les cibles de pull request et de rebase ne changent pas.",
+ "compareBaseRepositoryDefault": "Valeur par défaut du dépôt",
+ "compareBaseBranchUpstream": "Amont de la branche"
+ },
+ "HiddenExperimentalGroup": {
+ "d0f914a528": "Bascule fictive",
+ "1014ddbfaf": "Sans effet aujourd'hui. Réservée comme premier emplacement pour les options expérimentales masquées.",
+ "232cf83de8": "Bascules non répertoriées pour les tests internes. Rien ici n'est pris en charge.",
+ "3e9e827ca5": "Expérimental masqué"
+ },
+ "InputPane": {
+ "db15068196": "Activé par défaut sur Linux et macOS. Linux utilise le presse-papiers de sélection du système ; les autres plateformes utilisent un tampon privé.",
+ "ad31c3c5fb": "Collage de la sélection au clic milieu"
+ },
+ "IntegrationsPane": {
+ "2122e15517": "Chaque espace de travail Linear connecté possède une clé stockée par le runtime actif. Les clés à accès complet peuvent couvrir toutes les équipes auxquelles le propriétaire de la clé a accès ; les clés restreintes peuvent être remplacées à tout moment.",
+ "e7b2dd46f9": "Test en cours…",
+ "fe4d378dc4": "Vérifié",
+ "f5c5246514": "Ajouter l'accès Linear",
+ "6432f6522e": "Connecté",
+ "077844591a": "Ajouter un accès à l'espace de travail",
+ "264a9b6128": "Linear",
+ "4831ba1083": "Revérifier",
+ "01f6c7582e": "En savoir plus",
+ "1a62c295c6": "Les identifiants Gitea sont configurés mais l'authentification a échoué. Vérifiez le token, l'URL de base de l'API et les permissions du dépôt, puis redémarrez Orca si les variables d'environnement ont changé.",
+ "5a1f86225a": "uniquement quand Orca ne peut pas déduire l'URL de l'API depuis le remote.",
+ "6193444689": "ORCA_GITEA_API_BASE_URL",
+ "2c0330ec3e": "pour les dépôts privés, et définissez",
+ "e678d89e8c": "ORCA_GITEA_TOKEN",
+ "d9467ab026": "Les dépôts publics sont détectés via leur remote git. Définissez",
+ "4ab9b96925": "Gitea",
+ "953b7bf6f7": "Les identifiants Azure DevOps sont configurés mais l'authentification a échoué. Vérifiez le token, l'URL de base de l'API et les permissions du dépôt, puis redémarrez Orca si les variables d'environnement ont changé.",
+ "6f317f5132": "uniquement quand Orca ne peut pas déduire l'URL de base de l'API depuis le remote git.",
+ "ae6b7f5f40": "ORCA_AZURE_DEVOPS_API_BASE_URL",
+ "67a9f26a80": ". Définissez",
+ "8f960935c1": "ORCA_AZURE_DEVOPS_ACCESS_TOKEN",
+ "ce3c58cd63": ", ou définissez",
+ "5ee6ef6405": "ORCA_AZURE_DEVOPS_TOKEN",
+ "4ee74d1470": "Définissez",
+ "5efce6953d": "Azure DevOps",
+ "3c3cf05c63": "Les identifiants Bitbucket sont configurés mais l'authentification a échoué. Vérifiez le token et les permissions du dépôt, puis redémarrez Orca si les variables d'environnement ont changé.",
+ "6e0ff3403e": "ORCA_BITBUCKET_ACCESS_TOKEN",
+ "44cde4aa01": "ORCA_BITBUCKET_API_TOKEN",
+ "a6c2816115": "et",
+ "b8a7efb3f6": "ORCA_BITBUCKET_EMAIL",
+ "8489c0aa49": "Bitbucket",
+ "e74de656ce": "glab auth login",
+ "05e5245af7": "La CLI GitLab est installée mais non authentifiée. Exécutez cette commande dans un terminal :",
+ "a83cac5726": "Installer la CLI GitLab",
+ "35a3379372": "Installez la CLI GitLab pour activer les merge requests, les issues et les pipelines.",
+ "ea160a9978": "CLI.",
+ "a3326f6f1b": "glab",
+ "027440e1cb": "Merge requests, issues, todos et pipelines via la",
+ "513abfe47d": "GitLab",
+ "51000487c4": "gh auth login",
+ "09285e9fe6": "La CLI GitHub est installée mais non authentifiée. Exécutez cette commande dans un terminal :",
+ "399cf46867": "Installer la CLI GitHub",
+ "c0c8575e05": "Installez la CLI GitHub pour activer les pull requests, les issues et les checks.",
+ "f36365ed45": "gh",
+ "de6a0d13ab": "Pull requests, issues et checks via la",
+ "70c5f74f36": "GitHub",
+ "8e078e480c": "Déconnecter {{value0}}",
+ "95b9a87e7e": "Tester",
+ "62b20292de": "error",
+ "ae38fc62a8": "ok",
+ "33ae9730a8": "Ajoutez un accès Linear pour parcourir et lier des issues.",
+ "98ded79cd7": "Connecté{{value1}} : {{value0}} espace de travail",
+ "4bdc6fe4f5": "not-configured",
+ "4972f3c95d": "configuré",
+ "3614887c40": "vérification",
+ "45bf5e6e4b": "Échec de l'authentification",
+ "e1bd5364e6": "Configuration facultative",
+ "e7a961e1c5": "Configuré",
+ "6bd148dcb5": "Pull requests et statuts de commits via l'API REST Gitea.",
+ "6355fe585e": "Pull requests et statuts de commits pour les dépôts détectés",
+ "1fac9b4910": "{{value0}} · Pull requests et statuts de commits",
+ "f92fbf11aa": "Non configuré",
+ "6791d7af95": "Pull requests et statuts de builds via des tokens de l'API REST Azure DevOps.",
+ "e3d5a24979": "Pull requests et statuts de builds pour les Azure Repos détectés",
+ "277fc23929": "{{value0}} · Pull requests et statuts de builds",
+ "295154e54e": "connecté",
+ "0879860c58": "Pull requests et statuts de builds via des tokens de l'API Bitbucket Cloud.",
+ "9707523939": "Pull requests et statuts de builds",
+ "a565377c38": "not-installed",
+ "15cf990798": "Non authentifié",
+ "f7eb5f0b24": "Non installé",
+ "3ba07f933b": "Connectez les trackers d'issues qu'Orca peut utiliser pour parcourir les tâches et démarrer des espaces de travail avec le contexte associé.",
+ "70e885705b": "Fournisseurs de tâches",
+ "1683acbac4": "Connectez les hébergeurs de code qu'Orca peut utiliser pour les pull requests, merge requests, checks et statuts de revue.",
+ "298c65ecac": "Fournisseurs de revue"
+ },
+ "KagiSessionLinkForm": {
+ "92f0b4e472": "Effacer",
+ "9f741627a7": "Lien de session Kagi effacé.",
+ "d5c8b94c5b": "Enregistrer",
+ "ff450194cd": "Lien de session privée Kagi",
+ "e383683485": "https://kagi.com/search?token=...",
+ "81409d9362": "Lien de session privée facultatif pour l'authentification Kagi.",
+ "3e5b7c6c25": "Lien de session Kagi enregistré.",
+ "0911d5fa4c": "Saisissez un lien de session privée Kagi de la forme https://kagi.com/search?token=..."
+ },
+ "KeybindingsFileActions": {
+ "abc49853fb": "Recharger depuis le disque",
+ "a8a8d6b9d3": "Révéler dans le gestionnaire de fichiers",
+ "9e24c0e858": "Ouvrir dans Cursor",
+ "1637f64033": "Ouvrir dans VS Code",
+ "98f1a23e1c": "Ouvrir avec l'application par défaut",
+ "400397a10d": "Ouvrir le menu du fichier de raccourcis clavier",
+ "1c2be2b2c6": "Modifier le fichier dans Orca",
+ "c5886a31cc": "Échec de l'ouverture de l'éditeur externe.",
+ "cdf794f46d": "Le fichier de raccourcis clavier n'est pas disponible.",
+ "dd532a01ce": "Échec de l'ouverture des raccourcis clavier dans Orca."
+ },
+ "ManageSessionKillDialog": {
+ "6bf4627168": "Annuler",
+ "ad9832aa26": ". Tout travail non enregistré dans ce volet sera perdu. L'action est irréversible.",
+ "8401328fed": "Force la fermeture de",
+ "87dcafc85c": "Tuer cette session ?",
+ "0b0db4c68c": "Tuer la session",
+ "d3dba51b15": "Arrêt forcé…"
+ },
+ "ManageSessionsSection": {
+ "33c2a1e1b4": "Tuer la session {{value0}}",
+ "2896a50f50": "Aller au terminal {{value0}}",
+ "e26a60d9eb": "Aucune session.",
+ "39c53d6d74": "Chargement…",
+ "5ed15e778c": "Redémarrer le daemon",
+ "3282db098c": "Tuer toutes les sessions",
+ "b3b1cc5708": "Actualiser",
+ "a795a9552a": "Sessions",
+ "7c4889a724": "Récupérez un terminal gelé ou qui se comporte mal en tuant des sessions ou en redémarrant le démon sous-jacent.",
+ "d1b80fd5cd": "Gérer les sessions",
+ "9c940434af": "Repassez au runtime local pour redémarrer ou tuer les sessions du démon local.",
+ "ad467eaadc": "La gestion des sessions est indisponible tant qu'un serveur runtime distant est actif.",
+ "8dbd96b463": "Impossible de tuer la session.",
+ "0735b7a586": "Impossible de tuer la session — elle a peut-être déjà disparu.",
+ "bfba05dccd": "Session tuée.",
+ "c535cbdd09": "Impossible de charger les sessions.",
+ "e3d1fbe008": "Redémarrer",
+ "a06ababda0": "killAll"
+ },
+ "McpConfigFileRow": {
+ "b145eb6009": "env:",
+ "e720c139cd": "Ouvert",
+ "845ae248e8": "valid"
+ },
+ "McpConfigSection": {
+ "4d16a0d9ac": "Vérifié",
+ "b900cd6282": "Aucune configuration MCP trouvée. Ajoutez une configuration d'espace de travail vide si vous voulez que ce dépôt définisse ses propres serveurs MCP.",
+ "3b224167ff": "serveur",
+ "251b96564a": "détecté ·",
+ "f34c152dc0": "Actualiser les configurations MCP",
+ "6bac9ddfc6": "Les dépôts SSH sont lus via le système de fichiers distant. La création de starters est limitée à la configuration racine de l'espace de travail.",
+ "96f5609b04": "Inspectez les définitions de serveurs MCP utilisables par les agents travaillant dans ce dépôt.",
+ "55eea3ef47": "Configurations MCP",
+ "9ee215caf6": ".mcp.json",
+ "1f3665e35a": "Configuration MCP créée",
+ "82436439eb": "Ajouter une configuration MCP",
+ "0a5c1ead54": "Créer une configuration vide"
+ },
+ "MobileEmulatorAgentControlRow": {
+ "1861982430": "Échec du chargement de l'état de la CLI.",
+ "8af7a8bc38": "Les commandes ciblent l'émulateur actif du worktree courant. Les coordonnées sont normalisées entre 0 et 1.",
+ "c7f3fe0a6e": "Commandes d'émulateur courantes",
+ "d94ca6a623": "Permet aux agents d'utiliser les commandes CLI d'Orca, y compris le contrôle de l'émulateur mobile.",
+ "67e19ee03c": "Compétence CLI Orca",
+ "aaf62a3dd2": "Installé dans",
+ "2fef055608": "Enregistre la commande CLI Orca pour que les agents puissent contrôler l'émulateur actif depuis leur shell.",
+ "4f2205f3b6": "Activer Orca CLI",
+ "ff4b7e65d6": "Laisser les agents de codage contrôler l'émulateur mobile actif avec des commandes CLI Orca.",
+ "2a674aa810": "Contrôle de l'émulateur mobile par les agents",
+ "cdeaed9e37": "Orca CLI enregistré dans PATH.",
+ "3d34423e88": "Enregistrement de la CLI Orca",
+ "3be27641c9": "afin que les commandes d'émulateur puissent s'exécuter depuis les shells des agents.",
+ "3941719a56": "Vérification de la CLI Orca avant d'ouvrir la configuration du skill."
+ },
+ "MobileEmulatorExamples": {
+ "edf13dd03b": "Copier",
+ "c12b253997": "Copier l'exemple de prompt",
+ "d151e25078": "\"",
+ "b525ff2b12": "\"",
+ "4daa95f25a": "Collez l'un de ces prompts dans Claude Code, Codex ou un autre agent, dans un projet où le skill CLI Orca est installé.",
+ "0820b3f84f": "Essayez — exemples de prompts",
+ "1f608e7d60": "Échec de la copie du prompt.",
+ "2b077b5544": "Prompt copié."
+ },
+ "MobileEmulatorSettingsPane": {
+ "19d39113b6": "Laisser les agents de codage contrôler l'émulateur mobile actif avec des commandes CLI Orca.",
+ "f2f8d97bb6": "Contrôle de l'émulateur mobile par les agents",
+ "143961d031": "Appareil par défaut",
+ "8aec2f99a0": "Actualiser la disponibilité des émulateurs",
+ "ae1612c58c": "Disponibilité",
+ "f9af91ea26": "Affiche l'action Nouvel émulateur mobile et permet aux agents de se rattacher à l'émulateur actif.",
+ "700ddbf9b1": "Activer l'émulateur mobile",
+ "bc39d0f115": "Configurez la prise en charge des émulateurs mobiles pour Orca et les agents de codage.",
+ "6593c9ddd3": "Émulateur mobile",
+ "a4f1c82d90": "Désactivé",
+ "b5e2d93e01": "Vérification...",
+ "c6f3ea4f12": "Prêt",
+ "d704fb5023": "Configuration requise",
+ "06b06429c6": "Vérification de la prise en charge du SDK Android et du simulateur iOS.",
+ "6d1483d4a0": "1 appareil émulateur détecté.",
+ "0a452d4d3b": "{{value0}} appareils émulateur détectés.",
+ "f62a1bb759": "Orca sélectionnera automatiquement un appareil émulateur une fois les appareils détectés.",
+ "b2fd62ea75": "Appareil par défaut pour les nouveaux onglets d'émulateur et les commandes d'attachement des agents. La sélection automatique privilégie un appareil déjà démarré."
+ },
+ "MobileNetworkInterfaceSection": {
+ "63d5e4ae1e": "Régénérez le QR code et scannez-le depuis l'app mobile Orca.",
+ "87985ba6f5": "Dans ce menu Interface réseau, choisissez l'adresse Tailscale, généralement une IP en 100.x.y.z.",
+ "1f7c26d36a": "Connectez-vous au même tailnet sur les deux appareils.",
+ "668016be7a": "sur votre ordinateur et votre téléphone.",
+ "1dc87a7fbc": "Tailscale",
+ "51d29927eb": "Installer",
+ "9fc5d203ff": "Orca Mobile se connecte directement à cet ordinateur. Pour l'utiliser loin du même réseau local, placez votre ordinateur et votre téléphone sur le même réseau privé superposé (overlay), puis générez le QR code avec cette adresse réseau sélectionnée.",
+ "39fad211d9": "Se connecter hors de votre Wi-Fi grâce à un tailnet",
+ "a9db5d771d": "Actualiser les interfaces réseau",
+ "b2c384cfd6": "Aucune interface trouvée",
+ "d536b5e20d": "Choisissez l'adresse réseau à annoncer dans le QR code. Utilisez votre adresse LAN pour un appairage sur le même réseau, ou une adresse de réseau superposé (Tailscale, ZeroTier) pour un accès entre réseaux distincts.",
+ "406a35121c": "Interface réseau",
+ "c541f67790": "Générer le QR code",
+ "1e64659126": "Régénérer"
+ },
+ "MobilePane": {
+ "dd3cd78d04": "Scanner avec Orca Mobile",
+ "35100bca5d": "Pendant que vous utilisez un terminal sur votre téléphone, Orca le réduit pour tenir sur l'écran du téléphone. À la fermeture de l'app ou quand vous passez à autre chose, ce réglage détermine s'il garde la taille téléphone (pour que les outils CLI interactifs ne se reforment pas) ou reprend sa taille bureau. Vous pouvez toujours utiliser Restaurer ce terminal ou Restaurer tous les terminaux sur la bannière pour redimensionner manuellement.",
+ "ee56f1c7e4": "Quand vous quittez l'app mobile",
+ "3939fd062c": "La révocation d'un appareil le déconnecte immédiatement.",
+ "254a6d09e4": "Appairé",
+ "d7ce676270": "Appareils appairés",
+ "e778ecb209": "Ou collez ce code dans l'app mobile :",
+ "310924ad2c": "Scannez ce code avec l'app mobile Orca. Chaque code crée un jeton d'appareil unique.",
+ "870e1b5ca5": "Échec de la révocation de l'appareil",
+ "2e3dd0bc29": "Appareil révoqué",
+ "711231348f": "Échec de la copie du code d'appairage",
+ "e3c427e020": "Échec de la génération du QR code",
+ "cb9067c1c1": "Le transport WebSocket n'est pas actif",
+ "d714614dbf": "Échec de l'actualisation des interfaces réseau",
+ "ff865419dc": "Après 30 minutes",
+ "d4ba07d914": "Après 5 minutes",
+ "c474aa09d8": "Après 1 minute",
+ "aa1263e881": "Conserver à la taille téléphone (par défaut)",
+ "6436e56546": "QR code pour l'appairage mobile",
+ "1b1b70279a": "Aucun appareil appairé pour le moment.",
+ "1592afcc7a": "Aucun appareil appairé pour le moment. Scannez le QR code avec l'app mobile Orca.",
+ "relayDegradedNotice": "Le relais est injoignable — ce code ne fonctionne que sur votre LAN ou via Tailscale. Régénérez-le pour réessayer.",
+ "pairingQrError": "Ce code d'appairage n'a pas pu être rendu sous forme de QR code. Copiez-le plutôt dans Orca Mobile.",
+ "pairingCodeReady": "Code d'appairage prêt",
+ "copyPairingCode": "Copier le code d'appairage",
+ "diagnosticsCopied": "Diagnostics copiés",
+ "diagnosticsCopyFailed": "Échec de la copie des diagnostics"
+ },
+ "MobileSettingsPane": {
+ "9a3c280e49": "GitHub Releases",
+ "b0088412a1": "ou l'APK Android depuis",
+ "b5a2ed83ff": "App Store",
+ "installIntro": "Installez Orca Mobile depuis",
+ "installOutro": ", puis appairez ci-dessous.",
+ "androidApkLabel": "APK Android",
+ "c8491c17ef": "Contrôlez Orca depuis votre téléphone en scannant un QR code. Bêta / avant-première — attendez-vous à des bugs et à des changements incompatibles. Obtenez l'app iOS depuis",
+ "e7a3ae8c4e": "Mobile",
+ "174f4a3c6d": "Contrôlez les terminaux et les agents depuis votre téléphone.",
+ "1de96ec8a6": "Afficher le bouton Orca Mobile",
+ "682293cadf": "Afficher le bouton Orca Mobile en haut de la barre latérale gauche.",
+ "d4f2b65f30": "Afficher le raccourci Orca Mobile dans la barre latérale."
+ },
+ "NotificationsPane": {
+ "906b4afebf": "Envoyer une notification de test",
+ "2772d2f257": "Ignorer les notifications quand le worktree déclencheur est déjà visible.",
+ "00cd406dbb": "Masquer quand la fenêtre est active",
+ "2a42dd8d6f": "Volume du son de notification",
+ "4aa5085cd7": "Personnalisé :",
+ "c258cb96dc": "Choisir le son de notification",
+ "2a2033c388": "Choisissez l'alerte jouée par Orca lors d'une notification bureau.",
+ "88686e6ca8": "Son de notification",
+ "b6fc369244": "Un terminal en arrière-plan émet un caractère de sonnerie.",
+ "591fe605b9": "Sonnerie du terminal",
+ "55f901a59b": "Un agent de codage termine et devient inactif.",
+ "ca76d06fd2": "Tâche d'agent terminée",
+ "deff6d30da": "Notifications système natives pour les événements en arrière-plan.",
+ "841c8c549f": "Activer les notifications",
+ "0fadad17ce": "Impossible de lire le son de notification",
+ "406feb0aa6": "La notification de test n'a pas été délivrée",
+ "6fc3781729": "Les notifications sont désactivées",
+ "4676a95bc3": "Vérifiez les réglages de notification bureau d'Orca.",
+ "0cb93240b8": "Le système n'a pas affiché la notification",
+ "145227ca2b": "Ouvrir les paramètres",
+ "d3d54e0915": "Notification de test envoyée",
+ "115437bc35": "Si aucune bannière macOS n'est apparue, activez « Autoriser les notifications » pour Orca.",
+ "7f45542625": "Notification de test demandée",
+ "98d70fb261": "Impossible de lire le son de notification personnalisé",
+ "c83b05a055": "Les notifications ne sont pas prises en charge sur ce système",
+ "274af61bc0": "système",
+ "6e6df3a09a": "Choisir un fichier personnalisé",
+ "76e02467b8": "Changer de fichier personnalisé",
+ "d3756cf5bc": "désactivé"
+ },
+ "OpenAiTranscriptionKeyDialog": {
+ "fa83512e48": "Enregistrer la clé",
+ "07b26f2742": "Effacer la clé",
+ "d246b2bdb3": "Les clés locales d'exécution sont stockées dans ~/.orca via le stockage chiffré d'Electron quand celui-ci est disponible.",
+ "c3380e4ca5": "sk-...",
+ "2f797018f0": "Clé API configurée",
+ "16015322f9": "Clé API",
+ "07ed3e512e": "L'audio n'est envoyé à OpenAI que lorsqu'un modèle vocal OpenAI est sélectionné.",
+ "439e91879e": "Transcription OpenAI"
+ },
+ "OpenAiTranscriptionSettingsRow": {
+ "85c589cd61": "Ajouter une clé API",
+ "ae2df8f511": "Déconnecter la clé API OpenAI",
+ "a622bc3b37": "Remplacer la clé",
+ "3b0ab3fc0b": "Connecté",
+ "27e0cb656d": "Transcription OpenAI",
+ "893790e13b": "Ajoutez une clé API OpenAI avant de sélectionner des modèles de transcription vocale cloud.",
+ "b59b9b2b51": "Clé API configurée pour les modèles de transcription vocale cloud."
+ },
+ "OpenInMenuSetting": {
+ "03b00b1f64": "App personnalisée",
+ "c1d817e027": "Ajoutée",
+ "e4064916aa": "Ajouter une app",
+ "9d0413817d": "Choisissez les applications proposées dans le menu « Ouvrir dans » d'un espace de travail.",
+ "6ed52fe71e": "Applications « Ouvrir dans »",
+ "eb55b87570": "La commande à saisir dans Terminal pour ouvrir cette app.",
+ "ba1422ee07": "Commande de terminal",
+ "e1fc0085c6": "Libellé du menu",
+ "a261931d29": "Supprimer l'app",
+ "af7d1c3656": "Modifier l'app",
+ "494ed535cd": "Réduire les détails de l'app",
+ "810ef39b56": "cursor",
+ "3ebe650f74": "Nom de l'app",
+ "3743ed080c": "Définir la commande",
+ "f79084947b": "Nouvelle app"
+ },
+ "OrchestrationPane": {
+ "52e0634e2c": "Demandez à un agent coordinateur d'utiliser l'orchestration pour les passations, les transferts de worktree et les agents enfants séquentiels ou parallèles.",
+ "ae79504732": "Comment l'utiliser",
+ "7bc082f4de": "Copier la commande d'installation",
+ "832f1f3ee6": "Vous préférez votre propre terminal ?",
+ "9bedd2a6e5": "Permet aux agents de transmettre le contexte et de coordonner le travail via Orca.",
+ "07641b9768": "Skill d'orchestration",
+ "2aacdb0517": "Coordonnez les agents de codage entre passations, transferts de worktree et travaux d'agents enfants.",
+ "191ac34567": "Orchestration d'agents"
+ },
+ "OrchestrationSetupCard": {
+ "e7d2a5146c": "Permet aux agents de transmettre le contexte et de coordonner le travail via Orca.",
+ "2777ff0fdc": "Skill d'orchestration"
+ },
+ "OrchestrationSkillAgentCoverage": {
+ "6dec5ce2d2": "Couverture des agents",
+ "ffe13e36fb": "Manquant",
+ "1e8f8d8fae": "Prêt",
+ "checking": "Vérification des agents installés et des chemins des skills…",
+ "noAgents": "Aucune CLI d'agent détectée sur le PATH. Installez des agents dans Paramètres → Agents, puis relancez la vérification.",
+ "fullCoverage_one": "L'unique agent détecté possède le skill.",
+ "fullCoverage_other": "Tous les {{value0}} agents détectés possèdent le skill.",
+ "noCoverage": "Installez le skill ci-dessus, puis relancez la vérification.",
+ "partialCoverage": "{{value0}} des {{value1}} agents détectés possèdent le skill."
+ },
+ "OrchestrationSkillPromptDialog": {
+ "f08d45293d": "Copier la commande",
+ "35550f3b3b": "Terminé",
+ "1bdce1911e": "Copier la commande d'installation du skill d'orchestration",
+ "b99f375eb2": "Exécutez cette commande dans un terminal pour installer le skill d'orchestration pour vos agents.",
+ "2914abcfa2": "Installer le skill d'orchestration",
+ "d3dc559225": "Échec de la copie de la commande d'installation.",
+ "239bf9132b": "Commande d'installation copiée."
+ },
+ "PrivacyDiagnosticBundleControls": {
+ "dc8404a930": "Créer le fichier de diagnostic",
+ "a5acaffdb6": "Abandonner",
+ "aca2c8a367": "Envoyer au support",
+ "798b6f0be5": "Ouvrir le fichier de révision",
+ "2ae9a6b63e": "Terminé",
+ "7f14a1733c": "Supprimer le fichier envoyé",
+ "2801d4ce22": "Copier l'ID de référence",
+ "d8be621237": "Ouvrez d'abord le fichier de révision.",
+ "61676df223": "Diagnostics envoyés. Communiquez cet ID de référence au support : {{value0}}.",
+ "fd7b3891af": "Vous avez ouvert le fichier de révision ({{value0}}). Envoyez ce fichier au support, ou abandonnez-le.",
+ "62340d4439": "Votre fichier de révision est prêt ({{value0}}). Ouvrez-le pour voir ce qui serait envoyé, puis choisissez de l'envoyer ou non au support.",
+ "19ec5e29b3": "Collecte l'activité récente de l'app et les erreurs dans un fichier expurgé que vous pouvez consulter avant l'envoi. Rien n'est téléversé tant que vous n'avez pas choisi de l'envoyer."
+ },
+ "PrivacyDiagnosticsSection": {
+ "af2fc82cde": "Envoyer les diagnostics de l'app au support",
+ "c18cbe45df": "Diagnostics envoyés supprimés",
+ "7a4944595b": "Impossible de copier l'ID de référence",
+ "13eb2c65a1": "ID de référence copié",
+ "860bca9ec9": "Fichier de révision abandonné",
+ "49fc6c80e8": "Diagnostics envoyés",
+ "db3228e01a": "Fichier de révision ouvert",
+ "a2b3505c77": "Fichier de révision créé"
+ },
+ "PrivacyPane": {
+ "36e0e2e63b": "variable d'environnement. Supprimez-la et redémarrez pour réactiver.",
+ "79a0f3c16c": "La télémétrie est désactivée par la",
+ "e3970bbbf5": "La télémétrie est désactivée car une variable d'environnement CI est définie. Supprimez-la et redémarrez.",
+ "fe904ac984": "Partager les données d'utilisation anonymes",
+ "77410e0566": "Politique de confidentialité",
+ "8bfdd23a88": "Aidez-nous à décider quoi développer ensuite. Orca envoie anonymement des comptages des fonctionnalités que vous utilisez et des endroits où ça casse.",
+ "afec8b03be": "ci"
+ },
+ "QuickCommandsList": {
+ "noSearchMatches": "Aucune commande ne correspond à cette recherche."
+ },
+ "QuickCommandsToolbar": {
+ "searchLabel": "Rechercher des commandes"
+ },
+ "QuickCommandsPane": {
+ "8764c6e9e4": "Supprimer {{value0}}",
+ "7d90fd5299": "Modifier {{value0}}",
+ "8c877dec41": "Global",
+ "c6b155911b": "Toutes les commandes",
+ "5aacc8f7dc": "Ajouter une commande",
+ "c36912efd5": "Exécutez-les depuis le bouton Commandes rapides de la barre d'onglets, ou faites un clic droit dans n'importe quel terminal.",
+ "f91b649324": "Commandes enregistrées",
+ "3d9dc558e8": "Cette commande rapide sera retirée de votre liste enregistrée.",
+ "3edf3deaf8": "Supprimer « {{value0}} » ?",
+ "9fcfc29519": "Insérer",
+ "9b3e338d62": "Enter",
+ "4ccc63da87": "Agent",
+ "0252ddd578": "Aucun texte de commande",
+ "7784912ed6": "dépôt",
+ "2bb9e38e93": "Sans titre",
+ "3eb9897ab0": "Aucune commande dans les portées sélectionnées.",
+ "38d61927e6": "Aucune commande rapide enregistrée.",
+ "44923dd982": "destructive",
+ "ec1ed99e70": "Supprimer",
+ "d1d0976320": "Aucun",
+ "89f7e57fcc": "Enregistrée le",
+ "d59bd333c3": "Mettez à jour ce serveur Orca pour gérer ses commandes rapides.",
+ "f2bf411640": "Impossible de charger les commandes depuis cet hôte.",
+ "7ecfee5b8e": "Réessayer",
+ "601d6af51f": "Chargement des commandes…",
+ "923ba89646": "Impossible d'actualiser les commandes depuis cet hôte. Affichage des dernières commandes chargées.",
+ "8d525e5f15": "Copied",
+ "53b17a4b1b": "Impossible de copier",
+ "a9a564b7e7": "Copier {{value0}}",
+ "69a1441a21": "Rien à copier"
+ },
+ "RecentTabOrderControl": {
+ "3b17c81ede": "Ordre de la barre d'onglets",
+ "6e6a3fcc61": "Plus récents",
+ "7a546f2309": "Ordre des onglets",
+ "a867a0889f": "Récents ou barre d'onglets."
+ },
+ "RepositoryHooksSection": {
+ "af49e2a19e": "Le fichier est présent, mais Orca n'a pas trouvé de définitions valides de `scripts` ou d'`issueCommand`.",
+ "3397879bee": "et des commandes locales existent, choisissez celles qui s'exécutent.",
+ "39da2ae12f": "orca.yaml",
+ "ac9038d2cc": "Lorsque les deux",
+ "32fec28f5b": "Source des commandes",
+ "bbbd6e0bc4": "Source des commandes et orca.yaml",
+ "c9bc1bfd8f": "Avancé",
+ "610d90fdbd": "Détails sur la source des commandes et orca.yaml.",
+ "52aef29e69": "Laissez vide pour utiliser la valeur par défaut du dépôt définie dans",
+ "4084720f47": "Terminer {{artifact_url}}",
+ "13394103bd": "Commande d'issue GitHub personnalisée",
+ "70ad20f883": "pour l'URL de l'issue ou de la pull request liée.",
+ "b997331366": "Remplacement facultatif. Utilisez",
+ "2cc27dc12b": "Remplacement facultatif par utilisateur pour la commande d'issue liée.",
+ "b91a0f297d": "Scripts locaux et partagés exécutés avant l'archivage d'un worktree.",
+ "9a100323ff": "Script d'archivage",
+ "21fb607a87": "Comportement par défaut à la création d'un nouveau worktree.",
+ "793dcee97d": "Moment d'exécution",
+ "63e1783173": "Choisissez le comportement par défaut quand un script de setup est disponible.",
+ "fb6bebcf7e": "Quand exécuter le setup",
+ "30d555acd2": "Scripts locaux et partagés exécutés après la création d'un nouveau worktree.",
+ "52b31baf02": "Script de setup",
+ "8567127a40": "Scripts exécutés à la création ou à l'archivage des worktrees. Les scripts locaux sont stockés sur cette machine ; les scripts `orca.yaml` sont partagés avec votre équipe.",
+ "ff082fe7c6": "Hooks de worktree",
+ "5d940bde5c": "Ajouter un script local",
+ "8c2893fae0": "S'exécute comme un script shell unique. Enregistré sur cette machine.",
+ "40a446ae16": "- rien que pour vous, sur cette machine",
+ "2d03a514db": "local",
+ "7e4427b4a2": "pour changer.",
+ "b113344b6a": "Édition",
+ "f828e1de19": "- partagés avec votre équipe",
+ "673a7fd10e": "Vérification...",
+ "5426ecbdcb": "Les scripts locaux ne s'exécuteront pas",
+ "b2b06c7ce8": "Variables d'environnement disponibles (survolez pour les détails) :",
+ "95a0411b3e": "template",
+ "175daba180": "Exemple",
+ "b20c5df6ca": "Ajoutez un fichier `orca.yaml` pour activer des valeurs par défaut partagées de setup, d'archivage ou d'automatisation d'issues pour ce dépôt. Exemple de modèle :",
+ "56f9a4a1d0": "Utilisation de `orca.yaml`",
+ "623e0c9f31": "`orca.yaml` n'a pas pu être analysé",
+ "5a67e4793d": "Aucun `orca.yaml` détecté",
+ "07ba35bc68": "Vérifiez l'indentation sous `scripts:`. Les clés de hook doivent être indentées de deux espaces, et les lignes de commande de quatre.",
+ "787ca433ef": "Définissez uniquement les clés prises en charge : `scripts`, `setup`, `archive` et `issueCommand`.",
+ "ecc73d9125": "Comparez votre fichier au modèle fonctionnel ci-dessous et recopiez cette structure si besoin.",
+ "925f9e0dc4": "text-foreground",
+ "0cc712b823": "Le fichier de configuration principal existe à la racine du dépôt, mais Orca n'a pas encore pu analyser les définitions de hooks prises en charge.",
+ "c90b858573": "text-amber-700 dark:text-amber-300",
+ "aba825233f": "Le fichier contient des clés de configuration que cette version d'Orca ne reconnaît pas. Vous devrez peut-être mettre à jour Orca, ou vérifier le fichier pour écarter une faute de frappe.",
+ "ca424ff135": "Les hooks partagés et les valeurs par défaut d'automatisation d'issues sont définis dans le dépôt et accessibles à tous ceux qui l'utilisent.",
+ "32f417fe17": "text-emerald-700 dark:text-emerald-300",
+ "8bfe65fc60": "Utiliser les commandes locales",
+ "8d6c56bff8": "Exécuter les deux",
+ "0fa21e19ec": "Nom de l'espace de travail, généralement basé sur le nom de branche.",
+ "54c73d88d0": "Chemin du worktree en cours de création. Les commandes de setup s'exécutent depuis ce répertoire.",
+ "30952c4aa4": "Chemin vers le checkout principal du dépôt. Utile pour copier des fichiers partagés, comme .env, dans un worktree.",
+ "9b821fa19d": "# ex. echo \"Nettoyage de $ORCA_WORKSPACE_NAME\"",
+ "6f90ebe3fd": "S'exécute avant qu'un worktree soit archivé ou supprimé.",
+ "a3fc966677": "# ex. pnpm install cp \"$ORCA_ROOT_PATH/.env\" \"$ORCA_WORKTREE_PATH/.env\"",
+ "f0710e1c83": "S'exécute après la création d'un nouveau worktree : installer les dépendances, copier les fichiers env, lancer les migrations.",
+ "8561b0665f": "d'abord orca.yaml, puis vos commandes locales.",
+ "0e8b2a520d": "Ignore orca.yaml ; exécute uniquement vos commandes locales.",
+ "83dc78202a": "Local uniquement",
+ "29397e8bbc": "Exécute uniquement les commandes commises du dépôt ; ignore les commandes locales.",
+ "d88b6ff88f": "orca.yaml uniquement",
+ "99e3264a49": "N'exécute le setup que si vous le choisissez.",
+ "15debc1fd9": "Ignorer par défaut",
+ "022ba10cf2": "Exécute le setup automatiquement.",
+ "d3ef1ab247": "Exécuter par défaut",
+ "90b1f50137": "Demander confirmation avant d'exécuter le setup.",
+ "e03d9a8f38": "Demander à chaque fois",
+ "8dbe6bedf5": "invalide",
+ "0e0dd5b9a5": "chargé",
+ "9b12f15b1e": "lorsqu'il en existe un.",
+ "c85c2c88a2": "{{artifact_url}}",
+ "fac13f8c1e": "faisant foi",
+ "0518758f38": "les deux",
+ "d2b3016c20": "partagé",
+ "4611b78617": "source des commandes",
+ "c5a55a2d2e": "avancé",
+ "b5e3e77e89": "action",
+ "0ce113fd7b": "Les scripts locaux sont enregistrés, mais la source des scripts est réglée sur orca.yaml uniquement.",
+ "7f78e5eea6": "Les scripts locaux sont enregistrés. Orca analyse encore orca.yaml avant de pouvoir recommander la source de scripts à utiliser.",
+ "2b6356e744": "Enregistré",
+ "81057d5f71": "Enregistrement...",
+ "da37d6f10e": "Copier",
+ "3149964b66": "Copied",
+ "waitForSetupBeforeAgent": "Attendre la fin du setup avant de démarrer l'agent",
+ "waitForSetupBeforeAgentHelp": "Activez cette option quand le setup installe des dépendances, des serveurs MCP ou des fichiers de configuration dont l'agent a besoin au démarrage."
+ },
+ "RepositoryIconPicker": {
+ "2b7d27b93c": "Utiliser la couleur de dépôt {{value0}}",
+ "fde066a63b": "Les PNG téléversés doivent peser 256 Ko au maximum.",
+ "cc1286e263": "Favicon",
+ "03ca1a4e9b": "example.com",
+ "381b4844fd": "Téléverser un PNG",
+ "7da623abcc": "Utilisé par défaut — GitHub en fournit toujours un, même quand le propriétaire n'a pas défini d'image personnalisée.",
+ "39da8a10bf": "Utiliser l'avatar GitHub",
+ "c490787d24": "Émoji",
+ "b2d7fd2116": "Icône",
+ "2d8bd302fa": "Avatar",
+ "913c55833d": "Couleur de dépôt personnalisée {{value0}}",
+ "0e5f0693c1": "Choisir une couleur de dépôt personnalisée",
+ "642dc29c6d": "Couleur",
+ "549d126081": "Réinitialiser",
+ "4e2a14f967": "Icône du dépôt",
+ "d71df44587": "Échec de la résolution du dépôt GitHub.",
+ "f79972271a": "Aucun remote GitHub trouvé pour ce dépôt.",
+ "4d039317f4": "Favicon du site web",
+ "acf31559a0": "Saisissez une URL de site web valide.",
+ "868c5c9b56": "Échec de l'importation de l'icône du dépôt",
+ "emojiTooLongForRepoIcon": "Cet émoji ne peut pas servir d'icône de dépôt.",
+ "currentEmojiSelection": "Actuel : {{value0}}",
+ "searchEmojiPlaceholder": "Rechercher un émoji"
+ },
+ "RepositoryPane": {
+ "15a99d9b9f": "Les chemins relatifs sont résolus depuis la racine de ce projet.",
+ "8ccacbeb5a": "Utiliser le réglage global",
+ "e9bd57a336": "Emplacement des worktrees",
+ "e63bb96a9b": "Répertoire spécifique au projet pour les nouveaux worktrees.",
+ "f88db4fece": "Base de worktree par défaut",
+ "8984d06520": "Branche ou ref de base par défaut à la création des worktrees.",
+ "e641c359de": "Icône et couleur du projet utilisées dans la barre latérale et les onglets.",
+ "26fef02bf3": "Icône du projet",
+ "c7ef4415de": "Nom d'affichage",
+ "b0a0c14a1c": "Détails d'affichage propres au projet pour la barre latérale et les onglets.",
+ "removeProjectAllHosts": "Retirer ce projet d'Orca sur tous les hôtes configurés.",
+ "0909e5d650": "Retirer le projet",
+ "ee5a290616": "Ouvert en tant que dossier. Les fonctions Git sont indisponibles pour cet espace de travail.",
+ "323debba71": "Type :",
+ "availableHosts": "Hôtes disponibles",
+ "availableHostsDescription": "Hôtes où ce projet est configuré.",
+ "availableHostsHelp": "Les chemins de projet et les réglages de worktree sont propres à chaque hôte ; la création d'un espace de travail peut cibler toute configuration prête.",
+ "viewingHost": "Hôte affiché",
+ "currentSetup": "Actuel",
+ "hostSetupStateReady": "Prêt",
+ "hostSetupStateNotSetUp": "Non configuré",
+ "hostSetupStateSettingUp": "Configuration en cours",
+ "hostSetupStateError": "Erreur",
+ "hostSetupStateUnsupported": "Non pris en charge",
+ "setupPathPending": "Chemin en attente",
+ "openSetup": "Ouvert",
+ "removeSetup": "Supprimer",
+ "hostSetupBlockedVersion": "Version du serveur Orca incompatible",
+ "hostSetupMissingCapability": "Mettez à jour Orca sur cet hôte pour configurer des projets",
+ "hostSetupConnectionRequired": "Connectez cet hôte avant d'importer ou de cloner le projet",
+ "setupProjectOnHost": "Configurer sur un autre hôte",
+ "setupProjectOnHostHelp": "Choisissez un hôte, puis importez un checkout existant, clonez-y le dépôt, ou suivez une configuration qui sera provisionnée plus tard.",
+ "setupExistingFolder": "Importer un dossier existant",
+ "setupExistingFolderHelp": "Rendez ce projet disponible sur un autre hôte en reliant un checkout qui y existe déjà.",
+ "setupExistingFolderPathPlaceholder": "/path/to/project/on/host",
+ "cloneUrlPlaceholder": "URL du dépôt",
+ "cloneDestinationPlaceholder": "/destination/on/host",
+ "setupKindGit": "Dépôt Git",
+ "setupKindFolder": "Dossier",
+ "settingUpHost": "Import...",
+ "setupHost": "Importer",
+ "cloningHost": "Clonage...",
+ "cloneHost": "Cloner",
+ "creatingPendingSetup": "Création...",
+ "createPendingSetup": "Suivre la configuration",
+ "499a437335": "Identité",
+ "hostSetupCheckingCapability": "Vérification des capacités de l'hôte",
+ "hostAvailability": "Disponibilité de l'hôte",
+ "hostAvailabilityHelp": "Ajoutez ce même projet sur un autre hôte connecté.",
+ "addToAnotherHost": "Ajouter à un autre hôte",
+ "addProjectHost": "Ajouter le projet à l'hôte",
+ "addProjectHostHelp": "Choisissez où ce projet doit aussi être disponible.",
+ "closeHostSetup": "Fermer",
+ "setupHostLabel": "Hôte",
+ "browseFolder": "Parcourir le dossier",
+ "browseFolderHelp": "Utilisez un checkout ou un dossier existant sur cet hôte.",
+ "otherWaysToAdd": "Autres moyens d'ajout",
+ "cloneFromUrl": "Cloner depuis une URL",
+ "cloneFromUrlHelp": "Clonez ce dépôt sur l'hôte sélectionné.",
+ "addPlannedHost": "Placeholder pour l'ajout d'hôte",
+ "addPlannedHostHelp": "Mémorisez cet hôte et terminez l'ajout du projet plus tard.",
+ "existingFolder": "Dossier existant",
+ "addPlannedHostToHost": "Ajouter {{host}}",
+ "addPlannedHostConfirm": "Ceci enregistre seulement que le projet doit être disponible sur cet hôte. Vous pourrez ajouter le dossier ou cloner plus tard.",
+ "projectRuntime": "Runtime des projets",
+ "projectRuntimeDescription": "Choisissez si ce projet s'exécute sous Windows ou WSL.",
+ "hostStateDisconnected": "Déconnecté",
+ "hostStateUnknown": "Inconnu",
+ "nestedHostLabel": "{{value0}} via {{value1}}",
+ "hostStateWorkspaceWindowClosed": "Fenêtre de l'espace de travail fermée",
+ "hostWorkspaceWindowClosedHelp": "Le serveur est joignable mais sa fenêtre Orca est fermée. Ouvrez Orca sur {{value0}} pour utiliser cette configuration.",
+ "externalWorktrees": "Worktrees externes",
+ "externalWorktreesDescription": "Définir si les worktrees créés hors d'Orca apparaissent pour ce projet.",
+ "externalWorktreesInherited": "Utilise le réglage global : {{value0}}",
+ "show": "Afficher",
+ "hide": "Masquer",
+ "externalWorktreesGlobal": "Réglage global par défaut : {{value0}}",
+ "useGlobal": "Utiliser la valeur globale"
+ },
+ "RepositorySourceControlAiActionRows": {
+ "548a6e1281": "Modèle de commande",
+ "7a3a8e431d": "Arguments CLI",
+ "2b2f38652b": "Commande personnalisée",
+ "0ffb081b3a": "Utiliser l'agent par défaut",
+ "f4310cf63f": "Agent",
+ "1cd88d470a": "Personnaliser",
+ "403876bb48": "Utiliser la valeur globale",
+ "f0aa2cfaea": "Recettes d'action"
+ },
+ "RepositorySourceControlAiCustomCommand": {
+ "0704dd55cd": "Commande du dépôt",
+ "e56668c291": "Utiliser la valeur globale",
+ "fbb77e122a": "Repli du dépôt pour les actions de texte qui sélectionnent Commande personnalisée.",
+ "ebffc5a28c": "Commande personnalisée",
+ "f9941f0caf": "par ex. ollama run llama3.1 {prompt}"
+ },
+ "RepositorySourceControlAiEnablement": {
+ "84233d1bb3": "Désactivé",
+ "bea897eec2": "Activé",
+ "62511a575d": "Utiliser la valeur globale",
+ "30ae6dcce8": "La valeur globale par défaut est",
+ "cf5959c834": "IA du contrôle de code source activée",
+ "show": "Afficher",
+ "hide": "Masquer",
+ "showActionsLabel": "Afficher les actions Source Control AI",
+ "visibilityHelper": "Détermine si les boutons IA du contrôle de code source sont affichés pour ce dépôt. La génération utilisée par des fonctionnalités distinctes suit les réglages de ces fonctionnalités. Réglage global par défaut : {{value0}}."
+ },
+ "RepositorySourceControlAiHostedReviewDefaults": {
+ "053ccfbf52": "Désactivé",
+ "777443bf89": "Activé",
+ "ffc3b26b26": "Utiliser la valeur globale",
+ "a68849a859": "La valeur globale par défaut est",
+ "aa6ee4b7d6": "Valeurs par défaut de création de hosted review",
+ "629ed8a9d3": "Ouvrir la hosted review après création",
+ "14f1eb99d0": "Générer les détails à l'ouverture de Create PR",
+ "d32b87e754": "Utiliser le template de review quand disponible",
+ "981eae7e14": "Brouillon par défaut"
+ },
+ "RepositorySourceControlAiSection": {
+ "67b3ff5467": "Abandonner",
+ "8b8bc5913a": "Recettes d'actions du dépôt. Les réglages globaux s'appliquent tant que ce dépôt ne les personnalise pas.",
+ "71b003b62b": "IA du contrôle de code source",
+ "152268c295": "Enregistrer",
+ "57e6e9d4b1": "Enregistrement...",
+ "ccb07dd027": "Enregistré",
+ "e57dde9d93": "Modifications non enregistrées"
+ },
+ "RuntimeAccessGrantList": {
+ "8b82879581": "Toute personne disposant d'une autorisation active peut se connecter jusqu'à sa révocation. Révoquer l'accès partagé déconnecte immédiatement les clients actifs.",
+ "68ec21309f": "Révoquer l'accès",
+ "6f6d5188ed": "Révoquer {{value0}}",
+ "87b16cd11d": "Créé",
+ "434e4a6af6": "Lien actuel",
+ "fd83b94095": "Aucun accès serveur partagé pour le moment.",
+ "27cf8507ad": "Actualiser l'accès partagé",
+ "f031182867": "Accès serveur partagé",
+ "df142657a5": "Pas encore utilisé",
+ "b18d1764ef": "Dernière utilisation {{value0}}"
+ },
+ "RuntimeEnvironmentsPane": {
+ "aeb26635d2": "Supprimer {{value0}}",
+ "af53761f31": "Annuler",
+ "bb90dd6487": "Supprimer le serveur",
+ "d2e00809e4": "Basculer",
+ "05e0fc3ebf": "Basculer vers",
+ "b2290ed203": "Orca mettra cet hôte au premier plan et chargera ses projets. Les terminaux et onglets de navigateur existants sur les autres hôtes restent actifs.",
+ "d570c35a99": "Changer de serveur",
+ "f3a3d6d834": "Capacités de {{value0}}",
+ "0ef838094a": "Protocole {{value0}}",
+ "9a91c4a0eb": "Compatible",
+ "86ed75bec8": "Mettre à jour le serveur",
+ "62ac182a27": "Mettre à jour le client",
+ "c8791efc45": "Statut indisponible",
+ "5120beaac6": "Vérification…",
+ "84b9b2be05": "Créez une autorisation d'accès révocable pour qu'un navigateur ou un autre client Orca puisse se connecter.",
+ "6e1280ca55": "Partager ce serveur Orca",
+ "9a3758d983": "Aucun serveur enregistré.",
+ "9bee6bbeeb": "Ajouter un serveur",
+ "55fcc964cd": "sur le serveur et collez l'URL d'appairage affichée.",
+ "960e901ae4": "orca serve --pairing-address ",
+ "163671f7b5": "Exécution",
+ "c3d772c514": "orca://pair?code=...",
+ "9bc9b83474": "Code d'appairage",
+ "e038625857": "Machine de dev",
+ "54ebacc600": "Nom du serveur",
+ "1826bd0608": "Serveurs enregistrés",
+ "6ce4664003": "Actualiser les serveurs",
+ "b07070ed3c": "Aucun serveur connecté",
+ "78692becbd": "Bureau local",
+ "64b6bea541": "Serveur actif",
+ "99ac81fb43": "Passé à {{value0}}.",
+ "b5b5114cb0": "{{value0}} supprimé.",
+ "6cb6eae14f": "Échec de l'enregistrement de l'environnement d'exécution.",
+ "7b5986c8df": "{{value0}} enregistré. Utilisez « Serveur actif » pour basculer quand vous le souhaitez.",
+ "5ef712f407": "Un serveur nommé « {{value0}} » existe déjà.",
+ "0c55a47480": "Le nom et le code d'appairage sont requis.",
+ "e6410d72c3": "Échec du chargement des environnements d'exécution.",
+ "6ef71985da": "Aucun endpoint",
+ "ed3e3f069d": "Ceci supprime le serveur enregistré d'Orca. Cela ne change pas le serveur actif.",
+ "b2fda48c39": "Supprimer le serveur actif déconnecte ce navigateur de cet hôte. Les sessions existantes de l'hôte ne sont pas touchées.",
+ "9f7665a01b": "Supprimer le serveur actif fait d'abord repasser Orca sur Bureau local. Les sessions existantes de l'hôte ne sont pas touchées.",
+ "3595fd1948": "Nouveau lien",
+ "54dee18f5c": "Masquer le formulaire",
+ "8cf8790697": "Les serveurs enregistrés acheminent ce navigateur via un runtime Orca appairé.",
+ "f75ce1c7a5": "Local conserve le comportement bureau actuel. Les serveurs enregistrés acheminent les appels client pris en charge via le runtime distant.",
+ "d25f0688b1": "Supprimer",
+ "4b5c6d7e8f": "Aucune capacité signalée",
+ "hostModelCapabilityUnknown": "Prise en charge des modèles côté hôte : vérification des capacités du serveur",
+ "hostModelCapabilitySupported": "Prise en charge des modèles côté hôte : prête",
+ "hostModelCapabilityMissing": "Prise en charge des modèles côté hôte : mettre à jour le serveur pour {{value0}}",
+ "hostModelCapabilityProjectSetup": "configuration de projet",
+ "hostModelCapabilityTaskSourceContext": "contexte de source de tâche",
+ "hostModelCapabilityWorkspaceRunContext": "contexte d'exécution de l'espace de travail",
+ "3f67e8078a": "Utilise cet ordinateur par défaut. Ne choisissez un serveur enregistré que si vous voulez faire passer projets, fichiers, terminaux, vérifications de fournisseurs et passation navigateur/mobile pris en charge par ce serveur.",
+ "2c85efb3e8": "Sélectionner un serveur enregistré fait de ce runtime Orca appairé l'Hôte par défaut de ce navigateur.",
+ "serverConnected": "Connecté",
+ "serverChecking": "Vérification…",
+ "serverDisconnected": "Déconnecté",
+ "disconnectedServer": "Déconnecté de {{value0}}.",
+ "connectToRemoteServers": "Se connecter aux serveurs distants",
+ "connectToRemoteServersHelp": "Appairez un autre runtime Orca, puis connectez-le ou déconnectez-le ici.",
+ "activeServerRowHelp": "Serveur actif pour les projets, terminaux et vérifications de fournisseurs routés via le serveur.",
+ "disconnect": "Déconnecter",
+ "connect": "Se connecter",
+ "advanced": "Avancé",
+ "serverDetails": "Détails du serveur",
+ "advertiseThisApp": "Annoncer cette app comme serveur",
+ "advertiseThisAppHelp": "Créez des liens d'accès pour que des navigateurs, des clients mobiles ou un autre client Orca se connectent à cette app en cours d'exécution.",
+ "runtimeReachable": "{{value0}} est joignable.",
+ "updateAvailableOne": "1 mise à jour disponible",
+ "updatesAvailable": "{{value0}} mises à jour disponibles",
+ "versionUnavailable": "Version d'Orca indisponible",
+ "updateServer": "Mettre à jour",
+ "reviewServerUpdates": "Rechercher des mises à jour du serveur",
+ "updatingServers": "Mise à jour des serveurs…",
+ "orcaVersion": "Orca v{{value0}}",
+ "removeActiveServerBlocked": "Choisissez un autre Serveur actif dans Avancé avant de supprimer ce serveur.",
+ "removeActiveServerDescription": "Choisissez un autre Serveur actif dans Avancé avant de supprimer ce serveur. Les sessions existantes de l'hôte ne sont pas touchées.",
+ "workflow": "Flux de travail avec serveur distant",
+ "connectWorkflow": "Se connecter à un hôte",
+ "connectWorkflowHelp": "Cette app rejoint une autre machine",
+ "shareWorkflow": "Partager cet hôte",
+ "shareWorkflowHelp": "D'autres appareils rejoignent cette machine",
+ "troubleshootWorkflow": "Dépannage de la connexion",
+ "sshTunnelRequired": "Tunnel SSH requis",
+ "troubleshootTitle": "Créez un nouveau lien sur l'autre hôte",
+ "troubleshootDescription": "Un lien qui utilise 127.0.0.1 pointe vers l'appareil qui l'ouvre, pas vers l'ordinateur qui l'a créé.",
+ "troubleshootStepShare": "Sur l'autre ordinateur, ouvrez « Partager cet hôte ».",
+ "troubleshootStepAddress": "Choisissez « Autre appareil » et sélectionnez son adresse Tailscale ou LAN.",
+ "troubleshootStepRegenerate": "Générez un nouveau lien d'accès et utilisez ici uniquement le lien le plus récent.",
+ "troubleshootTunnel": "Vous utilisez une redirection locale SSH ? Revenez à « Se connecter à un hôte », collez le lien loopback, puis activez « J'utilise un tunnel SSH » sous « Avancé ».",
+ "cloudVmWorkflow": "VM cloud",
+ "cloudVmWorkflowHelp": "Gérer les machines cloud créées par recette"
+ },
+ "RuntimePairingGeneratedUrlRows": {
+ "0495f68959": "Copier {{value0}}"
+ },
+ "RuntimePairingUrlGenerator": {
+ "849825e829": "Collez cette URL d'appairage dans un autre client Orca.",
+ "2e5c4e3c93": "Appairer un autre client Orca",
+ "f7cafdc9f3": "Lien navigateur indisponible dans cette version. L'URL d'appairage reste fonctionnelle pour les clients Orca.",
+ "6b9ca3e69b": "Ouvrir dans le navigateur",
+ "1ca2e5194d": "Utilisez cette URL depuis un navigateur capable de joindre l'adresse sélectionnée.",
+ "8de0f84fff": "Générer un lien d'accès",
+ "279e0dcb57": "127.0.0.1 ne fonctionne que sur cet ordinateur. Utilisez une adresse LAN, Tailscale ou personnalisée pour un autre appareil.",
+ "45cf476df3": "hôte, hôte:port ou wss://host/path",
+ "4531ea3158": "Adresse personnalisée",
+ "360c548cf3": "Actualiser les adresses de connexion",
+ "de6d5cff95": "Cet ordinateur (",
+ "de77eb1b65": "Adresse de connexion",
+ "ff80904fc4": "Créez un droit d'accès révocable pour les clients navigateur ou bureau.",
+ "f8500e134a": "Partager ce serveur Orca",
+ "d6c081adf4": "Échec de la copie de l'URL.",
+ "df0aa45a86": "URL d'appairage copiée.",
+ "13704d635e": "URL du client web copiée.",
+ "e8d83f2b0f": "Échec de la révocation de l'accès partagé.",
+ "9f8e037c4a": "Accès partagé révoqué.",
+ "d797f516b1": "L'accès partagé était déjà révoqué.",
+ "2ed55c841a": "Échec de la génération de l'URL d'appairage.",
+ "11d5248e62": "URL d'appairage générée.",
+ "6dd594a507": "URL du client web générée.",
+ "2752126f3e": "L'appairage à l'exécution est indisponible.",
+ "95b8be4cea": "Échec de l'actualisation des interfaces réseau.",
+ "1b4e0bbcc5": "Échec du chargement des droits d'accès partagés.",
+ "b91e36a986": "web",
+ "custom-option": "{{address}} (personnalisée)",
+ "add-custom": "Ajouter une adresse personnalisée…",
+ "custom-title": "Adresse de connexion personnalisée",
+ "custom-description": "Annoncez une adresse joignable par un autre appareil — un hôte LAN ou Tailscale, ou une URL ws(s):// complète.",
+ "custom-hint": "Saisissez un hôte, un hôte:port ou une URL ws(s)://.",
+ "custom-cancel": "Annuler",
+ "custom-use": "Utiliser l'adresse",
+ "intentQuestion": "Où ce lien sera-t-il ouvert ?",
+ "anotherDevice": "Autre appareil",
+ "anotherDeviceHelp": "Tailscale, LAN ou autre adresse joignable",
+ "localOnly": "Cet ordinateur uniquement",
+ "localOnlyHelp": "Un navigateur ou un client Orca sur cet ordinateur",
+ "customAddress": "Adresse personnalisée",
+ "customAddressHelp": "Tunnel SSH, proxy inverse ou nom d'hôte personnalisé",
+ "recommended": "Recommandé",
+ "localLink": "Lien local uniquement",
+ "localLinkHelp": "Ce lien ne fonctionne que dans un navigateur ou un client Orca exécuté sur cet ordinateur.",
+ "noExternalAddress": "Aucune adresse pour un autre appareil n'a été trouvée. Connectez cet ordinateur à un LAN ou à Tailscale, actualisez, ou choisissez « Adresse personnalisée ».",
+ "staleAddress": "L'adresse de connexion a changé. Générez un nouveau lien pour {{address}}.",
+ "customInvalid": "Saisissez un hôte, un hôte:port, une adresse IPv6 ou une URL ws(s):// valide."
+ },
+ "Settings": {
+ "3bf149e873": "Paramètres du projet > {{value0}}",
+ "075341c763": "Nouvelles fonctionnalités encore en cours de construction. Essayez-les.",
+ "8b017f2506": "Expérimental",
+ "499c1cd7f9": "Paramètres de compatibilité bas niveau pour le dépannage.",
+ "1c87f8d024": "Avancé",
+ "c1b43dc4e2": "Données d'utilisation anonymes et contrôles de télémétrie.",
+ "d7e3f62d70": "Confidentialité & télémétrie",
+ "9b83cc62c2": "Accès de confidentialité macOS pour les outils de développement lancés depuis le terminal.",
+ "65660d4548": "Autorisations macOS",
+ "c6c01ac209": "Contrôlez les terminaux et les agents depuis votre téléphone.",
+ "c40dadaac8": "Mobile",
+ "c2ee313198": "Utilisez des machines existantes via SSH pour les fichiers, les terminaux, Git et les espaces de travail.",
+ "9b02492d1f": "Hôtes SSH",
+ "b5ee17826b": "Appairez des runtimes Orca distants pour des sessions persistantes, un état distant plus riche et un transfert web ou mobile.",
+ "7686cb5c36": "Connectez ce navigateur à un serveur Orca enregistré.",
+ "bd0181eeca": "Serveurs Orca distants",
+ "8acf3f22e0": "Statistiques Orca, plus analyses de tokens Claude, Codex, OpenCode et utilisation d'abonnement Grok.",
+ "954a8f5aef": "Statistiques & usage",
+ "a737a4bb22": "Raccourcis clavier pour les actions courantes.",
+ "23bf7a1ad4": "Raccourcis",
+ "7210ac09c4": "Notifications natives du bureau pour l'activité des agents et les événements du terminal.",
+ "9907545fa3": "Notifications",
+ "d0b7021d64": "Comportement de sélection et d'édition.",
+ "d7a3e635b6": "Saisie & édition",
+ "6d1a27e193": "Thème, zoom, apparence de l'app et du terminal, barres latérales et barre d'état.",
+ "2b4474780a": "Apparence",
+ "3d9adfe6a5": "Onglets globaux de terminal, de navigateur et de markdown.",
+ "3eb22a3ada": "Espace de travail flottant",
+ "01f9d36292": "Configurez la prise en charge des émulateurs mobiles pour Orca et les agents de codage.",
+ "f75daf1002": "Émulateur mobile",
+ "ad9788036f": "Page d'accueil, routage des liens et cookies de session.",
+ "c46215ea03": "Navigateur",
+ "6742c7932c": "Commandes de terminal enregistrées, globales ou par projet.",
+ "13d4fe30ad": "Commandes rapides",
+ "b79b5b31e9": "Shells, moteur de rendu, sessions et comportement du terminal.",
+ "3de4bbb841": "Terminal",
+ "dd72ed437a": "Choisissez les fournisseurs de tâches affichés dans la page Tâches et la barre latérale.",
+ "11faa2f7dd": "Sources de tâches",
+ "cfa34f4465": "Nommage des branches, refs de base et Git AI Author.",
+ "70100f94c7": "Git & gestion de code source",
+ "b07041697f": "Connectez GitHub, GitLab, Linear et les services d'hébergement de sources.",
+ "c9ca101a3b": "Intégrations",
+ "f9b77539fd": "Valeurs par défaut des espaces de travail, configuration de l'app et maintenance.",
+ "7807c11c4d": "Général",
+ "6855b0f77d": "Terminez les workflows essentiels qui rendent Orca utile au travail parallèle avec des agents.",
+ "6d119427ef": "Checklist d'intégration",
+ "eb1176a14e": "Dictée vocale locale avec modèles embarqués sur l'appareil.",
+ "5063bb47a5": "Voix",
+ "7118953f14": "Permettez aux agents de contrôler n'importe quelle app de votre ordinateur.",
+ "c9841721cb": "Computer Use",
+ "475980f53d": "Coordonnez plusieurs agents de codage via Orca.",
+ "00c3a7950d": "Orchestration",
+ "21f09426ea": "Facultatif. Orca fonctionne avec vos connexions de fournisseurs existantes ; ajoutez des comptes uniquement si vous voulez qu'Orca aide à basculer entre eux.",
+ "ad6c529693": "Comptes de fournisseurs d'IA",
+ "ec1ba547f7": "Gérez les agents IA, définissez-en un par défaut et personnalisez les commandes.",
+ "8afa676615": "Agents",
+ "add3b97ee6": "\"",
+ "3c88ec55d6": "Aucun paramètre trouvé pour \"",
+ "c7ad095d96": "Chargement des paramètres...",
+ "acc7bbdefd": "Appuyez à nouveau sur ESC pour quitter les paramètres",
+ "43b68e10f0": "Vous avez des modifications Git AI Author non enregistrées. Quitter les abandonnera.",
+ "17bdee4ff1": "Abandonner les modifications Git AI Author non enregistrées ?",
+ "084d8fac5b": "Confidentialité et sécurité",
+ "23931df7e8": "Hôtes distants",
+ "mobile_group": "Mobile",
+ "8bd117d669": "Interface",
+ "e1578cd4bc": "Workflows",
+ "9abb9be3bc": "Configurer",
+ "23c6874fdf": "Capacités IA",
+ "2309068a6f": "destructive",
+ "65358016ea": "Abandonner",
+ "dev": "Outils dev",
+ "devDescription": "Outils réservés au développement pour exercer les états de l'UI.",
+ "linearTitle": "Linear",
+ "linearDescription": "Fonctionnement de Linear dans Orca, checklist de configuration, skill d'agent et exemples de prompts.",
+ "tasksDescription": "Connectez des fournisseurs, installez le skill Linear et choisissez ce qui apparaît dans Tâches."
+ },
+ "SettingsFormControls": {
+ "42a4d15a30": "Aucune police correspondante.",
+ "b55371ea18": "Polices",
+ "c766f8ac75": "Activer/désactiver les suggestions de polices",
+ "74bcecd5ec": "Effacer",
+ "a4ff6143f8": "Effacer la sélection de police",
+ "b661b034ec": "· Par défaut :",
+ "builtin_themes": "Intégrée",
+ "ceefb9d7f1": "Aucun thème trouvé.",
+ "imported_from": "Importé depuis {{value0}}",
+ "imported_themes": "Importé",
+ "9119fb2268": "Actuel",
+ "4e11f87ca6": "Affichage de",
+ "fbb428db98": "Sélectionné :",
+ "fac59213fc": "Rechercher parmi les thèmes intégrés",
+ "search_terminal_themes": "Rechercher des thèmes de terminal",
+ "cb330ef7f8": " sur {{value0}}",
+ "c822571b2e": " correspondant à \"{{value0}}\"",
+ "3119c012a5": "string"
+ },
+ "SettingsSidebar": {
+ "e0900f83e7": "SSH",
+ "5c9669ff9c": "Projets",
+ "dbceaa8840": "Rechercher dans les paramètres",
+ "60f8a673a7": "Retour à l'application",
+ "6503182299": "Checklist d'intégration",
+ "82db1b7de4": "Checklist d'intégration, {{value0}} sur {{value1}} terminés. Afficher le guide de configuration.",
+ "df38d612b7": "Aucun projet ajouté pour le moment.",
+ "3e483e256b": "Aucun paramètre de projet correspondant."
+ },
+ "ShortcutFilterRail": {
+ "28b63545bf": "Statut",
+ "8a1e78c14b": "Filtres d'état des raccourcis",
+ "df8466f3fc": "Effacer la recherche de raccourcis",
+ "f733c4b89f": "Rechercher une commande ou des touches",
+ "02dc7d4251": "Trouver des raccourcis",
+ "1d5634ba31": "Raccourci {{value0}}"
+ },
+ "ShortcutRowsList": {
+ "4ce3cd24d9": "Aucun raccourci ne correspond à ces filtres."
+ },
+ "ShortcutTerminalPolicyControl": {
+ "0762983d13": "Terminal en priorité",
+ "63308571d8": "Orca en priorité",
+ "c43c7ff5f9": "Décidez qui intercepte en premier les raccourcis",
+ "c3a554288e": "Raccourcis dans le terminal",
+ "0f55c6f15c": "Choisissez si Orca ou le terminal ciblé l'emporte lorsque des raccourcis se chevauchent."
+ },
+ "ShortcutsPane": {
+ "4b7ae34062": "directement.",
+ "38e86e206a": "Personnalisez les raccourcis visuellement ou modifiez",
+ "47f8f7aef9": "Raccourcis clavier",
+ "f0b35b0b2e": "Désactivé lorsqu'un terminal ou une TUI a le focus clavier.",
+ "5c65d5db9d": "Terminal en priorité",
+ "dfa8ff612f": "S'exécute également lorsqu'un terminal ou une TUI a le focus clavier.",
+ "2a0e8aeccf": "Orca en priorité",
+ "3c0fac059a": "S'exécute quand même lorsqu'un terminal a le focus clavier.",
+ "25b0004fbf": "Terminal actif",
+ "781cb74d22": "S'exécute depuis les volets de terminal.",
+ "cb02e00202": "Terminal",
+ "d8c988dab4": "~/.orca/keybindings.json",
+ "shortcutUnavailable": "Le raccourci n'est plus disponible."
+ },
+ "SourceControlAiActionRecipeDefaults": {
+ "2576299196": "args",
+ "b3914ecbbc": "Abandonner",
+ "fb09da4345": "Modèle de commande",
+ "2cb4bb7e5d": "Arguments CLI",
+ "0740d30915": "Commande personnalisée",
+ "ee0e5c2a48": "Utiliser l'agent par défaut",
+ "bf84dea6af": "Utilisez les variables uniquement si vous voulez qu'Orca injecte du contexte. Laissez l'agent sur « Par défaut » pour suivre votre préférence d'agent habituelle.",
+ "a79c567194": "Recettes d'action",
+ "cf01d41bce": "Agent, arguments CLI et modèle de commande utilisés par chaque bouton IA de contrôle de code source.",
+ "a9359c8aa9": "Erreur inconnue",
+ "b5f46664d3": "Échec de l'enregistrement de l'action IA par défaut du contrôle de code source : {{value0}}",
+ "d18d665e12": "Enregistrer",
+ "4f549a5fa8": "Enregistrement...",
+ "9d3cc627f8": "Enregistré",
+ "817128d94e": "Modifications non enregistrées",
+ "7ab1437a12": "pull request",
+ "e5b24893ba": "commit",
+ "06a9dab64d": "checks",
+ "cb67b938c5": "fix",
+ "2037c78a6f": "template",
+ "eb7e8f3b39": "model",
+ "d74fdc776c": "command",
+ "673369fe0c": "cli",
+ "db9bd75d10": "arguments",
+ "926d58e87f": "agent"
+ },
+ "SparsePresetSettingsSection": {
+ "6fa754d20f": "Supprimer",
+ "fe1f2c6572": "Modifier {{value0}}",
+ "88bfbf1a9c": "Aucun préréglage sparse enregistré pour ce dépôt.",
+ "d7565029a9": "Nouveau préréglage",
+ "17f8c4ce10": "Gérez les ensembles de répertoires enregistrés pour la création de worktrees sparse.",
+ "388513be2d": "Préréglages de sparse checkout",
+ "a05bc9183f": "Enregistrer le préréglage",
+ "2d7d45e991": "Annuler",
+ "c240a16f25": "Utilisez des chemins relatifs au dépôt, comme packages/web ou apps/api.",
+ "fde7ff2cc3": "packages/web shared/ui",
+ "caf33029cc": "Répertoires",
+ "3b6f1abd3e": "ex. web-only",
+ "a6fcdd9e3c": "Nom",
+ "b9922ec194": "Annuler la modification du préréglage",
+ "694cc55ecb": "Les répertoires enregistrés sont utilisés lors de la création de worktrees sparse pour ce dépôt.",
+ "8b64731aaf": "+{{value0}} autres",
+ "755c6a1a0d": "Confirmer",
+ "a7bcf206b1": "Suppression en cours",
+ "ba9ad2d4cd": "Date de mise à jour inconnue",
+ "568d7e1e49": "{{value0}} mis à jour",
+ "d7b3f0bdc3": "{{value0}} répertoires",
+ "9d3c087fc0": "1 répertoire",
+ "8deb7024ab": "Chargement des préréglages sparse...",
+ "92c08ccae3": "Impossible de charger les préréglages sparse.",
+ "3dfa765ca7": "{{value0}} répertoires seront enregistrés.",
+ "b532b9c17d": "1 répertoire sera enregistré.",
+ "623b4cf910": "Modifier le préréglage",
+ "68bbcd864a": "nouveau",
+ "2ef2b2674b": "Supprimer {{value0}}"
+ },
+ "SshDestructiveActionDialog": {
+ "895b216267": "Annuler"
+ },
+ "SshPane": {
+ "c0f1c80166": "Aucune cible SSH configurée.",
+ "639ceb3698": "Ajouter une cible",
+ "51d7dba44d": "Importer",
+ "a7d28dff81": "Ajoutez un hôte distant pour vous y connecter depuis Orca.",
+ "94c5284560": "Cibles",
+ "f495689b82": "Échec de l'importation",
+ "f8050f6307": "Synchronisation terminée : {{value0}} serveur{{value1}}",
+ "68c13b4589": "Échec du test",
+ "81d08bcddf": "Connexion réussie",
+ "2c4ee7332b": "Échec de la réinitialisation du relais distant",
+ "db2e48975e": "Relais distant réinitialisé",
+ "025e107643": "Échec de l'arrêt des terminaux distants",
+ "90e308c98b": "Terminaux distants arrêtés",
+ "a43de1d3ee": "Échec de la déconnexion",
+ "e95d5ae10e": "Échec de la connexion",
+ "c2a69510e3": "Échec de la suppression de la cible",
+ "a0237eb1ca": "Cible supprimée",
+ "2227ce47b6": "Échec de l'enregistrement de la cible",
+ "f602009125": "Cible ajoutée",
+ "b4ba0ce33d": "Cible mise à jour",
+ "3879cbaa52": "Le délai d'inactivité du terminal doit être compris entre 60 et {{value0}} secondes, ou maintenez les terminaux actifs jusqu'à la réinitialisation.",
+ "4db9afce1c": "Le port doit être compris entre 1 et 65535",
+ "0e5aa04161": "Un hôte ou un alias de config SSH est requis",
+ "f1fc50dad2": "Échec du chargement des cibles SSH",
+ "0cda732f43": "Échec du test de connexion"
+ },
+ "SshPassphraseDialog": {
+ "d5a234456f": "Annuler",
+ "c3ce71aad6": "Saisissez la phrase secrète",
+ "abaa0dc653": "Saisissez le mot de passe",
+ "ce4fdf7914": "Saisissez la phrase secrète pour",
+ "dbf9b6f2d0": "Saisissez le mot de passe pour",
+ "c55f105262": "Échec de l'annulation de la demande d'identifiants SSH",
+ "b8e88fd0de": "Échec de l'envoi des identifiants SSH",
+ "405066423c": "Déverrouiller",
+ "bec2c1318f": "Se connecter",
+ "8a349e3fac": "Phrase secrète pour {{value0}}",
+ "cab3d5f5a5": "Mot de passe pour {{value0}}",
+ "1f3dde805d": "Phrase secrète de la clé SSH",
+ "106bd57f4a": "Mot de passe SSH"
+ },
+ "SshTargetCard": {
+ "a883f5a00f": "délai d'inactivité du terminal : {{value0}}",
+ "8ce71262f4": "terminaux jusqu'à la réinitialisation",
+ "ec6543cee9": "Se connecter",
+ "0e53e9f8e8": "Tester",
+ "1810b51482": "Connexion",
+ "4c86f30877": "Déconnecter",
+ "7f7b3d7ab4": "Supprimer la cible",
+ "3d21a22d0e": "Suppression de la cible",
+ "3d8af2949f": "Modifier la cible",
+ "762a48c662": "Réinitialiser le relais distant",
+ "97dea4e8cf": "Réinitialisation du relais distant",
+ "da16e108e6": "Arrêter les terminaux distants",
+ "c77f1abfe3": "Arrêt des terminaux distants",
+ "18968ede9e": "Erreur",
+ "f0871e6bfb": "connect",
+ "47e94bd6ba": "connecté"
+ },
+ "SshTargetDestructiveActions": {
+ "7e66942808": "Ceci arrêtera les sessions de terminal actives sur cette cible SSH. La reconnexion ne les restaurera pas.",
+ "accf177a03": "Arrêter les terminaux distants ?",
+ "26be00392d": "Ceci force l'arrêt du relais distant pour cette cible SSH. Les terminaux distants actifs et les redirections de port de cette cible seront terminés.",
+ "570a7a0574": "Réinitialiser le relais distant ?",
+ "3bb0cf0ee4": "Ceci supprimera la cible et arrêtera tous les terminaux distants actifs.",
+ "4808966c41": "Supprimer la cible SSH"
+ },
+ "SshTargetForm": {
+ "fea9cb402e": "Annuler",
+ "1b19b00e93": "Les délais limités doivent être compris entre 60 secondes et 7 jours.",
+ "7c13f58c91": "Jusqu'à la réinitialisation",
+ "55c56cf2c7": "Délai après déconnexion (secondes)",
+ "137e88ce8d": "Les terminaux distants continuent de s'exécuter après la déconnexion d'Orca de cet hôte.",
+ "b574994adc": "Utilisez « Arrêter les terminaux distants » ou « Réinitialiser le relais » quand vous voulez les arrêter.",
+ "71fc546097": "Garder les terminaux actifs jusqu'à la réinitialisation",
+ "92f80edbfd": "Persistance des terminaux distants",
+ "feae1d1e69": "Facultatif. Équivalent à ProxyJump / ssh -J.",
+ "11bcb4507a": "bastion.example.com",
+ "b2ab248ded": "Hôte de rebond",
+ "3b01ca44a0": "Facultatif. Utilisé pour le tunneling (ex. Cloudflare Access, ProxyCommand).",
+ "f42d844544": "ex. cloudflared access ssh --hostname %h",
+ "c7d0e18ecb": "Commande de proxy",
+ "cb91f6375c": "Facultatif. L'agent SSH est utilisé par défaut.",
+ "d6a5f2ee5c": "~/.ssh/id_ed25519 (laisser vide pour l'agent SSH)",
+ "63c0c145c1": "Fichier d'identité",
+ "c94cfa634c": "Port",
+ "47e082bc17": "deploy",
+ "dc1dc52aaa": "Nom d'utilisateur",
+ "2ee9bcd2e8": "server, deploy@server:2222, ssh://server",
+ "ce370ce674": "Hôte ou alias *",
+ "b8dab0aa7b": "Mon serveur",
+ "298de87a88": "Label",
+ "9518545cb6": "Ajouter une cible",
+ "a62b4cb39a": "Enregistrer les modifications",
+ "29af933cd5": "Nouvelle cible SSH",
+ "f2331ce599": "Modifier la cible SSH",
+ "4a342f44c1": "Connexion avancée",
+ "e9609ddca6": "Proxy, hôte de rebond et réutilisation de la connexion",
+ "8c922dffba": "Réutiliser la connexion SSH pour une configuration plus rapide",
+ "53e9aabfc0": "Utilise le multiplexage OpenSSH quand il est disponible. Désactivez pour les hôtes soumis à des restrictions SSH particulières.",
+ "editTitle": "Modifier l'hôte SSH",
+ "addTitle": "Ajouter un hôte SSH",
+ "editDescription": "Mettez à jour les détails de connexion de cette machine. Les modifications s'appliquent à la prochaine connexion.",
+ "addDescription": "Ajoutez une machine permanente sur laquelle vous pouvez vous connecter en SSH.",
+ "editingPrefix": "Modification"
+ },
+ "TasksPane": {
+ "f71d8a9dd3": "Fournisseurs de tâches",
+ "6b23a34f6d": "Jira",
+ "09ae2d7c51": "Linear",
+ "7c5d7fdc20": "GitLab",
+ "e14063e727": "GitHub",
+ "connectProviderTitle": "Connecter {{provider}}",
+ "connectCodeHostDescription": "Installez et authentifiez la CLI sous Intégrations pour qu'Orca puisse charger les issues.",
+ "connectionCheckUnavailable": "Orca n'a pas pu vérifier cette connexion. Réessayez, ou ouvrez Intégrations pour le détail de la configuration.",
+ "retryConnection": "Réessayer",
+ "openIntegrations": "Intégrations",
+ "connectInIntegrations": "Configurer dans Intégrations",
+ "connectJiraTitle": "Connecter Jira",
+ "connectJiraDescription": "Ajoutez un site Jira Cloud ou une instance auto-hébergée avec un jeton d'API ou un PAT.",
+ "manageJira": "Gérer les clés",
+ "addJira": "Ajouter un accès Jira",
+ "githubDescription": "Parcourez les issues GitHub et lancez des espaces de travail à partir de celles-ci.",
+ "gitlabDescription": "Parcourez les issues GitLab et lancez des espaces de travail à partir de celles-ci.",
+ "linearDescription": "Connectez Linear, installez le skill d'agent et affichez-le dans Tâches.",
+ "jiraDescription": "Connectez Jira Cloud ou Jira auto-hébergé et affichez-le dans Tâches.",
+ "setupTitle": "Configuration de la gestion des tâches",
+ "setupDescription": "Terminez connexion + visibilité pour chaque fournisseur au même endroit. Linear nécessite aussi le skill d'agent afin que les agents de codage puissent lire et mettre à jour les tickets. Au moins un fournisseur doit rester visible.",
+ "incompleteBannerTitle": "Certains fournisseurs visibles doivent encore être configurés",
+ "incompleteBannerBody": "Masquez les fournisseurs que vous n'utilisez pas, ou dépliez une carte et terminez ses étapes.",
+ "providersDescription": "Chaque carte décrit la connexion (et le skill pour Linear), ainsi que son affichage dans Tâches.",
+ "integrationsHint": "Les identifiants de tous les fournisseurs se trouvent aussi sous",
+ "integrationsLink": "Intégrations",
+ "skillHint": ". Une fois Linear connecté, des exemples d'utilisation restent disponibles sous Paramètres → Linear.",
+ "incompleteBannerBodyWithLinear": "Masquez les fournisseurs que vous n'utilisez pas, ou dépliez une carte et terminez ses étapes. Pour Linear : accès API, skill d'agent et Afficher dans Tâches."
+ },
+ "TerminalAppearanceSection": {
+ "a14a427ae4": "Épaisseur de la ligne de séparation des volets.",
+ "f27a99978d": "Épaisseur du séparateur",
+ "db632cb50e": "Opacité appliquée aux volets qui ne sont pas actuellement actifs.",
+ "a6fdd6a3b1": "Opacité des volets inactifs",
+ "1b79379d4f": "Contrôle l'assombrissement des volets inactifs et l'épaisseur du séparateur.",
+ "e1a5c25555": "Volets de terminal",
+ "04cdf85dec": "Opacité du curseur du terminal.",
+ "b9f1804422": "Opacité du curseur",
+ "2de6b5a699": "Utilise la variante clignotante de la forme de curseur sélectionnée.",
+ "74736cc9b1": "Curseur clignotant",
+ "2e5aec3cf6": "Souligné",
+ "52854a5608": "Bloc",
+ "e070e8aeba": "Barre",
+ "db270cc9a9": "Forme du curseur",
+ "d455f2ef4f": "Apparence par défaut du curseur pour les volets de terminal Orca.",
+ "abcb4dd019": "Curseur du terminal",
+ "70beb1bbc7": "Aperçu",
+ "31f6e61085": "Les ligatures sont actuellement",
+ "870377082f": "Désactivé",
+ "84bd22f2cd": "Activé",
+ "bc9ff84d61": "Auto",
+ "be8da35e7f": "Ligatures de police",
+ "4b1f29598e": "Auto - désactivées pour \"{{value0}}\".",
+ "400e950ca5": "Auto - activées pour \"{{value0}}\".",
+ "04569feb07": "Toujours désactivées, même pour les polices qui en proposent.",
+ "7234abcd08": "Toujours activées. Les polices sans ligatures s'affichent simplement telles quelles.",
+ "7233d594bf": "Affiche les ligatures de programmation (ex. =>, !=, ===) pour les polices qui en proposent. \"Auto\" n'active les ligatures que pour les polices réputées pour leurs ligatures (Fira Code, JetBrains Mono, Cascadia Code, Iosevka, etc.).",
+ "bafc80efbc": "Contrôle le multiplicateur de hauteur de ligne du terminal.",
+ "c084eb7d4c": "Hauteur de ligne",
+ "36af8ad94c": "Contrôle la graisse de la police du texte du terminal.",
+ "4aae5db258": "Graisse de la police",
+ "f04b17a50e": "Famille de polices du terminal par défaut pour les nouveaux volets et les mises à jour en direct.",
+ "a408266e67": "Famille de polices",
+ "855a76343a": "Importer depuis Ghostty",
+ "711e589f18": "Typographie du terminal par défaut pour les nouveaux volets et les mises à jour en direct.",
+ "048aac8a64": "Typographie du terminal",
+ "4415beb958": "désactivé",
+ "4e7d41a9f0": "activé",
+ "e90afcc44f": "désactivé",
+ "16c471ee03": "activé",
+ "typographyAdvanced": "Typographie",
+ "dimUnfocusedPanes": "Assombrir les volets sans focus."
+ },
+ "TerminalAdvancedTypographyControls": {
+ "f5fa1a08f1": "Graisse de la police en gras",
+ "0e0d1ee15a": "À ajuster indépendamment de Graisse de la police. Certaines polices associent plusieurs valeurs à un même dessin ; baissez la graisse ou choisissez une autre police si le gras semble inchangé."
+ },
+ "TerminalFontSizeSetting": {
+ "9b5252c85a": "px",
+ "0f4c92e595": "Taille de police du terminal par défaut pour les nouveaux volets et les mises à jour en direct.",
+ "a4a352b1e9": "Taille de police"
+ },
+ "TerminalPane": {
+ "4263e940e0": "L'appui sur la touche Yen JIS (¥) envoie un antislash (\\\\) à la place.",
+ "19f4935159": "Yen JIS (¥) vers antislash (\\\\)",
+ "1c337bef4a": "Contrôle si l'appui sur la touche Yen JIS (¥) envoie un antislash (\\\\) à la place.",
+ "3fe1c5bfe0": "Désactivé",
+ "c73d510938": "Droite",
+ "e7aec1fd60": "Gauche",
+ "badb1219fc": "Les deux",
+ "43c2ff7b0e": "Auto",
+ "0a10420e1a": "Option comme Alt",
+ "ce3aadf0b2": "La touche Option {{value0}} envoie Alt/Esc ; l'autre compose des caractères spéciaux.",
+ "b62373091a": "Les deux touches Option envoient des séquences Alt/Esc.",
+ "d8998bb328": "Option compose des caractères spéciaux selon votre disposition de clavier.",
+ "d21c493808": "Auto — détecté : {{value0}}.",
+ "2561d3fc1b": "Contrôle si la touche Option macOS envoie des séquences Alt/Esc ou compose des caractères.",
+ "96be03b8eb": "PowerShell 7+",
+ "d26174e1dd": "Windows PowerShell",
+ "fe20f79dd1": "Version de PowerShell",
+ "822f62ddcd": "Télécharger PowerShell 7+",
+ "a016ffbeed": "Auto utilise Windows PowerShell pour le moment et bascule vers PowerShell 7+ une fois installé.",
+ "5ed5c95344": "Choisissez entre Windows PowerShell et PowerShell 7+ pour les nouveaux volets de terminal.",
+ "3d88af864d": "Choisissez si l'option de shell PowerShell lance Windows PowerShell ou PowerShell 7+ pour les nouveaux volets de terminal.",
+ "8a956cc91e": "Caractères traités comme séparateurs de mots pour la sélection par double-clic.",
+ "4bebcc2b2c": "Séparateurs de mots",
+ "12e06178fa": "lignes",
+ "907b0b9d3e": "Personnalisé",
+ "5336c096af": "{{value0}} lignes",
+ "81d86b2dd2": "Lignes de terminal de bureau conservées pour les nouveaux volets et les volets ouverts.",
+ "9df53f7c14": "Lignes de scrollback",
+ "c3810b2b42": "Lignes de terminal de bureau conservées.",
+ "267d020745": "Défilement, séparateurs de mots et comportements de terminal propres à chaque plateforme.",
+ "5e5f06c82c": "Avancé",
+ "003df129fe": "Fractionner horizontalement",
+ "623e62df99": "Fractionner horizontalement",
+ "332e8a2872": "Fractionner verticalement",
+ "691ce810e0": "Fractionner verticalement",
+ "1158f8fd55": "Nouvel onglet",
+ "6c6a054a1c": "Exécuter dans un nouvel onglet",
+ "a9d47451d1": "« Nouvel onglet » ouvre la commande de configuration dans un onglet en arrière-plan intitulé « Configuration », sans voler le focus.",
+ "d23b43c5be": "Emplacement du script de configuration",
+ "34a0dfa06e": "Endroit où s'exécute le script de configuration du dépôt lors de la création d'un nouveau espace de travail.",
+ "21f8da2078": "Script de configuration de l'espace de travail",
+ "6e6480a7df": "Permet aux programmes du terminal (Zellij, tmux, Neovim, fzf, Grok, SSH) de copier vers le presse-papiers système.",
+ "3338dcf8c1": "Autoriser les écritures presse-papiers des TUI (OSC 52)",
+ "69c64a479c": "Permet à Zellij, tmux, Neovim, fzf et Grok de copier vers le presse-papiers système via le PTY (y compris via SSH).",
+ "4729c645fc": "Copier automatiquement les sélections du terminal vers le presse-papiers.",
+ "902f5dee1f": "Copie à la sélection",
+ "9129b7e805": "Survoler un volet de terminal l'active sans avoir à cliquer.",
+ "8eefeaa3da": "Le focus suit la souris",
+ "96fe15def8": "Comportement souris et presse-papiers des volets de terminal.",
+ "45721f3e67": "Interaction avec le terminal",
+ "9c0b1c1792": "Activé",
+ "c1fc9e9444": "Accélération GPU",
+ "e0996d141a": "Auto tente WebGL, avec repli DOM pour les moteurs de rendu non pris en charge ou risqués.",
+ "7eaccc1424": "WebGL est toujours tenté pour les volets de terminal.",
+ "fe4acf36c6": "WebGL désactivé ; moteur de rendu DOM pour une compatibilité maximale.",
+ "f07dfb4466": "Contrôle si le terminal utilise le rendu WebGL xterm.js. Auto tente WebGL lorsque le moteur est pris en charge, avec repli Linux conservateur pour les rendus logiciels ou GPU inconnus.",
+ "72bc9334a0": "Comportement du moteur de rendu du terminal pour les volets existants et les nouveaux volets.",
+ "2fba319f21": "Rendu",
+ "cc8c5ca224": "Défaut Windows",
+ "d78fc4fdef": "Chargement des distributions",
+ "219aaa59f4": "Distribution WSL",
+ "2503f1e86b": "Utilisée pour les nouveaux volets de terminal WSL et la détection des agents locaux quand l'espace de travail actif n'est pas déjà dans WSL.",
+ "5fe79a5e56": "Choisissez la distribution WSL utilisée par les nouveaux terminaux WSL et les analyses d'agents locaux.",
+ "b637dd57a7": "WSL",
+ "f61ac77f16": "Git Bash",
+ "0f1b8669e6": "Invite de commandes",
+ "eb7fc4d98a": "PowerShell",
+ "27e301f22c": "Shell par défaut",
+ "09bf02de9a": "Shell utilisé à l'ouverture d'un nouveau volet de terminal. Prend effet pour les nouveaux terminaux.",
+ "bd68f3170d": "Choisissez le shell par défaut pour les nouveaux volets de terminal sous Windows.",
+ "a55eee649f": "Shell par défaut des nouveaux volets de terminal sous Windows.",
+ "87e678a8af": "Shell Windows",
+ "05efc0bada": "true",
+ "348246b06f": "false",
+ "5936387ddd": "auto",
+ "adbafefe56": "custom",
+ "16753eea48": "Le clic droit colle le presse-papiers. Ctrl+clic droit ouvre le menu contextuel.",
+ "9c178cf8aa": "Clic droit pour coller",
+ "af0c3b6e39": "Le clic droit colle le presse-papiers dans le terminal. Utilisez Ctrl+clic droit pour ouvrir le menu contextuel.",
+ "29154326bb": "activé",
+ "ab20575a8a": "désactivé",
+ "ab3a1f9068": "wsl.exe",
+ "ask_before_closing_running_terminals_title": "Demander avant de fermer les terminaux en cours d'exécution",
+ "ask_before_closing_running_terminals_description": "Afficher une confirmation avant de fermer un terminal où tourne une commande ou un agent.",
+ "scrollSpeed": {
+ "title": "Vitesse de défilement",
+ "description": "Ajustez le défilement normal du terminal, le défilement rapide avec modificateur et la vitesse de molette des TUI plein écran.",
+ "helper": "Ajustez le ressenti de la molette dans le scrollback et dans les applications de terminal sensibles à la souris.",
+ "reset": "Réinitialiser",
+ "normal": "Normal",
+ "normalDescription": "Multiplicateur de molette du scrollback.",
+ "fast": "Rapide",
+ "fastDescription": "Multiplicateur supplémentaire lors du défilement avec une touche modificateur.",
+ "tui": "TUI",
+ "tuiDescription": "Rapports de molette discrets pour les applications de terminal plein écran."
+ }
+ },
+ "TerminalSettingsPreview": {
+ "a63953a48a": "Prévisualiser le thème {{value0}}",
+ "2c248fcc27": "Prévisualiser le thème",
+ "f8931d407d": "Afficher le séparateur de volets dans l'aperçu",
+ "50419052fe": "Séparateur de volets",
+ "d06664e889": "sombre"
+ },
+ "TerminalThemeSections": {
+ "db210115c5": "Aperçu du mode clair",
+ "5e0c24b5c8": "Contrôle la ligne de séparation entre volets en mode clair.",
+ "ec2e33ad80": "Couleur du séparateur en mode clair",
+ "d56af60e6f": "Choisissez le thème utilisé quand Orca est en mode clair.",
+ "8273bc75d7": "Thème clair",
+ "bc8e8a251a": "Aperçu du mode sombre",
+ "cbe56a0f79": "Contrôle la ligne de séparation entre volets en mode sombre.",
+ "b739d2abfe": "Couleur du séparateur en mode sombre",
+ "7add204bd5": "Choisissez le thème de terminal utilisé en mode sombre.",
+ "9499ad1dc4": "Thème sombre",
+ "f012172e21": "Choisissez le thème utilisé pour les volets de terminal en mode sombre.",
+ "catalog_title": "Thèmes du terminal",
+ "catalog_description": "Choisissez les thèmes de terminal et les couleurs de séparateur pour les modes sombre et clair.",
+ "target_title": "Mode de thème",
+ "target_label": "Mode de thème",
+ "target_aria": "Mode de thème du terminal",
+ "target_dark": "Sombre",
+ "target_light": "Clair",
+ "match_dark_mode": "Aligner sur le mode sombre",
+ "match_dark_mode_description": "Partage le thème de terminal sombre et la couleur de séparateur en mode clair.",
+ "light_preview_description": "Affiche l'apparence effective du terminal en mode clair.",
+ "dark_preview_description": "Affiche l'apparence effective du terminal en mode sombre."
+ },
+ "TerminalWindowSection": {
+ "1705318506": "Couleur ANSI magenta",
+ "03c855d15f": "Réinitialiser toutes les surcharges de couleurs",
+ "63f8d9336e": "Surcharges de couleurs",
+ "e86e09b5c7": "Remplacer individuellement les couleurs du terminal.",
+ "1d1920dc8a": "Masquer le curseur de la souris pendant la saisie dans le terminal.",
+ "3530908ef9": "Masquer la souris pendant la saisie",
+ "1846f6ee6a": "Marge verticale autour de la grille du terminal, en pixels.",
+ "1afcc1d973": "Marge verticale",
+ "25e2f8e8e1": "Marge horizontale autour de la grille du terminal, en pixels.",
+ "36b8402015": "Marge horizontale",
+ "53ce336e15": "Redémarrez Orca pour appliquer la modification du flou de fenêtre.",
+ "c65bb9ce63": "Redémarrage requis",
+ "97950bb087": "Applique un flou d'arrière-plan à la fenêtre du terminal. Nécessite un redémarrage.",
+ "2b82242f43": "Flou de fenêtre",
+ "809f37738d": "Contrôle la transparence de l'arrière-plan du terminal. 1 est totalement opaque, 0 totalement transparent.",
+ "ea7b1a158e": "Opacité de l'arrière-plan",
+ "03acb60aa0": "Contrôle la transparence de l'arrière-plan du terminal.",
+ "00eaa6b881": "Réglages d'apparence et d'arrière-plan de la fenêtre.",
+ "b96ba13ed1": "Fenêtre",
+ "42e01a6055": "Couleur ANSI blanc vif",
+ "16948119cb": "Blanc vif",
+ "1601140f03": "Couleur ANSI cyan vif",
+ "f94adc4113": "Cyan vif",
+ "fe4d89ef85": "Couleur ANSI magenta vif",
+ "e56e7d6ea0": "Magenta vif",
+ "bef6c0f6bf": "Couleur ANSI bleu vif",
+ "66820332fa": "Bleu vif",
+ "e2ef5f4ab7": "Couleur ANSI jaune vif",
+ "936a326be3": "Jaune vif",
+ "0ffb02f921": "Couleur ANSI vert vif",
+ "7dafd57730": "Vert vif",
+ "667de68863": "Couleur ANSI rouge vif",
+ "32b1b6acd7": "Rouge vif",
+ "f30c492769": "Couleur ANSI noir vif",
+ "260d69ce9a": "Noir vif",
+ "1be593d3e8": "ANSI vif",
+ "28846b1ca6": "Couleur blanche ANSI",
+ "0cb4459fb8": "Blanc",
+ "bd4c759327": "Couleur cyan ANSI",
+ "fb8bb4eb1f": "Cyan",
+ "d5e92fcd94": "Magenta",
+ "9635a71c51": "Couleur bleue ANSI",
+ "292a4c7316": "Bleu",
+ "09c1c6b096": "Couleur jaune ANSI",
+ "bb516de873": "Jaune",
+ "8a673d4206": "Couleur verte ANSI",
+ "8f2092b315": "Vert",
+ "b41270f5ca": "Couleur rouge ANSI",
+ "3a78f30b50": "Rouge",
+ "cf4437a2f7": "Couleur noire ANSI",
+ "adfdee23cb": "Noir",
+ "68e9f07de0": "ANSI normal",
+ "862e463f7f": "Texte en gras",
+ "b2c0857c49": "Couleur du texte sélectionné",
+ "8b450b5305": "Premier plan de la sélection",
+ "74d8555f85": "Couleur d'arrière-plan du texte sélectionné",
+ "40c3cfd30a": "Arrière-plan de la sélection",
+ "7f4063076c": "Couleur du texte sous le curseur (curseur bloc)",
+ "a2d9f095a7": "Texte du curseur",
+ "cd0700762b": "Couleur du curseur",
+ "c9e1fdf42f": "Cursor",
+ "da64e8f4c1": "Couleur d'arrière-plan du terminal",
+ "cc1b2ffeb2": "Arrière-plan",
+ "026a0b8013": "Couleur du texte principal",
+ "79f6bfb76e": "Premier plan",
+ "cf37ff69f6": "Base",
+ "8abdab9f7c": "Redémarrer maintenant",
+ "907131d741": "Redémarrage…",
+ "605a35d600": "Non encore appliqué par le moteur de rendu du terminal — xterm.js n'a pas d'emplacement dédié aux couleurs en gras. Une valeur enregistrée est conservée."
+ },
+ "UIZoomControl": {
+ "c2c64b24d0": "Réinitialiser"
+ },
+ "VoicePane": {
+ "6fa734ed95": "Supprimer {{value0}}",
+ "68de13f72c": "Échec de la suppression du modèle.",
+ "1ba81c0ff0": "recommandé",
+ "cfde55c7b0": "Échec du téléchargement du modèle.",
+ "43fd4f454b": "Modèle vocal",
+ "7cf715f891": "est maintenu.",
+ "295d84b849": "une fois pour démarrer, à nouveau pour arrêter. Maintien : dictez tant que",
+ "ff9a680010": "Bascule : appuyez",
+ "ba4a900d1d": "Mode de dictée",
+ "0121960365": "Activer la dictée vocale",
+ "366e1b4f36": "pour dicter du texte dans n'importe quel volet actif.",
+ "4465596675": "Appuyez",
+ "62d2a84d31": "Échec de l'effacement de la clé API OpenAI",
+ "37aba8bb63": "Clé API OpenAI effacée",
+ "8572bbb537": "Échec de l'enregistrement de la clé API OpenAI",
+ "506df81ba6": "Clé API OpenAI enregistrée",
+ "91980ce124": "{{value0}} Mo",
+ "61a16c8141": "Extraction...",
+ "b6536a1d12": "extraction",
+ "8f4d2a51d7": "hors ligne",
+ "d504ab05f0": "streaming",
+ "fbe5990716": "Sélectionner un modèle",
+ "e24f7d43d2": "Sélectionnez un modèle vocal. Les modèles locaux fonctionnent hors ligne ; les modèles cloud nécessitent une clé API.",
+ "174da92062": "Maintien",
+ "118b3c2dee": "Bascule",
+ "901985625d": "bascule",
+ "ad5d036ecc": "Impossible de demander l'autorisation d'accès au micro. La dictée vocale n'a pas été activée.",
+ "f9a9cf6928": "L'autorisation d'accès au micro est requise avant d'activer la dictée vocale.",
+ "1eac933202": "Panneau Confidentialité et sécurité de macOS ouvert. Réactivez la dictée après avoir accordé l'accès.",
+ "cd9fe37556": "Autorisation d'accès au micro accordée"
+ },
+ "WorktreeSymlinksSection": {
+ "1c1e35b219": "Supprimer {{value0}}",
+ "b814c618e2": "Chemins liés",
+ "31ebab5403": "Aucun chemin partagé configuré pour ce dépôt.",
+ "ea06227efa": "ajouté",
+ "b2429aeb31": "Ajouter",
+ "ab40b8a5f1": "Aucun résultat. Continuez à saisir pour ajouter un chemin personnalisé.",
+ "4cd2a4c077": "Saisissez un chemin (ex. .env ou node_modules)…",
+ "241325302c": "Ajouter un chemin",
+ "7ff265071d": "Lors de la création d'un nouveau worktree, chaque chemin listé ici est copié par clone APFS sur macOS quand c'est possible, sinon créé en lien symbolique depuis le checkout principal.",
+ "4755f120b6": "Chemins partagés des worktrees",
+ "b07ef5a8b6": "Chemins à matérialiser depuis le checkout principal vers les nouveaux worktrees.",
+ "d72ba8dc68": "{{value0}} chemins",
+ "9ea912d811": "1 chemin"
+ },
+ "WslCliRegistration": {
+ "c6f6f89d7c": "Annuler",
+ "119fef6cd2": "Chemin cible :",
+ "1dbb0377d9": "Cible du lanceur existante :",
+ "554305956d": "Chemin de la commande :",
+ "9b6627522c": "Actualiser",
+ "ab6b022a5c": "Actualiser l'état de la CLI WSL",
+ "d9c6880dbd": "Commande shell WSL",
+ "52d990420e": "Échec de la suppression de `{{value0}}` de WSL.",
+ "89c7414cf5": "`{{value0}}` supprimé de WSL.",
+ "6f91ad1333": "Échec de l'enregistrement de `{{value0}}` dans WSL.",
+ "951536dda5": "`{{value0}}` enregistré dans WSL.",
+ "26b4b3b00f": "Échec du chargement de l'état de la CLI WSL.",
+ "290bfff3ab": "Enregistrer",
+ "f951f85196": "Supprimer",
+ "4c4a9178a3": "Enregistrement...",
+ "41a1480d3e": "installer",
+ "4598b18464": "Suppression...",
+ "7c3bb36706": "supprimer",
+ "7ee4e52b99": "Orca enregistrera {{value0}} pour que la commande fonctionne depuis les terminaux WSL.",
+ "d8216eb22e": "Cela supprime la commande shell WSL. Orca lui-même reste installé sous Windows.",
+ "e49688f67f": "Enregistrer `{{value0}}` dans WSL ?",
+ "61ac55278e": "Supprimer `{{value0}}` de WSL ?",
+ "e2b0ee267f": "périmé",
+ "7aa456a460": "Enregistre `orca-ide` dans ~/.local/bin au sein de WSL.",
+ "0307677bb9": "Vérification de l'enregistrement de la CLI WSL..."
+ },
+ "accounts": {
+ "search": {
+ "86edc96bc9": "barre d'état",
+ "e949b08ffb": "limite de débit",
+ "7e67d7d1b6": "wrk",
+ "421c6be25e": "id",
+ "be8b621bdc": "espace de travail",
+ "8dcbef1856": "opencode",
+ "38d22ff8d6": "Remplacement facultatif de l'ID de workspace si la recherche automatique échoue.",
+ "4ee2029e9c": "ID de workspace OpenCode Go",
+ "9c4e40cf6b": "session",
+ "61f7d1fcbe": "cookie",
+ "d1d2ae383c": "Collez votre cookie de session opencode.ai pour récupérer les rate limits.",
+ "6ed1401020": "Cookie de session OpenCode Go",
+ "b7c2cee442": "expérimental",
+ "7118d2f908": "identifiants",
+ "933deaf732": "oauth",
+ "8630464352": "cli",
+ "e8e1ff3887": "gemini",
+ "bada4a3218": "Extrait les identifiants OAuth de votre installation locale de Gemini CLI pour l'authentification auprès de Google.",
+ "d819755b02": "Utiliser les identifiants Gemini CLI",
+ "35b461d817": "connexion",
+ "f2d666a886": "facultatif",
+ "8b06729e0f": "actif",
+ "5b3f18ef4a": "bascule",
+ "06662af91e": "compte",
+ "70d1b8def5": "codex",
+ "87a4a8584e": "Choisissez le compte Codex facultatif enregistré qui alimente la lecture des quotas en temps réel.",
+ "a4bcfd6f86": "Compte Codex actif",
+ "042885c07c": "périmé",
+ "02c438bc7b": "expiré",
+ "77e32a2ad3": "réauthentifier",
+ "c759741d77": "quota",
+ "b40d5b6570": "Changement de compte facultatif pour Codex et récupération en direct des limites de débit.",
+ "17c5d244eb": "Comptes Codex",
+ "e14049e1a8": "claude",
+ "dd75a73991": "Changement de compte facultatif pour Claude tout en préservant le contexte de conversation partagé.",
+ "75682e1b62": "Comptes Claude",
+ "e02c136ad0": "auth",
+ "9f70aa706c": "fournisseur",
+ "488a7e9206": "linux",
+ "0b4d948eb5": "wsl",
+ "bdbd1e668e": "windows",
+ "593720c17f": "emplacement",
+ "b84a5b0c8a": "Choisissez si les comptes des fournisseurs sont inspectés et ajoutés sur cet appareil ou dans WSL.",
+ "d09fb5ca92": "Emplacement des comptes",
+ "733f9e2a93": "Utilisation MiniMax",
+ "f8374c3151": "Collez votre cookie de session platform.minimax.io pour récupérer localement les limites de débit.",
+ "f4a8c2e1b7": "Utilisation Grok (xAI)",
+ "e3b7d1f9a2": "Connexion OAuth via Grok CLI (grok login) pour l'utilisation hebdomadaire des crédits.",
+ "d2c6a0e8f1": "grok",
+ "c1b5f9d7e0": "xai",
+ "b0a4e8c6d9": "oauth",
+ "a9f3d7b5c8": "connexion"
+ }
+ },
+ "advanced": {
+ "search": {
+ "a7002e1ac4": "programme de mise à jour",
+ "e61ed8ab33": "mises à jour",
+ "6576fce4d2": "dépannage",
+ "79e0947e95": "assistance",
+ "4383251647": "vpn",
+ "f98a60af11": "proxy",
+ "65bf6af262": "compatibilité",
+ "621233008b": "http/1.1",
+ "f8ff125ebe": "http1",
+ "a0f71bd909": "http/2",
+ "4b4ae4345a": "http2",
+ "48a1c8f534": "http",
+ "4d44352eea": "réseau",
+ "2b4d26d11e": "connectivité réseau",
+ "e04e9db503": "avancé",
+ "585f56fae0": "Utiliser HTTP/1.1 pour la mise en réseau d'Electron lorsque HTTP/2 échoue derrière un proxy.",
+ "11eea3da72": "Compatibilité HTTP/1.1"
+ }
+ },
+ "agents": {
+ "search": {
+ "2814401339": "installé",
+ "719f53350c": "chemin",
+ "839e82c81f": "détection",
+ "f622b8eb2a": "linux",
+ "d608654c03": "wsl",
+ "77c02fa3c3": "windows",
+ "d2952dfd74": "emplacement",
+ "96ba2373b6": "agent",
+ "cbdd7f3b9e": "Choisissez si les agents installés sont détectés sur cet appareil ou dans WSL.",
+ "ef804b7337": "Emplacement des agents",
+ "01926b9d8c": "Configurer les agents de codage IA, l'agent par défaut et les remplacements de commandes.",
+ "bb9ad95777": "Agents",
+ "d8f3a8b8a0": "default",
+ "167daeb5e9": "command",
+ "be59907510": "remplacement",
+ "a6d594c17d": "installer",
+ "f2932bf22b": "détecté",
+ "2afd3b5858": "activer",
+ "60393e1b17": "désactiver",
+ "2e188c771c": "masquer",
+ "87fffe6c20": "afficher",
+ "e2b7c0dcd7": "github",
+ "66b6b82eb4": "éveil",
+ "dbc8aca6b0": "veille",
+ "845ad9128a": "alimentation",
+ "48f84d10f1": "en cours",
+ "affbf130f6": "travaille",
+ "0d1c334987": "couvercle",
+ "ff8de8a2ad": "écran",
+ "0d752916f8": "hooks",
+ "6984d4291a": "statut",
+ "13b20636a6": "en attente",
+ "8599603496": "terminé",
+ "ea71995548": "supprimer",
+ "c1317fe641": "restaurer",
+ "5963143e00": "paramètres",
+ "042c551bc5": "config",
+ "f412abbba5": "claude",
+ "5ded38b843": "codex",
+ "be7ea3553b": "onglet",
+ "6956646a1e": "titre",
+ "32836788b0": "titre généré",
+ "966890236d": "nom",
+ "848dcae8d3": "généré",
+ "52115d0d7c": "auto",
+ "c64059f50d": "prompt",
+ "5784ae8c43": "renommer",
+ "8a17fd6026": "stable",
+ "a79d266f71": "session",
+ "afbf35be68": "session stable",
+ "agentPermissions": "Permissions des agents",
+ "agentPermissionsDescription": "Basculer les autorisations d'agent par défaut entre Yolo et Manuelle.",
+ "agentRuntime": "Runtime des agents",
+ "agentRuntimeDescription": "Choisissez si les agents sont détectés et lancés par défaut sous Windows ou dans WSL."
+ }
+ },
+ "appearance": {
+ "search": {
+ "9a115966d3": "Réduire dans la zone de notification à la fermeture",
+ "4d5b9427b5": "Quand activé, fermer la fenêtre laisse Orca tourner dans la zone de notification au lieu de quitter.",
+ "468448bba4": "aquarelle",
+ "f586abfa35": "bleu",
+ "651f35b2c6": "sélecteur",
+ "e5bc35d59e": "fenêtre",
+ "d18b54ca90": "dock",
+ "1f2880a9d5": "orca",
+ "2cfb3420c0": "icône de l'app",
+ "e80c2af428": "Choisissez l'icône d'app affichée dans le Dock et le sélecteur de fenêtres.",
+ "2b313598c6": "Icône de l'app",
+ "839fb1e3ed": "boîte à outils",
+ "ac79fe4a04": "afficher",
+ "648eeada79": "masquer",
+ "6cf5f54ce1": "bouton",
+ "5bff6a2ef0": "barre latérale",
+ "5e5b8878bf": "téléphone",
+ "74618577c7": "mobile",
+ "682293cadf": "Afficher le bouton Orca Mobile en haut de la barre latérale gauche.",
+ "1de96ec8a6": "Afficher le bouton Orca Mobile",
+ "4c920ab2d1": "planification",
+ "58f4e22fa2": "automatisation",
+ "b186f3cefb": "automatisations",
+ "ae13a0d340": "Afficher le bouton Automatisations en haut de la barre latérale gauche.",
+ "caa27e1a8e": "Afficher le bouton Automatisations",
+ "6b846424cc": "linear",
+ "2ee4810f38": "github",
+ "0d5a74b606": "tâches",
+ "9a248333c7": "Afficher le bouton Tâches en haut de la barre latérale gauche.",
+ "155a1e7438": "Afficher le bouton Tâches",
+ "a895d0f938": "marque",
+ "51f957ce39": "nom",
+ "36e006efc1": "app",
+ "bed343b03e": "barre de titre",
+ "18b4c4c30b": "Afficher Orca dans la barre de titre.",
+ "fdd31b00d0": "Nom de l'app dans la barre de titre",
+ "c1bca1885a": "explorateur de fichiers",
+ "9f2df826ac": "ignoré",
+ "08c86bf58e": "gitignore",
+ "bce3ac317a": "git",
+ "7164edf71a": "Atténuer les fichiers correspondant à .gitignore dans l'explorateur de fichiers.",
+ "f8129fb544": "Afficher les fichiers ignorés par Git",
+ "2f12e1aa3a": "ui",
+ "5095258df2": "interface",
+ "fab91464dd": "ide",
+ "8b36fb3f64": "typographie",
+ "a0e09aed9c": "police",
+ "24094af355": "police",
+ "07c7c38fac": "Choisissez la police utilisée par l'interface d'Orca.",
+ "ddb991024d": "Police de l'IDE",
+ "0c83659f48": "raccourci",
+ "0952091186": "échelle",
+ "3ae5de6101": "zoom",
+ "adddb91a3d": "Met toute l'interface de l'application à l'échelle.",
+ "c5e933970f": "Zoom de l'interface",
+ "3a9b69d734": "système",
+ "44d873fd18": "clair",
+ "262fe1d24f": "sombre",
+ "0709c794f7": "Choisissez l'apparence d'Orca dans la fenêtre de l'app.",
+ "71e06350b4": "Thème",
+ "dc02c8759d": "espace de travail",
+ "43cfba3b95": "serveur",
+ "46d21eef62": "localhost",
+ "006e67b279": "ports",
+ "896eb53fd4": "barre d'état",
+ "0ececfa190": "Afficher les ports actifs des espaces de travail dans la barre d'état.",
+ "cf409b6c4d": "Ports",
+ "cb1cc62cf8": "espace",
+ "90bdc043ea": "disque",
+ "96b4fb0064": "terminal",
+ "4ddbde4999": "cpu",
+ "4355f18ac6": "mémoire",
+ "9c4d5f0894": "gestionnaire",
+ "c690a15849": "ressource",
+ "81ef5abc2f": "Afficher l'utilisation CPU, mémoire et disque des espaces de travail ainsi que les sessions de terminal dans la barre d'état.",
+ "7cf005b29f": "Gestionnaire de ressources",
+ "fe192b060e": "hôte",
+ "f4997e0f8a": "connexion",
+ "a278406ed5": "distant",
+ "6ecad74eb3": "ssh",
+ "f17d66d0d2": "Afficher l'état de connexion à l'hôte distant dans la barre d'état.",
+ "57fb424c56": "Hôtes distants",
+ "35565867cb": "moonshot",
+ "de586def95": "abonnement",
+ "00a028f25f": "utilisation",
+ "40e5c3c285": "kimi",
+ "c927a155d5": "Afficher l'utilisation de l'abonnement Kimi dans la barre d'état.",
+ "3a6c028ea8": "Utilisation Kimi",
+ "25e51b62ee": "limite de débit",
+ "d9e7cef86f": "cookie",
+ "d16378a88f": "minimax",
+ "e46178eb1b": "Afficher l'utilisation de l'abonnement MiniMax dans la barre d'état.",
+ "0f08f6b483": "Utilisation MiniMax",
+ "edbf0f63a0": "coût",
+ "afbb6a3767": "tokens",
+ "d77537b580": "opencode-go",
+ "a9d56852eb": "opencode",
+ "7f72de7cbe": "Afficher l'utilisation des tokens et des coûts d'OpenCode Go dans la barre d'état.",
+ "bc046e7899": "Utilisation OpenCode Go",
+ "51b0ccd6a2": "google",
+ "2804a920ad": "gemini",
+ "9660c5b2f1": "Afficher l'utilisation des tokens et des coûts de Gemini dans la barre d'état.",
+ "5bfb874d05": "Utilisation Gemini",
+ "97957e374e": "openai",
+ "8dfd676c28": "codex",
+ "e9e4412545": "Afficher l'utilisation des tokens et des coûts de Codex dans la barre d'état.",
+ "54b1acf24f": "Utilisation Codex",
+ "dea0a9a665": "anthropic",
+ "c9fe3a7876": "claude",
+ "de50c6f516": "Afficher l'utilisation des tokens et des coûts de Claude dans la barre d'état.",
+ "9dc15020d7": "Utilisation Claude",
+ "language": {
+ "locale": "paramètres régionaux",
+ "i18n": "i18n",
+ "translation": "traduction"
+ },
+ "leftSidebarAppearance": {
+ "title": "Apparence de la barre latérale gauche",
+ "description": "Accordez la barre latérale gauche à votre terminal, gardez-la par défaut ou appliquez une teinte."
+ },
+ "workspaceCardLayout": {
+ "title": "Disposition des cartes d'espace de travail",
+ "description": "Les cartes d'espace de travail peuvent adopter une disposition compacte ou détaillée.",
+ "compact": "compact",
+ "compactDisplay": "affichage compact",
+ "workspaceCards": "cartes d'espace de travail",
+ "worktreeCards": "cartes de worktree",
+ "cardLayout": "disposition des cartes",
+ "workspaceOptions": "options d'espace de travail",
+ "detailed": "détaillé"
+ },
+ "showPinnedWorktreesInGroups": {
+ "title": "Afficher aussi les worktrees épinglés dans leurs listes d'origine",
+ "description": "Les worktrees épinglés restent dans Épinglés et apparaissent aussi dans Tous, Projet, Statut et PR."
+ },
+ "antigravityUsageTitle": "Utilisation Antigravity",
+ "antigravityUsageDescription": "Afficher l'utilisation de l'abonnement Antigravity dans la barre d'état.",
+ "antigravityKeyword": "antigravity",
+ "f8e2a1c4b6": "Utilisation Grok",
+ "e7d1b0f3a5": "Afficher l'utilisation hebdomadaire des crédits Grok issue de l'OAuth de Grok CLI.",
+ "d6c0a9e2f4": "grok",
+ "c5b9f8d1e3": "xai",
+ "usagePercentageDisplayTitle": "Pourcentages d'utilisation",
+ "usagePercentageDisplayDescription": "Choisissez si les limites des fournisseurs affichent le pourcentage utilisé ou restant."
+ }
+ },
+ "auto": {
+ "rename": {
+ "branch": {
+ "search": {
+ "0971762141": "kebab-case",
+ "a482f6a423": "slug",
+ "7adefcdd94": "template",
+ "10485c4fc5": "command",
+ "50139297e6": "prompt intégré",
+ "502aa57681": "instructions",
+ "40d21f2efc": "prompt",
+ "672387fb77": "Modèle de commande d'agent utilisé lors de la génération des noms de branche.",
+ "722551c5b3": "Modèle de commande de nom de branche",
+ "f41833025e": "générer",
+ "ed677944cc": "worktree",
+ "3ef3cbe98c": "agent",
+ "f0acf64301": "nom de créature",
+ "7803423877": "auto",
+ "55a1860e47": "renommer",
+ "9319bd9827": "branche",
+ "ea94b9da8a": "Renommer la branche générée automatiquement d'après le travail dès qu'un agent démarre.",
+ "427f2cd1eb": "Renommage auto de la branche"
+ }
+ }
+ }
+ },
+ "browser": {
+ "search": {
+ "7539f6336c": "profil",
+ "1c1e097985": "arc",
+ "533a253deb": "edge",
+ "75a0d435b7": "chrome",
+ "854ef6ce83": "connexion",
+ "3910a41f32": "auth",
+ "2e7f951773": "importer",
+ "66dd641a47": "session",
+ "29193a51d5": "cookies",
+ "2d2d995c58": "navigateur",
+ "060ac1fcba": "Importer les cookies de Chrome, Edge ou d'autres navigateurs pour utiliser vos sessions existantes dans Orca.",
+ "96afedcb5c": "Session & cookies",
+ "a7a07d5415": "éditeur",
+ "8dd4805991": "fichier",
+ "68d1db8929": "markdown",
+ "90425d313c": "shift",
+ "linkRoutingModifier": {
+ "routing": "routage",
+ "modifier": "modificateur",
+ "invert": "inverser",
+ "opposite": "opposé"
+ },
+ "terminalLinkActions": {
+ "terminal": "terminal",
+ "click": "clic",
+ "actions": "actions",
+ "popover": "popover",
+ "menu": "menu",
+ "disable": "désactiver"
+ },
+ "72c58f7792": "webview",
+ "82ba1c80ea": "localhost",
+ "bea27bac4b": "liens",
+ "44d14df30d": "aperçu",
+ "5cb082b3e3": "Routage des liens",
+ "95944898e0": "pourcentage",
+ "483a0eb5e0": "nouvel onglet",
+ "726f2a8556": "zoom de page",
+ "5448f4097b": "default",
+ "54f4ea55f7": "échelle",
+ "4a98ed195f": "zoom",
+ "c3d89ed4d0": "Niveau de zoom appliqué aux nouveaux onglets du navigateur.",
+ "072b7f5c1f": "Zoom par défaut",
+ "0bb34eacc9": "requête",
+ "8b8ed06e4b": "omnibox",
+ "3538b3aaeb": "token",
+ "0732ebe6fb": "privé",
+ "e1c2a57f07": "kagi",
+ "ad40e75d13": "bing",
+ "1f8153acfb": "duckduckgo",
+ "8a489aab8d": "google",
+ "72b4b89970": "moteur",
+ "16bd69cd82": "recherche",
+ "0628d5943b": "Moteur de recherche utilisé quand on saisit du texte autre qu'une URL dans la barre d'adresse.",
+ "5e755920c9": "Moteur de recherche par défaut",
+ "4596a52cf7": "landing",
+ "5164c47e31": "vierge",
+ "4fda4fb066": "url",
+ "0dbb1eaf4e": "page d'accueil",
+ "291f480a5e": "accueil",
+ "a942905148": "URL ouverte à la création d'un nouvel onglet du navigateur. Laissez vide pour un onglet vierge.",
+ "c3903322d2": "Page d'accueil par défaut",
+ "19ea5607cf": "Libellés localhost des worktrees",
+ "4e0fdf0a3f": "Ouvrir les ports des espaces de travail sous forme d'URL localhost Orca propres à chaque worktree, pour mieux distinguer les onglets du navigateur."
+ },
+ "use": {
+ "search": {
+ "3f4c559deb": "profil Arc",
+ "d5afa54d21": "profil Edge",
+ "22fb801af8": "profil Chrome",
+ "59968bb9b4": "navigateur authentifié",
+ "62e2a790c0": "session existante",
+ "63a66da648": "navigateur système",
+ "20c1323d1e": "computer use",
+ "ab349a2dd0": "arc",
+ "2e1b09897b": "edge",
+ "088e7a9012": "chrome",
+ "96ce3d2de2": "auth",
+ "48557f639c": "connexion",
+ "d5ad1f7aad": "importer",
+ "02837ee497": "session",
+ "fb8178824f": "cookies",
+ "ba4eb53b72": "browser use",
+ "2fb24d17db": "Importez les cookies depuis Chrome, Edge ou d'autres navigateurs pour que les agents réutilisent vos connexions.",
+ "614c756ab1": "Importer les cookies du navigateur",
+ "cee44fb442": "automatisation",
+ "a57c2172dc": "agent-browser",
+ "6ea88e5206": "npx",
+ "f5b8fdddf5": "orca-cli",
+ "e5a784bc54": "installer",
+ "9d97446873": "agent",
+ "a2d489263e": "skill",
+ "a7e82445fa": "Installez la skill Browser Use pour que les agents pilotent le navigateur d'Orca.",
+ "a1414dcefb": "Installer la skill Browser Use",
+ "e56c7b55c9": "configuration",
+ "034c5e8d7f": "activer",
+ "7e0dcb257a": "shell",
+ "3ffafc9b95": "command",
+ "30c74aaa1f": "chemin",
+ "ff05cbc344": "orca",
+ "85fab5e12c": "cli",
+ "890ddf943d": "Enregistrer Orca CLI pour que les agents pilotent le navigateur.",
+ "50f0860e18": "Activer Orca CLI"
+ }
+ }
+ },
+ "commit": {
+ "message": {
+ "ai": {
+ "search": {
+ "3766941527": "agent",
+ "181cdb0637": "ouvrir",
+ "8e9cc598d7": "générer",
+ "b7d50da4d8": "template",
+ "7e264b926b": "brouillon",
+ "b261c88609": "pr",
+ "110be48b81": "pull request",
+ "001ca3f2af": "Valeurs par défaut appliquées à l'ouverture du composeur de création de PR.",
+ "eefd33788c": "Valeurs par défaut de création de PR",
+ "d32936bb2a": "branche",
+ "127d512e75": "commit",
+ "d22a6459e4": "conflits",
+ "53e8504fb2": "ci",
+ "c46e665f7e": "checks",
+ "37c65bbb44": "fix",
+ "402f101af8": "prompt",
+ "8e0bcc5d99": "model",
+ "f4731b22bf": "command",
+ "57c851a68c": "cli",
+ "61117e57f3": "args",
+ "0f29331fed": "arguments",
+ "18b6d38835": "Agent, arguments CLI et modèle de commande utilisés par chaque bouton IA de contrôle de code source.",
+ "3c4e5e5938": "Recettes d'action",
+ "ee14a9e9f7": "activé",
+ "82109d627d": "contrôle de code source",
+ "542e1a00a7": "codex",
+ "f121bec167": "claude",
+ "93e5210da8": "message",
+ "c33cb1b982": "ai",
+ "0b946b2abe": "Ajoute des recettes d'actions pour les actions commit, pull request, nom de branche et correction de Source Control.",
+ "24dbdfca78": "Afficher les actions Source Control AI"
+ }
+ }
+ }
+ },
+ "computer": {
+ "use": {
+ "search": {
+ "6e88da3508": "skill",
+ "798be54d7e": "automatisation",
+ "e27f8bafbf": "capture d'écran",
+ "26c1290d83": "enregistrement d'écran",
+ "82f01c2d2c": "accessibilité",
+ "fefb452f5b": "computer use",
+ "9210db582b": "Autoriser les agents à analyser des captures d'écran et à piloter des applications locales à votre demande.",
+ "442bec10fe": "Computer Use"
+ }
+ }
+ },
+ "developer": {
+ "permissions": {
+ "search": {
+ "3363889768": "LAN, USB et Bluetooth",
+ "6c82846f66": "appareil",
+ "11653d3f42": "mdns",
+ "78a10b826f": "bonjour",
+ "e3fbc48083": "bluetooth",
+ "c4a4a02ea4": "usb",
+ "fa3239cd42": "réseau local",
+ "87620e6416": "lan",
+ "acad3d4743": "Autoriser les outils d'appareils et l'accès LAN utilisés depuis les sessions de terminal.",
+ "3e0131e45d": "icloud",
+ "ce07159ff5": "bureau",
+ "a0c19119fb": "téléchargements",
+ "4438f81bfa": "documents",
+ "c10e36cbd1": "accès complet au disque",
+ "05ab708ee5": "Ouvrir le volet de confidentialité de macOS pour l'accès aux fichiers protégés des projets et worktrees.",
+ "bbf543a3a1": "Accès complet au disque",
+ "7f145a3984": "fenêtre",
+ "5610022e1e": "automatisation",
+ "0a467b750e": "capture d'écran",
+ "08f8039ca9": "accessibilité",
+ "3cd51d18a1": "enregistrement d'écran",
+ "2e5d98ab56": "Autoriser les captures d'écran, l'inspection de l'écran, la saisie clavier et l'automatisation des fenêtres.",
+ "39bb49e662": "Enregistrement d'écran et accessibilité",
+ "00e954319e": "whisper",
+ "1e6e27b202": "ffmpeg",
+ "f061f08b7b": "sox",
+ "a765112513": "vidéo",
+ "b192432ef0": "audio",
+ "af122938a3": "voix",
+ "259b829b84": "caméra",
+ "ed7c12bdb4": "micro",
+ "6eca1636b7": "Autoriser les outils de voix, de transcription, de webcam et de capture multimédia.",
+ "302c0c42f9": "Micro et caméra",
+ "4e225e7c56": "outils développeur",
+ "6db4fca386": "macos",
+ "0c13b249e3": "tcc",
+ "2270ccff3f": "confidentialité",
+ "a98aa11a9c": "autorisations",
+ "bc8ac95310": "Autorisations macOS pour les outils développeur lancés depuis le terminal.",
+ "e92cb0896d": "Autorisations développeur"
+ }
+ }
+ },
+ "experimental": {
+ "search": {
+ "44c7f209d5": "node_modules",
+ "4ad605f222": "env",
+ "3021571c30": "partagé",
+ "f082788cfe": "liens",
+ "3028f0bd3a": "lien",
+ "bff1ff7768": "liens symboliques",
+ "c387565812": "lien symbolique",
+ "10b52f79c1": "worktrees",
+ "d23ae13990": "worktree",
+ "0d24759f14": "expérimental",
+ "603d29ed74": "Matérialiser automatiquement les fichiers ou dossiers configurés dans les nouveaux worktrees créés, par clone APFS sur macOS quand c'est possible, sinon par liens symboliques.",
+ "78c2a8dc74": "Chemins partagés sur les worktrees",
+ "7b79081695": "non lu",
+ "f10d307468": "achèvement",
+ "5f067ba0f9": "agent",
+ "7695fd30e9": "notification",
+ "8facf10138": "cloche",
+ "edc49480a1": "volet",
+ "268e99d957": "surlignage",
+ "01567f19ca": "attention",
+ "9bb3bd5098": "terminal",
+ "11877246fc": "Surlignage persistant du volet pour les sonneries de terminal et les fins d'agents.",
+ "9e4ddf776d": "Attention du terminal",
+ "agentHibernation": {
+ "agent": "agent",
+ "agents": "agents",
+ "description": "Interrompt les terminaux d'agents en arrière-plan devenus inactifs après la durée d'inactivité configurée et reprend les sessions prises en charge lors de leur réouverture. La veille des agents conserve les options de lancement des agents démarrés par Orca ; les agents démarrés manuellement peuvent reprendre avec les valeurs par défaut actuelles d'Orca.",
+ "minutes": "minutes",
+ "sleep": "veille",
+ "terminal": "terminal",
+ "title": "Mise en veille des agents"
+ },
+ "newWorktreeCardStyle": {
+ "card": "carte",
+ "cards": "cartes",
+ "description": "Prévisualise la nouvelle mise en page des cartes de worktree, l'emplacement des métadonnées, les options du menu d'affichage des cartes et la présentation des statuts.",
+ "menu": "menu",
+ "metadata": "métadonnées",
+ "status": "statut",
+ "title": "Nouveau style de carte",
+ "worktree": "worktree",
+ "worktrees": "worktrees"
+ },
+ "fe5688b761": "barre latérale",
+ "ca5d1f3f46": "chronologie",
+ "d01b3882ba": "notifications",
+ "244a0ecd3d": "activité",
+ "92a9357d1f": "vue des agents",
+ "fa72e71f05": "agents",
+ "4d63251595": "Flux groupé dans la barre latérale gauche pour les achèvements d'agents et les états bloquants.",
+ "ccc5548ac5": "Vue Agents",
+ "9af7a518db": "personnage",
+ "791fefc0b0": "coin",
+ "65df471ab2": "animé",
+ "9f5609bfb8": "superposition",
+ "2a33975d72": "mascotte",
+ "b54cea709b": "compagnon",
+ "051203d37c": "animal",
+ "6b5a56ac35": "Compagnon animé flottant dans le coin inférieur droit.",
+ "87d99e634b": "Compagnon",
+ "nativeChat": {
+ "title": "Chat UI",
+ "description": "Prévisualisez la surface de chat de bureau pour les sessions de terminal d'agents prises en charge.",
+ "grok": "grok"
+ },
+ "agentDashboard": {
+ "title": "Tableau de bord des agents",
+ "description": "Tableau Kanban pour surveiller les agents de tous les worktrees, dans la fenêtre ou en fenêtre indépendante."
+ }
+ }
+ },
+ "floating": {
+ "workspace": {
+ "search": {
+ "94f4d013c8": "barre d'état",
+ "a452146574": "bouton de bascule",
+ "6765b85e48": "répertoire de lancement",
+ "a38bfc3f77": "panneau rapide",
+ "52db6e3baf": "notes",
+ "156ffeee08": "note",
+ "884e5e6132": "markdown",
+ "49db74a92d": "navigateur",
+ "6410fe83d8": "terminal",
+ "2b5efa55c9": "global",
+ "ebeedb2f6a": "terminal rapide",
+ "6f183fa1b9": "terminal flottant",
+ "a08e482f6d": "espace de travail flottant",
+ "b96b5ee6cf": "Activer l'espace de travail flottant, choisir où s'ouvrent les nouveaux onglets et où apparaît le bouton de bascule.",
+ "b2b60e7163": "Workspace flottant"
+ }
+ }
+ },
+ "general": {
+ "search": {
+ "bdfb6dc21b": "j'aime",
+ "e6b01c8e30": "commentaires",
+ "b65665703a": "assistance",
+ "06ea5a69a6": "github",
+ "e4fb4516d0": "étoile",
+ "e0b8c8bc25": "Soutenez le projet avec une étoile GitHub via la CLI gh.",
+ "36a72f0d9e": "Ajouter une étoile à Orca sur GitHub",
+ "c61b14be7c": "grok",
+ "5d9ba08673": "copilot",
+ "f472e97440": "aider",
+ "3c30fe2d51": "gemini",
+ "5fdf1dc2d1": "omp",
+ "9b0bc30160": "pi",
+ "882c4896fd": "opencode",
+ "27d9b996ba": "codex",
+ "5baf51c4d9": "open claude",
+ "aea7d2cccb": "openclaude",
+ "95b63edde7": "claude",
+ "41c2f9a025": "default",
+ "8ea37a05bc": "agent",
+ "e2da948f59": "Présélectionner un agent de codage IA dans le composeur de nouvel espace de travail.",
+ "db11502270": "Agent par défaut",
+ "3462308bd3": "tokens",
+ "660528b048": "coût",
+ "585beac3f8": "ttl",
+ "0efc9d96ad": "prompt",
+ "939b80f5fd": "minuteur",
+ "b2601a778c": "cache",
+ "40c9585e43": "Minuteur à rebours affichant le temps restant avant l'expiration du cache de prompt (agents Claude).",
+ "1e0f28c6f1": "Minuteur de cache de prompt",
+ "e49e739a59": "téléchargement",
+ "c9d8c1ce66": "notes de version",
+ "9e86ccd05c": "version",
+ "f89a94773c": "mise à jour",
+ "79ff46776e": "Recherche les mises à jour de l'application et installe une version plus récente d'Orca.",
+ "e15af4eb64": "Rechercher des mises à jour",
+ "6382fe9724": "npx",
+ "baa263d6d8": "agents",
+ "bda108e66c": "skill",
+ "244e3fb4c8": "Installer le skill Orca pour que les agents sachent utiliser la CLI Orca.",
+ "2d9f7b42df": "Skill d'agent",
+ "0a00691c06": "commande shell",
+ "dbeb1f348e": "command",
+ "88d3df9ce9": "terminal",
+ "fb4f338a3d": "chemin",
+ "924a660a78": "cli",
+ "ca529079bf": "Enregistrer ou supprimer la commande CLI Orca.",
+ "327e3fa70d": "Orca CLI",
+ "fb84767421": "bascule",
+ "f8f0ac213a": "séquentiel",
+ "12ecc640a8": "mru",
+ "54ba13831a": "récent",
+ "750420dd9a": "contrôle",
+ "fe62b3f09f": "ctrl",
+ "2a254b725e": "onglet",
+ "ca812803ea": "ordre récent des onglets",
+ "e53d585ed6": "Récents ou barre d'onglets.",
+ "256d92554d": "Ordre des onglets",
+ "22572e99c1": "annotations",
+ "1ff67ba40c": "notes",
+ "4dd5684836": "revue",
+ "d05f629d2c": "markdown",
+ "694613d47f": "Afficher les contrôles des notes de revue markdown locales en mode éditeur enrichi.",
+ "128bc09325": "Notes de revue Markdown",
+ "a0014961ae": "défilement",
+ "3ca5ab78a5": "code",
+ "e3919429c0": "vue d'ensemble",
+ "9c72990db8": "minimap",
+ "716a4dfb1f": "Afficher la minimap lors de l'édition d'un fichier.",
+ "6f584fcb48": "Minimap",
+ "19baae651b": "barre latérale",
+ "973ed6bfbf": "diff combiné",
+ "0a02059549": "arborescence de fichiers",
+ "2f42852568": "arborescence",
+ "3b5733573e": "diff",
+ "dec71988f0": "Afficher ou masquer l'arborescence de fichiers à l'ouverture des vues de diff combinées.",
+ "adec13f2ef": "Arborescence de fichiers des diffs par défaut",
+ "be24c7cd67": "scindé",
+ "233f7e2f37": "côte à côte",
+ "0a5fa65926": "en ligne",
+ "2b463f0bf9": "vue",
+ "ecb9415c80": "Format de présentation préféré pour l'affichage par défaut des diffs git.",
+ "2760c9933f": "Vue de diff par défaut",
+ "b2799ba622": "millisecondes",
+ "146728ac2c": "délai",
+ "86f54575c7": "enregistrement automatique",
+ "8ea61ad55c": "Délai d'attente d'Orca après votre dernière modification avant l'enregistrement automatique.",
+ "14e46c745b": "Délai d'enregistrement automatique",
+ "4469b6fa4e": "enregistrer",
+ "e9d948d3c3": "Enregistre automatiquement les modifications de l'éditeur et des diffs éditables après une courte pause.",
+ "ae21e806ce": "Enregistrement automatique des fichiers",
+ "c56cb6f1c2": "réseau",
+ "3566fce83f": "localhost",
+ "91a46caafc": "no_proxy",
+ "3a73054565": "contourner",
+ "20b711ac9e": "proxy",
+ "eb8946b2c9": "Hôtes qui doivent contourner le proxy HTTP configuré.",
+ "8436ff6f8e": "Règles de contournement du proxy",
+ "e55d62dfa4": "launchpad",
+ "9da6c875e5": "dock",
+ "b9096a44cf": "https_proxy",
+ "8f03d44672": "http_proxy",
+ "e3b1d42f95": "URL de proxy pour les requêtes réseau d'Orca et les processus enfants des terminaux locaux.",
+ "c29f23ab57": "Proxy HTTP",
+ "6c2ce8457c": "explorateur de fichiers",
+ "c9d9636f24": "Finder",
+ "68d03d9980": "vscode",
+ "ebf8f056b5": "zed",
+ "0cb3d94f00": "cursor",
+ "8fb00fcd05": "lanceur",
+ "e1ee631696": "éditeur",
+ "5a9df5566f": "ouvrir le menu",
+ "b8093e9a93": "ouvrir dans",
+ "a916662068": "Choisissez les applications proposées dans le menu « Ouvrir dans » d'un espace de travail.",
+ "451d4af994": "Applications « Ouvrir dans »",
+ "7e9b556873": "ignorer",
+ "ca86dd6e27": "boîte de dialogue",
+ "9f8558233a": "confirmer",
+ "7edf4f69e2": "automatisation",
+ "84c67d0108": "supprimer",
+ "a0c44061ee": "Afficher une boîte de dialogue de confirmation avant de supprimer une automatisation et son historique d'exécution.",
+ "d0a65b27fd": "Confirmer avant de supprimer les automatisations",
+ "df10666259": "worktree",
+ "ae98c9cf36": "Afficher une boîte de dialogue de confirmation avant de supprimer un espace de travail.",
+ "913242091d": "Confirmer avant de supprimer les espaces de travail",
+ "93f6ec5e70": "répertoire",
+ "9bde064915": "sous-dossier",
+ "ec5049e510": "imbriqué",
+ "b9cffd374d": "Créer les espaces de travail dans un sous-dossier nommé d'après le dépôt.",
+ "141f71c69f": "Imbriquer les espaces de travail",
+ "7887a2c262": "dossier",
+ "7baf524b04": "espace de travail",
+ "d0bc793689": "Dossier racine où sont créés les dossiers des espaces de travail.",
+ "4c95d08fa2": "Dossier des espaces de travail",
+ "161a86a9da": "Confirmer avant de fermer les onglets épinglés",
+ "8e593f04fc": "Afficher une boîte de dialogue de confirmation avant de fermer un onglet épinglé.",
+ "defaultProjectRuntime": "Runtime par défaut des projets",
+ "defaultProjectRuntimeDescription": "Choisissez le runtime hérité par les projets Windows locaux.",
+ "d2d2d929c0": "Vérification orthographique du Markdown enrichi",
+ "4497e2e2bb": "Affiche les soulignements d'orthographe et les suggestions du navigateur pendant l'édition du Markdown enrichi.",
+ "e61157e926": "Retour à la ligne dans l'éditeur",
+ "005be5c699": "Renvoie à la ligne les lignes longues dans les éditeurs de fichiers au lieu d'imposer un défilement horizontal.",
+ "editorFontFamily": "Police de l'éditeur",
+ "editorFontFamilyDesc": "Police utilisée par les éditeurs de fichiers et les vues de diff. Laissez vide pour suivre la police du terminal.",
+ "externalWorktrees": "Worktrees externes",
+ "externalWorktreesDescription": "Choisissez si les worktrees créés en dehors d'Orca apparaissent par défaut."
+ }
+ },
+ "git": {
+ "search": {
+ "16f53f7323": "gh",
+ "d088806071": "github",
+ "40f9b815fd": "budget api",
+ "b7e52124c7": "limite de débit",
+ "ead733645f": "glab",
+ "4808f065b3": "gitlab",
+ "2b4a72885d": "En-têtes de limite de débit REST actuels de la CLI GitLab, lorsqu'ils sont disponibles.",
+ "83ecb3f470": "Budget API GitLab",
+ "65b69d9f80": "graphql",
+ "1139f61512": "Limites de débit REST, Search et GraphQL actuelles de la CLI GitHub.",
+ "ff86e354c4": "Quota d'API GitHub",
+ "035134fcd9": "worktree",
+ "0c75583ca9": "sans risque",
+ "bae91effdd": "base à jour",
+ "de06e9d105": "ref de base",
+ "ab0e22c9f6": "actualiser le main local",
+ "d9f70d51a0": "main obsolète",
+ "0849b571fe": "à jour",
+ "c41e345153": "en retard sur main",
+ "6ee3cfff02": "git diff",
+ "564942ffc5": "origin/main",
+ "28192e3a63": "master",
+ "e3e9adde59": "main",
+ "0e993bf00f": "Lorsque vous créez un espace de travail, Orca actualise la base distante et fait avancer en fast-forward, sans risque, votre branche locale correspondante, telle que main ou master. Cela évite que des commandes comme git diff main...HEAD comparent avec un historique obsolète. Orca ignore cette mise à jour si cette branche contient des modifications non committées ou des commits locaux uniquement.",
+ "f8bda25f29": "Garder la branche main locale à jour",
+ "769ddd7f81": "custom",
+ "1d2fae1fa2": "nom d'utilisateur git",
+ "f83c8937c4": "nommage des branches",
+ "5ecd91c5ef": "Préfixe ajouté aux noms de branches à la création des worktrees.",
+ "68bd65fdb8": "Préfixe de branche",
+ "sourceControlGroupOrderTitle": "Ordre des groupes du contrôle de code source",
+ "sourceControlGroupOrderDescription": "Choisissez si Modifications, Modifications indexées ou Fichiers non suivis apparaissent en premier dans le contrôle de code source.",
+ "groupOrder": "ordre des groupes",
+ "changesFirst": "modifications d'abord",
+ "stagedFirst": "staged d'abord",
+ "untrackedFirst": "untracked d'abord",
+ "sourceControl": "contrôle de code source",
+ "gitChanges": "modifications git",
+ "compareAgainstUpstreamTitle": "Base de comparaison par défaut",
+ "compareAgainstUpstreamDescription": "Choisissez la base que le contrôle de code source utilise par défaut pour comparer les changements commités. L'amont de la branche suit automatiquement la branche courante et revient à la branche par défaut du dépôt en l'absence d'amont. Vous pouvez toujours changer la base de comparaison d'un worktree depuis son panneau Git. Les cibles de pull request et de rebase ne changent pas.",
+ "compareBase": "base de comparaison",
+ "defaultCompareBase": "base de comparaison par défaut",
+ "defaultBranch": "branche par défaut",
+ "repositoryDefault": "défaut du dépôt",
+ "branchUpstream": "upstream de la branche",
+ "currentBranch": "branche actuelle",
+ "upstream": "upstream",
+ "localChanges": "modifications locales",
+ "originMaster": "origin/master",
+ "committedChanges": "modifications committées"
+ }
+ },
+ "input": {
+ "search": {
+ "886597d6b3": "macos",
+ "26c83b06c5": "linux",
+ "71905435dd": "x11",
+ "7059cfb00a": "presse-papiers",
+ "c4440c3986": "coller",
+ "5fb84ba77f": "bouton du milieu",
+ "31ba58c8ae": "clic milieu",
+ "de51e18ee9": "sélection primaire",
+ "e5cd0e7a46": "sélection",
+ "e25165320e": "édition",
+ "b51d47ceb7": "entrée",
+ "874d88f4a6": "Activé par défaut sur Linux et macOS. Linux utilise le presse-papiers de sélection du système ; les autres plateformes utilisent un tampon privé.",
+ "d952ce9b46": "Collage de la sélection au clic milieu"
+ }
+ },
+ "integrations": {
+ "search": {
+ "a626990bd2": "déconnexion",
+ "3c3d3d8ffa": "connect",
+ "faa0b5a0d9": "clé API",
+ "c450244ad7": "intégration",
+ "7319e3015b": "linear",
+ "16a486a49d": "Connectez Linear pour parcourir et lier des issues.",
+ "b027b4b318": "Intégration Linear",
+ "20540996ef": "identifiants",
+ "2ec2bd328c": "jeton API",
+ "7345b7c3e6": "atlassian",
+ "e1263dd748": "jira",
+ "76f6af7c57": "Connectez Jira Cloud ou mettez à jour les identifiants du jeton API Jira.",
+ "617603509b": "Intégration Jira",
+ "8c568d761c": "pull request",
+ "33180e8c10": "auto-hébergé",
+ "129fc59aa8": "gitea",
+ "d0d019dc29": "Authentification Gitea via des variables d'environnement de jeton API.",
+ "aab86d64e5": "Intégration Gitea",
+ "03a7b275be": "ado",
+ "ed63380247": "azure repos",
+ "b38b5d27f1": "azure devops",
+ "7b1f3984bb": "Authentification Azure DevOps Repos via des variables d'environnement de jeton.",
+ "af6611fa6e": "Intégration Azure DevOps",
+ "50d20817f7": "bitbucket",
+ "c97d58a0f3": "Authentification Bitbucket Cloud via des variables d'environnement de jeton API.",
+ "67a2a0e868": "Intégration Bitbucket",
+ "371ee914d2": "merge request",
+ "581844769a": "mr",
+ "b40cbe5de4": "glab",
+ "b939695c69": "gitlab",
+ "6e2ab619c6": "Authentification GitLab via la CLI glab.",
+ "b50b71ef9d": "Intégration GitLab",
+ "41ccade05c": "gh",
+ "b79c21bd42": "github",
+ "7166b9090c": "Authentification GitHub via la CLI gh.",
+ "f16e41cc72": "Intégration GitHub",
+ "760e2446e0": "Authentification Bitbucket Cloud avec un jeton API enregistré ou des variables d'environnement.",
+ "af5ae87847": "jeton d'accès"
+ }
+ },
+ "jira": {
+ "integration": {
+ "card": {
+ "8ff73fef62": "Les jetons Jira sont chiffrés par le runtime actif et stockés localement. Ressaisir la même URL de site et le même e-mail remplace le jeton API de ce site.",
+ "9046a20d4c": "Déconnecter {{value0}}",
+ "eaffa454e9": "Mettre à jour",
+ "cec06a0f79": "Test en cours…",
+ "ab350991b8": "Vérifié",
+ "d914d7ab70": "Vérification…",
+ "5936977fcd": "Annuler",
+ "1666f8d562": "Créer un jeton API Atlassian",
+ "1ab7f551f3": "Token API Atlassian",
+ "09d310e42d": "you@example.com",
+ "27dae4ab60": "https://example.atlassian.net",
+ "a28f417220": "Connecter Jira",
+ "9bb34706ca": "Connecté",
+ "efaab83c5d": "Ajouter un site",
+ "09742875cd": "Jira",
+ "255bfe98ec": "Tester",
+ "5fb1562315": "error",
+ "3df81cb0ac": "ok",
+ "2e8bb790fd": "Se connecter",
+ "33a8b261ee": "Mettre à jour les identifiants",
+ "d5c5b47bb9": "mise à jour",
+ "fb854902d9": "connexion en cours",
+ "9a9f8d4910": "Connectez Jira Cloud pour parcourir, créer et lier des issues.",
+ "74f3063026": "{{value0}} site{{value1}} connecté",
+ "statusConnected": "Connecté",
+ "statusNotConnected": "Non connecté"
+ }
+ }
+ },
+ "mobile": {
+ "emulator": {
+ "search": {
+ "2348045036": "Choisissez l'appareil émulateur qu'Orca ouvre par défaut.",
+ "2bb2e09225": "skill mobile",
+ "bbe4267416": "type d'émulateur",
+ "64494f03c3": "attach de l'émulateur",
+ "6f728f1456": "tap de l'émulateur",
+ "f8b871d655": "CLI de l'agent",
+ "2e0b45b2ba": "Utilisez les commandes CLI d'Orca pour lister, attacher, taper et saisir du texte dans un émulateur mobile.",
+ "ea3eac39bb": "Contrôle par CLI d'agent",
+ "8ef0f08d36": "runtime",
+ "27397fe8e9": "outils de ligne de commande Xcode",
+ "7650063d17": "simctl",
+ "3211e7acf9": "xcrun",
+ "42bfab45d8": "disponibilité",
+ "ea1f51b980": "Vérifiez que Xcode, simctl, serve-sim et les appareils émulateurs sont prêts.",
+ "0b95dfd5b3": "Disponibilité de l'émulateur",
+ "04c5f5d901": "appareil",
+ "25d7bfbcd4": "udid",
+ "ec3c4043fd": "iPad par défaut",
+ "1dc8c52ffa": "iPhone par défaut",
+ "ab4814f3c5": "simulateur par défaut",
+ "54184cb9c5": "Appareil émulateur par défaut",
+ "b8ddd13195": "émulateur de l'agent",
+ "1ad6fb6230": "appareil par défaut",
+ "ac0a985873": "skill émulateur",
+ "9353854ff3": "émulateur Orca",
+ "d4b7833894": "CLI Orca",
+ "84e5706975": "serve-sim",
+ "7c5a8a2bee": "xcode",
+ "bec7231663": "iPad",
+ "49727355a3": "iPhone",
+ "6b6407dc1f": "émulateur",
+ "2d67f708ce": "simulateur",
+ "c5eca29310": "simulateur iOS",
+ "25159de808": "émulateur mobile",
+ "9595354cff": "Configurez la prise en charge des émulateurs mobiles pour Orca et les agents de codage.",
+ "cdd3c31918": "Émulateur mobile"
+ }
+ },
+ "pane": {
+ "search": {
+ "dbccde3a60": "clôturer",
+ "9e16be01d6": "arrière-plan",
+ "3a5e31e84b": "laisser",
+ "8015fd9523": "conserver",
+ "aa3f736042": "redimensionner",
+ "356c31d6dc": "largeur",
+ "fadcbfdd99": "ajuster",
+ "ad08035c5f": "téléphone",
+ "6cd2bfdb0e": "restaurer",
+ "b34ad5b3a7": "terminal",
+ "6db86f445f": "mobile",
+ "707fc78052": "Choisissez ce qu'il advient des terminaux consultés sur mobile après la fermeture de l'app ou un changement de contexte.",
+ "1e711aca11": "Quand vous quittez l'app mobile",
+ "126afc5dbd": "distant",
+ "70f505f3c3": "lan",
+ "1802188b5d": "Wi-Fi",
+ "dd6e671aa9": "adresse",
+ "1f70d63998": "IP",
+ "d0c89bc4a9": "superposition",
+ "87711f4b8f": "vpn",
+ "16bff559a0": "tailnet",
+ "c690e3ee38": "tailscale",
+ "a023683767": "interface",
+ "7b37c2e557": "réseau",
+ "3190ef67a4": "Choisissez l'adresse réseau à utiliser pour l'appairage mobile.",
+ "d96c315227": "Interface réseau",
+ "7d01f93ec0": "connecté",
+ "5e8fda4d7f": "appairé",
+ "905c65a308": "révoquer",
+ "82783d9b71": "appareils",
+ "13419718b3": "Gérez les appareils mobiles appairés.",
+ "9d3a9397ba": "Appareils connectés",
+ "2128a21096": "scanner",
+ "e518cbd61c": "appairer",
+ "4a0c826f3d": "code",
+ "3c1807a81a": "QR",
+ "7fb728fb2b": "Appairez un appareil mobile en scannant un code QR.",
+ "d49925710a": "Appairage mobile"
+ }
+ },
+ "settings": {
+ "search": {
+ "b730ff7049": "expérimental",
+ "8d4ba0ef09": "bêta",
+ "6bfa001752": "APK",
+ "a7eececc1d": "android",
+ "7e801801ac": "distant",
+ "0b7e585cb9": "scanner",
+ "59b1d75fd1": "code",
+ "87816d1c59": "QR",
+ "cf2c93b479": "appairer",
+ "f4ed142753": "téléphone",
+ "f213400800": "mobile",
+ "671eb4173c": "Contrôlez les terminaux et les agents depuis votre téléphone.",
+ "ffd52a96e4": "Mobile",
+ "1de96ec8a6": "Afficher le bouton Orca Mobile",
+ "682293cadf": "Afficher le bouton Orca Mobile en haut de la barre latérale gauche.",
+ "e4f4daea0e": "relais",
+ "5d5af8e041": "iPhone"
+ }
+ }
+ },
+ "notifications": {
+ "search": {
+ "aa288005c3": "test",
+ "ca8faa40d7": "notifications",
+ "4e30b1925e": "Déclenchez une notification de bureau d'exemple via le chemin de livraison natif.",
+ "ef9b311346": "Envoyer une notification de test",
+ "ecdeff4993": "niveau sonore",
+ "d58b64dddf": "volume",
+ "dc7d7c07cd": "son",
+ "eeb6f77322": "Volume de lecture des sons de notification non système.",
+ "aace1a62c6": "Volume des notifications",
+ "ef86a782cc": "bong",
+ "3014ad1b8f": "ding",
+ "079c29aeb5": "flac",
+ "722face52f": "aac",
+ "6ecb8418cb": "m4a",
+ "d16ae23645": "ogg",
+ "57e34a31cd": "wav",
+ "5362074f19": "mp3",
+ "6e08f78315": "audio",
+ "c718793e95": "Choisissez le fichier audio intégré, système ou local qu'Orca lit pour les notifications de bureau.",
+ "ea8cb8d9ce": "Son de notification",
+ "4ada6bfde9": "filtrage",
+ "fa60d8e4ab": "masquer",
+ "a4c3b29a3c": "focus",
+ "7247b97a31": "Évitez de notifier quand Orca est au premier plan sur le worktree actif.",
+ "96562a72c6": "Masquer quand la fenêtre est active",
+ "a2ab73b325": "attention",
+ "ae0487f8fd": "cloche",
+ "c638ae989d": "terminal",
+ "d3f1c48677": "Notifiez quand un terminal en arrière-plan émet un caractère de cloche (bell).",
+ "a5edee1d99": "Sonnerie du terminal",
+ "193e1f107c": "tâche",
+ "dd9d3e5f0f": "idle",
+ "5f7472d3fb": "terminé",
+ "7fa07e9600": "agent",
+ "10d83ef8dc": "Notifiez quand un agent de codage passe de l'état actif à l'état inactif.",
+ "bdc1edaeb4": "Tâche d'agent terminée",
+ "adbc3a0fcf": "natif",
+ "72539aede4": "système",
+ "51ae2183e1": "bureau",
+ "0534c76311": "Interrupteur principal des notifications de bureau d'Orca.",
+ "4a210b2f72": "Activer les notifications"
+ }
+ },
+ "orchestration": {
+ "search": {
+ "f5d39af41e": "agents enfants",
+ "c766a01978": "handoff",
+ "08c65b12a2": "exemples",
+ "f278fd04db": "codex",
+ "32c5098e7b": "claude",
+ "21c28ccdf7": "coordinateur",
+ "741dfc03fa": "worker",
+ "ca54c69806": "DAG",
+ "7ad948b714": "tâche",
+ "eee028ae14": "dispatch",
+ "9a5ebdca31": "messagerie",
+ "91fc8ab7e5": "coordination",
+ "13ba5c6cbd": "agents",
+ "d86705ba77": "multi-agents",
+ "a7f76b4ca7": "orchestration",
+ "e05ff36753": "Coordonnez plusieurs agents de codage via messagerie, DAG de tâches, dispatch et points de décision.",
+ "c34045764e": "Orchestration d'agents"
+ }
+ },
+ "privacy": {
+ "search": {
+ "3922051573": "données",
+ "e8bc614a18": "désactiver",
+ "d8191ae5ca": "variable d'environnement",
+ "94e04427f6": "env",
+ "664f1a8984": "intégration continue",
+ "5854a5c752": "ci",
+ "69637f4dc4": "orca_telemetry_disabled",
+ "058550f6bc": "do_not_track",
+ "83a6cd79b3": "do not track",
+ "f7a2d9f137": "Variables d'environnement qui désactivent l'envoi des données de télémétrie.",
+ "e058a3c98d": "Variables d'environnement de télémétrie",
+ "1686c07fee": "assistance",
+ "c0494ff48a": "diagnostics",
+ "8b08f32366": "Diagnostics de l'app et contrôles de partage avec le support.",
+ "6d258d2ed6": "Diagnostics",
+ "ead1deded2": "partager",
+ "27a27b2f63": "refuser",
+ "4d4bb76bf4": "accepter",
+ "b021b9cb81": "anonyme",
+ "79c319948b": "utilisation",
+ "77d3180def": "télémétrie",
+ "b707cc3981": "Aidez à améliorer Orca en envoyant des événements anonymes d'utilisation des fonctionnalités.",
+ "57b283461a": "Partager les données d'utilisation anonymes",
+ "2b5a5c312f": "posthog",
+ "4104f6f0f3": "analytique",
+ "10124159f1": "confidentialité",
+ "aa3b794c17": "Données d'utilisation anonymes du produit, diagnostics et contrôles de télémétrie.",
+ "5c508bad41": "Confidentialité & télémétrie"
+ }
+ },
+ "quick": {
+ "commands": {
+ "search": {
+ "3c316e6ef8": "yarn",
+ "b86c727100": "npm",
+ "b949a7c0a0": "pnpm",
+ "0b78c4a165": "lancer",
+ "2d8aff42be": "exécuter",
+ "1c5bdcd0f2": "dépôt",
+ "89d2a9ad9f": "dépôt",
+ "f58b92a48f": "projet",
+ "8bf43c2dad": "global",
+ "a26ecdb77b": "snippet",
+ "d07d130849": "raccourci",
+ "0073cf8ce9": "terminal",
+ "cfffa6cdb6": "commandes",
+ "fecb031823": "command",
+ "236d4cfac8": "rapide",
+ "d691c4e8d8": "Commandes de terminal enregistrées, lançables depuis n'importe quel terminal, à portée globale ou propre à un projet donné.",
+ "4c8945952b": "Commandes rapides"
+ }
+ }
+ },
+ "repository": {
+ "search": {
+ "bc7e504b8e": ".orca/issue-command",
+ "603c68b68c": "orca.yaml",
+ "9dc60d7f6d": "github",
+ "ec70364df2": "workflow",
+ "66b584bd6c": "commande d'issue",
+ "2011a6a4f2": "commande d'issue GitHub",
+ "d42d1e49c0": "Commande d'issue liée définie dans un fichier, configurée via orca.yaml et une surcharge locale facultative.",
+ "d86ea12d16": "Commande d'issue GitHub personnalisée",
+ "c5e8bdbcbb": "ignorée par défaut",
+ "a69c5cbe90": "exécutée par défaut",
+ "80c490b012": "demander",
+ "f9d84b7971": "politique d'exécution du setup",
+ "c00a549e03": "Choisissez le comportement par défaut quand un script de setup est disponible.",
+ "cdfe398068": "Quand exécuter le setup",
+ "5e9445bbfd": "faisant foi",
+ "f1e1bfa89f": "source",
+ "1d90a6cfbb": "les deux",
+ "fcb8fa8144": "partagé",
+ "0432d2fb7c": "local",
+ "ed269fad69": "source des commandes",
+ "19f58d6d89": "avancé",
+ "d141897c90": "Détails sur la source des commandes et orca.yaml.",
+ "cc11699c3d": "Avancé",
+ "bf460fded8": "yaml",
+ "9cad92fe77": "hooks orca.yaml",
+ "6b80f7d3c8": "scripts des paramètres locaux",
+ "a1a4c51d58": "commande d'archive",
+ "fbfd2386e8": "script d'archive",
+ "4c17787d7b": "archiver",
+ "8655e3387b": "hooks",
+ "acd1157f0c": "Scripts locaux et partagés exécutés avant l'archivage d'un worktree.",
+ "bce0ca23c6": "Script d'archivage",
+ "491b05d6e6": "commande de setup",
+ "a31b43a7f8": "script de setup",
+ "5590388dfa": "configuration",
+ "baaf70bb37": "Scripts locaux et partagés exécutés après la création d'un nouveau worktree.",
+ "b79df26937": "Script de setup",
+ "d73fb47b45": ".claude/mcp.json",
+ "db11b337c4": ".claude.json",
+ "26f42fe773": ".cursor/mcp.json",
+ "e760e3fae7": ".mcp.json",
+ "16dc7a4637": "model context protocol",
+ "343f0a508c": "mcp",
+ "3c31801626": "Inspectez les fichiers de configuration des serveurs MCP au niveau du projet.",
+ "31bd0a2420": "Configurations MCP",
+ "84da7fa2d7": "node_modules",
+ "0a3a582794": "env",
+ "3c180a251c": "lien",
+ "f1c53f2820": "worktree",
+ "7e228fc439": "liens symboliques",
+ "c06adcf136": "lien symbolique",
+ "copy": "copier",
+ "clone": "cloner",
+ "apfs": "apfs",
+ "ed885e589f": "Chemins à matérialiser depuis le checkout principal vers les nouveaux worktrees.",
+ "01b3377ebc": "Chemins partagés des worktrees",
+ "fff8834983": "prompt",
+ "fa3131f223": "model",
+ "130d76dc16": "renommer",
+ "917dce844a": "nom de branche",
+ "8068d8d0f1": "pr",
+ "5ff7fe1ade": "pull request",
+ "eec39b3de6": "message de commit",
+ "cfad7ce5f3": "ai",
+ "a47f51127e": "contrôle de code source",
+ "6cc5c65e64": "Surcharges de génération git propres au projet.",
+ "eec3995dc6": "Auteur IA Git",
+ "availableHosts": "Hôtes disponibles",
+ "availableHostsDescription": "Hôtes où ce projet est configuré.",
+ "host": "hôte",
+ "ssh": "ssh",
+ "remote": "distant",
+ "vm": "VM",
+ "cc876ca5f2": "dépôt",
+ "6469de5368": "projet",
+ "3067595d82": "supprimer",
+ "c86478c3d8": "Retirez ce projet d'Orca.",
+ "c5266c2c9d": "Retirer le projet",
+ "4b9a18a56d": "monorepo",
+ "4e2529722c": "répertoires",
+ "1ff4f12c0c": "répertoire",
+ "9f5ae26ccd": "préréglages",
+ "095fca94fe": "préréglage",
+ "aa42616e3d": "checkout",
+ "4f3c0230c2": "sparse",
+ "90a331fd68": "Ensembles de répertoires enregistrés pour la création de worktrees sparse.",
+ "1f0f20bbb6": "Préréglages de sparse checkout",
+ "4733ec2395": "../worktrees",
+ "58d8bca414": "relatif",
+ "a325a89dff": "chemin de l'espace de travail",
+ "f3e6dee5fe": "chemin du worktree",
+ "cd33a5525e": "Répertoire spécifique au projet pour les nouveaux worktrees.",
+ "443d127b5a": "Emplacement des worktrees",
+ "9811f3d152": "branche",
+ "f41cef5083": "ref de base",
+ "f571081ec4": "Branche ou ref de base par défaut à la création des worktrees.",
+ "094adbe930": "Base de worktree par défaut",
+ "27733eb6c1": "favicon",
+ "1e73e840ff": "emoji",
+ "cb4b4de666": "avatar",
+ "c1075178cf": "badge",
+ "6d8de2f090": "hex",
+ "8d045419b1": "couleur",
+ "b2546efab5": "icône du dépôt",
+ "6438a94c63": "icône du projet",
+ "a1f3a2bd47": "Icône et couleur du projet utilisées dans la barre latérale et les onglets.",
+ "b24f00294a": "Icône du projet",
+ "cd73b976d7": "nom du dépôt",
+ "92af66c7ce": "nom du projet",
+ "883aad2801": "Détails d'affichage propres au projet pour la barre latérale et les onglets.",
+ "7e1e456a95": "Nom d'affichage",
+ "keepForkUpToDate": "Garder le fork à jour",
+ "keepForkUpToDateDescription": "Faites avancer ce fork en fast-forward depuis upstream, sans risque.",
+ "projectRuntime": "Runtime des projets",
+ "projectRuntimeDescription": "Choisissez si ce projet s'exécute sous Windows ou WSL.",
+ "externalWorktrees": "Worktrees externes",
+ "externalWorktreesDescription": "Définir si les worktrees créés hors d'Orca apparaissent pour ce projet."
+ }
+ },
+ "runtime": {
+ "environments": {
+ "search": {
+ "772e3b4753": "VM",
+ "45501ff2c3": "cloud",
+ "2bd988d041": "code d'appairage",
+ "5cd7dca3b8": "distant",
+ "d760866285": "client",
+ "09568ccc65": "serveur",
+ "ebd5369acf": "environnement",
+ "d198440ce3": "runtime",
+ "baec27aa8f": "Connectez ce navigateur à un serveur Orca enregistré.",
+ "3517fb2ec0": "Serveur actif",
+ "c6e5a03aa0": "dev box",
+ "f1575f1e09": "client web",
+ "81444c4102": "URL d'appairage",
+ "104f4d7dbd": "appairage",
+ "4575341c77": "Choisissez le bureau local, ajoutez un serveur Orca distant enregistré ou générez une URL d'appairage."
+ }
+ }
+ },
+ "shortcuts": {
+ "search": {
+ "ca6a0c2df7": "raccourci",
+ "groupShortcut": "Raccourci {{value0}}",
+ "4811a8264a": "terminal d'abord",
+ "afda131738": "Orca d'abord",
+ "0ecfc47434": "conflit",
+ "0f8cb15582": "agent",
+ "f1adebbe8c": "shell",
+ "7f1b38f59a": "TUI",
+ "7e3fc707aa": "terminal",
+ "0ecba9aa5f": "clavier",
+ "ebd7d81e1d": "Choisissez si Orca ou le terminal ciblé l'emporte lorsque des raccourcis se chevauchent.",
+ "f052906167": "Raccourcis dans le terminal"
+ }
+ },
+ "ssh": {
+ "search": {
+ "d41f296f64": "ping",
+ "237b391f7c": "connexion",
+ "8cb870b109": "test",
+ "7efd17e816": "ssh",
+ "96ca5d9a0b": "Testez la connectivité vers une cible SSH.",
+ "a3058f3605": "Tester la connexion",
+ "2cd40ba0d0": "hôtes",
+ "5220501141": "config",
+ "3b12e064a4": "importer",
+ "7f251a45a8": "Importez les hôtes depuis ~/.ssh/config.",
+ "41a3127094": "Importer depuis la config SSH",
+ "f9493b80c0": "serveur",
+ "8fb1cc87cc": "hôte",
+ "09395490af": "cible",
+ "00d1fda01a": "nouveau",
+ "f7b6383aec": "ajouter",
+ "62826efbe9": "Ajoutez une nouvelle cible SSH distante.",
+ "f5a691bb6c": "Ajouter une cible SSH",
+ "d4bcd497c7": "distant",
+ "74c6d90d78": "Gérez les cibles SSH distantes.",
+ "380a788da7": "Connexions SSH"
+ }
+ },
+ "tasks": {
+ "search": {
+ "58cda6f9c0": "masquer",
+ "44083ae418": "écran",
+ "604d8e4089": "atlassian",
+ "5430396e11": "jira",
+ "412ec3c702": "linear",
+ "11f001cdd4": "gitlab",
+ "c10ac2125e": "github",
+ "3d81c26d78": "source",
+ "cf0e3e0c2f": "fournisseur",
+ "2ec54bee51": "tâches",
+ "5b8e4aace5": "Fournisseurs de tâches",
+ "providersDescription": "Connectez des fournisseurs de tâches, installez le skill d'agent Linear et choisissez ce qui apparaît dans Tâches.",
+ "setup": "configuration",
+ "apiKey": "clé API",
+ "skill": "skill",
+ "connect": "connect"
+ }
+ },
+ "terminal": {
+ "clipboard": {
+ "search": {
+ "5fb3512e8c": "coller",
+ "a38508c419": "copier",
+ "d106f44fb4": "distant",
+ "043b32faa1": "ssh",
+ "9fda309db9": "fzf",
+ "64533e30cc": "nvim",
+ "2061d8db1a": "neovim",
+ "5ffcd13c90": "tmux",
+ "10d73e22d3": "presse-papiers",
+ "9dfc125cd3": "osc52",
+ "62d1208b90": "osc 52",
+ "459fea094a": "Laissez les programmes du terminal copier vers le presse-papiers système via OSC 52, y compris par-dessus SSH.",
+ "74db8721e4": "Autoriser les écritures presse-papiers des TUI (OSC 52)",
+ "4043e294d2": "gnome",
+ "cf83ac3dbd": "linux",
+ "737cef6de1": "x11",
+ "e87c6d776d": "automatique",
+ "664789b73a": "auto",
+ "c38c18be15": "sélection",
+ "797fdfe4ca": "sélectionner",
+ "603818e8d8": "Copiez automatiquement les sélections du terminal vers le presse-papiers dès qu'une sélection est faite.",
+ "3bdc84f059": "Copie à la sélection"
+ }
+ },
+ "search": {
+ "c047f398cc": "lancer",
+ "b872de3926": "emplacement",
+ "fd6c24313d": "nouveau",
+ "f44643328e": "onglet",
+ "18ce996647": "vertical",
+ "54a9b3725b": "horizontal",
+ "de7bc1d5f5": "scindé",
+ "7a48c7715b": "espace de travail",
+ "6b659fff2a": "script",
+ "4529806908": "configuration",
+ "2610ee3b56": "Où s'exécute le script de setup du dépôt lorsqu'un nouvel espace de travail est créé : un fractionnement vertical (par défaut), un fractionnement horizontal ou un onglet en arrière-plan intitulé « Setup ».",
+ "5be2d67678": "Emplacement du script de configuration",
+ "0ce176909a": "thème",
+ "4ba8623632": "palette",
+ "11fd3fbcf2": "ansi",
+ "d8bd6182b8": "remplacement",
+ "674b7c8436": "couleur",
+ "3023e01415": "Remplacer individuellement les couleurs du terminal.",
+ "aed2a4b4eb": "Surcharges de couleurs",
+ "6eaf7ee0e4": "cursor",
+ "34fe1af39d": "saisie",
+ "ee611ae238": "masquer",
+ "ea364ce6e4": "souris",
+ "77201c0bb2": "Masquer le curseur de la souris pendant la saisie dans le terminal.",
+ "d1fe5f99ff": "Masquer la souris pendant la saisie",
+ "f25d948664": "marge",
+ "b2f52cb96c": "espacement",
+ "e8baf0d12c": "marge interne",
+ "4655567c37": "Marge verticale autour de la grille du terminal, en pixels.",
+ "692c4ad032": "Marge verticale",
+ "75691e4911": "Marge horizontale autour de la grille du terminal, en pixels.",
+ "b4f182f24d": "Marge horizontale",
+ "6c2f9f05c8": "vibrance",
+ "4f7f8f28ca": "transparence",
+ "f6dd9ff606": "arrière-plan",
+ "71eb45e293": "flou",
+ "0838b3717b": "fenêtre",
+ "bc2054657a": "Applique un flou d'arrière-plan à la fenêtre du terminal. Nécessite un redémarrage.",
+ "72d0482137": "Flou de fenêtre",
+ "7db59c4738": "alpha",
+ "46d99ef4bb": "opacité",
+ "4c643695aa": "Contrôle la transparence de l'arrière-plan du terminal.",
+ "b36fd2416d": "Opacité de l'arrière-plan",
+ "d4daf4f612": "dégeler",
+ "88561b3499": "figé",
+ "0a05629060": "récupérer",
+ "f66a7cf715": "terminal",
+ "6892fb1019": "restart",
+ "cde233f5da": "scrollback",
+ "3982d88725": "historique",
+ "456da64d4d": "effacer",
+ "920573d65b": "tout tuer",
+ "a3e5297c10": "tuer",
+ "a8d2784214": "gérer",
+ "d802a578bf": "sessions",
+ "9f2dda133c": "pty",
+ "f35400f7e8": "daemon",
+ "f72abc493c": "Récupérez des terminaux figés en tuant des sessions, en effaçant le scrollback enregistré ou en redémarrant le daemon.",
+ "6f5d486a68": "Gérer les sessions",
+ "10f9fb6fea": "paramètres",
+ "2ade3ea490": "config",
+ "fd752b3cac": "importer",
+ "73e9422f19": "Import unique des paramètres de terminal Ghostty pris en charge.",
+ "a979df0083": "Importer depuis Ghostty",
+ "warp_import": {
+ "title": "Importer depuis Warp",
+ "description": "Importez des thèmes Warp comme thèmes de terminal Orca.",
+ "keyword_warp": "warp",
+ "keyword_themes": "thèmes",
+ "keyword_yaml": "yaml",
+ "keyword_legacy_title": "Importer des thèmes depuis Warp"
+ },
+ "yaml_import": {
+ "title": "Importer depuis YAML",
+ "description": "Importez des fichiers YAML de thèmes comme thèmes de terminal Orca.",
+ "keyword_yaml": "yaml",
+ "keyword_custom": "custom"
+ },
+ "4cec42dbf7": "intl",
+ "b495dc6a9f": "jis",
+ "d8d6f7a3c5": "macos",
+ "1ab57a0fbd": "Mac",
+ "abaa24752d": "clavier",
+ "24f7977756": "japonais",
+ "98059d0944": "antislash",
+ "9c35f56625": "yen",
+ "063914c486": "Détermine si la touche Yen JIS (¥) envoie un antislash (\\) à la place.",
+ "694b8764ac": "Yen JIS (¥) en antislash (\\)",
+ "fae142a354": "readline",
+ "b3b94cfcb5": "international",
+ "dd4f6cb541": "allemand",
+ "983d45cf4c": "composer",
+ "7ace5beec9": "Meta",
+ "38f1b4f4cb": "touche",
+ "c4427dc5ff": "Alt",
+ "b37edfc65a": "Option",
+ "1f8b00f5ce": "Détermine si la touche Option de macOS envoie des séquences Alt/Esc ou compose des caractères. Équivaut à macos-option-as-alt de Ghostty.",
+ "9bd7229927": "Option comme Alt",
+ "affb14efd4": "sélection",
+ "d2a366c7f9": "double-clic",
+ "4ed3e239a8": "limite",
+ "d4aeafac10": "séparateur",
+ "7286cd2566": "mot",
+ "3ab64c47d8": "Caractères traités comme séparateurs de mots pour la sélection par double-clic.",
+ "957a0203fc": "Séparateurs de mots",
+ "56fff3d113": "mémoire",
+ "fffdff40a7": "buffer",
+ "rows": "lignes",
+ "f7d56b6281": "Lignes de terminal de bureau conservées.",
+ "7674e758e1": "Lignes de scrollback",
+ "411229c636": "clair",
+ "781f49d942": "diviseur",
+ "77d9f9cd55": "Contrôle la ligne de séparation entre volets en mode clair.",
+ "595b97b446": "Couleur du séparateur en mode clair",
+ "7718d70356": "aperçu",
+ "1dee533bd9": "Choisissez le thème utilisé quand Orca est en mode clair.",
+ "1d89457764": "Thème clair",
+ "da864e6cec": "mode clair",
+ "f268092ee3": "Le mode clair peut utiliser son propre thème de terminal.",
+ "match_dark_mode_title": "Aligner sur le mode sombre",
+ "f785374072": "sombre",
+ "9c32726f47": "Contrôle la ligne de séparation entre volets en mode sombre.",
+ "8987db7ff2": "Couleur du séparateur en mode sombre",
+ "13f6310dd3": "Choisissez le thème de terminal utilisé en mode sombre.",
+ "ec07ce9b02": "Thème sombre",
+ "f036794286": "actif",
+ "846a7a1204": "volet",
+ "d1fa00a9cb": "survol",
+ "b5116e7b12": "suit",
+ "f5d1e3d472": "focus",
+ "17cc3ea102": "Le survol d'un volet de terminal l'active sans clic nécessaire. Équivaut au paramètre focus-follows-mouse de Ghostty. Les sélections et le changement de fenêtre restent sûrs.",
+ "c6178a2b4d": "Le focus suit la souris",
+ "f637a7dee9": "épaisseur",
+ "e58d4040d0": "Épaisseur de la ligne de séparation des volets.",
+ "2d5ab88b7c": "Épaisseur du séparateur",
+ "6c4c85ba43": "atténuation",
+ "18dd5026c6": "Opacité appliquée aux volets qui ne sont pas actuellement actifs.",
+ "72bbcbd1dd": "Opacité des volets inactifs",
+ "d4f7d1ce5c": "Opacité du curseur du terminal.",
+ "7f1e356a54": "Opacité du curseur",
+ "25f606d9e5": "clignotement",
+ "a27f6edf52": "Utilise la variante clignotante de la forme de curseur sélectionnée.",
+ "b03d01fd49": "Curseur clignotant",
+ "eefd1d8332": "souligné",
+ "015c82349f": "bloc",
+ "a6e9dcc829": "barre",
+ "275a9d6395": "Apparence par défaut du curseur pour les volets de terminal Orca.",
+ "97bcfff662": "Forme du curseur",
+ "1abcf4d7de": "linux",
+ "7d924d870d": "graphismes",
+ "bc7ae1f7c0": "rendu",
+ "fffa9ab980": "moteur de rendu",
+ "6cddc858ba": "WebGL",
+ "4b4e80d850": "accélération",
+ "db82cb13b0": "GPU",
+ "8f9f953de7": "Détermine si le terminal utilise le rendu WebGL de xterm.js. Auto essaie WebGL quand le moteur de rendu est pris en charge, avec repli prudent pour un rendu logiciel ou un GPU inconnu.",
+ "13a2502dfc": "Accélération GPU",
+ "d5e6c7fab1": "fonctionnalités de police",
+ "a16224d16a": "calt",
+ "6ded6297fe": "iosevka",
+ "e3aeea308e": "cascadia code",
+ "35c2311a33": "jetbrains mono",
+ "7f7640c29e": "fira code",
+ "7ab424c4d3": "ligature",
+ "afc8d5f790": "ligatures",
+ "103cdb862f": "typographie",
+ "893aa92997": "Affiche les ligatures de programmation (p. ex. => → ≠ ≥) pour les polices qui en proposent. « Auto » n'active les ligatures que pour les polices à ligatures connues (Fira Code, JetBrains Mono, Cascadia Code, Iosevka, etc.).",
+ "58da1ae45d": "Ligatures de police",
+ "7341e3d00e": "hauteur de ligne",
+ "36a1b38bc8": "Contrôle le multiplicateur de hauteur de ligne du terminal.",
+ "0f2fb0cb74": "Hauteur de ligne",
+ "20ce287cc6": "graisse",
+ "98c18f2c77": "Contrôle la graisse de la police du texte du terminal.",
+ "28ea41bd2d": "Graisse de la police",
+ "b0bb76ae6b": "police",
+ "0acdc17891": "Famille de polices du terminal par défaut pour les nouveaux volets et les mises à jour en direct.",
+ "e989914ad6": "Famille de polices",
+ "33031c1465": "taille du texte",
+ "0fe0073f0c": "Taille de police du terminal par défaut pour les nouveaux volets et les mises à jour en direct.",
+ "5930244899": "Taille de police",
+ "ask_before_closing_running_terminals_title": "Demander avant de fermer les terminaux en cours d'exécution",
+ "ask_before_closing_running_terminals_description": "Afficher une confirmation avant de fermer un terminal où tourne une commande ou un agent.",
+ "scrollSpeed": {
+ "title": "Vitesse de défilement",
+ "description": "Ajustez le défilement normal du terminal, le défilement rapide avec modificateur et la vitesse de molette des TUI plein écran."
+ },
+ "theme_target": {
+ "title": "Mode de thème",
+ "keyword_target": "cible",
+ "keyword_editing": "édition"
+ }
+ },
+ "windows": {
+ "search": {
+ "4d09141a42": "menu contextuel",
+ "fcfa53920b": "coller",
+ "e55186fe2b": "clic droit",
+ "28ff08ed35": "windows",
+ "e7d2793b03": "terminal",
+ "8ba875c132": "Le clic droit colle le presse-papiers dans le terminal. Utilisez Ctrl+clic droit pour ouvrir le menu contextuel.",
+ "f0b8448570": "Clic droit pour coller",
+ "04994f6929": "default",
+ "fc564eadaf": "debian",
+ "4ee2579c32": "ubuntu",
+ "5074ad8b5f": "distro",
+ "2b4a340ce0": "distribution",
+ "02c772582a": "linux",
+ "6e3adf4cba": "wsl",
+ "978457945b": "Choisissez la distribution WSL utilisée par les nouveaux terminaux WSL et les analyses d'agents locaux.",
+ "1f402b3651": "Distribution WSL",
+ "d57f870938": "avancé",
+ "4af2f7526e": "version",
+ "d414022016": "pwsh",
+ "768613e483": "PowerShell 7",
+ "f9162f0b8e": "Windows PowerShell",
+ "2d99cd91be": "powershell",
+ "41a69bc24d": "Choisissez si l'option de shell PowerShell lance Windows PowerShell ou PowerShell 7+ pour les nouveaux volets de terminal.",
+ "860e0e6402": "Version de PowerShell",
+ "07ec155fb6": "bash.exe",
+ "5a2db98d23": "bash",
+ "591912177b": "Git Bash",
+ "12519edb5d": "invite de commandes",
+ "6cd20b9e64": "cmd",
+ "7c7056940a": "shell",
+ "713c4a2f92": "Choisissez le shell par défaut pour les nouveaux volets de terminal sous Windows.",
+ "13715f9d23": "Shell par défaut"
+ }
+ }
+ },
+ "voice": {
+ "pane": {
+ "search": {
+ "f6e0dfa61c": "cloud",
+ "2d206de105": "clé API",
+ "04c25a6fb0": "openai",
+ "b9dee49cd7": "téléchargement",
+ "10d45a9fce": "STT",
+ "3d8b853963": "parole",
+ "080202facb": "model",
+ "7640ed9848": "voix",
+ "56defcd6c3": "Sélectionnez un modèle de reconnaissance vocale local ou cloud à utiliser pour la dictée.",
+ "7e62cd7c41": "Modèle vocal",
+ "931b1a9e53": "push-to-talk",
+ "064a9bd94a": "conserver",
+ "6fa48bcd41": "bascule",
+ "d86f5600da": "mode",
+ "089d31a45b": "dictée",
+ "748b33e531": "Comportement de dictée : bascule ou maintien pour parler.",
+ "6a3abb4338": "Mode de dictée",
+ "e360027a65": "micro",
+ "698376a38d": "Interrupteur principal des fonctions de dictée vocale.",
+ "20574cbc72": "Activer la dictée vocale",
+ "322d457a0d": "transcription",
+ "dcc7846641": "Configurez la clé d'API OpenAI utilisée pour les modèles de reconnaissance vocale dans le cloud.",
+ "ebfd0b32e5": "Transcription OpenAI",
+ "microphoneTitle": "Microphone",
+ "microphoneDescription": "Choisissez le périphérique d'entrée utilisé par la dictée vocale.",
+ "micInput": "entrée",
+ "micDevice": "appareil",
+ "micAirpods": "airpods",
+ "micDefault": "système par défaut"
+ }
+ }
+ },
+ "source": {
+ "control": {
+ "action": {
+ "recipe": {
+ "options": {
+ "commitMessage": "Génère le message de commit à partir des changements indexés.",
+ "pullRequest": "Génère le titre et la description de la hosted review.",
+ "branchName": "Renomme les branches créées par Orca à partir de la tâche initiale de l'agent.",
+ "fixCommitFailure": "Démarre un agent lorsqu'un hook de commit ou un git commit échoue.",
+ "fixChecks": "Démarre un agent à partir des vérifications de hosted review en échec.",
+ "resolveConflicts": "Démarre un agent pour les conflits de merge locaux ou de hosted review.",
+ "customCommand": "Commande personnalisée",
+ "supportedAgents": "Agents pris en charge pour cette recette : {{value0}}.",
+ "unsupportedSavedAgent": "{{value0}} ne peut pas exécuter cette recette de génération de texte. Choisissez l'un des agents pris en charge ci-dessous.",
+ "resolveComments": "Démarre un agent à partir des commentaires PR ou MR non résolus sélectionnés.",
+ "fixPushFailure": "Démarre un agent lorsqu'un hook pre-push ou un git push échoue."
+ }
+ }
+ }
+ }
+ },
+ "WorkspaceDirectorySetting": {
+ "1a2b3c4d5e": "Valeur par défaut du client",
+ "2b3c4d5e6f": "Appliquer à",
+ "3c4d5e6f7a": "Remplace la valeur par défaut du client",
+ "4d5e6f7a8b": "Hérite de la valeur par défaut du client",
+ "5e6f7a8b9c": "Réinitialiser",
+ "6f7a8b9cad": "Utilisez un chemin relatif (ex. .orca/worktrees) pour un emplacement par projet, ou un chemin absolu pour un dossier partagé unique."
+ },
+ "agent-awake-copy": {
+ "e5995ce268": "Empêcher la mise en veille de l'ordinateur pendant que les agents travaillent",
+ "95d3031db2": "Maintient cet ordinateur et cet écran éveillés pendant que les agents travaillent. Le comportement à la fermeture du capot suit les paramètres d'alimentation de cet appareil.",
+ "a42f6fbdd8": "Maintient cet ordinateur et cet écran éveillés pendant que les agents travaillent. Orca demande également à cet appareil de rester éveillé lorsque le capot est fermé, sous réserve de sa politique d'alimentation.",
+ "modeTitle": "Empêcher la mise en veille de l'ordinateur",
+ "modeDescriptionWindows": "Choisissez Activé, Agent ou Désactivé. Le mode Agent reste éveillé tant que des agents travaillent ; le comportement à la fermeture du capot suit les paramètres d'alimentation de cet appareil.",
+ "modeDescriptionDefault": "Choisissez Activé, Agent ou Désactivé. Le mode Agent reste éveillé tant que des agents travaillent. Orca demande également à cet appareil de rester éveillé lorsque le capot est fermé, sous réserve de sa politique d'alimentation."
+ },
+ "agent-status-hooks-copy": {
+ "7707c15abb": "Hooks de statut des agents",
+ "a68a642835": "Affiche les états en cours, en attente et terminé dans Orca. Désactivez pour supprimer les hooks gérés par Orca et arrêter de les réinstaller."
+ },
+ "agent-generated-tab-title-copy": {
+ "19ad21615a": "Générer automatiquement les titres des onglets",
+ "b036c7a409": "Déduit des noms d'onglets courts et stables à partir de la première requête connue de l'agent. Les renommages manuels ont toujours priorité."
+ },
+ "cli": {
+ "source": {
+ "control": {
+ "integration": {
+ "cards": {
+ "d5b3be8ecd": "Revérifier",
+ "8cbc39f862": "En savoir plus",
+ "707180d09c": "glab auth login",
+ "4be0616873": "La CLI GitLab est installée mais non authentifiée. Exécutez cette commande dans un terminal :",
+ "54a640af7a": "Installer la CLI GitLab",
+ "b56fd5676a": "Installez la CLI GitLab pour activer les merge requests, les issues et les pipelines.",
+ "faddeb763d": "Le statut de GitLab CLI n'est pas encore disponible dans ce runtime.",
+ "a47f71e357": "CLI.",
+ "2a6b359e75": "glab",
+ "1f2b347bd3": "Merge requests, issues, todos et pipelines via la",
+ "8d90249d22": "gh auth login",
+ "2e44dda68a": "La CLI GitHub est installée mais non authentifiée. Exécutez cette commande dans un terminal :",
+ "7755c28af5": "Installer la CLI GitHub",
+ "23cb5a0dee": "Installez la CLI GitHub pour activer les pull requests, les issues et les checks.",
+ "6f30fc4216": "Le statut de GitHub CLI n'est pas encore disponible dans ce runtime.",
+ "6b2cfb52b4": "gh",
+ "b4d900e7f1": "Pull requests, issues et checks via la",
+ "account_scope_prefix": "Portée des comptes",
+ "statusConnected": "Connecté",
+ "statusUnavailable": "Indisponible",
+ "statusNotInstalled": "Non installé",
+ "statusNotAuthenticated": "Non authentifié"
+ }
+ }
+ }
+ }
+ },
+ "task": {
+ "tracker": {
+ "integration": {
+ "cards": {
+ "c90f2ef419": "Revérifier",
+ "dd3529015d": "Déconnecter {{value0}}",
+ "8b2408a8e5": "Jira est connecté pour ce runtime. Relancez la vérification si la liste des sites connectés semble obsolète.",
+ "8c20e76308": "Chaque site Jira connecté dispose d'un jeton stocké par le runtime actif.",
+ "c24e56c532": "Tester",
+ "3e7c10d286": "Test en cours...",
+ "a2c0015fb8": "Vérifié",
+ "e2ff968276": "Connecter Jira",
+ "60996beda6": "Ajouter un site Jira",
+ "7ca5ffffdb": "Parcourez, créez et démarrez du travail depuis les tickets Jira Cloud.",
+ "a1093a06c7": "Vérification de l'accès Jira avant d'afficher les actions de configuration.",
+ "9fa04a032e": "{{value0}} site{{value1}} connecté",
+ "cef18762a2": "Ajoutez l'accès avec une clé d'API personnelle depuis vos paramètres Linear. Les clés à accès complet peuvent voir toutes les équipes accessibles au propriétaire de la clé.",
+ "6224fe9d34": "Chaque espace de travail Linear connecté possède une clé stockée par le runtime actif. Les clés à accès complet peuvent couvrir toutes les équipes auxquelles le propriétaire de la clé a accès ; les clés restreintes peuvent être remplacées à tout moment.",
+ "1a12e33fe5": "Ajouter l'accès Linear",
+ "622c224082": "Ajouter un accès à l'espace de travail",
+ "eae4a9f16b": "Ajoutez un accès Linear pour parcourir et lier des issues.",
+ "fe9231215b": "Vérification de l'accès Linear avant d'afficher les actions de configuration.",
+ "e1f5e6424c": "Connecté{{value1}} : {{value0}} espace de travail",
+ "disconnect_all": "Déconnecter",
+ "account_scope_prefix": "Portée des comptes",
+ "2d60ec7921": "Connectez un site Jira Cloud avec un jeton d'API, ou un Jira auto-hébergé avec un jeton d'accès personnel ou un nom d'utilisateur et mot de passe. Les identifiants sont envoyés au runtime distant sélectionné et y sont stockés avec le chiffrement pris en charge par le runtime.",
+ "977e360b71": "Connectez un site Jira Cloud avec un jeton d'API, ou un Jira auto-hébergé avec un jeton d'accès personnel ou un nom d'utilisateur et mot de passe. Les identifiants sont stockés localement et chiffrés lorsque le stockage du runtime local le prend en charge.",
+ "statusConnected": "Connecté",
+ "statusNotConnected": "Non connecté"
+ }
+ }
+ }
+ },
+ "token": {
+ "source": {
+ "control": {
+ "integration": {
+ "cards": {
+ "793a06e899": "Revérifier",
+ "1a9475dace": "En savoir plus",
+ "19fb419c12": "Les identifiants Gitea sont configurés mais l'authentification a échoué. Vérifiez le token, l'URL de base de l'API et les permissions du dépôt, puis redémarrez Orca si les variables d'environnement ont changé.",
+ "60708f23da": "uniquement quand Orca ne peut pas déduire l'URL de l'API depuis le remote.",
+ "709057ad91": "ORCA_GITEA_API_BASE_URL",
+ "6da9dfa5de": "pour les dépôts privés, et définissez",
+ "6d5c2a3005": "ORCA_GITEA_TOKEN",
+ "fcbe0469fd": "Les dépôts publics sont détectés via leur remote git. Définissez",
+ "0613928cb3": "Le statut de Gitea n'est pas encore disponible dans ce runtime.",
+ "05863d2599": "Pull requests et statuts de commits via l'API REST Gitea.",
+ "52f75876be": "Pull requests et statuts de commits pour les dépôts détectés",
+ "0b5242f8a2": "{{value0}} · Pull requests et statuts de commits",
+ "40f678df73": "Les identifiants Azure DevOps sont configurés mais l'authentification a échoué. Vérifiez le token, l'URL de base de l'API et les permissions du dépôt, puis redémarrez Orca si les variables d'environnement ont changé.",
+ "7bd345e3f6": "uniquement quand Orca ne peut pas déduire l'URL de base de l'API depuis le remote git.",
+ "186a6689df": "ORCA_AZURE_DEVOPS_API_BASE_URL",
+ "b8a10b07c1": ". Définissez",
+ "fbfd237f5e": "ORCA_AZURE_DEVOPS_ACCESS_TOKEN",
+ "087feb92f1": ", ou définissez",
+ "48842720d2": "ORCA_AZURE_DEVOPS_TOKEN",
+ "7bbc9c64f0": "Définissez",
+ "f3f47dc7de": "Le statut d'Azure DevOps n'est pas encore disponible dans ce runtime.",
+ "0eb50d5593": "Pull requests et statuts de builds via des tokens de l'API REST Azure DevOps.",
+ "54636c65d4": "Pull requests et statuts de builds pour les Azure Repos détectés",
+ "ea204f5e03": "{{value0}} · Pull requests et statuts de builds",
+ "6154b02093": "Les identifiants Bitbucket sont configurés mais l'authentification a échoué. Vérifiez le token et les permissions du dépôt, puis redémarrez Orca si les variables d'environnement ont changé.",
+ "e63fe8f627": "ORCA_BITBUCKET_ACCESS_TOKEN",
+ "19416c874c": "ORCA_BITBUCKET_API_TOKEN",
+ "fc71a0e7aa": "et",
+ "63a7f47392": "ORCA_BITBUCKET_EMAIL",
+ "24ac1c69dc": "Le statut de Bitbucket n'est pas encore disponible dans ce runtime.",
+ "a924e8dcd1": "Pull requests et statuts de builds via des tokens de l'API Bitbucket Cloud.",
+ "0fa5629dad": "Pull requests et statuts de builds",
+ "statusConnected": "Connecté",
+ "statusUnavailable": "Indisponible",
+ "statusNotConfigured": "Non configuré",
+ "statusAuthFailed": "Échec de l'authentification",
+ "statusConfigured": "Configuré",
+ "statusOptionalSetup": "Configuration facultative"
+ }
+ }
+ }
+ }
+ },
+ "ProviderHostScopeControl": {
+ "scope_label": "{{value0}} : {{value1}}",
+ "change_host": "Ouvrir les serveurs distants"
+ },
+ "computerUseSkillRuntime": {
+ "thisDevice": "Cet appareil"
+ },
+ "computerUseSummary": {
+ "checkingTitle": "Vérification de l'accès Computer Use.",
+ "checkingDescription": "Orca vérifie les autorisations de confidentialité macOS pour l'assistant Computer Use.",
+ "unavailableTitle": "Computer Use est indisponible.",
+ "unavailableDescription": "Les autorisations Computer Use sont indisponibles car {{value0}}.",
+ "readyTitle": "Computer Use est prêt.",
+ "readyDescription": "Les agents peuvent inspecter et manipuler des fenêtres d'applications à votre demande.",
+ "permissionsTitle": "Terminez la configuration pour utiliser les applications locales.",
+ "permissionsRequired_one": "1 autorisation requise avant que les agents puissent manipuler des fenêtres d'applications.",
+ "permissionsRequired_other": "{{value0}} autorisations requises avant que les agents puissent manipuler des fenêtres d'applications."
+ },
+ "providerAccountScope": {
+ "remoteServer": "Serveur distant : {{value0}}",
+ "remoteServerCredentials": "Les identifiants et vérifications de compte pour ce fournisseur appartiennent à ce serveur distant. Utilisez Paramètres > Serveurs distants Orca > Avancé pour modifier la portée d'un autre runtime par défaut.",
+ "localMac": "Mac local",
+ "localCredentials": "Les identifiants et vérifications de compte pour ce fournisseur appartiennent à ce client de bureau. Utilisez Paramètres > Serveurs distants Orca > Avancé pour modifier les identifiants détenus par le serveur.",
+ "remoteServerRateLimit": "Le budget d'API {{value0}} est récupéré via la CLI sur ce serveur distant. Utilisez Paramètres > Serveurs distants Orca > Avancé pour afficher le budget d'un autre runtime par défaut.",
+ "localRateLimit": "Le budget d'API {{value0}} est récupéré via la CLI sur ce client de bureau. Utilisez Paramètres > Serveurs distants Orca > Avancé pour afficher les budgets détenus par le serveur."
+ },
+ "settingOwnership": {
+ "clientDefault": "Valeur par défaut du client",
+ "sourceControlAiDefaults": "Recettes, prompts et valeurs par défaut de hosted review sont partagés par ce client ; les choix de modèle et la découverte restent limités à l'hôte où l'agent s'exécute.",
+ "projectOnThisHost": "Projet sur cette machine",
+ "repositorySourceControlAi": "Ces substitutions s'appliquent à cette configuration de projet et héritent des valeurs par défaut IA du contrôle source côté client tant qu'elles ne sont pas personnalisées.",
+ "agentLaunchDefaults": "L'agent par défaut, les substitutions de commandes, les arguments CLI et l'environnement de lancement sont des préférences du client. Les lancements SSH et serveur distant valident toujours la disponibilité de l'hôte à l'exécution.",
+ "clientDefaultProjectScopes": "Par défaut du client + portées projet",
+ "terminalQuickCommands": "Les commandes sont enregistrées sur ce client, puis définies globalement ou pour une configuration de projet afin de s'exécuter depuis le contexte de terminal sélectionné.",
+ "hostOverride": "Substitution par hôte",
+ "workspaceDirectory": "La valeur par défaut du client est héritée tant qu'un hôte n'a pas besoin de son propre répertoire de worktrees.",
+ "providerHost": "Hôte du fournisseur",
+ "providerAccounts": "Les identifiants et vérifications de compte appartiennent au client local ou au serveur distant sélectionné qui détient l'intégration du fournisseur.",
+ "hostCollectionProjectScopes": "Collection de l'hôte + portées projet",
+ "terminalQuickCommandHostCollections": "Les commandes sont enregistrées sur l'hôte Orca sélectionné, puis définies globalement ou pour une configuration de projet. Les commandes de cet appareil restent également disponibles dans les espaces de travail distants."
+ },
+ "RepositoryForkSyncSection": {
+ "defaultBranch": "branche par défaut",
+ "synced": "Fork mis à jour",
+ "syncedDescriptionSingular": "Avance rapide de {{branch}} de 1 commit.",
+ "syncedDescriptionPlural": "Avance rapide de {{branch}} de {{count}} commits.",
+ "upToDate": "Fork déjà à jour",
+ "upToDateDescription": "{{branch}} correspond déjà à upstream.",
+ "missingOrigin": "Le remote origin est absent.",
+ "missingUpstream": "Le remote upstream est absent.",
+ "upstreamMismatch": "Le remote upstream ne correspond plus à ce fork.",
+ "missingUpstreamBranch": "Impossible de déterminer la branche par défaut d'upstream.",
+ "missingOriginBranch": "origin n'a pas la branche par défaut d'upstream.",
+ "diverged": "origin contient des commits absents d'upstream.",
+ "blocked": "Synchronisation du fork ignorée",
+ "blockedFallback": "Orca n'a pas pu avancer ce fork en fast-forward en toute sécurité.",
+ "failed": "Échec de la synchronisation du fork",
+ "title": "Garder le fork à jour",
+ "description": "Faites avancer ce fork en fast-forward depuis upstream, sans risque.",
+ "longDescription": "Quand ce fork est en retard sur upstream, Orca peut avancer sa branche par défaut en fast-forward sans risque. Orca ignore la mise à jour si la branche contient des commits uniquement locaux ou des conflits.",
+ "forkOf": "Fork de {{owner}}/{{repo}}",
+ "syncing": "Synchronisation en cours",
+ "syncNow": "Synchroniser maintenant",
+ "modeLabel": "Mode de synchronisation du fork",
+ "ask": "Demander",
+ "safeAuto": "Auto sécurisé",
+ "off": "Désactivé"
+ },
+ "DefaultWindowsProjectRuntimeSetting": {
+ "defaultRuntime": "Runtime de projet par défaut",
+ "windows": "Windows",
+ "wsl": "WSL",
+ "selectDistro": "Sélectionner une distro",
+ "windowsDescription": "Les projets héritent de Windows sauf si un projet le remplace.",
+ "wslUnavailable": "WSL n'est pas disponible. Les projets qui héritent de WSL devront être réparés.",
+ "distroRequired": "Choisissez une distro WSL avant que les projets puissent hériter de WSL.",
+ "wslDescription": "Les projets héritent de {{value0}} via WSL sauf si un projet le remplace."
+ },
+ "ProjectWindowsRuntimeSetting": {
+ "projectRuntime": "Runtime du projet",
+ "defaultRuntime": "Par défaut ({{value0}})",
+ "windows": "Windows",
+ "wsl": "WSL",
+ "selectDistro": "Sélectionner une distro",
+ "runtimeChangeHelp": "Les changements de runtime s'appliquent aux nouveaux terminaux, aux vérifications d'agents et à la découverte de skills pour ce projet. Les terminaux existants conservent leur runtime actuel.",
+ "wslUnavailable": "WSL n'est pas disponible. Basculez ce projet sur Windows ou réparez WSL.",
+ "distroMissing": "{{value0}} n'est pas installé dans WSL. Choisissez une distro installée ou basculez ce projet sur Windows.",
+ "distroRequired": "Choisissez une distro WSL ou basculez ce projet sur Windows.",
+ "inheritedWsl": "Aucune substitution au niveau projet. Les paramètres généraux sélectionnent {{value0}} via WSL.",
+ "projectWsl": "Ce projet s'exécute dans {{value0}} via WSL.",
+ "inheritedWindows": "Aucune substitution au niveau projet. Les paramètres généraux sélectionnent Windows.",
+ "projectWindows": "Ce projet s'exécute sous Windows.",
+ "liveTerminalSingular": "{{count}} terminal actif",
+ "liveTerminalPlural": "{{count}} terminaux actifs",
+ "activeTaskSingular": "{{count}} tâche active",
+ "activeTaskPlural": "{{count}} tâches actives",
+ "runtimeSessionJoin": "{{value0}} et {{value1}}",
+ "runtimeSessionWarning": "{{value0}} continuera de s'exécuter dans le runtime actuel. Laissez les tâches se terminer ou redémarrez les terminaux avant de continuer.",
+ "pendingRuntimeChange": "Changement de runtime en attente. Le nouveau travail sur le projet utilisera le runtime sélectionné après application.",
+ "cancel": "Annuler",
+ "applyRuntimeChange": "Appliquer le changement de runtime"
+ },
+ "PrivacyDiagnosticsRows": {
+ "5a7cbe069a": "DO_NOT_TRACK=1 est défini — la création et l'envoi de fichiers de diagnostic sont désactivés.",
+ "63d03261d1": "ORCA_TELEMETRY_DISABLED=1 est défini — la création et l'envoi de fichiers de diagnostic sont désactivés.",
+ "d37e92a06b": "ORCA_DIAGNOSTICS_DISABLED=1 est défini — les diagnostics de l'app sont désactivés.",
+ "5ebb31e1fb": "Exécution en CI — diagnostics désactivés.",
+ "e27c8d45bf": "Les diagnostics sont désactivés par une variable d'environnement."
+ },
+ "ShortcutCommandBlock": {
+ "eb72c52c28": "Appuyez sur un raccourci, ou appuyez deux fois sur une touche modificatrice (ex. {{value0}}). Échap annule.",
+ "70b5d25583": "Raccourci {{value0}}",
+ "287e07ddde": "Modifié",
+ "3c83cd7d1c": "Désactivé",
+ "07939d084e": "Réinitialiser {{value0}} à la valeur par défaut",
+ "9b02917027": "Rétablir la valeur par défaut",
+ "a799f90f82": "Désactiver {{value0}}",
+ "25e6e76618": "Désactiver le raccourci",
+ "035a822ef0": "Ajouter un raccourci",
+ "a0e2ef0e61": "Ajouter un autre raccourci pour {{value0}}",
+ "245c83af24": "Ajouter un autre raccourci",
+ "482a60225d": "Activer {{value0}}",
+ "6287677c37": "Activer",
+ "01481b964c": "Ajouter un raccourci pour {{value0}}"
+ },
+ "ShortcutRecorderButton": {
+ "1a13bb054d": "Appuyez sur les touches du raccourci pour {{value0}}. Échap annule.",
+ "3732775d74": "Ajouter un raccourci pour {{value0}}",
+ "88764af2c1": "Modifier le raccourci pour {{value0}}",
+ "30feb099d6": "Modifier le raccourci {{value0}} sur {{value1}} pour {{value2}}",
+ "5d982a2a1f": "En écoute du raccourci",
+ "152e0bcd64": "Ajouter un raccourci",
+ "5bd56445da": "Modifier le raccourci",
+ "f5ed5dcbf6": "Appuyez sur des touches…"
+ },
+ "ShortcutRemoveButton": {
+ "9e29aff18b": "Supprimer le raccourci {{value1}} de {{value0}}",
+ "2a9588b1c2": "Supprimer cette association"
+ },
+ "BrowserLocalhostWorktreeLabelsSetting": {
+ "8ac8c3ad19": "Libellés localhost des worktrees",
+ "1db3c8b983": "Ouvrir les ports des espaces de travail sous forme d'URL localhost Orca propres à chaque worktree, pour mieux distinguer les onglets du navigateur."
+ },
+ "AgentRuntimeSetting": {
+ "label": "Runtime des agents",
+ "wsl": "WSL",
+ "loadingWsl": "Chargement de WSL",
+ "selectDistro": "Sélectionner une distro",
+ "windowsDescription": "Détecte et lance les agents sous Windows pour les projets qui ne remplacent pas leur runtime.",
+ "wslUnavailable": "WSL n'est pas disponible sur cette machine.",
+ "distroRequired": "Choisissez une distro WSL avant que les projets puissent hériter de WSL.",
+ "wslDescription": "Détecte et lance les agents dans {{value0}} via WSL pour les projets qui ne remplacent pas leur runtime."
+ },
+ "MobileEmulatorSdkStatus": {
+ "536026130e": "SDK d'émulateurs",
+ "dde0ec1cd8": "Chaînes d'outils qu'Orca utilise pour exécuter des émulateurs. Android fonctionne sur tout OS via le SDK Android ; les simulateurs iOS nécessitent Xcode sur macOS.",
+ "027cbf668a": "Android SDK",
+ "f6d080d128": "Utilise le chemin configuré",
+ "7fe4bd5907": "Détecté à",
+ "2784f0b22d": "Introuvable. Installez Android Studio, puis créez un Virtual Device.",
+ "b94ff260e6": "Télécharger Android Studio",
+ "18925b082d": "Localiser le dossier du SDK",
+ "8c52684db8": "Effacer",
+ "76eb88b88e": "Simulateur iOS (Xcode)",
+ "c6f3ea4f12": "Prêt",
+ "e4f14b50d7": "Installez Xcode et ajoutez un runtime de simulateur iOS.",
+ "63fe73a1ea": "Impossible de mettre à jour le dossier du SDK Android."
+ },
+ "AppearanceAdvancedDisclosure": {
+ "advanced": "Avancé"
+ },
+ "DevToolsPane": {
+ "nativeActionLayoutCheck": "Vérification de la disposition des actions natives",
+ "secondary": "Secondaire",
+ "secondaryActionClicked": "Action secondaire cliquée",
+ "primaryAction": "Action principale",
+ "primaryActionClicked": "Action principale cliquée",
+ "branchHasChanges": "la branche a des modifications",
+ "viewChangesClicked": "« Voir les modifications » cliqué",
+ "forceDeleteClicked": "« Forcer la suppression » cliqué",
+ "devOnlyCallback": "Callback réservé au développement.",
+ "infoToast": "Toast d'information",
+ "infoToastDescription": "Texte informatif long sans action explicite.",
+ "localCanaryBehind": "Le canary local est en retard sur origin/canary",
+ "successToast": "Toast de succès",
+ "successToastDescription": "Court état de confirmation.",
+ "settingsSaved": "Paramètres enregistrés",
+ "shortSuccessCopy": "Court texte de succès.",
+ "errorToast": "Toast d'erreur",
+ "errorToastDescription": "Texte d'erreur long sans actions de récupération.",
+ "failedToSyncWorkspaceMetadata": "Échec de la synchronisation des métadonnées de l'espace de travail",
+ "nativeActionToast": "Toast avec action native",
+ "nativeActionToastDescription": "Utilise les emplacements action et cancel de Sonner.",
+ "deleteFailureToast": "Toast d'échec de suppression",
+ "deleteFailureToastDescription": "Pied personnalisé avec « Afficher » et « Forcer la suppression ».",
+ "behindBaseRefToast": "Toast de retard sur la ref de base",
+ "behindBaseRefToastDescription": "Invite persistante avec un lien Paramètres intégré et une action en pied de page.",
+ "notificationPlayground": "Terrain d'essai des notifications",
+ "notificationPlaygroundDescription": "Déclencheurs réservés au développement pour vérifier la disposition des toasts, les actions de récupération et le retour à la ligne des textes longs.",
+ "devOnly": "Réservé au développement",
+ "orcaCloud": "Orca Cloud",
+ "orcaCloudDescription": "Aperçu réservé au développement de la connexion au cloud officiel. Masqué en production ; en dev, il apparaît aussi dans le sélecteur de comptes de la barre latérale dès que ORCA_CLOUD_API_URL et ORCA_CLOUD_CLIENT_ID sont définis.",
+ "orcaCloudStatus": "Statut",
+ "orcaCloudSignOut": "Se déconnecter",
+ "orcaCloudConnect": "Connecter le profil",
+ "orcaCloudRefresh": "Actualiser le statut",
+ "orcaCloudNotConfigured": "Définissez ORCA_CLOUD_API_URL et ORCA_CLOUD_CLIENT_ID pour prévisualiser la connexion à Orca Cloud dans ce build de développement."
+ },
+ "EphemeralVmRecipeRow": {
+ "useInWorkspace": "Utiliser dans l'espace de travail"
+ },
+ "EphemeralVmRuntimesSection": {
+ "cleanupFailed": "Échec du nettoyage",
+ "cleanupRunning": "Nettoyage en cours",
+ "cleanupDisabled": "Nettoyage désactivé",
+ "running": "En cours",
+ "failed": "Échec",
+ "loadFailed": "Impossible de charger les runtimes de VM temporaires.",
+ "cleanupFailedToast": "Impossible de nettoyer le runtime de VM temporaire.",
+ "markedCleaned": "Runtime de VM temporaire marqué comme nettoyé.",
+ "cleaned": "Runtime de VM temporaire nettoyé.",
+ "copiedCleanupCommand": "Commande de nettoyage copiée.",
+ "copiedCleanupPayload": "Contenu de nettoyage copié.",
+ "copyCleanupFailed": "Impossible de copier la commande de nettoyage.",
+ "title": "Runtimes de VM temporaires",
+ "description": "Les runtimes créés par recette appartiennent à l'espace de travail. Nettoyez les entrées obsolètes après un plantage, une création échouée ou une récupération manuelle.",
+ "refresh": "Actualiser les runtimes de VM temporaires",
+ "loading": "Vérification des runtimes de VM temporaires…",
+ "empty": "Aucun runtime de VM temporaire à nettoyer.",
+ "copyCleanup": "Copier la commande",
+ "retry": "Réessayer le nettoyage",
+ "cleanup": "Nettoyage",
+ "cloudVmLoadFailed": "Impossible de charger les runtimes de VM Cloud.",
+ "cloudVmCleanupFailedToast": "Impossible de nettoyer le runtime de VM Cloud.",
+ "cloudVmMarkedCleaned": "Runtime de VM Cloud marqué comme nettoyé.",
+ "cloudVmCleaned": "Runtime de VM Cloud nettoyé.",
+ "cloudVmTitle": "Runtimes de VM Cloud",
+ "cloudVmRefresh": "Actualiser les runtimes de VM Cloud",
+ "cloudVmLoading": "Vérification des runtimes de VM Cloud…",
+ "cloudVmEmptyWithSetup": "Aucun runtime de VM Cloud pour l'instant. Créez-en un depuis un espace de travail avec une recette d'environnement.",
+ "stoppingCleanup": "Arrêt…",
+ "stopCleanup": "Arrêter le nettoyage",
+ "cleanupStopped": "Nettoyage arrêté",
+ "stopCleanupFailed": "Impossible d'arrêter le nettoyage de la VM Cloud."
+ },
+ "ephemeralVms": {
+ "search": {
+ "description": "Découvrez comment les recettes détenues par le dépôt offrent à chaque workspace son propre environnement jetable à la demande.",
+ "cloudVmTitle": "VM cloud"
+ }
+ },
+ "EphemeralVmsPane": {
+ "loadError": "Impossible de charger les recettes.",
+ "copyError": "Impossible de copier le prompt.",
+ "skillDescription": "Configure, construit, authentifie et valide les recettes d'environnement détenues par le dépôt.",
+ "whatTitle": "Ce que fait la skill, avec vous",
+ "whatScaffold": "Écrit la recette et les scripts pour votre fournisseur — connecté via un serveur Orca ou SSH.",
+ "whatBuild": "Construit une image de base réutilisable et connecte votre agent.",
+ "whatValidate": "La valide pour que vous puissiez créer un espace de travail dessus.",
+ "promptHint": "Dans n'importe quel espace de travail, demandez à votre agent :",
+ "copy": "Copier",
+ "copied": "Copied",
+ "recipes": "Recettes",
+ "recipesHelp": "Les recettes issues d'orca.yaml et des plugins activés apparaissent ici, prêtes à lancer un espace de travail.",
+ "checking": "Vérification des recettes...",
+ "none": "Aucune recette trouvée pour l'instant.",
+ "cloudVmSkillTitle": "Skill de configuration de VM Cloud",
+ "cloudVmRefresh": "Actualiser les recettes de VM Cloud",
+ "cloudVmTerminalTitle": "Configuration de VM Cloud",
+ "cloudVmTerminalAriaLabel": "Terminal d'installation de la skill VM Cloud"
+ },
+ "ephemeralVmsExperimentalSetting": {
+ "description": "Affiche les contrôles de configuration et les cibles d'exécution espace de travail pour les environnements détenus par le dépôt, à la demande.",
+ "cloudVmToggleLabel": "Activer/désactiver Cloud VM"
+ },
+ "SourceControlActionRepoOverrideNote": {
+ "agent": "Agent",
+ "agentArgs": "Arguments CLI",
+ "commandTemplate": "Modèle de commande",
+ "more": "+{{count}} autres",
+ "plural": "Les enregistrements globaux ne modifieront pas {{count}} dépôts ayant leurs propres recettes.",
+ "recipe": "Recette",
+ "review": "À examiner",
+ "reviewFirst": "Vérifier d'abord",
+ "singular": "Les enregistrements globaux ne modifieront pas 1 dépôt ayant sa propre recette.",
+ "tooltipTitle": "Substitutions au niveau dépôt"
+ },
+ "linear": {
+ "agent": {
+ "skill": {
+ "install": {
+ "cta": {
+ "copiedCommand": "Commande copiée.",
+ "copyFailed": "Échec de la copie de la commande.",
+ "skillLabel": "Skill d'agent :",
+ "checking": "Vérification...",
+ "installed": "Installés",
+ "notInstalled": "Non installé",
+ "recheck": "Revérifier",
+ "installedDescription": "Skill d'agent installée. Pour la mettre à jour, exécutez :",
+ "description": "Permet aux agents de lire et modifier les tâches Linear. La configuration guidée complète (connexion + skill + visibilité) se trouve dans Paramètres → Task Sources.",
+ "copyCommand": "Copier la commande"
+ }
+ },
+ "search": {
+ "title": "Linear",
+ "description": "Statut de la skill Linear, exemples d'utilisation et liens vers la configuration de Task Sources."
+ }
+ }
+ }
+ },
+ "GrokAccountsSection": {
+ "a1b2c3d4e5": "Grok (xAI)",
+ "f6e5d4c3b2": "Affiche l'utilisation hebdomadaire de crédits issue de votre connexion Grok CLI (fichier de session ~/.grok/auth.json).",
+ "0d8e77bc40": "Documentation Grok CLI",
+ "ad47a33f72": "Chargement…",
+ "b2c3d4e5f6": "Connecté",
+ "c3d4e5f6a7": "Connecté. Orca lit uniquement ce fichier sur disque — relancez grok login si l'affichage de l'utilisation échoue.",
+ "d4e5f6a7b8": "Session expirée — exécutez grok login dans un terminal pour l'actualiser.",
+ "e5f6a7b8c9": "Non connecté à Grok CLI",
+ "f6a7b8c9d0": "Dans un terminal, exécutez grok login, puis cliquez ici sur Actualiser l'utilisation.",
+ "3325d996cb": "Actualiser l'utilisation",
+ "a8f3e2c1b4": "Crédits hebdomadaires",
+ "b7e2d9f0a3": "Même % de crédits hebdomadaires que l'écran grok /usage dans le terminal.",
+ "c6d1a8f4e2": "Réinitialisation {{when}}",
+ "e6dadc1e2b": "Utilisation mensuelle",
+ "75e396bf42": "Utilisation mensuelle incluse pour les comptes Grok à facturation unifiée.",
+ "b36fa2c908": "Connecté. Orca lit la session Grok CLI stockée sur disque.",
+ "f08c41de73": "Session expirée — lancez grok sur l'ordinateur qui exécute Orca et attendez son démarrage. Complétez la connexion si une invite apparaît, puis cliquez sur Actualiser l'utilisation. Aucun message dans le chat n'est nécessaire."
+ },
+ "AppearanceWindowSidebarSection": {
+ "usagePercentageDisplayUsed": "Utilisé",
+ "usagePercentageDisplayRemaining": "Restant"
+ },
+ "TerminalInteractionSection": {
+ "567633ff50": "Le clic droit colle le presse-papiers dans le terminal. Control-clic pour ouvrir le menu contextuel.",
+ "c64497148a": "Le clic droit colle le presse-papiers. Control-clic ouvre le menu contextuel."
+ },
+ "MobileRelayStatusSection": {
+ "registered": "Enregistré",
+ "connecting": "Connexion",
+ "reconnecting": "Reconnexion",
+ "offline": "Hors ligne",
+ "title": "Orca Relay",
+ "automatic": "Connexion depuis n'importe où quand un téléphone est jumelé",
+ "signInPrompt": "Connectez-vous sur cet ordinateur pour accéder depuis n'importe où",
+ "directStillAvailable": "Les connexions LAN et Tailscale restent disponibles.",
+ "directNeedsNoAccount": "Le jumelage LAN et Tailscale fonctionne toujours sans compte.",
+ "signIn": "Se connecter",
+ "unavailable": "Indisponible",
+ "standby": "Veille — aucun appareil via Relay"
+ },
+ "MobilePairingConnectionOptions": {
+ "ready": "Prêt",
+ "connecting": "Connexion",
+ "available": "Disponibles",
+ "reconnecting": "Reconnexion",
+ "unavailable": "Indisponible",
+ "pathGroup": "Comment le téléphone joint cet ordinateur",
+ "anywhereTitle": "Orca Relay",
+ "anywhereDescription": "Le téléphone peut être en données mobiles ou sur n'importe quel Wi‑Fi. Connexion requise uniquement pour Relay.",
+ "signInRequired": "Relay uniquement — le LAN n'a pas besoin de compte.",
+ "relayUnavailable": "Orca Relay n'est pas disponible dans ce build. Utilisez le LAN.",
+ "signIn": "Se connecter pour Relay",
+ "signInAgain": "Se reconnecter pour Relay",
+ "localTitle": "LAN",
+ "localDescription": "Le téléphone doit être sur ce Wi‑Fi ou connecté via Tailscale. Aucun compte requis.",
+ "retrying": "Nouvelle tentative"
+ },
+ "MobilePairingSetupSection": {
+ "title": "Jumeler un téléphone",
+ "overview": "Générez un QR code, puis scannez-le dans Orca Mobile sous Jumeler le bureau.",
+ "step1Title": "Connexion",
+ "step2Title": "Adresse de cet ordinateur",
+ "step2LocalDescription": "Le téléphone doit pouvoir joindre cette adresse via Tailscale ou le Wi‑Fi.",
+ "regenerate": "Régénérer le QR code",
+ "generate": "Générer le QR code",
+ "refresh": "Actualiser les interfaces réseau",
+ "step2RelayDescription": "Facultatif. Choisissez l'adresse Wi‑Fi ou Tailscale que votre téléphone utilisera à proximité — généralement plus rapide que Relay. Relay reste fonctionnel quand vous êtes loin.",
+ "step2RelayDisclosure": "Utiliser aussi un chemin local plus rapide"
+ },
+ "MobileRelayBetaAvailability": {
+ "about": "À propos de la bêta d'Orca Relay",
+ "beta": "Beta",
+ "availability": "Disponible sur",
+ "testFlight": "TestFlight",
+ "androidApk": "APK Android"
+ },
+ "SkillUsageExampleDialog": {
+ "copiedPrompt": "Prompt d'exemple copié.",
+ "copyFailed": "Échec de la copie du prompt.",
+ "copyExampleAria": "Copier le prompt d'exemple {{value0}}",
+ "done": "Terminé",
+ "copyPrompt": "Copier le prompt"
+ },
+ "LinearAgentSkillPane": {
+ "title": "Linear",
+ "description": "Fonctionnement de Linear dans Orca : parcourez les tickets, démarrez des espaces de travail liés et laissez les agents mettre à jour les tickets avec /orca-linear.",
+ "skillTitle": "Skill Linear",
+ "howToUse": "Prompts d'exemple",
+ "howToUseDescription": "Cliquez sur une carte pour copier un prompt. Utilisez-les dans un worktree lié à Linear après l'installation de la skill.",
+ "terminalTitle": "Configuration de la skill Linear",
+ "terminalAriaLabel": "Terminal d'installation de la skill Linear",
+ "manageConnectionHint": "Consultez les espaces de travail Linear connectés et les clés d'API dans",
+ "manageConnectionLink": "Intégrations"
+ },
+ "MobileRelayBetaNotice": {
+ "notice": "Orca Relay est en bêta."
+ },
+ "EditorFontFamilySetting": {
+ "title": "Police de l'éditeur",
+ "description": "Police utilisée par les éditeurs de fichiers et les vues de diff. Laissez vide pour suivre la police du terminal.",
+ "placeholder": "Identique à la police du terminal"
+ },
+ "GeneralRemoteServerUpdates": {
+ "serverCount": "{{value0}} serveurs jumelés",
+ "availableCount": "{{value0}} prêts pour la mise à jour",
+ "currentCount": "{{value0}} à jour",
+ "manualCount": "{{value0}} manuels",
+ "offlineCount": "{{value0}} hors ligne",
+ "title": "Serveurs Orca distants",
+ "description": "Vérifiez et mettez à jour les serveurs Orca jumelés depuis ce client.",
+ "updating": "Mise à jour des serveurs…",
+ "reviewUpdates": "{{value0}} mises à jour disponibles",
+ "reviewServers": "Rechercher des mises à jour du serveur",
+ "serverCountOne": "1 serveur jumelé",
+ "reviewUpdateOne": "1 mise à jour disponible"
+ },
+ "RemoteServerUpdateDialog": {
+ "versionUnavailable": "Version indisponible",
+ "restartingHelp": "En attente de la reconnexion du serveur de remplacement sur la nouvelle version.",
+ "retry": "Réessayer",
+ "update": "Mettre à jour ce serveur",
+ "title": "Mettre à jour les serveurs distants Orca",
+ "description": "Consultez les serveurs jumelés et mettez à jour les installations prises en charge depuis ce client Orca.",
+ "restartWarning": "La mise à jour redémarre ces serveurs. {{value0}} onglets actifs et {{value1}} volets de terminal peuvent se déconnecter brièvement.",
+ "checking": "Vérification des serveurs jumelés…",
+ "empty": "Aucun serveur distant Orca jumelé.",
+ "checkAgain": "Rechercher des mises à jour du serveur",
+ "updating": "Mise à jour des serveurs…",
+ "updateAll": "Mettre à jour tous les {{value0}} serveurs",
+ "noUpdates": "Tous les serveurs sont à jour.",
+ "updateOne": "Mettre à jour le serveur",
+ "downloadProgress": "Progression du téléchargement de {{value0}}",
+ "liveTabOne": "1 onglet actif",
+ "liveTabs": "{{value0}} onglets actifs",
+ "livePaneOne": "1 volet actif",
+ "livePanes": "{{value0}} volets actifs"
+ },
+ "RemoteServerUpdateStatus": {
+ "checking": "Vérification…",
+ "available": "Mise à jour disponible",
+ "current": "À jour",
+ "manual": "Mise à jour manuelle",
+ "offline": "Hors ligne",
+ "queued": "En file d'attente",
+ "checkingUpdate": "Vérification de la mise à jour…",
+ "downloading": "Téléchargement…",
+ "restarting": "Redémarrage…",
+ "updated": "Mis à jour",
+ "failed": "Échec de la mise à jour",
+ "serviceManagerHelp": "Mettez à jour Orca via le gestionnaire de services qui démarre ce serveur.",
+ "unpackedHelp": "Les builds de développement doivent être mis à jour depuis leur checkout source.",
+ "legacyHelp": "Mettez à jour ce serveur une fois manuellement pour activer les mises à jour à distance."
+ },
+ "BrowserLinkRoutingSetting": {
+ "description": "Ouvre les liens http(s) dans le navigateur intégré d'Orca — depuis le terminal, le markdown et l'éditeur. {{shortcut}} utilise toujours votre navigateur système.",
+ "descriptionBase": "Ouvre les liens http(s) dans le navigateur intégré d'Orca — depuis le terminal, le markdown et l'éditeur."
+ },
+ "BrowserLinkRoutingModifierSetting": {
+ "titleSystem": "Maintenez Shift pour ouvrir dans votre navigateur web",
+ "titleOrca": "Maintenez Shift pour ouvrir dans Orca",
+ "descriptionSystem": "Les liens s'ouvrent dans Orca ; {{chord}}+clic les envoie plutôt à votre navigateur système.",
+ "descriptionOrca": "Les liens s'ouvrent dans votre navigateur système. Une fois activé, {{chord}}+clic en ouvre un dans le navigateur intégré d'Orca."
+ },
+ "BrowserTerminalLinkActionsSetting": {
+ "title": "Afficher les actions de lien du terminal",
+ "description": "Affiche les actions disponibles quand vous cliquez sur un lien du terminal. Désactivez pour exiger un {{modifier}}+clic."
+ },
+ "PluginConsentDialog": {
+ "workerTrust": "Worker en arrière-plan — exécute son propre processus",
+ "instructionalTrust": "Contenu instructif — s'exécute plus tard sous l'autorité de l'utilisateur ou d'un agent",
+ "declarativeTrust": "Contenu déclaratif — aucun code de plugin",
+ "panelTrust": "Contenu de panneau ou intégré à l'hôte — aucun processus worker",
+ "trustShortWorker": "Worker",
+ "trustShortInstructional": "Instructif",
+ "trustShortPanel": "Panneau",
+ "trustShortDeclarative": "Déclaratif",
+ "reviewTitle": "Examiner le plugin",
+ "title": "Examiner les autorisations",
+ "mixedTitle": "Examiner l'accès et le contenu",
+ "instructionalTitle": "Examiner le contenu du plugin",
+ "subtitle": "{{value0}} v{{value1}} · {{value2}}",
+ "reconsent": "Les autorisations, le niveau de confiance du worker ou le contenu instructif ont changé depuis votre dernière revue de ce plugin. Revoyez-le avant qu'il puisse s'exécuter.",
+ "capabilities": "Ce plugin peut",
+ "warning": "Ces autorisations limitent la façon dont le plugin utilise l'API d'Orca. Son worker s'exécute néanmoins comme un processus normal sur votre ordinateur, avec un accès complet à vos fichiers, votre réseau et vos autres processus.",
+ "instructionalWarning": "Ce plugin n'a pas de processus worker. Son contenu instructif peut malgré tout déclencher des actions quand vous ou un agent l'utilisez. Examinez les instructions et commandes ci-dessous avant de l'activer.",
+ "panelWarning": "Ces autorisations limitent la façon dont le plugin utilise l'API d'Orca. Ce plugin n'a pas de worker en arrière-plan.",
+ "declarativeWarning": "Ce plugin fournit uniquement du contenu validé. Il n'exécute pas de worker en arrière-plan ni ne reçoit d'accès à l'API d'Orca.",
+ "keepDisabled": "Garder désactivé",
+ "enable": "Activer le plugin",
+ "capability": {
+ "workspaceRead": "Lire le nom, la branche et la liste des terminaux de votre worktree actif",
+ "terminalSend": "Saisir du texte dans un terminal visible (toujours un terminal spécifique)",
+ "notificationsShow": "Afficher des notifications de bureau libellées avec le nom du plugin",
+ "storage": "Stocker des données dans le dossier de stockage propre au plugin",
+ "secrets": "Stocker et lire des secrets dans le coffre chiffré propre au plugin",
+ "eventsSubscribe": "Être notifié quand des worktrees sont créés ou supprimés et quand le statut d'un agent change",
+ "settingsOwn": "Lire et modifier les paramètres propres au plugin"
+ },
+ "decisionFailed": "Impossible d'enregistrer la décision d'autorisation. Réessayez."
+ },
+ "PluginConsentProvenance": {
+ "official": "Officiel",
+ "bundled": "Fourni avec Orca",
+ "local": "Dossier local",
+ "community": "Communauté",
+ "source": "Source",
+ "sourceLabel": "Source",
+ "commit": "Commit épinglé",
+ "localCommit": "Dossier local — aucun commit",
+ "indexCommit": "Commit d'index"
+ },
+ "PluginDevelopmentSection": {
+ "saveFailed": "Impossible d'enregistrer les chemins de plugins de développement.",
+ "pathRequired": "Saisissez un chemin de dossier de plugin.",
+ "title": "Développement",
+ "help": "Chargez les plugins directement depuis des dossiers de cet ordinateur pendant leur développement. Les plugins de développement exigent toujours une revue des autorisations. Les workers s'exécutent sur cet hôte de bureau ; les actions SSH sur les espaces de travail passent par Orca, donc les chemins indiqués ici sont des chemins du bureau.",
+ "remove": "Supprimer",
+ "pathLabel": "Chemin du dossier de plugin de développement",
+ "placeholder": "/Users/you/plugins/my-plugin or C:\\Users\\you\\plugins\\my-plugin",
+ "add": "Ajouter un chemin"
+ },
+ "PluginInstallDialog": {
+ "localRequired": "Saisissez le chemin du dossier du plugin.",
+ "gitUrlRequired": "Saisissez une URL de dépôt.",
+ "gitUrlInvalid": "Utilisez une URL Git HTTPS ou SSH. Les protocoles de helper Git exécutables ne sont pas autorisés.",
+ "gitRefRequired": "Ajoutez une #ref explicite (tag ou commit) pour épingler l'installation — par exemple #v0.1.0.",
+ "title": "Installer le plugin",
+ "description": "L'installation copie le plugin dans Orca et affiche ses autorisations pour revue. Aucun code de plugin ne s'exécute tant que vous ne l'activez pas.",
+ "source": "Source d'installation",
+ "localTab": "Dossier local",
+ "gitTab": "URL Git",
+ "localLabel": "Chemin du dossier du plugin",
+ "localPlaceholder": "/Users/you/plugins/my-plugin or C:\\Users\\you\\plugins\\my-plugin",
+ "localHelp": "Chemin complet vers un dossier contenant orca-plugin.json sur cet ordinateur. Le chemin est utilisé exactement tel que saisi.",
+ "gitLabel": "URL de dépôt avec #ref",
+ "gitPlaceholder": "https://git.example/acme/orca-notes#v0.1.0",
+ "gitHelp": "Ajoutez une #ref explicite — un tag ou un commit — pour épingler l'installation. Fonctionne avec GitHub, GitLab et tout hôte git.",
+ "cancel": "Annuler",
+ "installing": "Installation…",
+ "install": "Installer",
+ "installFailed": "Échec de l'installation du plugin. Vérifiez la source et réessayez."
+ },
+ "PluginKeybindingConsentPreview": {
+ "heading": "Raccourcis clavier",
+ "worktree": "Ne fonctionne que lorsqu'un espace de travail est actif.",
+ "global": "Fonctionne dans l'app sans espace de travail actif requis.",
+ "shadows": "Remplace : {{value0}}"
+ },
+ "PluginMarketplaceBrowser": {
+ "loadFailed": "Impossible de charger les plugins du marketplace.",
+ "refreshFailed": "Impossible d'actualiser les marketplaces. Les listes en cache restent disponibles.",
+ "previewFailed": "Impossible de préparer ce plugin pour revue. Actualisez le marketplace et réessayez.",
+ "installFailed": "Impossible d'installer ce plugin. La source examinée a peut-être changé.",
+ "manageSources": "Gérer les sources",
+ "refreshing": "Actualisation…",
+ "refresh": "Actualiser",
+ "noInstalledTitle": "Aucun plugin installé",
+ "noInstalled": "Les plugins que vous installez apparaissent ici.",
+ "loading": "Chargement des plugins du marketplace…",
+ "tryAgain": "Réessayer",
+ "noSourcesTitle": "Aucun marketplace configuré",
+ "noSources": "Ajoutez un marketplace Git officiel, communautaire ou privé pour parcourir les plugins.",
+ "addSource": "Ajouter un marketplace",
+ "noResultsTitle": "Aucun plugin correspondant",
+ "noResults": "Aucun plugin du marketplace ne correspond à cette recherche.",
+ "clearSearch": "Effacer la recherche",
+ "emptyTitle": "Rien de listé pour l'instant",
+ "empty": "Les marketplaces configurés ne listent aucun plugin."
+ },
+ "PluginMarketplaceListingRow": {
+ "official": "Officiel",
+ "installed": "Installés",
+ "noDescription": "Aucune description fournie.",
+ "blocked": "Bloqué par la liste de sécurité d'Orca : {{value0}}",
+ "blockedAction": "Bloquée",
+ "checkUpdate": "Vérifier les mises à jour",
+ "install": "Installer"
+ },
+ "PluginMarketplacePreviewDialog": {
+ "languagePacksOne": "1 pack de langue",
+ "languagePacks": "{{value0}} packs de langue",
+ "commandsOne": "1 commande",
+ "commands": "{{value0}} commandes",
+ "keybindingsOne": "1 raccourci clavier",
+ "keybindings": "{{value0}} raccourcis clavier",
+ "vmRecipesOne": "1 recette de VM",
+ "vmRecipes": "{{value0}} recettes de VM",
+ "panelsOne": "1 panneau",
+ "panels": "{{value0}} panneaux",
+ "eventsOne": "1 abonnement aux événements",
+ "events": "{{value0}} abonnements aux événements",
+ "worker": "Worker en arrière-plan",
+ "versionLine": "v{{value0}} · {{value1}}",
+ "includes": "Inclut",
+ "noContributions": "Métadonnées du manifeste uniquement",
+ "capabilities": "Accès demandé",
+ "workerWarning": "Les capabilities limitent la façon dont ce plugin utilise l'API d'Orca. Son worker s'exécute toujours comme un processus normal sur cet ordinateur, avec un accès complet à vos fichiers, au réseau et aux autres processus.",
+ "blocked": "La liste de sécurité d'Orca bloque ce plugin : {{value0}}",
+ "current": "Ce contenu exact de plugin est déjà installé.",
+ "close": "Fermer",
+ "cancel": "Annuler",
+ "update": "Mettre à jour le plugin",
+ "install": "Installer le plugin"
+ },
+ "PluginMarketplaceSourceDialog": {
+ "addFailed": "Impossible d'ajouter ce marketplace. Vérifiez l'URL Git, la ref et vos identifiants Git.",
+ "refreshFailed": "Impossible d'actualiser ce marketplace. Son dernier index valide en cache reste disponible.",
+ "removeFailed": "Impossible de supprimer ce marketplace.",
+ "title": "Sources du marketplace",
+ "description": "Les marketplaces sont des dépôts Git épinglés. Orca utilise vos identifiants Git système existants pour les dépôts privés.",
+ "urlLabel": "URL Git",
+ "urlDescription": "Utilisez une URL de dépôt HTTPS ou SSH contenant orca-marketplace.json.",
+ "urlPlaceholder": "https://git.example.com/team/plugins.git",
+ "refLabel": "Ref Git",
+ "refDescription": "Choisissez une branche, un tag ou un commit. Chaque index récupéré est enregistré à un commit précis.",
+ "adding": "Ajout…",
+ "add": "Ajouter une source",
+ "configured": "Sources configurées",
+ "empty": "Aucune source de marketplace configurée.",
+ "official": "Officiel",
+ "owner": "Propriétaire : {{value0}}",
+ "pinnedCommit": "Épinglé à {{value0}}",
+ "stale": "Échec de l'actualisation. Navigation dans le dernier index valide en cache.",
+ "refreshLabel": "Actualiser {{value0}}",
+ "removeLabel": "Supprimer {{value0}}",
+ "done": "Terminé"
+ },
+ "PluginRemoveDialog": {
+ "title": "Supprimer le plugin ?",
+ "description": "Ceci supprime {{value0}} et ses données de plugin stockées de cet ordinateur. Vous pourrez le réinstaller plus tard.",
+ "cancel": "Annuler",
+ "remove": "Supprimer le plugin"
+ },
+ "PluginRollbackDialog": {
+ "title": "Restaurer le plugin ?",
+ "description": "Ceci désactive {{value0}} et restaure sa version immuable précédente. Si cette version demande un accès ou un contenu d'instructions différents, Orca exigera une nouvelle revue.",
+ "cancel": "Annuler",
+ "confirm": "Restaurer le plugin"
+ },
+ "PluginsSettingsSection": {
+ "systemLabel": "Système de plugins",
+ "systemDescription": "Détecte les plugins installés et permet de les activer individuellement. Rien ne s'exécute avant examen et activation. Les workers s'exécutent toujours sur cet ordinateur ; les actions sur les espaces de travail SSH passent par Orca.",
+ "featureOff": "Activez le système de plugins pour voir et gérer les plugins installés. Tout ce qui est déjà installé reste sur disque et désactivé tant que le système est coupé.",
+ "loading": "Chargement des plugins…",
+ "noInstalledResultsTitle": "Aucun plugin correspondant",
+ "noInstalledResults": "Aucun plugin installé ne correspond à cette recherche.",
+ "emptyTitle": "Aucun plugin installé pour l'instant",
+ "empty": "Parcourez l'onglet Tous pour installer des plugins depuis un marketplace.",
+ "loadFailed": "Impossible de charger les plugins.",
+ "settingsUpdateFailed": "Impossible d'enregistrer les paramètres du plugin.",
+ "install": "Installer le plugin",
+ "title": "Plugins",
+ "experimental": "Expérimental",
+ "description": "Installez et gérez les plugins Orca. Les plugins s'exécutent sur cet ordinateur, même pour les espaces de travail SSH.",
+ "logsFailed": "Impossible de charger les logs du plugin.",
+ "rollbackFailed": "Impossible de restaurer ce plugin. Une version immuable précédente n'est peut-être pas disponible."
+ },
+ "PluginSettingsRow": {
+ "blocked": "Bloquée",
+ "needsReview": "En attente de relecture",
+ "restarting": "Redémarrage",
+ "invalid": "Non valide",
+ "error": "Erreur",
+ "disabled": "Désactivé",
+ "running": "En cours",
+ "enabled": "Activé",
+ "loadingLogs": "Chargement des logs…",
+ "noLogs": "Aucune ligne de log enregistrée.",
+ "logCount": "{{value0}} dernières lignes sur un maximum de 200 lignes conservées",
+ "reviewAndEnable": "Examiner & activer",
+ "official": "Officiel",
+ "dev": "Dev",
+ "bundled": "Intégré",
+ "noDescription": "Aucune description fournie.",
+ "killListMessage": "La liste de sécurité d'Orca a désactivé ce plugin : {{value0}}",
+ "viewAdvisory": "Consulter l'avis de sécurité",
+ "runtimeError": "Le plugin s'est arrêté après une erreur d'activation ou de worker.",
+ "restartCount": " · {{value0}} redémarrages",
+ "moreActions": "Plus d'actions pour {{value0}}",
+ "hideLogs": "Masquer les logs",
+ "viewLogs": "Afficher les logs",
+ "rollback": "Restaurer",
+ "remove": "Supprimer",
+ "disableLabel": "Désactiver {{value0}}",
+ "enableLabel": "Activer {{value0}}",
+ "invalidPluginError": "Le manifeste du plugin ou les fichiers installés sont non valides. Corrigez le plugin, puis actualisez."
+ },
+ "PluginVmRecipeConsentPreview": {
+ "create": "Créer",
+ "suspend": "Suspendre",
+ "resume": "Reprendre",
+ "destroy": "Détruire",
+ "heading": "Commandes de recette de VM",
+ "commandLabel": "{{value0}} · {{value1}} commande"
+ },
+ "pluginError": {
+ "installManifestMissing": "Aucun orca-plugin.json lisible trouvé. Choisissez le dossier racine du plugin.",
+ "installManifestInvalid": "orca-plugin.json est non valide. Demandez à l'auteur du plugin de corriger le manifeste.",
+ "incompatible": "Ce plugin nécessite une autre version d'Orca.",
+ "installUnsafePath": "Le plugin contient un chemin de fichier ou un symlink dangereux et n'a pas été installé.",
+ "installLimit": "Le plugin dépasse les limites d'installation d'Orca en taille ou en nombre de fichiers.",
+ "installGit": "Orca n'a pas pu récupérer la révision Git épinglée. Vérifiez l'URL, la #ref, l'accès et la configuration Git du système.",
+ "invalidManifestMissing": "orca-plugin.json manque à la racine du plugin. Ajoutez-le, puis actualisez les plugins.",
+ "invalidManifest": "orca-plugin.json est non valide. Corrigez-le, puis actualisez les plugins.",
+ "invalidArtifact": "Un fichier worker ou panneau déclaré est manquant ou dangereux. Corrigez les fichiers du plugin, puis actualisez.",
+ "consentChanged": "Le plugin a changé pendant votre examen. Fermez cette boîte de dialogue et examinez les permissions mises à jour."
+ },
+ "plugins": {
+ "search": {
+ "title": "Plugins",
+ "description": "Installez et gérez les plugins Orca expérimentaux.",
+ "install": "installer le plugin",
+ "permissions": "permissions du plugin",
+ "logs": "logs du plugin",
+ "development": "plugins de développement"
+ }
+ },
+ "LinearAgentSkillGuide": {
+ "setupConnectTitle": "1. Connecter Linear",
+ "setupConnectBody": "Clé API personnelle pour qu'Orca liste les issues et ouvre les espaces de travail liés.",
+ "manageKeys": "Gérer les clés",
+ "addAccess": "Ajouter l'accès",
+ "setupSkillTitle": "2. Installer la skill de l'agent",
+ "setupSkillBody": "Donne aux agents de codage /orca-linear pour la lecture, les mises à jour, le tri et la jointure de pull requests ou merge requests.",
+ "setupVisibleTitle": "3. Afficher Linear dans Tasks",
+ "setupVisibleBody": "Conserve Linear dans le sélecteur de sources Tasks et les raccourcis de la barre latérale.",
+ "openTaskSources": "Sources de tâches",
+ "setupTitle": "Checklist de configuration",
+ "setupBody": "Les trois sont requis pour la boucle complète Tasks + agent. Le parcours de première configuration figure aussi sous Task Sources.",
+ "setupReady": "Tout est prêt",
+ "setupProgress": "{{done}} sur {{total}} prêts",
+ "notesTitle": "Bon à savoir",
+ "notesIntro": "Quelques rappels une fois Linear connecté et la skill installée.",
+ "noteLinkedTitle": "Partir d'une issue Linear",
+ "noteLinkedBody": "Les actions sur les tickets fonctionnent mieux dans un worktree créé depuis Tasks, pour que l'issue reste liée comme contexte.",
+ "noteSlashTitle": "Mentionner /orca-linear",
+ "noteSlashBody": "Dans le chat, utilisez /orca-linear (ou demandez en langage naturel) pour que l'agent charge la skill à ce tour.",
+ "noteKeysTitle": "Les clés suivent le runtime",
+ "noteKeysBody": "Les clés API et les espaces de travail sont stockés pour le runtime actif.",
+ "noteVisibilityTitle": "Masquer ≠ déconnecter",
+ "noteVisibilityBody": "Masquer Linear dans Task Sources le retire seulement du sélecteur. Cela ne supprime ni votre clé ni votre skill.",
+ "setupChecking": "Vérification…"
+ },
+ "TaskSourceLinearSetup": {
+ "connectTitle": "Connecter Linear",
+ "connectDescription": "Ajoutez une clé API personnelle pour qu'Orca puisse parcourir les issues et ouvrir des espaces de travail avec le contexte du ticket.",
+ "manageAccess": "Gérer les clés",
+ "addAccess": "Ajouter l'accès Linear",
+ "connectedHint": "Les espaces de travail et les clés sont stockés pour le runtime actif. Vous pouvez ajouter d'autres accès à tout moment.",
+ "recheck": "Revérifier la connexion",
+ "skillTitle": "Installer le skill d'agent Linear",
+ "skillDescription": "Donne aux agents /orca-linear pour lire les tickets, publier des mises à jour, changer les états et joindre des pull requests ou merge requests.",
+ "skillBlocked": "Connectez d'abord Linear, puis installez la skill pour les agents.",
+ "skillPanelTitle": "Skill Linear",
+ "terminalTitle": "Configuration de la skill Linear",
+ "terminalAriaLabel": "Terminal d'installation de la skill Linear",
+ "showDescription": "Incluez Linear dans le sélecteur de sources de la page Tasks et les raccourcis de la barre latérale."
+ },
+ "TaskSourceProviderCard": {
+ "statusChecking": "Vérification…",
+ "statusReady": "Prêt",
+ "statusConnectRequired": "Connexion requise",
+ "statusSkillRequired": "Skill requise",
+ "statusUnavailable": "Statut indisponible",
+ "statusHidden": "Masqué dans Tasks",
+ "statusIncomplete": "Configuration requise",
+ "collapseSetup": "Réduire les étapes de configuration de {{provider}}",
+ "expandSetup": "Afficher les étapes de configuration de {{provider}}"
+ },
+ "TaskSourceShowInTasksStep": {
+ "shown": "Affichée",
+ "show": "Afficher",
+ "hide": "Masquer",
+ "hideProviderAction": "Masquer {{provider}} dans Tasks",
+ "showProviderAction": "Afficher {{provider}} dans Tasks",
+ "lastProviderAction": "{{provider}} est affiché dans Tasks. Au moins un fournisseur doit rester visible.",
+ "lastProviderHint": "Au moins un fournisseur doit rester visible dans Tasks.",
+ "title": "Afficher dans Tasks",
+ "description": "Inclure ce fournisseur dans le sélecteur de sources Tasks et les raccourcis de la barre latérale."
+ },
+ "RuntimeHostAccessForm": {
+ "getLink": "Obtenir un lien d'accès depuis l'autre hôte",
+ "stepOpenShare": "Ouvrez Paramètres → Serveurs Orca distants → Partager cet hôte.",
+ "stepChooseAddress": "Choisissez Autre appareil et sélectionnez une adresse joignable.",
+ "stepCopyLink": "Générez le lien, puis copiez le lien « Associer un autre client Orca ».",
+ "name": "Nom dans Orca",
+ "namePlaceholder": "Station de travail Linux",
+ "nameHelp": "Cela change uniquement la façon dont l'ordinateur apparaît dans Orca.",
+ "accessLink": "Lien d'accès",
+ "accessLinkPlaceholder": "orca://pair?code=...",
+ "accessLinkHelp": "Orca affiche la destination avant de se connecter. Les identifiants restent masqués.",
+ "destination": "Destination du lien",
+ "loopbackTitle": "Ce lien pointe vers cet appareil lui-même",
+ "loopbackDescription": "Il utilise {{endpoint}}, qui pointe vers l'appareil qui ouvre le lien — et non vers l'autre ordinateur qui l'a créé.",
+ "loopbackRecovery": "Sur l'autre ordinateur, créez un nouveau lien avec Autre appareil et choisissez son adresse Tailscale ou LAN.",
+ "identityMismatch": "L'hôte Orca rejoint ne correspond pas à ce lien d'accès",
+ "identityMismatchHelp": "Orca a rejoint {{endpoint}}, mais cet hôte ne correspond pas à ce lien. Générez un nouveau lien sur l'autre hôte.",
+ "invalidLink": "Ce lien d'accès n'est plus valide",
+ "invalidLinkHelp": "Générez un nouveau lien d'accès sur l'autre hôte et réessayez.",
+ "incompatible": "Les versions d'Orca ne sont pas compatibles",
+ "incompatibleHelp": "Mettez à jour Orca sur cet appareil et sur l'autre hôte, puis réessayez.",
+ "interrupted": "Connexion interrompue",
+ "interruptedHelp": "La connexion s'est arrêtée pendant la vérification. Vérifiez le réseau ou le tunnel SSH et réessayez.",
+ "unavailable": "Hôte indisponible",
+ "unavailableHelp": "Vérifiez qu'Orca tourne sur l'autre hôte et que le réseau ou le tunnel SSH peut joindre {{endpoint}}.",
+ "advanced": "Avancé",
+ "sshTunnel": "J'utilise un tunnel SSH vers cette adresse locale",
+ "sshTunnelHelp": "Gardez le tunnel actif tant que cette connexion est utilisée.",
+ "headlessHelp": "Vous utilisez orca serve sans interface ? Exécutez orca serve --pairing-address sur l'autre ordinateur.",
+ "cancel": "Annuler",
+ "addWithTunnel": "Ajouter l'hôte via un tunnel",
+ "addHost": "Ajouter un hôte",
+ "connectionDetails": "Détails de la connexion",
+ "endpointKind": "Type d'endpoint",
+ "networkConnection": "Connexion réseau",
+ "notAttempted": "Non tenté",
+ "saveFailed": "Impossible d'enregistrer l'hôte",
+ "saveFailedHelp": "L'hôte a été vérifié, mais Orca n'a pas pu l'enregistrer. Vérifiez le nom et le stockage local des paramètres, puis réessayez."
+ },
+ "CloudVmSetupGuide": {
+ "title": "Créer une VM cloud",
+ "description": "Les VM cloud sont créées à partir de recettes d'environnement lors de la création d'un espace de travail.",
+ "setupRecipe": "Configurez une recette d'environnement pour votre fournisseur cloud.",
+ "createWorkspace": "Créez un espace de travail et sélectionnez cette recette sous Run on.",
+ "openSetup": "Configurer les recettes d'environnement"
+ },
+ "ReleaseChannelSection": {
+ "switchFailed": "Impossible de passer à ce build.",
+ "title": "Canal de release",
+ "description": "Changez de canal de mise à jour ou passez à n'importe quel build publié, y compris plus ancien. Les retours en arrière sont permis et les builds non vérifiés peuvent être défectueux.",
+ "devOnly": "Réservé au développement",
+ "channelAriaLabel": "Canal de mise à jour",
+ "hourlyWarning": "Les builds horaires sortent directement de main sans validation par tests, et ceux pour Windows ne sont pas signés. Gardez un build stable sous la main.",
+ "dailyWarning": "Les builds quotidiens sortent directement de main sans validation par tests, et ceux pour Windows ne sont pas signés. Gardez un build stable sous la main.",
+ "adhocWarning": "Les builds ad hoc viennent d'une branche non intégrée, et ceux pour Windows ne sont pas signés. Leur auteur peut les abandonner — gardez un build stable sous la main.",
+ "loadingBuilds": "Chargement des builds…",
+ "noBuilds": "Aucun build trouvé",
+ "refresh": "Actualiser la liste des builds",
+ "switchTo": "Basculer vers le build",
+ "alreadyRunning": "C'est le build que vous utilisez.",
+ "willSwitch": "{{value0}} → {{value1}}",
+ "webUnavailable": "Le changement de build n'est disponible que dans l'app de bureau.",
+ "devChannelUnsupportedAria": "{{value0}} ({{value1}} uniquement)",
+ "devChannelUnsupported": "Les builds {{value0}} ne sont produits que pour {{value1}}. Linux reste sur Stable ou RC.",
+ "downloadInstaller": "Télécharger l'installateur",
+ "manualInstallHint": "Les builds {{value0}} ne sont pas signés sous Windows ; la mise à jour intégrée ne peut donc pas installer l'un d'eux par-dessus un build signé. Exécutez une fois l'installateur téléchargé — Windows avertira d'un éditeur inconnu — puis tous les changements ultérieurs, y compris le retour à Stable, fonctionnent depuis ici."
+ },
+ "VoiceMicrophoneSetting": {
+ "systemDefault": "Valeur par défaut du système",
+ "unavailable": "indisponible",
+ "label": "Microphone",
+ "description": "Périphérique d'entrée utilisé pour la dictée vocale. Le défaut système suit le réglage du micro de l'OS.",
+ "accessHint": "Autorisez l'accès au microphone pour lister les périphériques d'entrée.",
+ "allowAccess": "Autoriser l'accès"
+ },
+ "TerminalTccAttributionNotice": {
+ "body": "Le daemon du terminal a été démarré par une installation d'Orca qui n'existe plus, donc macOS ne peut pas attribuer ses commandes à Orca — les autorisations Accessibilité et Automatisation sont ignorées silencieusement (osascript échoue avec l'erreur -25211). Redémarrer le daemon corrige le problème ; les sessions de terminal en cours seront fermées.",
+ "openManageSessions": "Ouvrir Gérer les sessions",
+ "title": "Les autorisations macOS n'arrivent pas aux terminaux"
+ },
+ "artifacts": {
+ "enable": "Activer Artifacts",
+ "enableDescription": "Ajouter Artifacts à la barre latérale pour ouvrir et supprimer les fichiers partagés.",
+ "account": "Compte Orca",
+ "connected": "Connecté",
+ "signInRequired": "La connexion est requise pour téléverser et gérer les artifacts.",
+ "signingIn": "Connexion…",
+ "signIn": "Se connecter à Orca",
+ "title": "Artefacts",
+ "description": "Partagez des fichiers HTML et Markdown avec votre équipe et gérez leurs liens publics.",
+ "howToTitle": "Comment utiliser Artifacts",
+ "howToDescription": "Publiez des fichiers HTML ou Markdown sous forme de liens publics, puis partagez-les avec votre équipe.",
+ "shareStepTitle": "Choisir un fichier à partager",
+ "shareStepDescription": "Ouvrez un fichier HTML ou Markdown et sélectionnez Partager comme artifact, ou demandez à un agent de le partager.",
+ "linkStepTitle": "Copier le lien public",
+ "linkStepDescription": "Après publication, copiez le lien et envoyez-le à votre équipe.",
+ "manageStepTitle": "Gérer dans Orca",
+ "manageStepDescription": "Ouvrez Artifacts depuis la barre latérale pour prévisualiser ou supprimer des liens.",
+ "openArtifacts": "Ouvrir Artifacts",
+ "openArtifactsDescription": "Consultez et supprimez les liens partagés via votre compte.",
+ "showButton": "Afficher le bouton Artifacts",
+ "showButtonDescription": "Afficher le raccourci Artifacts dans la barre latérale.",
+ "openArtifactsDescriptionV2": "Prévisualisez, copiez et gérez les liens partagés via votre compte.",
+ "signInTitle": "Se connecter pour partager des artifacts",
+ "signInDescription": "Utilisez votre compte Orca pour téléverser des artifacts et gérer leurs liens publics.",
+ "signInAgain": "Se reconnecter",
+ "enableStepTitle": "Activer le partage d'artifacts",
+ "enableStepWebDescription": "Ouvrez Paramètres → Artifacts dans l'app Orca de bureau sur l'appareil hôte et activez la publication.",
+ "enableStepDescription": "Activez « Autoriser la publication de liens d'artifacts publics » ci-dessus.",
+ "allowPublishing": "Autoriser la publication de liens d'artifacts publics",
+ "allowPublishingWebDescription": "Bureau uniquement. Ouvrez Paramètres → Artifacts sur l'appareil hôte pour modifier ce paramètre.",
+ "allowPublishingDescription": "Publiez des fichiers HTML et Markdown sous forme de liens ouvrables par quiconque possède l'URL. Les liens existants demeurent jusqu'à leur suppression dans Artifacts.",
+ "howToDescriptionDisabled": "Activez le partage d'artifacts ci-dessus pour publier des fichiers HTML ou Markdown sous forme de liens publics.",
+ "allowPublishingSearchDescription": "Autoriser Orca à publier des fichiers HTML et Markdown sous forme de liens publics."
+ },
+ "orcaAccount": {
+ "connected": "Connecté",
+ "reconnectRequired": "Votre session a expiré. Reconnectez-vous pour utiliser les fonctions cloud.",
+ "unavailable": "La connexion Orca n'est pas disponible dans ce build.",
+ "signedOut": "Connectez-vous pour étendre Orca avec des fonctions cloud, dont Artifacts et Orca Relay.",
+ "checking": "Vérification de l'état du compte…",
+ "account": "Compte Orca",
+ "signOut": "Se déconnecter",
+ "signingIn": "Connexion…",
+ "signInAgain": "Se reconnecter",
+ "signIn": "Se connecter à Orca",
+ "title": "Compte Orca",
+ "description": "Partagez instantanément votre travail et accédez à votre poste depuis Orca Mobile, où que vous soyez.",
+ "searchDescription": "Connectez-vous ou déconnectez-vous du compte utilisé par Artifacts et Orca Relay.",
+ "benefitsTitle": "Inclus avec votre compte",
+ "artifactsTitle": "Partage d'artifacts",
+ "artifactsDescription": "Publiez des fichiers HTML et Markdown, puis gérez chaque lien partagé depuis Orca.",
+ "relayTitle": "Orca Relay",
+ "relayDescription": "Connectez Orca Mobile à ce poste via réseau mobile ou n'importe quel Wi-Fi.",
+ "skillsTitle": "Partage de skills",
+ "skillsDescription": "Partagez une skill ou tout un ensemble derrière un lien non répertorié, et installez-les sur toutes les machines que vous utilisez."
+ },
+ "automations": {
+ "showButton": "Afficher le bouton Automatisations",
+ "showButtonDescription": "Afficher le raccourci Automations dans la barre latérale.",
+ "howItWorksTitle": "Fonctionnement des Automations",
+ "howItWorksDescription": "Planifiez le travail d'un agent une fois, puis laissez Orca créer chaque exécution et regrouper ses résultats.",
+ "defineStepTitle": "Décrire le travail",
+ "defineStepDescription": "Choisissez un projet, un agent, un prompt et une planification.",
+ "runStepTitle": "Orca lance chaque exécution",
+ "runStepDescription": "L'agent sélectionné reçoit un espace de travail neuf quand la planification arrive à échéance.",
+ "reviewStepTitle": "Consulter les résultats",
+ "reviewStepDescription": "Inspectez les exécutions récentes et poursuivez le travail dès que nécessaire.",
+ "openAutomations": "Ouvrir Automations",
+ "openAutomationsDescription": "Créez des planifications et inspectez les exécutions récentes.",
+ "title": "Automatisations",
+ "description": "Planifiez le travail des agents et choisissez si Automatisations apparaît dans la barre latérale."
+ },
+ "AgentAwakeSetting": {
+ "on": "Activé",
+ "auto": "Agent",
+ "off": "Désactivé"
+ },
+ "shareSkills": {
+ "title": "Partage de skills",
+ "description": "Partagez vos skills avec un lien non répertorié. Toute personne qui l'a peut les installer.",
+ "allowAgentPublishing": "Autoriser les agents et la CLI Orca à publier des liens de skills",
+ "allowAgentPublishingDescription": "Permet aux commandes de publier des skills installées nommées explicitement. Les dossiers de skills peuvent contenir des scripts, de la configuration ou des secrets, c'est donc désactivé par défaut.",
+ "allowAgentPublishingWebDescription": "Bureau uniquement. Ouvrez Paramètres → Partage de skills sur l'appareil hôte pour modifier ce paramètre.",
+ "selectTitle": "Sélectionnez une ou plusieurs skills",
+ "selectDescription": "Ouvrez Skills, choisissez Partager des skills, puis sélectionnez les skills à regrouper derrière un seul lien.",
+ "reviewTitle": "Vérifier et publier",
+ "reviewDescription": "Vérifiez les fichiers, scripts et exécutables inclus avant de téléverser le bundle immuable.",
+ "copyTitle": "Copier le lien non répertorié",
+ "copyDescription": "Quiconque possède le lien peut inspecter et installer toutes les skills ou celles sélectionnées, sans se connecter.",
+ "manageTitle": "Gérer ou révoquer les liens",
+ "manageDescription": "La révocation bloque tout accès futur. Les skills déjà installées sur une autre machine y restent.",
+ "linkTitle": "Liens de skills non répertoriés",
+ "linkDescription": "Les bundles partagés ne sont ni recherchables ni listés dans Orca. Le lien tient lieu d'identifiant : envoyez-le uniquement à des personnes de confiance.",
+ "signInTitle": "Se connecter pour partager des skills",
+ "signInWebDescription": "La publication et la gestion des liens sont disponibles dans l'app Orca de bureau.",
+ "signInDescription": "Utilisez votre compte Orca pour publier des bundles et gérer leurs liens. Les destinataires n'ont pas besoin de compte.",
+ "signingIn": "Connexion…",
+ "signInAgain": "Se reconnecter",
+ "signIn": "Se connecter à Orca",
+ "howToTitle": "Comment partager des skills",
+ "howToDescription": "Publiez une seule skill ou un bundle, par exemple une collection de 30 skills, derrière un seul lien.",
+ "openSkills": "Ouvrir Skills",
+ "openSkillsDescription": "Publiez un bundle, installez depuis un lien ou gérez les skills installées et partagées.",
+ "searchDescription": "Partagez vos skills avec un lien non répertorié. Toute personne qui l'a peut les installer.",
+ "linksReconnect": "Reconnectez-vous pour gérer les liens partagés.",
+ "linksUnavailable": "Les liens partagés sont indisponibles pour le moment.",
+ "linkCopied": "Lien de partage copié",
+ "linkRevoked": "Lien révoqué",
+ "revokeFailed": "Orca n'a pas pu révoquer ce lien.",
+ "activeLinks": "Liens partagés actifs",
+ "activeLinksDescription": "Seules les personnes disposant du lien peuvent l'ouvrir. Annulez le partage d'un lien pour bloquer toute inspection et installation futures.",
+ "refreshLinks": "Actualiser",
+ "noActiveLinks": "Aucun lien actif. Publiez un bundle de skills depuis Skills pour en créer un.",
+ "copyLink": "Copier le lien",
+ "confirmUnshare": "Confirmer l'arrêt du partage",
+ "unshare": "Ne plus partager",
+ "showButton": "Afficher le bouton Skills",
+ "showButtonDescription": "Afficher le raccourci Skills dans la barre latérale.",
+ "manageInSkills": "Gérer dans Skills"
+ },
+ "bitbucket": {
+ "credentials": {
+ "dialog": {
+ "connectFailed": "Échec de la connexion",
+ "title": "Connecter Bitbucket",
+ "description": "Utilisez un identifiant Bitbucket Cloud pour parcourir les pull requests et les statuts de build. Orca le vérifie avant de l'enregistrer.",
+ "remoteRuntime": "Les identifiants Bitbucket enregistrés ici ne sont stockés que sur cette machine locale. Définissez plutôt les variables d'environnement ORCA_BITBUCKET_* sur le runtime distant.",
+ "environmentManaged": "Bitbucket est déjà configuré via les variables d'environnement ORCA_BITBUCKET_*, qui ont priorité. Désactivez-les pour enregistrer un identifiant dans Orca.",
+ "authModeLabel": "Méthode d'authentification Bitbucket",
+ "modeBasic": "E-mail & token API",
+ "modeToken": "Token d'accès",
+ "accessToken": "Token d'accès",
+ "accessTokenPlaceholder": "Token d'accès de dépôt, projet ou espace de travail",
+ "email": "E-mail du compte Atlassian",
+ "emailPlaceholder": "you@example.com",
+ "apiToken": "Token API",
+ "apiTokenPlaceholder": "Token API Atlassian",
+ "baseUrl": "URL de base de l'API (facultatif)",
+ "baseUrlPlaceholder": "https://api.bitbucket.org/2.0",
+ "tokenHint": "Les tokens d'accès de dépôt, projet et espace de travail se créent depuis la page de paramètres Bitbucket correspondante et nécessitent un accès en lecture aux pull requests.",
+ "basicHint": "Créez un token API Atlassian pour votre compte, puis associez-le à l'adresse e-mail qui en est propriétaire.",
+ "docsLink": "Documentation des tokens API Bitbucket",
+ "storageNote": "Stocké sur cette machine avec chiffrement quand le trousseau de l'OS est disponible. Les variables d'environnement ORCA_BITBUCKET_* ont toujours priorité sur ce que vous enregistrez ici.",
+ "cancel": "Annuler",
+ "verifying": "Vérification...",
+ "connect": "Se connecter"
+ }
+ },
+ "integration": {
+ "card": {
+ "description": "Pull requests et statuts de build pour Bitbucket Cloud.",
+ "edit": "Modifier les identifiants",
+ "connect": "Se connecter",
+ "accountUnknown": "Bitbucket Cloud",
+ "authModeToken": "Token d'accès",
+ "authModeBasic": "E-mail & token API",
+ "disconnect": "Déconnecter Bitbucket",
+ "envManaged": "Configuré via des variables d'environnement. Désactivez les variables ORCA_BITBUCKET_* pour gérer cet identifiant dans Orca.",
+ "storedAuthFailed": "L'identifiant Bitbucket enregistré n'a pas réussi à s'authentifier. Modifiez-le ou vérifiez que le token garde l'accès aux pull requests.",
+ "storedCredential": "Enregistré dans Orca sur cette machine. Les variables d'environnement ORCA_BITBUCKET_* ont priorité quand elles sont définies.",
+ "notConfigured": "Connectez un compte Bitbucket Cloud avec un token API Atlassian ou un token d'accès. Les variables d'environnement ORCA_BITBUCKET_* marchent aussi et ont priorité.",
+ "disconnectFailed": "Impossible de supprimer l'identifiant Bitbucket enregistré."
+ }
+ }
+ },
+ "GlobalWorktreeVisibilitySourcesSetting": {
+ "saveFailed": "Impossible d'enregistrer les valeurs de visibilité par défaut.",
+ "updateServer": "Mettez à jour ce serveur pour configurer les sources par défaut.",
+ "updateServerDefaults": "Mettez à jour ce serveur pour configurer les visibilités par défaut."
+ },
+ "EphemeralVmCleanupStopDialog": {
+ "title": "Arrêter le nettoyage ?",
+ "description": "La VM peut continuer à tourner et générer des frais. Vous pourrez relancer le nettoyage plus tard.",
+ "cancel": "Continuer le nettoyage",
+ "stopping": "Arrêt…",
+ "confirm": "Arrêter le nettoyage"
+ },
+ "shortcutDefinitionCatalog": {
+ "missionControlConflict": "Bloqué par Mission Control. Remappez ici ou changez-le dans Réglages Système."
+ }
+ },
+ "right": {
+ "sidebar": {
+ "BulkActionBar": {
+ "79a9f5f712": "Retirer du staging (",
+ "ef5f5bd06e": "Ajouter au staging (",
+ "60ed678138": "sélectionnés"
+ },
+ "ChecksPanel": {
+ "2ef90c9819": "Un agent IA a été démarré pour les vérifications cassées.",
+ "a0181a8d76": "Un agent IA a été lancé pour les conflits.",
+ "34464d00b9": "mis à jour",
+ "058039787c": "Annuler",
+ "2ab7fd4b6d": "Enregistrer",
+ "dda5924a40": "Les checks exigent une branche Git et un contexte de revue hébergée",
+ "976cefd02f": "Checks indisponibles",
+ "b5dd73a105": "Sélectionnez un espace de travail pour voir les checks",
+ "a4ef4e0832": "Aucun espace de travail sélectionné",
+ "5594400d73": "Aucune vérification cassée à corriger.",
+ "abf59262fb": "Vérifiez le prompt avant de démarrer un agent.",
+ "4ede779461": "Résoudre les conflits de revue avec l'IA",
+ "3b203c62f8": "Le commentaire sera définitivement retiré de la PR.",
+ "ea9b649ce3": "Supprimer le commentaire ?",
+ "5788d1059d": "Impossible de mettre à jour le fil de revue. Vérifiez le budget de l'API GitHub.",
+ "07871c0589": "Lier une autre PR",
+ "7202f4a40a": "délier la PR",
+ "7f4489f370": "Actualiser",
+ "5c88c6db07": "Ouvrir sur {{value0}}",
+ "7fad8509fe": "Corriger avec l'IA",
+ "71026ca2cb": "Actualisation…",
+ "889cdfba04": "Créer {{value0}}",
+ "98f4c37b33": "Pusher & créer {{value0}}",
+ "b6ce28da5b": "{{value0}} #{{value1}} est déjà ouvert",
+ "cf9e69f3be": "{{value0}} est déjà ouvert",
+ "192e686e57": "Ouvrir sur {{value0}}",
+ "6633c7a1fb": "Publier la branche",
+ "fdb27637f2": "Publication…",
+ "e56c42122e": "destructive",
+ "786e3c143f": "Supprimer",
+ "653c105ecc": "Plus d'actions sur la PR",
+ "f316a8ca2b": "Aucun commentaire non résolu sélectionné.",
+ "d00ebdc402": "Résoudre {{value0}} commentaires avec l'IA",
+ "5eb2163b6b": "Vérifiez le prompt avant de lancer un agent. Une fois le prompt livré, Orca résout les fils sélectionnés côté hôte et répond aux commentaires qu'il ne peut pas résoudre.",
+ "f273f2271c": "Agent lancé. {{value0}} marqués résolus, {{value1}} réponses envoyées, {{value2}} ignorés, {{value3}} échecs.{{value4}}",
+ "aa95b81a3a": "Agent lancé. {{value0}} marqués résolus, {{value1}} réponses envoyées, {{value2}} ignorés, {{value3}} échecs.",
+ "495b2f8c4b": "Agent lancé, mais impossible de résoudre ou de répondre aux commentaires sélectionnés.",
+ "3c3ad3a1d2": "Agent lancé. Aucun des commentaires sélectionnés ne peut être marqué résolu côté hôte.",
+ "review": {
+ "auto_retry": "Orca réessaiera à {{time}}.",
+ "open_review": "Ouvrir la revue",
+ "retry": "Réessayer"
+ },
+ "sync": {
+ "pending": "Synchronisation…",
+ "branch": "Synchroniser la branche"
+ },
+ "7e4b2a19c0": "Impossible d'identifier la PR GitHub sur laquelle répondre.",
+ "430f1a62d4": "Impossible de résoudre le fil sélectionné côté hôte.",
+ "updateReactionFailed": "Échec de la mise à jour de la réaction."
+ },
+ "CreatePullRequestDialog": {
+ "2bc1b4345e": "Annuler",
+ "27ef4b195c": "Choisissez une autre branche de base avant de créer {{value0}}.",
+ "7ef56f3efe": "Créer en brouillon",
+ "0c9f9a568c": "Prend en charge le formatage Markdown. Utilisez Générer avec l'IA pour le remplir automatiquement à partir de vos modifications.",
+ "02b2ce911f": "Description (facultatif)",
+ "1cd53359db": "Description",
+ "68314b4369": "Titre",
+ "694550a610": "main",
+ "0fad57a14c": "Recherchez des branches distantes ou saisissez un nom de branche.",
+ "8584ccb43c": "Branche de base",
+ "6f5f1962b6": "Branche source",
+ "b504b3ceb1": "détails avant de créer la revue hébergée.",
+ "f658ff2455": "Confirmez la branche cible et les détails de {{value0}} avant de créer la revue hébergée.",
+ "b7f43474d7": "Créer {{value0}}",
+ "7a21f0dae8": "Ouvrir sur {{value0}}",
+ "edc35a7027": "{{value0}} #{{value1}} est déjà ouvert",
+ "a154fe55e6": "Pusher & créer {{value0}}",
+ "21c7a1daa0": "{{value0}} est déjà ouvert",
+ "db9cee18f7": "Créer {{value0}}"
+ },
+ "CreateHostedReviewComposer": {
+ "741ff8a0d2": "Pusher & créer {{value0}}"
+ },
+ "CreatePullRequestGenerateButton": {
+ "4012459f8a": "Générer avec l'IA",
+ "a0501572c1": "Générer les détails de {{value0}} avec l'IA",
+ "d47fd63012": "Génération des détails de {{value0}}. Cliquez pour arrêter.",
+ "bdf83ccb15": "Génération de {{value0}}",
+ "f5513bdeb1": "Génération",
+ "a6ea6dc3aa": "Génération…",
+ "e61d7e7ad4": "Arrêter la génération des détails de {{value0}}",
+ "e041998cad": "Arrêter la génération"
+ },
+ "FileExplorer": {
+ "79b1537dd3": "Sélectionnez un espace de travail pour parcourir les fichiers",
+ "4da4d89845": "Retour à l'explorateur",
+ "6ed5ce817b": "Recherche",
+ "2f4483d6c4": "Aucun fichier ne correspond à ce filtre"
+ },
+ "FileExplorerBackgroundMenu": {
+ "3b5e2dcb8d": "Nouveau dossier",
+ "21fe46ed36": "Nouveau fichier"
+ },
+ "FileExplorerRow": {
+ "addc01145f": "Supprimer",
+ "fc747429bf": "Renommer",
+ "0df0e5abac": "Rechercher dans le dossier",
+ "d6a25618aa": "Réduire le dossier",
+ "d87a4c42e1": "Ouvrir l'aperçu Markdown",
+ "c2112579f6": "Télécharger",
+ "7ac885bd2f": "Télécharger le dossier",
+ "dd112c81d2": "Ouvrir dans le navigateur Orca",
+ "1bb9be455c": "Ajouter comme projet...",
+ "0fec99bfd7": "Doublon",
+ "f61af83316": "Nouveau dossier",
+ "37c875d827": "Nouveau fichier",
+ "b3e288bf41": "Échec du téléchargement de « {{value0}} ».",
+ "f729bcd97d": "Échec du téléchargement du dossier « {{value0}} ».",
+ "1a3df04ae1": "Ouvert",
+ "bce4d4e44f": "« {{value0}} » téléchargé",
+ "a4029c996b": "Dossier « {{value0}} » téléchargé",
+ "e26010014a": "Ignoré par .gitignore",
+ "a06551beee": "Enter",
+ "128a99ed5e": "Non assigné",
+ "2de3b21934": "markdown",
+ "66a29dde82": "Copier le chemin relatif",
+ "42e10cbf57": "Copier les chemins relatifs",
+ "b5d436aa30": "Copier le chemin",
+ "98a79948b3": "Copier",
+ "b234ab25b4": "Impossible de copier le fichier dans le presse-papiers",
+ "f9d7ca753d": "Copier les chemins",
+ "3161c4e425": "dossier",
+ "e887fa4b2e": "Ouvrir dans le terminal",
+ "1d8e182c32": "Afficher le fichier",
+ "clipboardStagingUnavailable": "Impossible de copier le fichier car le stockage temporaire d'Orca est indisponible"
+ },
+ "FileExplorerToolbar": {
+ "d238264654": "Afficher les fichiers ignorés par Git",
+ "78f133232c": "Afficher les dotfiles",
+ "31b4c3195d": "Plus d'actions de l'explorateur",
+ "d95e30fe28": "Actualiser l'explorateur",
+ "6026b16950": "Tout réduire",
+ "693cbeadd0": "Recherche",
+ "c1f3f3ec70": "Rechercher dans le contenu des fichiers"
+ },
+ "FileExplorerTreeStatus": {
+ "ce03835e1f": "Aucun fichier dans cet espace de travail",
+ "c76693e456": "Impossible de charger les fichiers de cet espace de travail :"
+ },
+ "GitHistoryPanel": {
+ "cf7cad58d2": "Aucun commit pour le moment",
+ "781a8bcf7b": "Chargement du graphe...",
+ "d0fb0f4bf2": "Actualiser les commits",
+ "9f7535d22b": "Les refs sont des noms de branche ou de tag pointant vers ce commit exact. Elles n'apparaissent que là où Git possède une ref nommée pour ce commit.",
+ "9289ba0cb9": "Que sont les refs ?",
+ "d836037d02": "Commits",
+ "8232c8b2f2": "Ouvrir le commit {{value0}} : {{value1}}",
+ "9a8b85882d": "chargement",
+ "62e685d5ec": "idle",
+ "111e1d0db4": "error",
+ "e5e81e59a6": "Redimensionner les commits",
+ "6d1e0a7c3b": "Échec du chargement des fichiers du commit"
+ },
+ "HostedReviewActions": {
+ "4d5fb5a284": "Fermer",
+ "9845a71e17": "Plus d'actions",
+ "2bfaf4379c": "Plus d'actions {{value0}}",
+ "377269db6f": "Réouverture de {{value0}} effectuée",
+ "fa3ee9a515": "closed",
+ "closedToast": "Fermeture de {{value0}} effectuée",
+ "78f5ff294c": "Cela rouvrira {{value0}}.",
+ "a3d572a4de": "Cela fermera {{value0}}.",
+ "e4aca40024": "Supprimer l'espace de travail",
+ "eefd50457e": "Suppression...",
+ "3ce211ece6": "Rouvrir {{value0}}",
+ "6645ac7dd1": "Réouverture...",
+ "b25f63edd7": "ouvrir",
+ "d2ca293f3d": "Traitement...",
+ "ef064cb7c3": "default",
+ "59b4dccf70": "destructive",
+ "9a41a687b7": "Mettre en file via #{{pr}} · {{count}} PR",
+ "38a1bccb14": "Mettre en file via #{{pr}} · {{count}} PRs",
+ "3de88351c5": "GitHub ajoutera cette pull request et toutes celles situées en dessous à la file de merge.",
+ "a32fe6dba6": "GitHub mergera cette pull request et toutes celles situées en dessous dans la pile.",
+ "73e0e1819d": "Mise en file de la pile...",
+ "e555a41d32": "Merge de la pile..."
+ },
+ "PortsPanel": {
+ "3ea4a02a8f": "Annuler",
+ "4eb801ce93": "dev-server",
+ "8dfed0a15c": "Libellé (facultatif)",
+ "17bea6e391": "localhost",
+ "a3721a50b0": "Hôte distant",
+ "d57545ff92": "Identique au distant",
+ "b950b1948b": "Port local",
+ "9e5a4118b0": "Port distant",
+ "c9d106547a": "Transférer",
+ "c7e920aa7c": "annoncé comme {{value0}}",
+ "e740075063": "Supprimer",
+ "b3548e59f4": "Édition",
+ "fe2730d050": "Copier {{value0}}",
+ "b22b128b2a": "Ouvrir dans le navigateur",
+ "75aeea592f": "Ouvrir {{value0}} dans le navigateur",
+ "de349d4560": "ouvre {{value0}}",
+ "907eb53ed2": "Transférer un port",
+ "04efd3dad4": "Transférez un port pour accéder aux services distants depuis votre machine locale.",
+ "1f0d2a24f9": "Aucun port transféré",
+ "36b1b2984a": "Détecté",
+ "ddbe58d74e": "Transféré",
+ "a103dae837": "Ajouter",
+ "6bc058dbe1": "Ports",
+ "d4c3cd679c": "Reconnexion...",
+ "a2f1a47f42": "Connexion SSH perdue",
+ "409afcc145": "Aucun espace de travail sélectionné pour le navigateur.",
+ "153145e675": "Preuve",
+ "c7b4702b7b": "Espace de travail",
+ "57d930fa45": "PID",
+ "5dd86dcf2f": "Processus",
+ "b1ff94fa27": "Protocole",
+ "729be0b4e5": "Type",
+ "0f1d8cd324": "Bind",
+ "1c1c18cefc": "Adresse",
+ "f9528da632": "Arrêter le processus",
+ "a223459512": "Afficher les détails",
+ "bdac206faf": "Copier les détails",
+ "792baeb7ed": "Copier l'adresse",
+ "d41a8241ec": "Port",
+ "a2a9fc6899": "Aucun port local détecté",
+ "f59c783b7a": "Scan des ports indisponible sur {{value0}} : {{value1}}",
+ "7822e3edc6": "Actualiser les ports",
+ "c1b115c375": "Aucun espace de travail sélectionné",
+ "98e9a414f8": "Échec de l'ouverture du navigateur",
+ "a00f3a2840": "Échec de l'actualisation des ports",
+ "97b562d21d": "Processus arrêté sur :{{value0}}",
+ "9079776663": "Enregistrer",
+ "c57eda6822": "modifier",
+ "9f475dc994": "Transfert en cours...",
+ "d7c83cfd24": "Enregistrement...",
+ "31e80cff2d": "Transférez un port distant vers votre machine locale.",
+ "10360598a4": "Mettez à jour la configuration de transfert de port.",
+ "80206251c8": "Modifier le transfert de port",
+ "4bc9b00912": "espace de travail",
+ "3e13cb63ee": "Inconnu",
+ "472054d94c": "Port :{{value0}}",
+ "1119f90ad7": "conteneur",
+ "d32820d3e2": "Externe",
+ "4db4b5e435": "Autres espaces de travail",
+ "38b16cfbef": "Aucun port détecté",
+ "0d63d94db3": "Analyse...",
+ "935dda7718": "Espace de travail actif",
+ "740aca88ab": "Échec du scan des ports de l'espace de travail.",
+ "5be4f7f727": "Menu du port {{value0}}",
+ "7550998473": "Copier",
+ "1004af16ab": "Copier {{value0}}"
+ },
+ "Search": {
+ "1abfb25a66": "Saisissez pour rechercher dans les fichiers",
+ "d56d140747": "Appuyez sur Entrée pour rechercher",
+ "0b8104eaf2": "fichier",
+ "4107975b3a": "dans",
+ "6aeda362ed": "résultat",
+ "98c8435e36": "Sélectionnez un espace de travail pour rechercher",
+ "1ec640c9c7": "correspondance",
+ "dcc294f28d": "(résultats tronqués)"
+ },
+ "SearchFilters": {
+ "01e4671ccf": "fichiers à exclure (ex. *.min.js, dist/**)",
+ "0a6412a895": "Fichiers à exclure",
+ "8a77efcbd1": "fichiers à inclure (ex. *.ts, src/**)",
+ "a69ee1bd0e": "Fichiers à inclure"
+ },
+ "SearchHeader": {
+ "6234a5ef85": "Utiliser une expression régulière",
+ "4567e6e0b6": "Mot entier uniquement",
+ "464ae3974f": "Respecter la casse",
+ "693cbeadd0": "Recherche"
+ },
+ "SearchQueryRow": {
+ "queryLabel": "Rechercher dans les fichiers",
+ "clearLabel": "Effacer la recherche"
+ },
+ "SearchResultItems": {
+ "cc06595a3b": "Copier le chemin de la ligne",
+ "3596b9668d": "Copier le chemin"
+ },
+ "SourceControlEntryContextMenu": {
+ "a1f2c8d901": "Affichage"
+ },
+ "SourceControl": {
+ "1406954883": "Effacer toutes les notes...",
+ "conflictsSection": "Conflits",
+ "cc05b2d088": "Ouvrir dans l'explorateur de fichiers",
+ "03194cfff4": "État de session local issu d'un conflit que vous avez ouvert ici.",
+ "413a3ba113": "conflit",
+ "27a50fe970": "Examiner les conflits",
+ "f6cb48b6fe": "Résoudre avec l'IA",
+ "3eeccbb221": "Les fichiers résolus repassent dans les modifications normales une fois sortis de l'état de conflit actif.",
+ "c321542ee2": "Supprimer la note de la ligne {{value0}}",
+ "b656381c18": "Supprimer la note",
+ "c085946bda": "Copier la note de la ligne {{value0}}",
+ "1623bf4e19": "Copier la note",
+ "655633c08a": "Envoyé",
+ "3eb9b2805e": "Ouvrir la note sur {{value0}}",
+ "0d963bf982": "Ouvrir {{value0}}",
+ "59654650d3": "Effacer les notes de {{value0}}",
+ "ac8cbe3bf5": "Survolez une ligne dans la vue diff et cliquez sur le + pour ajouter une note.",
+ "286dbda4d6": "Réessayer",
+ "476b77745b": "Modifier la ref de base",
+ "ed34038d0d": "Actualiser la comparaison de branches",
+ "493f963029": "Modifier la ref de base",
+ "3278b2767b": "en avance",
+ "11b5dd8e41": "Comparaison avec",
+ "783a808870": "Fermer",
+ "a9bf7c171a": "Échec du commit",
+ "03d238218c": "Détails",
+ "011f9713fc": "Commit bloqué",
+ "cc199ccc5f": "Autres actions de commit et de remote",
+ "4d6e1fd7f3": "Plus d'actions",
+ "37a81f29ad": "Génération du message de commit. Cliquez pour arrêter.",
+ "b94112eb9e": "Message de commit",
+ "0d0a8359d3": "Message",
+ "15b7f210d7": "Vérifiez le prompt avant de démarrer un agent.",
+ "054ead86b1": "Corriger l'échec du commit avec l'IA",
+ "9e5ccd00aa": "Contexte de l'échec du commit indisponible",
+ "f0a2dc9e46": "Personnaliser le lancement...",
+ "ec7bfced55": "Choisir l'agent pour corriger l'échec du commit",
+ "dd43c47089": "Choisissez un agent pour cet échec de commit",
+ "30b8d4f181": "Corriger l'échec du commit avec l'IA",
+ "4b37ae99b0": "Démarrer l'agent IA par défaut pour corriger cet échec de commit",
+ "ae743199cd": "Choisissez une autre branche de base avant de créer {{value0}}.",
+ "318e2a7f88": "Attendez la fin de la génération par l'IA.",
+ "f76307c1f7": "Choisissez une branche de base.",
+ "4f76c0a9de": "La branche de base doit être différente de la branche courante.",
+ "c5e4175139": "Autres actions {{value0}} et de remote",
+ "78ddfd0bb4": "Créer en brouillon",
+ "e64a632456": "main",
+ "6055949c50": "Branche de base {{value0}}",
+ "1f7119f604": "Base",
+ "9484270f45": "Génération du titre et de la description…",
+ "a0dc20fc93": "Description (facultatif)",
+ "a8873e1d62": "Description de {{value0}}",
+ "7d6a8f0082": "Titre",
+ "a6eda33521": "Titre de {{value0}}",
+ "02d8c04339": "Générer les détails de {{value0}} avec l'IA",
+ "aee92f8684": "Générer",
+ "e868cec4e1": "Génération…",
+ "b355e740b2": "Arrêter la génération des détails de {{value0}}",
+ "527e130b6f": "Arrêter la génération",
+ "e1970d327d": "Nouvelle {{value0}}",
+ "f4c766f1ca": "Choisissez l'agent et le modèle de commande pour cette exécution.",
+ "1a6a6e0bc5": "Générer les détails de la revue hébergée",
+ "6b122529d4": "Générer le message de commit",
+ "e48caaf0dd": "Un agent IA a été lancé pour les conflits.",
+ "901140f47d": "Vérifiez le prompt avant de démarrer un agent.",
+ "19652ddd76": "Résoudre les conflits avec l'IA",
+ "c9ad22888e": "Choisissez la cible de comparaison de branches pour ce dépôt.",
+ "574d2f4413": "Effacer les notes",
+ "05bb8f4a48": "Annuler",
+ "48db37cca9": "Tout afficher",
+ "78ce2d37ac": "Pousse vers le fork",
+ "c05fe04839": "Pousse vers le fork situé à {{value0}} (pas origin)",
+ "c35baf2f1e": "Filtrer les fichiers…",
+ "2fe2a67580": "Autres actions de note",
+ "eae2d051af": "Copier toutes les notes",
+ "cc474e0b8c": "Notes",
+ "e131cd7128": "Le contrôle de code source n'est disponible que pour les dépôts Git",
+ "c07b236287": "Sélectionnez un espace de travail pour afficher les modifications",
+ "dc5a6465fc": "{{value0}} (ex. {{value1}}{{value2}})",
+ "8eb3782a0c": "Échec de l'abandon de {{value0}} fichier{{value1}}",
+ "a5e5a11090": "Échec de l'abandon global — impossible de sortir les fichiers de l'index avant l'abandon",
+ "8a5ba6a988": "Échec du chargement du diff du commit",
+ "fe5bd1a610": "Création de {{value0}}...",
+ "812cb992ee": "Ouvrir sur {{value0}}",
+ "eef5446523": "{{value0}} #{{value1}} est déjà ouvert",
+ "0453ca3a9a": "Création de {{value0}} effectuée, mais Orca n'a pas encore pu l'actualiser.",
+ "f99560ab29": "Échec de l'abandon de {{value0}}",
+ "eae7a1da5f": "Échec de l'effacement des notes.",
+ "657e0c90ad": "{{value0}} note{{value1}}",
+ "df5040e3c3": "Déstager",
+ "8cde1a2fb0": "Stager",
+ "d54dd48b0b": "Abandonner les modifications",
+ "989f3d5e34": "Restaurer le fichier",
+ "2830dd64a2": "supprimé",
+ "11463f7a98": "Supprimer le fichier non suivi",
+ "d62bc0c7d8": "non suivi",
+ "ab31221779": "Déstager le dossier",
+ "bfe9011a0e": "Stager le dossier",
+ "6d7f2a47e5": "Abandonner le dossier",
+ "9b367363b6": "Supprimer les non suivis du dossier",
+ "540ca8f78c": "Abandonner le merge",
+ "425f138269": "Abandonner le rebase",
+ "04832d8047": "rebase",
+ "c105a61960": "merge",
+ "d7a5942e41": "{{value0}} : {{value1}} non résolus",
+ "c56ba7fa06": "Diff",
+ "94c42b252e": "MD",
+ "e59bca888a": "markdown",
+ "b6922abb13": "Impossible de charger la comparaison de branches.",
+ "715d229c86": "Comparaison de branches indisponible",
+ "97d8b03cdf": "Échec de la comparaison de branches",
+ "424ee0e5bf": "error",
+ "834cb3f23d": "Corriger avec l'IA",
+ "60bd988f0b": "Correction IA",
+ "461575b9bc": "Générer le message de commit avec l'IA",
+ "b16b8f0e4b": "message commit IA",
+ "ddc1fbd690": "Arrêter la génération du message de commit",
+ "5acbcedc1a": "Créer {{value0}}",
+ "aaf1451654": "Créer un brouillon de {{value0}}",
+ "26511c22b4": "Création...",
+ "7a09d7f9d2": "base",
+ "383cf92c73": "arborescence",
+ "d7ae61269b": "Committé sur la branche",
+ "48a003c1b1": "Modifications stagées",
+ "d4ef4bafc5": "Modifications",
+ "522f44dce5": "Fichiers non suivis",
+ "3636d0f686": "prêt",
+ "d2e9189866": "tout",
+ "a0cc0e6b4e": "chargement",
+ "9339382454": "Déstager tout",
+ "24d2598eff": "Stager tout",
+ "ce41708855": "Tout abandonner",
+ "2f609a2e7c": "Supprimer tous les non suivis",
+ "9bb062a886": "non committé",
+ "9febd8ab5f": "create_pr",
+ "f62ce91ade": "origin",
+ "3a231c845b": "unknown",
+ "3baf6c77b4": "Copier toutes les notes dans le presse-papiers",
+ "72f2bea3f4": "Déplier les notes",
+ "d13edef890": "Replier les notes",
+ "0fad573938": "Non committé",
+ "77afaa8152": "Tous",
+ "d6fb1df5fe": "{{value0}} est déjà ouvert",
+ "05838cfdeb": "Conflit {{value0}}",
+ "d206117f90": "Conflit {{value0}} ({{value1}})",
+ "0b5b8c234c": "Ouvrir {{value0}} ({{value1}})",
+ "d97ef8f221": "lignes {{value0}}-{{value1}}",
+ "6f8bfa0eb9": "ligne {{value0}}",
+ "c569d29a02": "modifié des deux côtés",
+ "ea7287d84f": "ajouté des deux côtés",
+ "bd0151ef7b": "supprimé chez nous",
+ "44594e8c61": "supprimé chez eux",
+ "24773ee581": "ajouté chez nous",
+ "c03d7c952f": "ajouté chez eux",
+ "5b176fa431": "supprimé des deux côtés",
+ "31f6d46278": "Non résolu",
+ "2c417432b7": "Résolu localement",
+ "f3a8b2c1d0e5": "Saisissez un titre de {{value0}}.",
+ "e2b7a1c0d9f4": "Échec de la création de {{value0}}",
+ "hugeRepoIgnorePrompt": "Ce dépôt contient trop de modifications actives. Ajouter « {{value0}} » à .gitignore ?",
+ "hugeRepoIgnoreAction": "Ajouter à .gitignore",
+ "tooManyChanges": "Trop de modifications détectées. Seules les {{value0}} premières modifications sont affichées.",
+ "submoduleTruncated": "D'autres modifications de sous-modules ont été omises",
+ "bf5082de46": "{{value0}} copié",
+ "c06193ef57": "Échec de la copie : {{value0}}",
+ "d172a4f068": "Hash du commit",
+ "e283b50179": "Message de commit",
+ "f394c6128a": "Aucun agent disponible pour expliquer ce commit",
+ "04a5d7239b": "Ce dépôt n'a aucun remote web pris en charge",
+ "15b6e834ac": "Échec de l'ouverture du commit dans le navigateur",
+ "d37e68f61d": "Préparation de la branche pour la revue…",
+ "8d8f5c6c94": "Génération du message de commit…",
+ "fda060d6ce": "Relisez le message de commit, puis réessayez Créer une PR.",
+ "b75cb1fd0c": "Commit des modifications…",
+ "995c5e67ec": "La configuration de revue nécessite votre attention.",
+ "d7492cafce": "Impossible d'actualiser le contrôle de code source. Réessayez Créer une PR.",
+ "473f18758e": "Paramètres IA du contrôle de code source",
+ "createPrIntentConfigureAi": "Ajoutez un message de commit ou configurez les paramètres IA du contrôle de code source.",
+ "createPrIntentGenerateFailed": "Impossible de générer un message de commit. Ajoutez-en un et réessayez.",
+ "createPrIntentCommitFailed": "Impossible de committer les modifications. Corrigez le problème, puis réessayez Créer une PR.",
+ "createPrIntentNeedsSync": "Synchronisez cette branche avant de créer une revue.",
+ "createPrIntentBranchNotReady": "La branche n'est pas encore prête pour la création d'une revue.",
+ "createPrIntentPublishing": "Publication de la branche…",
+ "createPrIntentForcePushing": "Force push avec lease…",
+ "createPrIntentPushing": "Push des commits…",
+ "createPrIntentFastForwarding": "Mise à jour de la branche…",
+ "createPrIntentRemoteFailed": "Impossible de mettre à jour la branche distante. Réessayez Créer une PR.",
+ "createPrIntentGeneratingDetails": "Génération des détails de revue…",
+ "createPrIntentBranchChangedDuringDetails": "La branche a changé pendant la génération des détails de revue. Réessayez Créer une PR.",
+ "createPrIntentCreatingReview": "Création de la revue…",
+ "a91f8e2b01": "Afficher en liste",
+ "b82e9f3c12": "Afficher en arbre",
+ "f71c4a8d90": "Plus d'actions du contrôle de code source",
+ "c8e4a1f902": "Filtre : {{value0}}",
+ "b3c8f1a902": "Filtrer les fichiers par nom",
+ "d4f8c2a901": "Effacer et fermer le filtre",
+ "e8a1c4b203": "vs",
+ "f9b2441bb6": "1 commit d'avance sur {{value0}}",
+ "b715ef615b": "{{value0}} commits d'avance sur {{value1}}",
+ "c1a8f3e204": "1 commit de retard sur {{value0}}",
+ "d2b9g4f315": "{{value0}} commits de retard sur {{value1}}",
+ "4b4a7de138": "Ouvrir la page de revue dans le navigateur",
+ "createPrIntentCommitBlockedSummary": "Commit bloqué : {{value0}} Corrigez le problème, puis réessayez Créer une PR.",
+ "pushRecovery": {
+ "4b37ae99b0": "Démarrer l'agent IA par défaut pour corriger cet échec de push",
+ "30b8d4f181": "Corriger l'échec de push avec l'IA",
+ "dd43c47089": "Choisissez un agent pour cet échec de push",
+ "ec7bfced55": "Choisir l'agent pour corriger l'échec de push",
+ "9e5ccd00aa": "Contexte de l'échec de push indisponible",
+ "054ead86b1": "Corriger l'échec de push avec l'IA",
+ "15b7f210d7": "Choisissez l'agent et modifiez la commande complète avant le lancement.",
+ "011f9713fc": "Push bloqué",
+ "60bd988f0b": "Correction IA",
+ "03d238218c": "Détails",
+ "a9bf7c171a": "Échec du push",
+ "834cb3f23d": "Corriger avec l'IA",
+ "783a808870": "Fermer"
+ },
+ "97e7124eac": "Impossible d'actualiser le contrôle de code source. Réessayez.",
+ "b8c2e1a904": "{{value0}} → {{value1}}",
+ "a4e93c21d7": "Branche actuelle : {{value0}}",
+ "c7d4e2f801": "Modifier la ref de base : {{value0}}",
+ "f3a1b8c204": "upstream",
+ "createPrIntentEmptyGeneratedBody": "Les détails de revue générés n'incluent pas de description. Réessayez Créer une PR.",
+ "createPrIntentGenerateDetailsFailed": "Impossible de générer les détails de revue. Réessayez Créer une PR."
+ },
+ "SourceControlAgentActionDialog": {
+ "8e856842d1": "Impossible de démarrer l'agent sélectionné.",
+ "c075d00de1": "Impossible de résoudre la connexion de l'espace de travail.",
+ "38b899cc02": "Tous les dépôts",
+ "808cfe0a3b": "Ce dépôt",
+ "994cddd1f7": "Ne pas enregistrer"
+ },
+ "SourceControlAgentActionDialogForm": {
+ "013c9ac04a": "Enregistrer pour",
+ "1bb611240f": "Utilisez {basePrompt} pour le prompt par défaut d'Orca.",
+ "23280cbab1": "Ce modèle n'inclut pas {basePrompt}, l'agent ne recevra donc pas le prompt par défaut d'Orca.",
+ "5421a96acb": "Enregistrer et démarrer l'agent",
+ "6cefcdfba1": "Vous pourrez le modifier plus tard dans les paramètres IA du contrôle de code source.",
+ "c29f9cf266": "Enregistrer ce prompt et ne plus afficher cette revue la prochaine fois",
+ "d8f40128ee": "{basePrompt} est le prompt par défaut d'Orca.",
+ "ea4788705e": "Annuler",
+ "7ec6abbf2a": "Réinitialiser",
+ "f4f3c9ca4a": "Modèle de prompt",
+ "1bc0bdbb5e": "Lancement :",
+ "fe119187bb": "--model sonnet",
+ "bc8dc39f4b": "Arguments CLI",
+ "b99c33cec5": "Paramètres",
+ "15c5d85706": "Agent",
+ "3e8f21954f": "error",
+ "74168d7ada": "idle",
+ "1d47db9bf0": "Aucun agent activé",
+ "c7ff8cef11": "Détection des agents...",
+ "b0da3a4d3e": "Recette de lancement déjà enregistrée",
+ "bff4795a6d": "Modifiez l'agent, les arguments ou le modèle de prompt pour mettre à jour la recette enregistrée.",
+ "5c75b24735": "Personnalisez ce que reçoit l'agent avant qu'Orca ne le démarre.",
+ "repoAgentOverrideNote": "Ce dépôt remplace votre défaut global ({{global}}) et exécute actuellement {{effective}}. Enregistrez dans ce dépôt pour changer ce qui s'exécute ici."
+ },
+ "SourceControlTextGenerationDialog": {
+ "c5b7fa7cb6": "Enregistrer comme défaut global",
+ "7f1ec309a4": "Enregistrer comme défaut pour tous les dépôts",
+ "5959da1e4d": "Enregistrer pour ce dépôt uniquement",
+ "d054d5e0a0": "Les paramètres ne sont pas chargés."
+ },
+ "SourceControlTextGenerationDialogForm": {
+ "25fcd8e49a": "Enregistrer les défauts",
+ "d91b0a189d": "Enregistrer la recette",
+ "1f6fcfb6cf": "Modèle de commande",
+ "551ffd111b": "--model sonnet",
+ "4eab815004": "Arguments CLI",
+ "914c8f6ac2": "Commande personnalisée",
+ "cce2cbd01d": "Choisir un agent",
+ "9c14186dd2": "Agent"
+ },
+ "activity": {
+ "bar": {
+ "buttons": {
+ "1fd284e931": "Autres onglets de la barre latérale",
+ "f1132ea95d": "neutre"
+ }
+ }
+ },
+ "checks": {
+ "panel": {
+ "content": {
+ "3916814392": "en retard (commit de base :",
+ "755be805f6": "Aucun commentaire",
+ "751f7c6e5c": "Affichage des 100 premiers commentaires par source",
+ "94557d68e2": "Commentaires",
+ "3fff651d32": "Ajouter un commentaire de PR",
+ "ea9fd5ed6a": "Démarrer une conversation...",
+ "0fc6f743b3": "par",
+ "8987d5a3dd": "Résolu",
+ "ba20d1a896": "Répondre à {{value0}}",
+ "f6a40263ff": "Enregistrer",
+ "b062f55f29": "Annuler",
+ "c1f6fc006a": "Répondre",
+ "2ba0a32bdd": "bot",
+ "6cc6eace26": "Supprimer",
+ "03ca88f623": "Édition",
+ "d3923d18fe": "Aller au commentaire",
+ "1abb17aac9": "Plus",
+ "74c6885b8a": "Plus d'actions de commentaire",
+ "cbcc4ab3db": "Affichage des 100 premières vérifications",
+ "0dca6bfab5": "Ouvrir les détails de la vérification",
+ "991f50c7e4": "Aucune vérification configurée",
+ "9ad98f2a17": "en attente",
+ "5e52f4ef7f": "en échec",
+ "02ca4f9074": "en succès",
+ "checksUnresolvedChip": "non résolu",
+ "checksUnresolvedStripHint": "Ces vérifications se sont terminées sans verdict de succès ni d'échec.",
+ "e15a8b77ef": "Aucun détail en ligne n'est disponible pour cette vérification.",
+ "dcb3c546fe": "Réessayer",
+ "679bf2093c": "Copier l'extrait de log",
+ "d713f500b2": "Extrait de log",
+ "a916648574": "Ouvrir les détails",
+ "07eccfa397": "Aucun détail disponible pour cette vérification.",
+ "49731703ea": "Jobs",
+ "f2fe8a4e8f": "Annotations",
+ "d098e5529a": "Sortie",
+ "2dd5ddabc4": "workflow #",
+ "aa8494ae3c": "vérification #",
+ "00e1c1658a": "Terminé",
+ "fd46a70f1a": "Démarré",
+ "a54ae21c6f": "Statut :",
+ "e4e3af15ee": "Voir tous les détails",
+ "b8c4e2a1f7": "Voir tous les logs",
+ "a2fb3f4408": "Affichage des 100 premiers jobs",
+ "df137989b3": "Affichage des 20 premières annotations",
+ "1f2b980522": "Chargement des détails de la vérification…",
+ "0c96cd25e5": "Résoudre",
+ "3a71a6ed0b": "Résolvez les conflits pour que les vérifications et le merge puissent aboutir.",
+ "60186d8498": "Des conflits bloquent ceci",
+ "c16762ac8c": "Les vérifications et commentaires ci-dessous montrent le contexte récupéré actuellement.",
+ "9d0e7bcefc": "Aucune action PR bloquante",
+ "5856874b59": "Orca actualisera les vérifications tant que ce panneau reste ouvert.",
+ "5341023167": "check",
+ "b45db92d0e": "Corriger",
+ "5d4ebf9391": "Inspectez les détails ou lancez une passe de correction IA.",
+ "b652f38caf": "vérification en échec",
+ "87cd07c69a": "Cette branche contient des conflits qui doivent être résolus",
+ "0975eeaaef": "Fichiers en conflit",
+ "6fa7f8723f": "commit",
+ "2b2be92919": "Ajouter un commentaire",
+ "7440d09d2c": "Démarrer une conversation",
+ "b37ebdc51c": "Commentaires indisponibles.",
+ "90206b6353": "comment",
+ "95ad090b01": "thread",
+ "365254cc1b": "Marquer comme non résolu",
+ "7f793b571d": "Glisser pour redimensionner les vérifications",
+ "ee07b33924": "unknown",
+ "cdbfda4dec": "Annotation",
+ "066fedd446": "Jobs en échec",
+ "ae8a04ef17": "Les détails du fichier en conflit sont indisponibles",
+ "73d0675356": "Actualisation des détails de conflit…",
+ "f5bc5c4cf1": "Le fournisseur d'hébergement signale des conflits, mais Git en local ne les a pas reproduits. Actualisez la revue ou poussez la branche pour recalculer la possibilité de merge.",
+ "5bc9bda2af": "Exécuter depuis ce worktree",
+ "e87fb3d929": "Copier les commandes de recalcul du merge",
+ "1e53e45072": "Copied",
+ "084c516efb": "Copier les commandes",
+ "5dc3af25c0": "Sélectionner le commentaire",
+ "d7a2f9c401": "Envoyer les {{value0}} commentaires non résolus",
+ "d91f2a6c39": "Envoyer les {{value0}} commentaires en file à l'IA",
+ "a6de3e5a20": "Vider les commentaires en file",
+ "49ea0937e4": "Ajouter le commentaire à la liste de résolution",
+ "9fecebb29d": "Ajouter",
+ "f8a2c91d04": "Mettre en file pour l'agent",
+ "b4e8a1c902": "En file d'attente",
+ "7c1f0a2b11": "Ouvert",
+ "e8b4c1a903": "Résolu · {{value0}}",
+ "c3a8e5d710": "À examiner · {{value0}}",
+ "a7f0c7e8d1": "File",
+ "8a621a2c4f": "Groupés",
+ "b13f85d75c": "Chronologie",
+ "f5cf324efa": "Options d'affichage des commentaires",
+ "5e6e5a13fa": "Affichage",
+ "actionRequiredHint": "Cette vérification nécessite une action manuelle sur GitHub (par exemple, approuver l'exécution du workflow) avant que le merge soit débloqué.",
+ "b3195cba33": "Retirer le marqueur bot de l'auteur",
+ "f588b46a6c": "Marquer l'auteur comme bot",
+ "e45324fbed": "Échec du chargement des détails de la vérification."
+ },
+ "empty": {
+ "state": {
+ "3322603418": "Statut de la pull request indisponible",
+ "5b0cfae9a5": "Créez une {{value0}} pour lancer les vérifications et la revue.",
+ "13e1c7d5ed": "Aucune {{value0}} trouvée",
+ "d372072df1": "L'actualisation GitHub est suspendue par le budget de rate-limit actuel",
+ "7c299df37b": "Aucune pull request trouvée",
+ "3d4af82ff4": "Actualisation du statut GitHub pour cette branche",
+ "938b5606a6": "Recherche de pull request",
+ "6ba2440770": "En attente de l'actualisation du statut GitHub pour cette branche",
+ "2bdd7aaf2d": "Le statut GitHub n'a pas pu être actualisé. Les données en cache ont été conservées.",
+ "5f478ab3d3": "Impossible d'actualiser la pull request",
+ "6ce9d4e069": "Poussez votre branche avant de créer une {{value0}}.",
+ "76e15946a9": "La branche contient des commits non poussés",
+ "f8543140cc": "Publiez cette branche avant de créer une {{value0}}.",
+ "41252bc53f": "Branche non publiée",
+ "05e4aec17b": "Les vérifications {{value0}} seront disponibles une fois l'opération terminée",
+ "d77c513c1e": "{{value0}} en cours",
+ "b597440265": "Actualisez le statut GitHub de cette branche pour charger les vérifications et la revue."
+ }
+ },
+ "review": {
+ "detail": {
+ "positive": "Orca dispose également d'informations {{reviewLabel}} enregistrées qu'il n'a pas pu vérifier.",
+ "rate_limited": "Orca n'a pas non plus pu vérifier le statut de la {{reviewLabel}} car {{provider}} limite temporairement les requêtes.",
+ "network": "Orca n'a pas non plus pu vérifier le statut de la {{reviewLabel}} car cet environnement n'a pas pu joindre {{provider}}.",
+ "untyped": "Orca n'a pas non plus pu confirmer si cette branche possède déjà une {{reviewLabel}}."
+ },
+ "positive": {
+ "title": "Détails de la {{reviewLabelCap}} indisponibles",
+ "body": "Orca possède des informations {{reviewLabel}} enregistrées pour cette branche, mais n'a pas pu confirmer son statut actuel."
+ },
+ "no_review": {
+ "title": "Aucune {{reviewLabel}} trouvée",
+ "body": "Créez une {{reviewLabel}} pour lancer les vérifications et la revue."
+ },
+ "active": {
+ "title": "Vérification du statut de la {{reviewLabel}}",
+ "body": "Orca interroge {{provider}} pour trouver une {{reviewLabel}} sur cette branche."
+ },
+ "git_loading": {
+ "title": "Vérification du statut de la branche",
+ "body": "Orca vérifie cette branche avant d'afficher les actions de création ou de publication."
+ },
+ "git_error": {
+ "title": "Impossible de vérifier le statut de la branche",
+ "body": "Orca n'a pas pu confirmer l'upstream de cette branche depuis cet environnement. Réessayez avant de publier ou de créer une {{reviewLabel}}."
+ },
+ "unknown": {
+ "title": "Statut de la {{reviewLabelCap}} indisponible",
+ "body": "Orca n'a pas confirmé le statut de la {{reviewLabel}} pour cette branche. Réessayez pour revérifier."
+ },
+ "paused": {
+ "title": "Actualisation {{provider}} suspendue",
+ "body": "{{provider}} limite temporairement les requêtes. Cela peut se produire même quand le quota API affiché n'est pas épuisé."
+ },
+ "network": {
+ "title": "Impossible de joindre {{provider}}",
+ "body": "Cet environnement n'a pas pu joindre {{provider}}. Vérifiez sa connexion, puis réessayez."
+ },
+ "unknown_error": {
+ "title": "Impossible de vérifier le statut de la {{reviewLabel}}",
+ "body": "La recherche a échoué : Orca n'a pas pu confirmer si cette branche possède déjà une {{reviewLabel}}."
+ },
+ "untyped": {
+ "title": "Statut de la {{reviewLabelCap}} indisponible",
+ "body": "Orca n'a pas pu confirmer si cette branche possède déjà une {{reviewLabel}}. Réessayez pour revérifier."
+ },
+ "auth": {
+ "title": "Échec de l'authentification {{provider}}",
+ "body": "{{provider}} n'a pas pu authentifier les identifiants disponibles dans cet environnement. Vérifiez le login {{provider}} ou le token d'environnement, puis réessayez."
+ },
+ "permission": {
+ "title": "Accès {{provider}} refusé",
+ "body": "Les identifiants {{provider}} actuels ne permettent pas de lire les {{reviewLabel}}s de ce dépôt. Vérifiez le compte, les scopes du token et l'accès au dépôt, puis réessayez."
+ },
+ "repo": {
+ "title": "Dépôt {{provider}} indisponible",
+ "body": "{{provider}} n'a pas pu résoudre ni accéder au dépôt pour le remote et le compte actuels. Vérifiez le remote et l'accès au dépôt, puis réessayez."
+ },
+ "cli": {
+ "title": "CLI {{provider}} indisponible",
+ "body": "Orca n'a pas pu exécuter la CLI {{provider}} dans cet environnement. Configurez-la ici, puis réessayez."
+ },
+ "skipped": {
+ "disconnected": {
+ "title": "Hôte déconnecté",
+ "body": "L'hôte d'exécution de ce dépôt est déconnecté ; Orca ne peut donc pas actualiser le statut de la {{reviewLabel}}."
+ },
+ "bare": {
+ "title": "Dépôt bare",
+ "body": "Ce dépôt est bare ; le statut de la {{reviewLabel}} n'est donc pas disponible ici."
+ },
+ "archived": {
+ "title": "Dépôt archivé",
+ "body": "Ce dépôt est archivé ; Orca n'actualise donc pas le statut de la {{reviewLabel}}."
+ },
+ "not_git": {
+ "title": "N'est pas un dépôt Git",
+ "body": "Orca n'a pas pu traiter ce dossier comme un dépôt Git pour le statut de {{reviewLabel}}."
+ },
+ "remote": {
+ "title": "Contexte distant uniquement",
+ "body": "Orca n'a pas pu actualiser le statut de {{reviewLabel}} pour ce contexte distant. Réessayez une fois l'hôte disponible."
+ }
+ },
+ "no_upstream": {
+ "title": "Aucun upstream configuré",
+ "body": "Publiez cette branche pour définir son upstream avant de créer une {{reviewLabel}}."
+ },
+ "needs_sync": {
+ "title": "La branche doit être synchronisée",
+ "body": "Synchronisez cette branche avec son upstream avant de créer une {{reviewLabel}}."
+ },
+ "auth_required": {
+ "title": "Connecter {{provider}}",
+ "body": "{{provider}} doit être connecté dans cet environnement avant qu'Orca puisse créer une {{reviewLabel}}."
+ },
+ "needs_push": {
+ "title": "La branche contient des commits non poussés",
+ "body": "Poussez les derniers commits avant de créer une {{reviewLabel}}."
+ },
+ "existing": {
+ "title": "{{reviewLabelCap}} existe déjà",
+ "body": "Orca a trouvé une {{reviewLabel}} existante pour cette branche."
+ },
+ "detached": {
+ "title": "Aucune branche courante",
+ "body": "Effectuez un checkout d'une branche avant de créer une {{reviewLabel}}."
+ },
+ "dirty": {
+ "title": "Commitez d'abord vos modifications",
+ "body": "Commitez ou stashez vos modifications avant de créer une {{reviewLabel}}."
+ },
+ "default_branch": {
+ "title": "Sur la branche par défaut",
+ "body": "Passez sur une branche de fonctionnalité avant de créer une {{reviewLabel}}."
+ },
+ "fork": {
+ "title": "Tête de fork non prise en charge",
+ "body": "Orca ne peut pas créer une {{reviewLabel}} à partir de cette tête de fork ici."
+ },
+ "base_missing": {
+ "title": "Branche de base absente du remote",
+ "body": "La base de cette branche n'est pas encore sur le remote ; une {{reviewLabel}} ne peut donc pas la cibler."
+ },
+ "unsupported": {
+ "title": "{{reviewLabelCap}} non prise en charge ici",
+ "body": "Ce fournisseur de dépôt ne prend pas en charge la création d'une {{reviewLabel}} depuis Orca."
+ }
+ }
+ }
+ },
+ "gitlab": {
+ "mr": {
+ "merge": {
+ "state": {
+ "04a3015a12": "Peut être fusionné",
+ "53c6d3b7e9": "GitLab indique que cette MR peut être fusionnée, mais le pipeline est toujours en cours",
+ "65c847ad1e": "Vérifications en attente",
+ "b41fbc180c": "GitLab indique que cette MR peut être fusionnée, mais certains jobs du pipeline ont échoué",
+ "49ac4fec10": "Vérifications échouées",
+ "22b7e50621": "GitLab signale des conflits de fusion",
+ "96b05e374c": "Conflits",
+ "d63bb6f76e": "Cette demande de fusion est encore au stade de brouillon",
+ "b2715092c6": "Brouillon",
+ "2388413f28": "Cette demande de fusion est fermée",
+ "88d044c42f": "Fermé",
+ "ee482a2bad": "Cette demande de fusion est déjà fusionnée",
+ "fae95ae20d": "Fusionnés",
+ "5105b0e584": "Approbation requise",
+ "46dc85711b": "GitLab exige une approbation avant que cette MR puisse être fusionnée",
+ "893a999c8c": "Modifications demandées",
+ "c65408a99d": "Un relecteur a demandé des modifications sur cette demande de fusion",
+ "01ab632a21": "Fils non résolus",
+ "4191fdfcec": "GitLab exige que les discussions non résolues soient résolues avant la fusion",
+ "bf628700f6": "Les vérifications doivent passer",
+ "37d8637c70": "GitLab exige que le pipeline réussisse avant que cette MR puisse être fusionnée",
+ "049ea91a82": "Le pipeline est toujours en cours",
+ "382c70faeb": "En retard",
+ "2c61100b3a": "Mettez à jour la branche avant de merger",
+ "195917574e": "Vérification",
+ "ff6db2db63": "GitLab calcule encore le statut de cette demande de fusion",
+ "4ecc0d90ee": "Bloquée",
+ "a0f3de7023": "GitLab signale que cette demande de fusion est bloquée",
+ "804ecf93e8": "GitLab n'a pas communiqué de statut de fusion final",
+ "a5c049afe4": "GitLab indique que cette MR peut être fusionnée"
+ }
+ }
+ }
+ },
+ "index": {
+ "70893f017b": "Côté",
+ "7b415c39e9": "Haut",
+ "864111caa2": "Position de la barre d'activité",
+ "e8e2e4ce74": "Basculer la barre latérale droite",
+ "441733b630": "Ports",
+ "83a10e3c44": "Vérifications",
+ "0314901467": "Contrôle de code source",
+ "06219e4cb1": "Recherche",
+ "8bc2bbc3a0": "Explorateur",
+ "45b78f03bc": "côté",
+ "34af8aadf5": "haut",
+ "9fffaf17c1": "Basculer la barre latérale droite ({{value0}})",
+ "b37ff4a89a": "ports",
+ "9f83375839": "checks",
+ "6306b48afd": "source-control",
+ "ef182dcb12": "recherche",
+ "fc3095d2ed": "explorateur",
+ "aiVaultSessionHistory": "Agents",
+ "folderWorkspaces": "Worktrees attachés",
+ "parentPrChecks": "Vérifications de la PR"
+ },
+ "right": {
+ "panel": {
+ "comment": {
+ "composer": {
+ "9bca633dee": "Annuler",
+ "cf5a7aba6f": "Liste",
+ "d6d9c3c947": "Citation",
+ "f49e0a21e0": "Code",
+ "542bf6a7e2": "Italique",
+ "256300f8ea": "Gras",
+ "87aff03d63": "Envoi…",
+ "commentTooLarge": "Le commentaire est trop volumineux pour être envoyé sans risque."
+ }
+ }
+ }
+ },
+ "source": {
+ "control": {
+ "ai": {
+ "commit": {
+ "failure": {
+ "launch": {
+ "a8b97d2318": "Un agent IA a été lancé pour l'échec du commit.",
+ "5540ff50cc": "Impossible de construire la commande de lancement de l'agent.",
+ "9bbd9077a2": "Aucun agent IA activé. Configurez les agents dans les paramètres.",
+ "d481ab22f9": "L'agent IA enregistré est indisponible. Utilisez « Personnaliser le lancement » pour choisir un autre agent.",
+ "f2b47026e8": "Le prompt d'échec de commit est vide. Mettez à jour les paramètres IA du contrôle de code source.",
+ "4f4e0418a0": "Impossible de construire le prompt de l'agent.",
+ "216f762bd7": "Impossible de résoudre la connexion de l'espace de travail."
+ }
+ }
+ },
+ "push": {
+ "failure": {
+ "launch": {
+ "216f762bd7": "Impossible de résoudre la connexion de l'espace de travail.",
+ "4f4e0418a0": "Impossible de construire le prompt de l'agent.",
+ "f2b47026e8": "Le prompt d'échec de push est vide. Mettez à jour les paramètres IA du contrôle de code source.",
+ "d481ab22f9": "L'agent IA enregistré est indisponible. Utilisez « Personnaliser le lancement » pour choisir un autre agent.",
+ "9bbd9077a2": "Aucun agent IA activé. Configurez les agents dans les paramètres.",
+ "5540ff50cc": "Impossible de construire la commande de lancement de l'agent.",
+ "a8b97d2318": "Un agent IA a été lancé pour l'échec du push."
+ }
+ }
+ },
+ "recovery": {
+ "launch": {
+ "4f4e0418a0": "Impossible de construire le prompt de l'agent.",
+ "push": {
+ "empty": "Le prompt d'échec de push est vide. Mettez à jour les paramètres IA du contrôle de code source."
+ },
+ "commit": {
+ "empty": "Le prompt d'échec de commit est vide. Mettez à jour les paramètres IA du contrôle de code source."
+ },
+ "d481ab22f9": "L'agent IA enregistré est indisponible. Utilisez « Personnaliser le lancement » pour choisir un autre agent.",
+ "9bbd9077a2": "Aucun agent IA activé. Configurez les agents dans les paramètres.",
+ "5540ff50cc": "Impossible de construire la commande de lancement de l'agent.",
+ "216f762bd7": "Impossible de résoudre la connexion de l'espace de travail.",
+ "success": "Un agent IA a été lancé pour l'échec de {{value0}}."
+ }
+ }
+ },
+ "discard": {
+ "confirmation": {
+ "2ae5a785b3": "Abandonner toutes les modifications non indexées ?",
+ "ddf36f291c": "Cela désindexera et annulera toutes les modifications indexées. Les nouveaux fichiers indexés seront supprimés. Cette action est irréversible.",
+ "5ddd8cac7f": "Abandonner toutes les modifications indexées ?",
+ "1426c2efff": "Cela annulera toutes les modifications de ce fichier. Cette action est irréversible.",
+ "d4df3a61df": "Abandonner les modifications de « {{value0}} » ?",
+ "40e9357b2a": "Cela restaurera le fichier depuis HEAD et annulera la suppression. Cette action est irréversible.",
+ "5c0bdbc4cb": "Restaurer « {{value0}} » ?",
+ "d97bf697c9": "Cela supprimera définitivement ce fichier. Cette action est irréversible.",
+ "96c772bee9": "Supprimer « {{value0}} » ?"
+ },
+ "dialog": {
+ "3bc61dc989": "Annuler",
+ "15efa778e3": "Abandonner",
+ "6de99d162b": "entrée",
+ "42f89dd030": "fichiers",
+ "e7611dca35": "fichier",
+ "48c5ef95d9": "zone",
+ "0d2d88cba5": "Cette action est irréversible.",
+ "1551c14668": "Abandonner les modifications ?"
+ }
+ },
+ "dropdown": {
+ "items": {
+ "7aad2c0240": "Opération de revue hébergée en cours…",
+ "9e779995dd": "Créer {{value0}}",
+ "226b85a3a7": "Fetch",
+ "323bb614aa": "Commit & Sync",
+ "2b8e6595fd": "Commit",
+ "04d709801d": "Fetch depuis le remote sans fusionner"
+ }
+ },
+ "primary": {
+ "action": {
+ "ed93b4f14f": "Commit",
+ "946a8a05ea": "Créer une {{value0}} pour cette branche",
+ "e7ffa46946": "Créer {{value0}}",
+ "95550cff15": "Push",
+ "d64292a938": "Pull",
+ "795f1509c5": "Synchroniser",
+ "390abeab93": "Force Push",
+ "1884cf34af": "Publier cette branche vers origin",
+ "7b4d02e6b8": "Publier la branche",
+ "3d5dccef0b": "Rien à commiter. La PR est déjà fusionnée.",
+ "41d4bcf157": "Vérification du statut de la PR…",
+ "acce237921": "Rien à commiter. La branche n'a aucune modification à publier.",
+ "fa3bd4f40c": "Indexez au moins un fichier pour commiter",
+ "5a477d80cb": "Indexer toutes les modifications",
+ "18a0fca877": "Tout indexer",
+ "f01f16d77f": "Saisissez un message de commit pour commiter",
+ "ab41fb926b": "Committer les modifications indexées",
+ "2d8f185fbc": "Indexez toutes les modifications avant de commiter des fichiers partiellement indexés",
+ "a6457b46a7": "Résolvez les conflits avant de commiter",
+ "484f45c439": "{{value0}} en cours…",
+ "6f7a8b9c0d": "Opération distante en cours…",
+ "7f8a9b0c1d": "Opération distante en cours — réessayez une fois celle-ci terminée",
+ "74fc171e99": "Force Push en cours…",
+ "16aee3a5c1": "Commit en cours…",
+ "e61b0d7a3c": "Effectuez un checkout d'une branche avant de publier des commits.",
+ "1d47e850cf": "Poussez les mises à jour vers la branche de revue liée",
+ "c39d0c75c3": "La cible de la branche de revue liée est indisponible.",
+ "8c6d15a07d": "Créer une PR",
+ "d37e68f61d": "Préparation de la branche pour la revue…",
+ "c72e5e65d1": "Préparez cette branche et créez une {{value0}}",
+ "b8e4f2a901": "Attendez la fin de l'opération distante.",
+ "c9f3a1b802": "Résolvez les conflits avant de créer une {{value0}}.",
+ "d2a8c4e703": "Aucune modification sur cette branche à inclure dans une {{value0}}.",
+ "e3b9d5f814": "Impossible de créer une {{value0}} depuis la branche par défaut.",
+ "f4c0e6a925": "Commitez vos modifications avant de créer une {{value0}}.",
+ "a5d1f7b036": "Publiez les commits avant de créer une {{value0}}.",
+ "b6e2a8c147": "Poussez les commits avant de créer une {{value0}}.",
+ "c7f3b9d258": "Synchronisez cette branche avant de créer une {{value0}}.",
+ "d8a4c0e369": "Authentifiez-vous avant de créer une {{value0}}.",
+ "e9b5d1f470": "Effectuez un checkout d'une branche avant de créer une {{value0}}.",
+ "f0c6e2a581": "Cette branche n'est pas encore prête pour une {{value0}}.",
+ "8f9a0b1c2d": "Rien à commiter. La branche est à jour.",
+ "h3i4j5k607": "Vérification de la possibilité de créer une {{value0}} pour cette branche…"
+ }
+ },
+ "branch": {
+ "line": {
+ "total": {
+ "chip": {
+ "daa8e8e59b": "{{value0}} lignes ajoutées, {{value1}} lignes supprimées",
+ "8a9b97b666": "{{value0}} lignes ajoutées",
+ "52c366d88d": "{{value0}} lignes supprimées",
+ "4c1f70ba92": "{{value0}} — code de test : {{value1}} lignes ajoutées, {{value2}} lignes supprimées",
+ "6b2d0f14a7": "Tests",
+ "9e4a3c5081": "Hors tests",
+ "3d7e9b1042": "Non généré",
+ "a1b2c3d4e5": "Répartition du code",
+ "b2c3d4e5f6": "Lignes de code",
+ "7f3e1a9c24": "{{value0}} — généré : {{value1}} lignes ajoutées, {{value2}} lignes supprimées",
+ "c8d5b21e07": "Source",
+ "7a04c6f8b3": "Généré"
+ }
+ }
+ }
+ },
+ "compare": {
+ "summary": {
+ "dd72a6fd37": "{{value0}} commit{{value1}} d'avance sur {{value2}}"
+ }
+ },
+ "conflict": {
+ "status": {
+ "cards": {
+ "5302a1ddba": "Conflits de fusion",
+ "7f3af87549": "Conflits de rebase",
+ "6a8e9ad490": "Conflits de cherry-pick",
+ "bdf8772106": "Conflits",
+ "edc2d82a2b": "Fusion en cours",
+ "5c3707aa44": "Rebase en cours",
+ "ffe53a1da6": "Cherry-pick en cours",
+ "35eb76d323": "Opération en cours"
+ }
+ }
+ },
+ "content": {
+ "status": {
+ "3f425c239c": "Aucune modification sur cette branche",
+ "640f6fdb36": "Cet espace de travail est propre et cette branche n'a aucune modification en avance sur {{value0}}",
+ "8deb86bbec": "base",
+ "978eba351e": "Le texte de recherche est trop volumineux",
+ "0bce43409f": "Utilisez un filtre de fichiers plus court.",
+ "1b6caf533d": "Aucun fichier correspondant",
+ "00c07771b7": "Aucun fichier modifié ne correspond à « {{value0}} »"
+ }
+ },
+ "diff": {
+ "comments": {
+ "list": {
+ "cefacd0ec7": "fichier entier"
+ }
+ }
+ },
+ "commit": {
+ "area": {
+ "cc5739bd1d": "Génération du message de commit…",
+ "4cbd0cd9d2": "Commit en cours…",
+ "e7876a2bde": "Indexez au moins un fichier pour générer un message.",
+ "0904be2505": "Effacez le message pour le régénérer.",
+ "1ea9ba37aa": "Choisissez un agent dans Paramètres -> Git -> IA du contrôle de code source."
+ }
+ }
+ }
+ },
+ "use": {
+ "source": {
+ "control": {
+ "ai": {
+ "cfafa92509": "Aucun conflit non résolu à envoyer."
+ },
+ "bulk": {
+ "actions": {
+ "2f67630884": "Échec de l'indexation/désindexation groupée"
+ }
+ }
+ }
+ }
+ },
+ "fileExplorerOperationOwner": {
+ "unresolved": "Impossible de déterminer quel hôte possède cet espace de travail. Vérifiez la connexion et réessayez."
+ },
+ "useFileDeletion": {
+ "72691dfebc": "Échec : {{value0}} « {{value1}} ».",
+ "96affe1302": "« {{value0}} » déplacé vers {{value1}}",
+ "74727df633": "« {{value0}} » supprimé",
+ "d979a4fbb5": "Supprimer définitivement « {{value0}} » ?",
+ "a76c74f105": "destructive",
+ "92276aceb7": "Supprimer",
+ "7fb9435c86": "Cette action supprime définitivement le répertoire et son contenu sur l'hôte distant. Elle est irréversible.",
+ "23e98f192f": "Cette action supprime définitivement le fichier sur l'hôte distant. Elle est irréversible.",
+ "8b8ee9d22f": "Impossible de déterminer quel hôte possède ce fichier. Vérifiez la connexion de l'espace de travail et réessayez.",
+ "af1270b90d": "Supprimer définitivement {{count}} éléments ?",
+ "dd029aa5cd": "Cette action supprime définitivement les éléments sélectionnés ainsi que le contenu des répertoires sur l'hôte distant. Elle est irréversible.",
+ "77fdc36183": "Supprimer {{count}} éléments ?",
+ "fca915a67a": "Les éléments distants sont définitivement supprimés, sans retour arrière. Les éléments locaux sont déplacés dans {{value0}}."
+ },
+ "useFileExplorerHandlers": {
+ "32cd9fd991": "Impossible d'ouvrir la cible du symlink"
+ },
+ "useFileExplorerImport": {
+ "25919b2050": "Ignoré : {{value0}} {{value1}}.",
+ "132fd0e1e9": "Échec de l'importation de {{value0}} {{value1}}."
+ },
+ "useFileExplorerKeys": {
+ "8adb953095": "Échec de l'opération"
+ },
+ "GitHistoryGraphSvg": {
+ "47eff48230": "HEAD"
+ },
+ "create": {
+ "pull": {
+ "request": {
+ "review": {
+ "copy": {
+ "a1f8c3d2e4": "Push réussi, mais la création de {{value0}} a échoué : {{value1}}"
+ }
+ }
+ }
+ },
+ "hosted": {
+ "review": {
+ "button": {
+ "label": {
+ "96ae7358e0": "Push et création de PR dans la stack",
+ "8e8149a0bf": "Créer une PR brouillon dans la stack",
+ "8df1a05952": "Créer une PR dans la stack"
+ }
+ }
+ }
+ }
+ },
+ "AiVaultPanel": {
+ "resumeCommandCopied": "Commande de reprise copiée",
+ "valueCopied": "{{value0}} copié",
+ "valueCopyFailed": "Impossible de copier {{value0}}",
+ "openWorkspaceBeforeResuming": "Ouvrez un espace de travail avant de reprendre une session.",
+ "localWorkspacesOnly": "La reprise depuis l'historique n'est disponible que dans les espaces de travail locaux.",
+ "agentSessionQueued": "Session {{value0}} en file d'attente",
+ "sessionHistory": "Historique des sessions d'agent",
+ "agents": "Agents",
+ "shownRecent": "{{value0}} affichées · {{value1}} récentes",
+ "sessionsShownCompact": "{{value0}} affichées",
+ "resumePastSessions": "Reprendre les sessions passées",
+ "refreshSessionHistory": "Actualiser l'historique des sessions",
+ "searchSessions": "Rechercher des sessions",
+ "clearSearch": "Effacer la recherche",
+ "remoteBrowseLocalHistory": "Les espaces de travail distants peuvent parcourir l'historique local. Les actions de reprise s'exécutent depuis les espaces de travail locaux.",
+ "transcriptsSkipped": "{{count}} transcript ignoré",
+ "noAgentSessionsFound": "Aucune session d'agent trouvée",
+ "noSessionsMatchFilters": "Aucune session ne correspond aux filtres actuels",
+ "noAgentsSelected": "Aucun agent sélectionné",
+ "sessionId": "ID de session",
+ "logPath": "Chemin du log",
+ "originalPaneUnavailable": "Le volet d'origine n'est plus disponible.",
+ "worktreeUnavailable": "Le worktree n'est plus disponible.",
+ "openSupportedWorkspace": "Ouvrez un espace de travail avant de reprendre une session.",
+ "sessionHostMismatchUnsupported": "Cette session appartient à un autre hôte. Ouvrez un espace de travail sur le même hôte pour la reprendre.",
+ "localSessionSshWorkspaceUnsupported": "L'historique de cette session est stocké sur cette machine ; impossible de la reprendre dans un espace de travail SSH. Ouvrez plutôt un espace de travail local.",
+ "prepareSessionResumeFailed": "Impossible de préparer cette session pour la reprise.",
+ "sessionDeleted": "Session supprimée",
+ "sessionDeleteFailed": "Impossible de supprimer la session"
+ },
+ "AiVaultPanelControls": {
+ "scanningSessions": "Analyse des sessions",
+ "scopeAriaLabel": "Portée de l'historique des sessions : {{value0}}",
+ "currentWorkspaceLower": "espace de travail actuel",
+ "currentWorktreeLower": "worktree actuel",
+ "allSessionsLower": "toutes les sessions",
+ "thisScope": "Actuel",
+ "allScope": "Tous",
+ "scope": "Portée",
+ "currentWorkspace": "Espace de travail actuel",
+ "allSessions": "Toutes les sessions",
+ "viewOptionsAriaLabel": "Options d'affichage de l'historique des sessions",
+ "viewOptions": "Options d'affichage",
+ "agents": "Agents",
+ "selectAllAgents": "Tout sélectionner",
+ "clearAgents": "Effacer",
+ "sort": "Trier",
+ "lastUpdated": "Dernière mise à jour",
+ "created": "Créé",
+ "group": "Regroupement",
+ "folder": "Dossier",
+ "agent": "Agent",
+ "resetView": "Réinitialiser la vue",
+ "hideEmptySessions": "Masquer les sessions vides",
+ "workspaceScope": "Espace de travail",
+ "worktreeScope": "Worktree",
+ "globalScope": "Global",
+ "projectScope": "Projet",
+ "currentProjectLower": "projet actuel",
+ "project": "Projet",
+ "hostScopeAriaLabel": "Hôte de l'historique des sessions : {{value0}}",
+ "host": "Hôte"
+ },
+ "AiVaultSessionDetails": {
+ "originalAsk": "Demande initiale",
+ "latestTurns": "Derniers échanges",
+ "noPreviewAvailable": "Aucun aperçu de conversation disponible",
+ "conversationNotSaved": "Conversation non enregistrée",
+ "recoverableEmptyDetail": "Cette session n'a pas de conversation enregistrée, mais {{value0}} élément(s) récupérable(s) subsistent.",
+ "recoverableEmptyOpenLogHint": "Ouvrez le log pour les récupérer.",
+ "emptyConversationDetail": "Cette session n'a pas de conversation enregistrée et ne peut pas être reprise.",
+ "queuedMessages": "{{value0}} message(s) en file d'attente",
+ "subagentTranscripts": "{{value0}} transcript(s) de sous-agent",
+ "messageCount": "{{value0}} msg",
+ "updated": "Mis à jour",
+ "created": "Créé",
+ "workingDir": "Répertoire de travail",
+ "unknownLocation": "Emplacement inconnu",
+ "branch": "Branche",
+ "model": "Modèle",
+ "usage": "Utilisation",
+ "usageValue": "{{value0}} msg{{value1}}",
+ "tokenSuffix": " · {{value0}} tok",
+ "session": "Session",
+ "sessionId": "ID de session",
+ "copyDetailValue": "Copier {{value0}}",
+ "latestLog": "Dernier log",
+ "noReadablePreview": "Aucun aperçu de message lisible dans ce transcript.",
+ "resumeCommand": "Commande de reprise",
+ "sessionActions": "Actions de la session {{value0}}",
+ "resumeInNewTab": "Reprendre dans un nouvel onglet",
+ "resumeInWorktree": "Reprendre dans le worktree",
+ "viewLog": "Afficher le log",
+ "copyResumeCommand": "Copier la commande de reprise",
+ "openLog": "Ouvrir le log",
+ "revealLog": "Révéler le log",
+ "openWorkingDirectory": "Ouvrir le répertoire de travail",
+ "copySessionId": "Copier l'ID de session",
+ "copyLogPath": "Copier le chemin du log",
+ "unknownTime": "Heure inconnue",
+ "unknown": "Inconnu",
+ "user": "Utilisateur",
+ "assistant": "Assistant",
+ "tool": "Outil",
+ "system": "Système",
+ "log": "Log",
+ "justNow": "À l'instant",
+ "minutesAgo": "il y a {{value0}} min",
+ "hoursAgo": "il y a {{value0}} h",
+ "daysAgo": "il y a {{value0}} j",
+ "monthsAgo": "il y a {{value0}} mois",
+ "yearsAgo": "il y a {{value0}} an(s)",
+ "userRole": "Vous",
+ "agentRole": "Agent",
+ "toolRole": "Outil",
+ "systemRole": "Système",
+ "sessionRole": "Session",
+ "jumpToOriginalPane": "Aller au volet d'origine",
+ "worktree": "Worktree",
+ "jumpToWorktree": "Aller au worktree",
+ "prompt": "Prompt",
+ "firstPrompt": "Premier prompt",
+ "recentPrompt": "Prompt récent",
+ "firstPromptCopied": "Premier prompt copié",
+ "recentPromptCopied": "Prompt récent copié",
+ "copyFirstPrompt": "Copier le premier prompt",
+ "copyRecentPrompt": "Copier le prompt récent",
+ "copyPrompt": "Copier le prompt",
+ "copied": "Copied",
+ "copy": "Copier",
+ "noFirstPromptAvailable": "Aucun premier prompt disponible",
+ "loadingFirstPrompt": "Chargement du premier prompt…"
+ },
+ "AiVaultSessionRow": {
+ "noPreviewAvailable": "Aucun aperçu de conversation disponible",
+ "recoverableBadge": "Non enregistré",
+ "dragToResume": "Glissez pour reprendre dans un nouvel onglet",
+ "resumeAgentSession": "Reprendre la session {{value0}}",
+ "resumeInNewTab": "Reprendre dans un nouvel onglet",
+ "toggleSessionDetails": "Détails de la session {{value0}}",
+ "copyResumeCommand": "Copier la commande de reprise",
+ "openLog": "Ouvrir le log",
+ "revealLog": "Révéler le log",
+ "openWorkingDirectory": "Ouvrir le répertoire de travail",
+ "copySessionId": "Copier l'ID de session",
+ "copyLogPath": "Copier le chemin du log",
+ "messageCount": "{{value0}} msg",
+ "tokenCount": "{{value0}} tok",
+ "hideDetails": "Masquer les détails",
+ "showDetails": "Afficher les détails",
+ "moreSessionActions": "Autres actions de session",
+ "moreActions": "Plus d'actions",
+ "userRole": "Vous",
+ "agentRole": "Agent",
+ "toolRole": "Outil",
+ "systemRole": "Système",
+ "sessionRole": "Session",
+ "jumpToOriginalPane": "Aller au volet d'origine",
+ "jumpToWorktree": "Aller au worktree",
+ "subagentCountSingular": "1 sous-agent",
+ "subagentCountPlural": "{{value0}} sous-agents",
+ "delete": "Supprimer",
+ "deleteReasonNonLocalHost": "Seules les sessions de cet appareil peuvent être supprimées.",
+ "deleteReasonSyntheticPath": "Cette session ne peut pas être supprimée depuis Orca.",
+ "deleteReasonUnsupportedAgent": "{{value0}} sessions ne peuvent pas être supprimées depuis Orca."
+ },
+ "AiVaultSessionDeleteDialog": {
+ "title": "Supprimer cette session ?",
+ "description": "« {{value0}} » sera supprimée. Une fois supprimée, elle ne pourra plus être reprise depuis la ligne de commande de {{value1}} non plus.",
+ "confirm": "Supprimer"
+ },
+ "FileExplorerNameFilter": {
+ "26fb73c6e3": "Rechercher des fichiers",
+ "4d5a6b2a49": "Effacer le filtre de fichiers",
+ "7a9fb1e6aa": "Contenu"
+ },
+ "FileExplorerViewSwitch": {
+ "c4e9a2b713": "Noms",
+ "b3c8f1a902": "Filtrer les fichiers par nom",
+ "f8a2c4d1e0": "Mode de recherche de l'explorateur"
+ },
+ "GitHistoryCommitFiles": {
+ "a1b2c3d4e5": "Chargement des fichiers…",
+ "b2c3d4e5f6": "Aucune modification de fichier dans ce commit",
+ "c3d4e5f6a7": "Ouvrir toutes les modifications ensemble"
+ },
+ "GitHistoryRow": {
+ "2f9c41ab07": "Afficher les fichiers du commit {{value0}} : {{value1}}",
+ "4a8d9e0c1f": "Masquer les fichiers du commit {{value0}} : {{value1}}"
+ },
+ "GitHistoryCommitContextMenu": {
+ "7b1c4e9a02": "Ouvrir le commit dans le navigateur",
+ "8c2d5fab13": "Copier le hash du commit",
+ "9d3e60bc24": "Copier le message de commit",
+ "ae4f71cd35": "Expliquer les modifications"
+ },
+ "pull": {
+ "policy": {
+ "notice": {
+ "merge": "Merge",
+ "mergeDescription": "Crée un commit de fusion quand il y a des changements à la fois en local et sur le remote.",
+ "rebase": "Rebase",
+ "rebaseDescription": "Rejoue les commits locaux au-dessus de la branche distante.",
+ "fastForwardOnly": "Fast-forward uniquement",
+ "fastForwardOnlyDescription": "Ne fait un pull que lorsqu'aucune fusion ni rebase n'est nécessaire.",
+ "title": "Le pull nécessite une stratégie",
+ "diverged": "Divergée",
+ "body": "Cette branche contient des commits locaux et distants. Exécutez une commande dans ce worktree ou sur l'hôte SSH, puis réessayez Pull ou Sync.",
+ "copyAria": "Copier la commande de pull pour la stratégie {{value0}}",
+ "copied": "Copied",
+ "copyCommand": "Copier la commande"
+ }
+ }
+ },
+ "AiVaultSessionWorktree": {
+ "currentWorktree": "Worktree actuel",
+ "activeWorktree": "Worktree actif",
+ "archivedWorktree": "Worktree archivé",
+ "unavailableWorktree": "Worktree indisponible",
+ "jumpToWorktree": "Aller au worktree",
+ "noRecordedWorktree": "Aucun worktree n'a été enregistré pour cette session.",
+ "archivedJumpUnavailable": "Cette session se trouve dans un worktree archivé.",
+ "noActiveWorktreeMatch": "Aucun worktree actif ne correspond à cette session.",
+ "noActiveWorktreeTarget": "Aucun worktree actif n'est disponible."
+ },
+ "AiVaultSessionSubagents": {
+ "subagentsCount": "Sous-agents ({{value0}})",
+ "messageCount": "{{value0}} msg",
+ "viewLog": "Afficher le log"
+ },
+ "aiVaultSessionLogOpen": {
+ "workspaceGone": "Impossible d'ouvrir le log — l'espace de travail n'est plus disponible.",
+ "notAuthorized": "Impossible d'ouvrir le log — chemin non autorisé.",
+ "alreadyEditable": "Le log est déjà ouvert en édition."
+ },
+ "github": {
+ "refresh": {
+ "error": {
+ "copy": {
+ "580025e7b7": "GitHub est indisponible",
+ "01c85b5770": "L'API de GitHub est temporairement indisponible. Ce panneau se recharge automatiquement dès qu'elle revient.",
+ "d1a9f2b165": "Impossible de joindre GitHub",
+ "7d01d42a3a": "GitHub est injoignable pour le moment. Vérifiez votre connexion, puis réessayez dans quelques instants.",
+ "e9d681894a": "Limite de requêtes GitHub atteinte",
+ "8c77434d6f": "GitHub limite actuellement les requêtes. Ce panneau s'actualisera dès que la limite sera réinitialisée.",
+ "79aa06bb2c": "Actualisation impossible. L'API de GitHub est temporairement indisponible. Affichage du dernier statut connu.",
+ "6ec12cee0c": "Actualisation impossible. GitHub est injoignable pour le moment. Affichage du dernier statut connu.",
+ "de088015e8": "Actualisation impossible. GitHub limite les requêtes. Affichage du dernier statut connu.",
+ "d9dd7c6687": "Actualisation depuis GitHub impossible. Affichage du dernier statut connu."
+ }
+ }
+ },
+ "pr": {
+ "stack": {
+ "confirmation": {
+ "84f6f5b9eb": "Inclus : {{numbers}}. ",
+ "541984b2eb": "Ajouter à la file via #{{pr}} ?",
+ "4809f55cdb": "{{included}}GitHub ajoutera {{count}} pull request à la file de fusion en une seule fois. La file choisit la méthode de fusion et peut les fusionner en groupes séparés.",
+ "be8f2621be": "{{included}}GitHub ajoutera {{count}} pull requests à la file de fusion en une seule fois. La file choisit la méthode de fusion et peut les fusionner en groupes séparés.",
+ "92ca033e72": "Mettre {{count}} PR en file",
+ "478a527b15": "Mettre {{count}} PR en file",
+ "1feef35ca4": "Fusionner via #{{pr}} ?",
+ "c3e036c99f": "{{included}}GitHub fusionnera {{count}} pull request de manière atomique avec {{method}}. Si elle ne peut pas être fusionnée, rien ne le sera.",
+ "369aba4b32": "{{included}}GitHub fusionnera {{count}} pull requests de manière atomique avec {{method}}. Si l'une ne peut pas être fusionnée, aucune ne le sera.",
+ "493c78f521": "Fusionner {{count}} PR",
+ "eb7051d268": "Fusionner {{count}} PR"
+ },
+ "merge": {
+ "55ae29b907": "Fusionner via #{{pr}} · {{count}} PR",
+ "b8446f6ec2": "Fusionner via #{{pr}} · {{count}} PR",
+ "189d0ec614": "#{{pr}} est toujours un brouillon.",
+ "640fb50d9c": "#{{pr}} est fermée.",
+ "46ffcbda75": "#{{pr}} présente des conflits de fusion.",
+ "6dabefd63e": "#{{pr}} a demandé des modifications.",
+ "2bb21fc326": "#{{pr}} attend encore une approbation de relecture.",
+ "c23faf74df": "#{{pr}} doit être mise à jour.",
+ "f561e80968": "#{{pr}} est bloquée."
+ }
+ }
+ }
+ },
+ "PluginPanel": {
+ "unavailable": "Ce panneau de plugin n'est plus disponible.",
+ "loading": "Chargement du panneau de plugin…",
+ "unresponsive": "Ce panneau de plugin a cessé de répondre et a été suspendu.",
+ "loadFailed": "Le panneau de plugin n'a pas pu être chargé."
+ },
+ "activityBar": {
+ "error": "Erreur"
+ },
+ "AiVaultSessionLimitMenu": {
+ "historyDepth": "Profondeur de l'historique : {{value0}}",
+ "performanceWarning": "Des historiques plus longs peuvent ralentir toute l'application, en particulier sur des hôtes distants. « Illimité » analyse tout l'historique disponible.",
+ "recommended": "Recommandé",
+ "mayBeSlower": "Peut être plus lent",
+ "slowest": "Le plus lent",
+ "unlimited": "Illimité"
+ },
+ "GitHubPRStackMap": {
+ "3511405914": "closed",
+ "8a9bdc36c0": "fusionnée",
+ "568c647ccd": "brouillon",
+ "bea9ade223": "conflits",
+ "838aadf512": "vérifications échouées",
+ "316039b5db": "vérifications en attente",
+ "4b1e5ee9d3": "modifications demandées",
+ "9a17b5255c": "relecture requise",
+ "d3d97cf3f2": "approuvée",
+ "e6cb964305": "ouvrir",
+ "7737bd66be": "Réduire la stack #{{value0}}",
+ "0c1645ebd0": "Développer la stack #{{value0}}",
+ "e3ee2daa32": "Stack #{{value0}}",
+ "cb440931b7": "{{value0}} sur {{value1}} · {{value2}}",
+ "525259fa17": "Les détails de la stack sont temporairement indisponibles."
+ },
+ "CreateHostedReviewBasePicker": {
+ "205ef284fa": "Branche de base",
+ "bb4b41d563": "à partir de {{value0}}",
+ "5a9315b61a": "Aucune branche ne correspond à « {{value0}} ». Appuyez sur Entrée pour l'utiliser quand même.",
+ "da4d57c9c2": "Laissez vide pour utiliser {{value0}}."
+ },
+ "CreateHostedReviewComposerFields": {
+ "90cabf6cfc": "Empiler cette PR au-dessus de #{{value0}}",
+ "ff81473a57": "Crée une GitHub Stack ou étend la stack existante du parent.",
+ "29732f2fb0": "nouvelle PR"
+ }
+ }
+ },
+ "repo": {
+ "NestedRepoChecklist": {
+ "f7e1170567": "sélectionnés",
+ "ea54c7bf8f": "sur",
+ "91b5bcadb6": "Tout sélectionner",
+ "929734aea5": "Tout désélectionner"
+ },
+ "NestedRepoScanLimitNotice": {
+ "642a43c139": "Limites du scan des dépôts imbriqués",
+ "574eb5408b": "Affichage de résultats de scan partiels.",
+ "03e9beab7b": "Le scan s'est arrêté prématurément."
+ },
+ "RepoCombobox": {
+ "b3e15f4525": "Ajouter un projet",
+ "b4a235e886": "Ajout d'un projet",
+ "3639fd9da2": "SSH",
+ "e7ed739236": "Aucun projet/dossier ne correspond à votre recherche.",
+ "a0c48f5f29": "Rechercher des projets/dossiers…",
+ "116812151a": "Ajout du projet…"
+ },
+ "repo": {
+ "icon": {
+ "0ad395d475": "Boîte",
+ "857977b901": "Formes",
+ "b1b8d99fc4": "AI",
+ "137bdb1856": "Métriques",
+ "d202c659a3": "Design",
+ "c4fd14299d": "Entreprise",
+ "4ab9433660": "Travail",
+ "febfbe0cd5": "Outils",
+ "ecf63ec3ef": "Lancement",
+ "31826b712e": "API",
+ "70bef15d40": "Calques",
+ "b5fac337aa": "Calcul",
+ "d37b4e2641": "Serveur",
+ "3c5a593bc8": "Web",
+ "477b28c948": "Base de données",
+ "787490e9bd": "Paquet",
+ "07012dc113": "Agent",
+ "3eba7387ab": "Terminal",
+ "65b437c381": "Code",
+ "bed2674f9d": "Dossier"
+ }
+ }
+ },
+ "pet": {
+ "pet": {
+ "models": {
+ "7433516faf": "Gremlin",
+ "a84d5677ff": "OpenCode",
+ "2528586aa7": "Claudino"
+ }
+ }
+ },
+ "onboarding": {
+ "AgentStep": {
+ "e6a369bd04": "Agents populaires",
+ "d7b3ef168b": "Détectés sur votre système",
+ "9c163bb0e0": "Instructions d'installation",
+ "69af7e9c1c": "n'est pas encore dans votre PATH. Orca le définira comme valeur par défaut et vous pourrez l'installer à tout moment.",
+ "1eee1c7bd8": "Aucun agent détecté dans votre PATH. Choisissez-en un à installer plus tard, ou continuez avec un terminal vide.",
+ "hideAgents": "Masquer les agents",
+ "showMoreAgents": "Afficher {{value0}} autres agents→",
+ "yoloPermissionsLabel": "Yolo / Ignorer dangereusement les permissions",
+ "yoloPermissionsInfo": "Informations sur les permissions des agents",
+ "yoloPermissionsTooltip": "Ignorer les vérifications de permissions des agents pour moins d'interruptions"
+ },
+ "FeatureSetupInlineTerminal": {
+ "789b59936e": "Appuyez sur Entrée pour exécuter la commande et confirmez npx si demandé. Vous pouvez aussi configurer cela plus tard dans les paramètres.",
+ "47fc6cc6dc": "Commande de configuration du skill",
+ "c767ab7061": "Configuration du skill"
+ },
+ "IntegrationsStep": {
+ "277f30eb34": "Linear, GitLab, Bitbucket, Azure DevOps, Gitea et Jira se trouvent dans Paramètres > Intégrations.",
+ "3a3e360289": "Autres sources de tâches",
+ "80e3ce0bc9": "Revérifier",
+ "04ef416712": "Ajouter l'accès Linear",
+ "dd9c186a8b": "Ajouter un accès à l'espace de travail",
+ "c91a5782f1": "Connecté",
+ "27743304b1": "Linear",
+ "af69f42372": "Appuyez sur Entrée pour lancer l'authentification GitHub CLI. Revérifiez GitHub une fois le flux navigateur ou appareil terminé.",
+ "f9d2e12d17": "Commande de connexion à GitHub",
+ "6d469169f2": "Configuration de GitHub",
+ "bd5d976fb2": "Installer gh",
+ "50db38cf4b": "Pull requests, issues et statut des vérifications.",
+ "c1547656f0": "Vérification…",
+ "8405043962": "Connexion requise",
+ "5c115cb713": "CLI non installé",
+ "217beb0658": "GitHub",
+ "4983ae7433": "Ajoutez l'accès à Linear avec une clé d'API personnelle. Les clés à accès complet peuvent afficher toutes les équipes accessibles au propriétaire de la clé.",
+ "b08a6ac93c": "Espace de travail {{value0}}{{value1}} associé. Ajoutez un autre espace de travail ou remplacez une clé restreinte à tout moment.",
+ "93f0c49ad1": "not-authenticated",
+ "a3bcf13694": "connecté",
+ "d6e5dba05a": "Se connecter",
+ "0b4a7d23ab": "Connexion en cours",
+ "a74c6d6b18": "not-installed"
+ },
+ "NotificationStep": {
+ "3bede04483": "Envoyer une notification de test",
+ "dc897423e1": "Choisir le son de notification",
+ "53aaffe49a": "Son de notification",
+ "0fe570690c": "Choisissez l'alerte jouée par Orca après l'envoi d'une notification bureau.",
+ "0af746e41f": "Choisir un son",
+ "8124d085a6": "Ouvrir les réglages Mac",
+ "aa36281b00": "Ouvrez Réglages Système et vérifiez qu'Orca est autorisé à envoyer des notifications.",
+ "d2dba86837": "Autoriser Orca dans macOS",
+ "e52aacf380": "Chargement des réglages de notification…",
+ "3cd5374e22": "Les réglages de notification sont encore en cours de chargement",
+ "b6a994e36e": "Impossible de lire le son de notification",
+ "c0692baa52": "Choisir un fichier personnalisé",
+ "ac80d97e02": "Changer de fichier personnalisé",
+ "56b836215c": "Vérification de l'autorisation de notification…",
+ "fd84d3e9b8": "Les notifications sont activées",
+ "4f7bce5644": "macOS vous alertera quand un agent aura terminé ou qu'un terminal demandera votre attention.",
+ "95d99b52fa": "Autoriser les notifications pour Orca",
+ "94562ba367": "macOS demande l'autorisation. Cliquez sur Autoriser dans la boîte de dialogue : cette étape se mettra à jour automatiquement.",
+ "4f6a1da718": "Ouvrir Réglages Système",
+ "90b5d2e363": "macOS ne délivre pas les notifications d'Orca",
+ "2c47f5465f": "Activez « Autoriser les notifications pour Orca » dans Réglages Système. Cette étape se met à jour automatiquement une fois l'option activée."
+ },
+ "OnboardingFlow": {
+ "1b5e182e9f": "Bienvenue dans Orca",
+ "4db04f2f57": "sur",
+ "adaa0aa627": "Aller à l'étape {{value0}} de la prise en main : {{value1}}",
+ "a249f81538": "Orca",
+ "277ba45540": "Prise en main d'Orca",
+ "97c42cda00": "Installez le CLI GitHub pour :",
+ "ae3b00ca82": "Configurer les tâches GitHub",
+ "ff92d15436": "Orca vous préviendra quand les agents auront terminé ou auront besoin d'aide.",
+ "b054332836": "Configurer les notifications",
+ "04ae28d8ca": "Choisissez le look que vous allez contempler pendant des heures.",
+ "f396db9f20": "Donnez-lui vos couleurs",
+ "322fc50a18": "Orca fonctionne avec tous les agents CLI. Choisissez celui que vous solliciterez le plus. Changez quand vous voulez.",
+ "198b148b3c": "Choisissez votre agent par défaut",
+ "a5e5da02f7": "intégrations",
+ "35bbaf5ae0": "notifications",
+ "984338477a": "thème",
+ "c47e1bd149": "agent",
+ "windowsTerminalTitle": "Définir les valeurs par défaut du terminal Windows",
+ "windowsTerminalSubtitle": "Choisissez le shell PAR DÉFAUT des nouveaux volets et le comportement du clic droit dans le terminal."
+ },
+ "OnboardingFooter": {
+ "ba58547306": "Retour",
+ "111d3f8d92": "Passer à la configuration du projet"
+ },
+ "OnboardingInlineCommandTerminal": {
+ "4123609efd": "Démarrage du terminal..."
+ },
+ "OnboardingSkipConfirmationDialog": {
+ "9f47f345a4": "Ça ne sera pas long !",
+ "e4726b2d50": "Passer la prise en main ?"
+ },
+ "ThemeStep": {
+ "a4b254779d": "Touche Option macOS",
+ "6c51398942": "Souris",
+ "8ca01945f2": "Séparateurs",
+ "b3a99a2d29": "Fenêtre",
+ "86c0f1caa2": "Marge interne",
+ "06a24f4f2d": "Couleurs",
+ "c021e9dddd": "Palette du thème",
+ "ab2a583a97": "Cursor",
+ "cc1858e19e": "Police",
+ "248c812283": "Importer",
+ "7ee9234e54": "Configuration Ghostty détectée.",
+ "78b6386140": "Importé depuis Ghostty.",
+ "2c3aa538f8": "Recherche d'une configuration Ghostty…",
+ "94b9dc561d": "Réglages → Terminal",
+ "dd5c16ad1b": "D'autres options de terminal, notamment police, curseur et palette, dans",
+ "ad192706e6": "Clair",
+ "fa7b673ea9": "Sombre",
+ "827ea7b4a2": "Système",
+ "699ddf83c2": "Échec de l'import des réglages Ghostty",
+ "16a9f0446a": "Aucun réglage Ghostty à importer",
+ "ad19e5c916": "Importation…",
+ "906c4373fe": "paramètres"
+ },
+ "use": {
+ "onboarding": {
+ "flow": {
+ "52acfbef51": "Impossible d'enregistrer la progression"
+ }
+ }
+ },
+ "WindowsTerminalStep": {
+ "powerShell": "PowerShell",
+ "powerShellPwsh": "Utilise PowerShell 7+ quand il est disponible, avec Windows PowerShell en repli.",
+ "powerShellInbox": "Utilise Windows PowerShell, présent sur toutes les installations de Windows prises en charge.",
+ "commandPrompt": "Invite de commandes",
+ "commandPromptDescription": "Ouvre les nouveaux volets de terminal avec le comportement classique de cmd.exe.",
+ "gitBash": "Git Bash",
+ "gitBashDescription": "Utilise le bash.exe de Git for Windows pour les workflows shell de type Unix.",
+ "gitBashUnavailable": "Sélectionné, mais Git Bash n'a pas été détecté sur cette machine.",
+ "wsl": "WSL",
+ "wslDescription": "Démarre les nouveaux volets de terminal dans la distribution par défaut de votre Windows Subsystem for Linux.",
+ "wslUnavailable": "Sélectionné, mais WSL n'a pas été détecté sur cette machine.",
+ "rightClickPaste": "Coller au clic droit",
+ "rightClickPasteDescription": "Le clic droit colle le presse-papiers. Ctrl+clic droit ouvre le menu contextuel.",
+ "rightClickMenu": "Ouvrir le menu contextuel",
+ "rightClickMenuDescription": "Le clic droit ouvre le menu du terminal. Collez depuis le menu ou le clavier.",
+ "loading": "Chargement des réglages du terminal...",
+ "defaultShell": "Shell par défaut",
+ "defaultShellDescription": "Choisissez le shell qu'Orca ouvre pour les nouveaux volets de terminal sous Windows.",
+ "wslDistribution": "Distribution WSL",
+ "wslDistributionDescription": "Utilisez la distribution Windows par défaut ou choisissez une distribution installée précise.",
+ "loadingDistros": "Chargement des distributions",
+ "windowsDefault": "Défaut Windows",
+ "rightClickBehavior": "Comportement du clic droit",
+ "rightClickBehaviorDescription": "Choisissez le comportement souris du terminal qui correspond à vos réflexes Windows."
+ },
+ "mac": {
+ "notification": {
+ "permission": {
+ "card": {
+ "f696515944": "Cliquez sur Autoriser dans la boîte de dialogue macOS.",
+ "3d18cf71f9": "Se met à jour automatiquement.",
+ "721d2bedb6": "Activez « Autoriser les notifications pour Orca » dans Réglages Système."
+ }
+ }
+ }
+ }
+ },
+ "new": {
+ "workspace": {
+ "SetProjectLocationDialog": {
+ "title": "Définir l'emplacement du projet",
+ "description": "Choisissez l'emplacement de {{project}} sur {{host}}.",
+ "browseFolder": "Parcourir le dossier",
+ "browseFolderHelp": "Utilisez un checkout ou un dossier existant sur cet hôte.",
+ "cloneFromUrl": "Cloner depuis une URL",
+ "cloneFromUrlHelp": "Clonez ce dépôt sur {{host}}.",
+ "saveLocation": "Définir l'emplacement"
+ },
+ "SmartWorkspaceNameField": {
+ "2a0d535f69": "Créer une nouvelle branche",
+ "a44229ce4d": "comme nom d'espace de travail",
+ "766083a596": "\"",
+ "34ca97bce3": "\"",
+ "b1a7d679ba": "Utiliser",
+ "e57c53727c": "Ajouter un projet...",
+ "a76fcb4fa0": "Basculer vers",
+ "eadf877af5": "Conserver",
+ "6859e2896c": "Annuler",
+ "9ef1a7c4b0": ", qui est différent du projet sélectionné.",
+ "ad188067ae": "L'URL GitHub pointe vers",
+ "4bd98f1091": "Changer de projet ?",
+ "0c9e668e3a": "Effacer",
+ "7199ff19c7": "Effacer la source sélectionnée",
+ "370a1faf67": "Ouvrir dans le navigateur",
+ "2c69728c2a": "Ouvrir le lien dans le navigateur",
+ "6f07a18604": "Nom",
+ "2e4c7c95fe": "Branche",
+ "2cfc6be192": "GitLab",
+ "7a47af0565": "Linear",
+ "0a180280bd": "GitHub",
+ "b3c60c2b7c": "Intelligent",
+ "26824f60dd": "Tous",
+ "6fad211c66": "Fermé",
+ "2319d87718": "Fusionnés",
+ "622864b52a": "Ouvert",
+ "fda67f0b61": "projet actuel",
+ "3e8bb1176a": "Connectez Linear dans les réglages pour rechercher des issues.",
+ "69ce292138": "linear",
+ "9c004911c3": "gitlab",
+ "switchTaskSourceTitle": "Changer de source de tâches ?",
+ "differentTaskSource": ", qui est différent de la source de tâches sélectionnée.",
+ "currentTaskSource": "source de tâches actuelle",
+ "placeholderNameOrLinearUrl": "Saisissez un nom, une URL Linear ou une URL Jira",
+ "placeholderWorkspaceName": "Saisissez un nom d'espace de travail",
+ "placeholderSmartWithBranchGitLabLinear": "Saisissez un nom, #1234, une branche, une URL GitHub/GitLab, Linear ou Jira",
+ "placeholderSmartGitLabLinear": "Saisissez un nom, #1234, une URL GitHub/GitLab, Linear ou Jira",
+ "placeholderSmartWithBranchGitLab": "Saisissez un nom, #1234, une branche, une URL GitHub, GitLab ou Jira",
+ "placeholderSmartGitLab": "Saisissez un nom, #1234, une URL GitHub, GitLab ou Jira",
+ "unavailable": "Indisponible",
+ "searchGitHub": "Rechercher des PR et issues GitHub",
+ "searchGitLab": "Rechercher des MR et issues GitLab",
+ "searchBranches": "Rechercher des branches",
+ "searchLinear": "Rechercher des issues Linear",
+ "workspaceName": "Nom de l'espace de travail",
+ "loadingJira": "Chargement de l'issue Jira…",
+ "jiraDisconnected": "Connectez Jira dans les réglages pour associer cette issue",
+ "jiraSiteNotConnected": "Ce site Jira n'est pas connecté",
+ "jiraRuntimeUpdate": "Mettez à jour le runtime distant pour associer Jira",
+ "jiraReadFailed": "Impossible de charger cette issue Jira",
+ "chooseJiraAccount": "Choisir un compte Jira",
+ "jiraLoaded": "Issue Jira chargée",
+ "openSettings": "Paramètres",
+ "retryJira": "Réessayer",
+ "searchJira": "Recherchez des issues Jira ou collez l'URL d'une issue",
+ "jiraMode": "Jira",
+ "jiraSelectBindFailed": "Impossible d'associer cette issue Jira. Sélectionnez le site correspondant ou reconnectez Jira, puis réessayez.",
+ "emoji": "Émoji",
+ "loadingLinearIssue": "Chargement de l'issue Linear…"
+ },
+ "ProjectCombobox": {
+ "empty": "Aucun projet ne correspond à votre recherche.",
+ "addProject": "Ajouter un nouveau projet",
+ "label": "Projet",
+ "browse": "Parcourir les projets",
+ "listLabel": "Projets",
+ "noProjects": "Aucun projet pour le moment."
+ },
+ "RunTargetCombobox": {
+ "listLabel": "Cibles d'exécution",
+ "label": "Exécuter sur",
+ "browse": "Parcourir les cibles d'exécution"
+ }
+ }
+ },
+ "mobile": {
+ "MobileHero": {
+ "a8fb43cf1c": "Continuer",
+ "3f90dbd274": "Terminé",
+ "b622eba64d": "Retour",
+ "65b3f2e8bc": "Génération…",
+ "27735e5f4e": "QR d'appairage",
+ "bb0074ce11": "Code QR d'appairage",
+ "010dddcf27": "Copier le code d'appairage",
+ "4c1df4eba7": "Impossible de scanner ?",
+ "85067b9e06": "Actualiser les interfaces réseau",
+ "ca85e595a7": "Aucune interface trouvée",
+ "79d2f480da": "Interface réseau à annoncer",
+ "dfd2aa9d5d": "Réseau",
+ "2f077ef4eb": ", puis scannez le code.",
+ "3aa7bb2d8b": "Appairer l'ordinateur",
+ "d1495e5e64": "Ouvrez Orca Mobile, touchez",
+ "3960f5c339": "Étape 2 sur 2",
+ "3241f3c26a": "QR d'installation",
+ "7af266b80d": "Code QR d'installation",
+ "aa97420ba4": "Copier le lien d'installation",
+ "ac1eb64952": "Android",
+ "711e6f4b47": "iOS",
+ "e75647ace0": "Scannez le code QR avec votre téléphone ou ouvrez le lien d'installation pour récupérer Orca Mobile.",
+ "0d9b33299e": "Récupérez l'app.",
+ "92ddfdfa1f": "Étape 1 sur 2",
+ "ff48d9d520": "Appairer un autre appareil",
+ "f9cbf4bb53": "Révoquer l'appareil",
+ "34f878d04f": "Révoquer {{value0}}",
+ "94829abdb1": "Appairé",
+ "266c18c105": "Ouvrez Orca Mobile pour reprendre là où vous en étiez, ou appairez un autre appareil.",
+ "5410d55d79": "Orca Mobile",
+ "10d27b4cba": "Commencer",
+ "da1d5e5ed0": "Disponible sur",
+ "ec0607bf66": "Plateformes mobiles prises en charge",
+ "b4ccce5cb7": "Pilotez Orca depuis votre téléphone. Suivez les agents, examinez les changements et lancez des tâches même loin de votre bureau.",
+ "cd4e5e816f": "Vos espaces de travail, dans votre poche.",
+ "a6cffbbb0b": "Générer le code",
+ "e59a252eca": "Régénérer le code",
+ "d0b52871ce": "Vos téléphones sont appairés.",
+ "051978a785": "Votre téléphone est appairé.",
+ "channel": {
+ "group": "Canal de release",
+ "preview": "Aperçu",
+ "stable": "Stable"
+ },
+ "relayDegradedNotice": "Relay injoignable — ce code ne fonctionne que sur votre LAN ou Tailscale.",
+ "pairingQrError": "Ce code d'appairage n'a pas pu être rendu sous forme de QR code. Copiez-le plutôt dans Orca Mobile.",
+ "noRelayCode": "Aucun code d'appairage disponible",
+ "noPairingCode": "Aucun code d'appairage disponible",
+ "qrSignInRequired": "Connectez-vous pour créer un code d'appairage Relay",
+ "qrRenderFailed": "Impossible d'afficher le code QR — copiez le code ci-dessous",
+ "qrGeneratePrompt": "Générez un code d'appairage pour continuer",
+ "pairingCodeReady": "Code d'appairage prêt",
+ "pairThisMac": "Appairez ce Mac.",
+ "pairThisPc": "Appairez ce PC.",
+ "pairThisComputer": "Appairez cet ordinateur.",
+ "directAddressDisclosure": "Utiliser aussi un chemin local plus rapide",
+ "directAddressHint": "Facultatif. Choisissez l'adresse Wi‑Fi ou Tailscale que votre téléphone utilisera à proximité — généralement plus rapide que Relay. Relay reste fonctionnel quand vous êtes loin.",
+ "androidHelp": {
+ "guide": "Guide d'installation"
+ }
+ },
+ "MobilePage": {
+ "e17393c6a3": "Aperçu du téléphone",
+ "baea63c445": "Échec de la copie du lien",
+ "fad833de8d": "Lien d'installation copié",
+ "6a66e38943": "Échec de la copie du code d'appairage",
+ "3c1f7168bb": "Code d'appairage copié",
+ "4c8bd11c1a": "Échec de la génération du code d'appairage",
+ "b353e18de1": "Le transport WebSocket n'est pas actif",
+ "4e1eb5d55c": "Échec de la révocation de l'appareil",
+ "255372e6e8": "Appareil révoqué",
+ "1b4509a8a1": "appairé",
+ "c5909374cf": "intro",
+ "diagnosticsCopied": "Diagnostics copiés",
+ "diagnosticsCopyFailed": "Échec de la copie des diagnostics"
+ },
+ "MobilePageToolbar": {
+ "ad2284a9e2": "Fermer · Esc",
+ "9883b58693": "Fermer Orca Mobile",
+ "fb5f28330e": "Afficher dans la barre latérale",
+ "c669abcf8f": "Masquer de la barre latérale",
+ "e1c7b4a92d": "À configurer dans Réglages > Mobile.",
+ "a4f8c2d91e": "Masquer de la barre latérale",
+ "b7e3d1c84a": "Afficher dans la barre latérale",
+ "f3d8e5b71a": "Remet le raccourci dans la barre latérale."
+ },
+ "PhoneCarousel": {
+ "96d651cb87": "Session de terminal",
+ "93217b41c1": "Liste des worktrees",
+ "89c7713645": "Écran d'accueil d'Orca Mobile"
+ },
+ "mobile": {
+ "platform": {
+ "copy": {
+ "2a532d6fd7": "Scannez avec l'appareil photo de votre Android pour télécharger le dernier APK depuis GitHub Releases.",
+ "432db52b73": "Scannez avec l'appareil photo de votre iPhone pour ouvrir l'App Store.",
+ "preview": {
+ "tagline": "Fonctionnalités les plus récentes, mises à jour chaque jour."
+ },
+ "stable": {
+ "tagline": "La version publique, mise à jour chaque semaine."
+ }
+ }
+ }
+ },
+ "slides": {
+ "HomeSlide": {
+ "a7d9e2c44d": "7 j",
+ "a3d5476811": "5 h",
+ "8a350a4784": "Utilisation du compte",
+ "e27fdaee51": "Nouvel espace de travail",
+ "4405f3c440": "Appairer l'ordinateur",
+ "0b00c98506": "Actions rapides",
+ "0bad5b07c8": "GitHub et Linear",
+ "d047197480": "GitHub · Linear",
+ "a4c3f7b7aa": "Tâches",
+ "d33d7a9c29": "orca · feat/mobile-page",
+ "25d6e8a491": "feat/mobile-page",
+ "c791677f2f": "Reprendre",
+ "cf3f98fa3f": "Déconnecté",
+ "091355da3d": "M1 Mini · home",
+ "0bc1881bc4": "Connecté · 40 worktrees · 5 actifs",
+ "19c212e25e": "MacBook Pro",
+ "2f1a1d10c4": "Ordinateurs",
+ "156db8a68a": "PRs créées",
+ "4a40af029b": "Temps agent",
+ "00a6903322": "Agents lancés",
+ "c0e2e9dcd9": "Bon retour",
+ "af761a0c0d": "Paramètres",
+ "5d94e8ddcc": "Orca"
+ },
+ "TerminalSlide": {
+ "0bb39f8fe6": "Envoyer",
+ "69334b4b10": "Dictée vocale",
+ "29f2d13839": "Saisissez une commande…",
+ "817090af40": "Ctrl+C",
+ "53ff909568": "Tab",
+ "4930eaaae7": "Esc",
+ "fa22927f13": "Coller",
+ "985373052e": "Basculer en mode téléphone",
+ "58a9ee6003": "avec mise en forme des tool calls. Je vous ajoute le diff ensuite ?",
+ "aa64b519c6": "écran de terminal. Palette Tokyonight, police Menlo, véritable claude",
+ "e75112c834": "J'ai remplacé la diapositive pair-scan par un",
+ "3ce3e8c892": "14 réussis, 1 ignoré (1,8 s)",
+ "4b3666f9a9": "src/cache/worktree-cache.test.ts",
+ "1d448b69f7": "PASS",
+ "d39445686a": "src/transport/host-store.test.ts",
+ "a6e7cdc688": "pnpm test --filter mobile",
+ "21b67dfc92": "Bash",
+ "d6d1041a1c": "⎿ Diapositive pair-scan remplacée par une session de terminal",
+ "336c0e070e": "mobile/orca-mobile-sidebar-mock-v3.html",
+ "6d4ebd5833": "Édition",
+ "fc83e0d5ef": "⎿ Lecture de 2103 lignes",
+ "80cc356591": "Lecture",
+ "2c10d43745": "claude",
+ "e0f98be657": "orca/feat-mobile-page",
+ "2defc05141": "dev@mac",
+ "da121ba48d": "PLAN.md",
+ "e4befee569": "shell",
+ "606aa93192": "Fichiers",
+ "94febb0976": "Contrôle de code source",
+ "8d6516312d": "2 terminaux · claude actif",
+ "8432787c4e": "feat/mobile-page",
+ "8fd998acd3": "Retour"
+ },
+ "WorktreeListSlide": {
+ "357a519567": "Actif",
+ "79a24ff530": "Épinglés",
+ "22971156df": "Dépôt",
+ "17f9e0d226": "Récents",
+ "0e3e809a4b": "Filtre",
+ "b4271864bd": "MacBook Pro",
+ "cefd048225": "Retour",
+ "c5ad56786d": "spinner"
+ }
+ },
+ "CustomNetworkAddressDialog": {
+ "title": "Adresse réseau personnalisée",
+ "description": "Annoncez une adresse que votre téléphone peut joindre — par exemple un nom d'hôte Tailscale, une adresse IP ou une URL de reverse proxy.",
+ "label": "Adresse",
+ "placeholder": "home.example.com:8443 or https://example.com/orca",
+ "hint": "Saisissez une adresse IPv4/IPv6, un nom d'hôte ou une URL HTTP(S)/WebSocket complète. Les ports sont facultatifs.",
+ "cancel": "Annuler",
+ "use": "Utiliser l'adresse",
+ "confirmationError": "Cette adresse n'a pas permis de générer un code d'appairage scannable. Vérifiez l'adresse et réessayez."
+ },
+ "NetworkInterfacePicker": {
+ "no-address-selected": "Aucune adresse sélectionnée",
+ "trigger-label": "Adresse réseau à annoncer",
+ "custom-option": "{{address}} (personnalisée)",
+ "add-custom": "Ajouter une adresse personnalisée…",
+ "custom-section": "Personnalisé",
+ "remove-custom": "Supprimer {{address}}"
+ },
+ "WindowsFirewallNotice": {
+ "repair-success": "Le Pare-feu Windows autorise désormais Orca Mobile sur les réseaux privés",
+ "repair-failed": "Impossible de mettre à jour les règles du Pare-feu Windows",
+ "public-title": "Windows marque ce réseau comme public",
+ "missing-title": "Autoriser les connexions des téléphones via le Pare-feu Windows",
+ "public-description": "Définissez ce réseau Wi-Fi approuvé comme Privé avant d'autoriser les connexions Orca Mobile.",
+ "missing-description": "Windows peut bloquer le serveur d'appairage. Ajoutez une règle pour cette application Orca et le port TCP {{port}} sur les réseaux privés.",
+ "open-settings": "Ouvrir les paramètres réseau",
+ "waiting": "Attente de Windows…",
+ "allow": "Autoriser les connexions des téléphones",
+ "repair-unverified": "Impossible de vérifier l'accès au Pare-feu Windows",
+ "blocked-title": "Windows bloque peut-être Orca Mobile",
+ "blocked-description": "Une règle entrante de blocage existante peut prendre le pas sur l'exception d'appairage. La réparation supprime les règles TCP conflictuelles de cette application Orca, puis autorise le port {{port}} sur les réseaux privés.",
+ "repair": "Réparer l'accès pare-feu",
+ "relay-note": "L'appairage fonctionne toujours via Orca Relay — cette autorisation ajoute seulement la connexion locale, plus rapide."
+ },
+ "MobileRelayMintFailureNotice": {
+ "retryingTitle": "Nouvelle tentative via Orca Relay…",
+ "unavailableTitle": "Orca Relay n'est pas disponible sur ce poste de bureau.",
+ "title": "Impossible de créer un code d'appairage Relay.",
+ "retryingBody": "Création d'un nouveau code d'appairage. Cela peut prendre un moment via une connexion distante.",
+ "unavailableBody": "Utilisez le LAN pour appairer via Tailscale ou le même Wi-Fi.",
+ "body": "Réessayez, ou utilisez le LAN pour appairer via Tailscale ou le même Wi-Fi.",
+ "useLan": "Utiliser le LAN",
+ "retrying": "Nouvelle tentative…",
+ "retry": "Réessayer Relay",
+ "copyDiagnostics": "Copier les diagnostics"
+ }
+ },
+ "gitlab": {
+ "gitlab": {
+ "rate": {
+ "limit": {
+ "display": {
+ "ebc0e8ecf1": "Chargement du quota de l'API GitLab...",
+ "a2d3d1fdde": "Le quota de l'API GitLab est indisponible.",
+ "a2f68645ac": "Actualiser le quota de l'API GitLab",
+ "2f9c16d6c3": "Orca utilise REST via le CLI GitLab.",
+ "14e144f7a7": "Budget API GitLab",
+ "3e2c982cfa": "restants, réinitialisation dans",
+ "ea8ad0bae8": "sur",
+ "0a891e8935": "API REST",
+ "953f7c6062": "Cet hôte GitLab n'a pas renvoyé d'en-têtes de limite de débit.",
+ "budget_scope_prefix": "Périmètre du quota"
+ }
+ }
+ }
+ }
+ },
+ "floating": {
+ "terminal": {
+ "FloatingTerminalIconContextMenu": {
+ "8e7d775287": "Masquer l'espace de travail flottant",
+ "763f5fa2c1": "Déplacer vers le bouton flottant",
+ "0ee79e0674": "Déplacer vers la barre d'état"
+ },
+ "FloatingTerminalOrchestrationDialog": {
+ "f726054620": "Permet aux agents de transmettre le contexte et de coordonner le travail via Orca.",
+ "1cd3f8af64": "Skill d'orchestration",
+ "6f0aed26b8": "Installez le CLI Orca et la skill d'orchestration pour que les agents puissent se coordonner via Orca.",
+ "05d7aabc20": "Non installé",
+ "630c0ac8c8": "Installés",
+ "dfd021ce46": "Vérification...",
+ "543f325a14": "Activer l'orchestration"
+ },
+ "FloatingTerminalPanel": {
+ "fc1042e92b": "Réduire",
+ "8b07759314": "Nouveau navigateur",
+ "88ffb502e5": "Ouvrir une note Markdown",
+ "629528690b": "Nouvelle note Markdown",
+ "3215fc73e9": "Nouveau terminal",
+ "da508bd7f5": "Enregistrer",
+ "918c2139f3": "Ne pas enregistrer",
+ "e7bf09d4d4": "Annuler",
+ "690b6fb98a": "Modifications non enregistrées",
+ "bbc177f98f": "Activer",
+ "adc281394d": "Ignorer",
+ "8cf80db43b": "Configurez le CLI Orca et la skill d'agent pour que les agents puissent se coordonner via Orca.",
+ "2a3c5ddf5e": "Activer l'orchestration",
+ "d6b563ae24": "Chargement de l'éditeur...",
+ "8b14ba6c17": "Nouvel onglet de navigateur",
+ "b085fb58b5": "Ce fichier contient des modifications non enregistrées.",
+ "5ddc688c52": "« {{value0}} » contient des modifications non enregistrées. Voulez-vous enregistrer avant de fermer ?",
+ "25d7817f79": "terminal"
+ },
+ "FloatingTerminalToggleButton": {
+ "3b04b065b5": "Afficher l'espace de travail flottant",
+ "4cb418b991": "Afficher l'espace de travail flottant, nouvelle activité",
+ "5785dd9148": "Réduire l'espace de travail flottant",
+ "bfe7809a70": "Espace de travail flottant {{value0}} ({{value1}})"
+ },
+ "FloatingTerminalWindowControls": {
+ "2f6054342c": "Réduire",
+ "1bbaa0302f": "Réduire l'espace de travail flottant",
+ "3f4ca29961": "Agrandir l'espace de travail flottant",
+ "1c79cba25d": "Restaurer l'espace de travail flottant",
+ "648352c51f": "Ouvrir {{value0}} dans l'espace de travail flottant",
+ "82da3701e7": "Impossible de construire la commande de lancement pour {{value0}}.",
+ "109870e023": "Agrandir",
+ "b5686fee1e": "Restaurer",
+ "1e502f1284": "Ouvert"
+ }
+ }
+ },
+ "feature": {
+ "wall": {
+ "AgentCapabilitiesSetupAction": {
+ "b8dc9dd8a2": "Installés",
+ "1b51644c2d": "Laissez les agents contrôler le bureau : déplacer le curseur, cliquer et taper dans n'importe quelle application.",
+ "362a07517d": "Computer Use",
+ "5e8fe5a72d": "Donnez aux agents un accès direct au navigateur d'Orca pour tester des pages, capturer des captures d'écran et agir sur ce qu'ils voient.",
+ "e638da007a": "Agent Browser Use",
+ "c61c91e642": "Laissez les agents se coordonner via Orca pour faire avancer jusqu'au bout les grandes tâches en plusieurs étapes.",
+ "ac07f8887f": "Orchestration d'agents",
+ "e9eb197e12": "Autorisations Computer Use ouvertes",
+ "3a59452a67": "Commande de skill copiée et insérée ci-dessous pour vérification.",
+ "c605f51f2b": "Configuration des capacités prête",
+ "1aa657d8f4": "Certaines configurations de capacités demandent votre attention",
+ "c89534cbe9": "Installer CLI & Skills"
+ },
+ "AiCommitPrSettingsCard": {
+ "8d4152701a": "ex. ollama run llama3.1 {{value0}}",
+ "9ee54037a4": "Commande personnalisée",
+ "4b2fc4b80c": "Effort de réflexion",
+ "be8917699e": "Modèle",
+ "4d9b6d84df": "non pris en charge. Choisissez Claude, Codex ou Personnalisé.",
+ "560d4feb00": "Personnalisé",
+ "29d119fe95": "Agent",
+ "f9382b48a1": "Activer l'auteur IA",
+ "1c0cb4fabb": "Auteur IA",
+ "bd14e9c42a": "Non configuré",
+ "1f9468c5c9": "{{value0}} non pris en charge"
+ },
+ "BrowserAnimatedVisual": {
+ "46df009982": "Démarrer l'essai gratuit",
+ "25f15c2219": "Pro",
+ "59ae327405": "Starter",
+ "9e0f530390": "Tarifs",
+ "0ce7c24b4d": "Traitement…",
+ "f2034c4930": ">",
+ "6e4616d039": "Claude",
+ "0f8481e1a7": "Envoyer à Claude",
+ "3d2352f94b": "Décrivez la modification…",
+ "d8856b604a": "div.pricing-grid > div.card.starter:nth-of-type(1) > a.cta",
+ "7da6eed7bf": "localhost:3000",
+ "0a2bd01c02": "Nouvel onglet de navigateur",
+ "04096318ab": "Terminal 1",
+ "eb88125c6f": "✓ Vérifié — Essai gratuit toujours fonctionnel.",
+ "051c97d15a": ".pp-card[data-card=\"starter\"] .pp-cta",
+ "4fa59ca545": "✓ Mis à jour",
+ "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": "Permet aux agents de naviguer et de vérifier des pages dans le navigateur d'Orca.",
+ "d5bb1cd4ba": "Skill Browser Use"
+ },
+ "ComputerUseAnimatedVisual": {
+ "d8401975b1": "approuvée",
+ "f27676a92c": "statut :",
+ "6804cb356f": "clic envoyé",
+ "1719b28a81": "trouvé « Approve »",
+ "79445f7512": "approuver la note dans mon app",
+ "99a8624bcb": ">",
+ "2adb561b44": "Session Claude Code démarrée",
+ "94787f01f8": "Claude Code",
+ "9cddfe96b2": "Application locale",
+ "9634d870d1": "Approuver",
+ "3cc2df3671": "Terminé",
+ "bdd5312213": "En attente",
+ "c11dda000b": "Approuvé"
+ },
+ "EditorAnimatedVisual": {
+ "7a763daf2f": "italique",
+ "8521536429": "gras ·",
+ "8341391520": "pour les blocs ·",
+ "3fe42a1da0": "Tapez",
+ "8268b2376b": "Bloc de code",
+ "37fa4948ce": "Liste à puces",
+ "f25687c588": "Citation",
+ "abbdeea15d": "Blocs de base",
+ "a26a68d30c": "Titre 2",
+ "722170663a": "Titre 1",
+ "1fb29ad710": "Titres",
+ "4426aab46f": "Mettre à jour l'index de la doc dès que la nouvelle tuile arrive.",
+ "95f0c3a46f": "Tester le parcours d'installation sur une machine vierge.",
+ "22ae7b4d9d": "Note rapide pour l'équipe — le point sur ce qui reste avant la sortie.",
+ "5a55c00a81": "Plan de lancement",
+ "218503f9f3": "enregistrement auto",
+ "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; }"
+ },
+ "FeatureWallBody": {
+ "25ec5356d6": "Configuration"
+ },
+ "FeatureWallModal": {
+ "33dca8bbbe": "Une brève visite d'Orca, workflow par workflow.",
+ "3567e147c8": "Découvrez Orca"
+ },
+ "FeatureWallPreview": {
+ "a666384798": "Aussi dans ce workflow"
+ },
+ "FeatureWallRail": {
+ "69ea857689": "Terminé",
+ "7593d15f94": "Workflows"
+ },
+ "FeatureWallSetupChecklist": {
+ "1a6a7d6c80": "Configuration",
+ "713cc529a5": "Jalons",
+ "b1f1981c5e": "Voir les tâches",
+ "0235b268b2": "Pas encore terminé",
+ "13294d3405": "Terminé"
+ },
+ "FeatureWallSetupWorkflowActions": {
+ "486c2f4d8d": "Ajoutez d'abord un projet git, puis configurez le script de setup de ce dépôt.",
+ "00078a6134": "Voir dans les réglages",
+ "14327073cc": "Enregistrer",
+ "88469e926b": "Script de setup",
+ "5c5b65044e": "pnpm install",
+ "a7463915b6": "Échec de l'enregistrement du script de setup",
+ "6299297dac": "Script de setup enregistré",
+ "f0bbf7da77": "Essayez",
+ "522cce9e33": "Ajouter un projet"
+ },
+ "FeatureWallTourPanel": {
+ "af7d622f6f": "Facultatif"
+ },
+ "KeepAwakeCard": {
+ "209713d3c7": "Facultatif"
+ },
+ "ReviewNotesAnimatedVisual": {
+ "5dbd27c4c2": "Codex",
+ "09094f25e2": "Claude Code",
+ "294aaff104": "Envoyer les notes à",
+ "ea4e45b71b": "Ajouter une note",
+ "271ea0cbf3": "Annuler",
+ "a7a89d8f94": "Ligne",
+ "5cb213f967": "Notes IA",
+ "1eee3a397e": "src/server/migrate.ts (diff)"
+ },
+ "ReviewPRViewAnimatedVisual": {
+ "7c2808ecff": "troncation avant le merge.",
+ "c2062da7ec": "stderr",
+ "6f4c2d7cb7": "Ajouter un cas de couverture pour",
+ "71828fba75": "Peut-on inclure la commande qui échoue dans la charge utile de diagnostic ?",
+ "fb1a856b6d": "ouvrir",
+ "7a8b896e11": "Commentaires",
+ "ca36f7b27c": "Réussi",
+ "25f6838e43": "lint",
+ "2ef0b97954": "typecheck",
+ "8ed213397c": "En cours",
+ "d340c052fb": "verify",
+ "9a097cae12": "1 en attente",
+ "2f37142229": "Squasher et merger",
+ "0aab7ab84a": "Ajouter le suivi d'erreurs des diagnostics locaux",
+ "dfe313e0c9": "OPEN",
+ "6e3f5223c5": "Explorateur",
+ "ab2901bce6": "Vérifications",
+ "d7f80060ca": "Contrôle de code source",
+ "8e715588e4": "Recherche",
+ "a6c8b9e32f": "Checks réussis",
+ "f4d5e1a7b2": "3 vérifications"
+ },
+ "ReviewShipAnimatedVisual": {
+ "4d99496b8c": "Créer une PR",
+ "62544e0852": "Annuler",
+ "bcd5cae3c4": "Description de la pull request",
+ "3774b80eae": "Description",
+ "07da9245cc": "Titre de la pull request",
+ "54a093c52d": "Titre",
+ "3b9b96d6a6": "main",
+ "ce7d5d3a18": "Branche de base",
+ "e4473d438f": "Générer avec l'IA",
+ "c30cd930ff": "Créer la pull request",
+ "ea0100dd15": "Tout afficher",
+ "e725000cd7": "Modifications",
+ "a079083a6c": "Commit",
+ "7347fa5839": "Message",
+ "d1a7f15876": "Générer le message de commit avec l'IA",
+ "cd8a3a39d7": "3 commits d'avance"
+ },
+ "TasksAnimatedVisual": {
+ "efba6f77eb": "Lecture de l'issue #",
+ "b68c92fbdc": "Démarrer l'espace de travail",
+ "4331c4d0f8": "Ouvert",
+ "b13375617e": "Le sélecteur de worktrees tronque les noms",
+ "72f9e516a3": "en appuyant sur",
+ "fe47c9c9e8": "Espace de travail prêt",
+ "61ffda7601": "Création de l'espace de travail"
+ },
+ "WorkbenchAnimatedVisual": {
+ "633a91e358": "Réflexion…",
+ "932c4b3a97": ">",
+ "ca2cfbf188": "Scinder le terminal vers le bas",
+ "e370fa8c2b": "Scinder le terminal vers la droite",
+ "b85eab49dd": "src/auth/session.ts",
+ "99f5224f1e": "Édition",
+ "0d93c298a7": "throw src/auth",
+ "17cfdc3344": "Grep",
+ "9923847785": "Lecture",
+ "c0eb94125e": "revue des cas limites d'authentification",
+ "431ca9842a": "Session Claude Code démarrée",
+ "000106adfe": "claude",
+ "7d9f1d5f7d": "(0,8 s)",
+ "944199e54a": "› la mise à jour du total du panier",
+ "623881d72e": "checkout.spec.ts",
+ "5c5cbd783f": "(1,2 s)",
+ "3261c6853b": "› la connexion fonctionne",
+ "defe550fe2": "login.spec.ts",
+ "0b20782e0f": "Exécution de 12 tests avec 4 workers",
+ "4371cc9931": "pnpm playwright test",
+ "16877e038d": "scinde vers le bas",
+ "a2b114dad0": "scinde à droite ·",
+ "0bc9ad0cd1": "Même volet :",
+ "fc84f17fe7": "Session Codex démarrée"
+ },
+ "agent": {
+ "capability": {
+ "setup": {
+ "status": {
+ "8eccfcb314": "Installés",
+ "21d4f79c93": "cliquez sur Installer CLI & Skills pour ouvrir les réglages d'accès macOS",
+ "5c9293e51a": "vérification de l'accès à l'app",
+ "aae94eeb52": "Cliquez sur Installer CLI & Skills",
+ "aa8e143a2f": "Impossible de vérifier l'installation",
+ "9b33e7fb13": "Vérification de l'installation",
+ "4c8e1f92a7": "ouvrez Orca Desktop sur ce Mac",
+ "6d2b0a84e1": "Indisponible dans ce build"
+ }
+ }
+ }
+ },
+ "feature": {
+ "wall": {
+ "usage": {
+ "tracking": {
+ "b94ec70eda": "Suivi non configuré",
+ "cc39a87288": "Connecté · Par défaut du système",
+ "00087eecb2": "Connecté · {{value0}}"
+ }
+ }
+ }
+ },
+ "review": {
+ "animated": {
+ "visual": {
+ "shared": {
+ "e7894927a2": "Codex",
+ "9deecb021c": "Claude"
+ },
+ "notes": {
+ "styles": {
+ "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 { 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 { 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 } }"
+ }
+ }
+ }
+ },
+ "notes": {
+ "diff": {
+ "rows": {
+ "f621c734f8": "Note · ligne"
+ }
+ }
+ }
+ },
+ "agents": {
+ "orchestration": {
+ "OrchestrationPage": {
+ "30b509a467": "2 enfants",
+ "862605d066": "2 espaces de travail enfants",
+ "coordinatorName": "refonte du flux d'authentification",
+ "childPr1Name": "PR 1/2 : migration de users.sql",
+ "childPr2Name": "PR 2/2 : middleware withSession"
+ },
+ "types": {
+ "coordinatorInitial": "Fractionnement de la refonte de l'authentification en 2 PR…",
+ "codexInitial": "Écriture de la migration de la table users…",
+ "claudeInitial": "Ébauche du middleware withSession…",
+ "codexBeat1": "Ajout de la colonne email_verified…",
+ "claudeBeat1": "Branchement du middleware withSession…",
+ "coordinatorBeat1": "PR 1/2 prête",
+ "coordinatorBeat2": "PR 2/2 prête"
+ },
+ "steps": {
+ "name": "Orchestration",
+ "subtitle": "Orchestration",
+ "description": "Permettez aux agents de gérer et de coordonner les espaces de travail Orca pour exécuter des tâches plus importantes."
+ },
+ "StatusesPage": {
+ "2f549fc0ba": "src/auth/session.test.ts",
+ "139e3d7458": "Mis à jour",
+ "7b26349cb2": "pnpm migrate latest",
+ "78f0318ac1": "Souhaite exécuter",
+ "79971d1539": "refonte du flux d'authentification"
+ },
+ "UsageAccountsCard": {
+ "6986b36708": "Affichez les limites de débit et changez de compte directement.",
+ "d90d2e1f6d": "Suivez l'utilisation par session et par semaine.",
+ "8919321417": "Échec de la connexion Codex.",
+ "c7b90c140b": "Compte Codex ajouté.",
+ "4e71d72912": "Échec de la connexion Claude.",
+ "9ddeb558f9": "Compte Claude ajouté.",
+ "29d0653961": "Se connecter",
+ "945865332e": "Connexion en cours"
+ },
+ "UsagePage": {
+ "64265cb295": "29 % utilisés sur 5 h",
+ "be5a165875": "Basculer vers",
+ "277a9c65a9": "Compte Codex",
+ "4dce5ca3aa": "Réinitialisation dans 4 j 3 h",
+ "05ce4ecdd3": "38 % utilisés",
+ "0470aaed99": "Hebdomadaire",
+ "f421abf962": "Session",
+ "5e45fb1238": "Mis à jour il y a 1 min",
+ "6a4b1d3c38": "Codex"
+ },
+ "orchestration": {
+ "cards": {
+ "da6f1f97c9": "travaille"
+ }
+ }
+ }
+ },
+ "ReviewAnimatedVisual": {
+ "8df4d52b68": "pr-view",
+ "8ab622e4d6": "notes"
+ },
+ "FeatureWallBrowserAction": {
+ "5022c43a88": "Impossible d'ouvrir le navigateur",
+ "c9eb68b474": "Aucun groupe d'espaces de travail n'est encore disponible pour ce worktree.",
+ "c9728107c5": "Essayez",
+ "25dd101f15": "La configuration du navigateur demande votre attention",
+ "e02b11e6b0": "Configuration du navigateur prête",
+ "d6d15077df": "Commande de skill copiée et insérée ci-dessous pour vérification.",
+ "78e65f19d9": "Échec de la configuration du navigateur",
+ "b7345c18db": "Une erreur inattendue s'est produite.",
+ "5f97caf76b": "Installation…",
+ "c2df599513": "Installer la CLI et le skill"
+ },
+ "ConnectIntegrationsList": {
+ "3dddb2d565": "connecté pour les tâches",
+ "33b650af52": "Connectez l'outil où votre équipe suit son travail. Orca démarre les espaces de travail avec le titre de l'issue, le lien et le contexte déjà rattachés.",
+ "5b3577a492": "connecté pour le statut de revue",
+ "list_end": ", et ",
+ "list_pair": " et ",
+ "list_mid": ", ",
+ "code_host_tasks_summary": "issues disponibles comme tâches · ajoutez Linear ou Jira si votre équipe y planifie son travail",
+ "code_host_tasks_caption": "Les issues de votre hébergeur de code servent aussi de tâches.",
+ "review_step_title": "Voyez le statut des PR pendant que les agents travaillent",
+ "review_step_description": "Connectez un fournisseur de revue pour qu'Orca affiche le statut des PR ou MR, les vérifications et les revues.",
+ "task_step_title": "Lancez des agents sur vos tâches sans quitter Orca"
+ },
+ "connect": {
+ "integration": {
+ "step": {
+ "0f47ff17c6": "Modifier",
+ "5538eb6743": "Terminé",
+ "open_step": "Ouvert",
+ "close_step": "Fermer"
+ }
+ }
+ },
+ "FullDiskAccessSetupPrompt": {
+ "bbb3f1e404": "Vérification",
+ "48d87edcd2": "Accordée",
+ "6db9a69f4e": "Recommandé",
+ "fa809e8ada": "Panneau Confidentialité et sécurité de macOS ouvert",
+ "bfa3402305": "Impossible de demander l'autorisation",
+ "c566bca278": "Accès complet au disque",
+ "0d6efe9cf4": "Recommandé sur macOS quand les projets ou les worktrees se trouvent dans des dossiers protégés.",
+ "dac08ec03e": "Ouverture...",
+ "6e3d62b816": "Ouvrir l'accès complet au disque"
+ }
+ },
+ "tips": {
+ "CliFeatureTipVisual": {
+ "badb4fc342": ">",
+ "22e62f3bab": "Session Claude Code démarrée"
+ },
+ "CliSkillSetupTerminal": {
+ "1953e90447": "Appuyez sur Entrée pour installer la skill d'orchestration Orca CLI destinée à vos agents.",
+ "43b60ec5c3": "Terminal d'installation de l'Orca CLI et de la skill d'orchestration",
+ "84e9576dac": "Configuration du skill",
+ "5c3aee22c0": "Copier la commande",
+ "5eca672aac": "Copier la commande d'installation de la skill",
+ "6ff813fc1d": "Impossible de copier la commande de la skill.",
+ "b8ad063571": "Commande d'installation de la skill copiée."
+ },
+ "CmdJPaletteTipDialog": {
+ "c0bb9f869b": "Paramètres → Raccourcis",
+ "8241897205": "Réassignez le raccourci à tout moment dans"
+ },
+ "FeatureTipActions": {
+ "eb04abece8": "Peut-être plus tard"
+ },
+ "FeatureTipsModal": {
+ "c169298e4d": "Terminé",
+ "3c6c478462": "X a terminé, envoyez-lui la tâche de revue. »",
+ "298301b7a0": "worktree",
+ "864e2db28f": "« Quand l'agent dans",
+ "7fc6f02099": "et créez une pull request pour chacune. »",
+ "27c567a89c": "worktrees",
+ "55846c7f95": "« Divisez cette pull request en deux",
+ "4795ac2d4a": "Essayez de demander :",
+ "53905bd076": "Aperçu de développement : ouverture du terminal de configuration des skills.",
+ "1da82af45b": "L'Orca CLI requiert votre attention",
+ "ce13a742d0": "`orca` enregistré dans le PATH.",
+ "d1a86c7eb5": "Ouvrez les paramètres pour terminer la configuration du CLI."
+ },
+ "CmdJPaletteFeatureTipVisual": {
+ "ab94e16d44": "Créer le worktree « {{value0}} »",
+ "d20ccf1e61": "terminé",
+ "379d776971": "saisie",
+ "0418f9becc": "ouvrir"
+ }
+ }
+ },
+ "error": {
+ "boundaries": {
+ "RecoverableRenderErrorBoundary": {
+ "55001880db": "Réessayer",
+ "34a189ae0f": "Le reste de l'application fonctionne toujours. Réessayez ici, ou changez de vue puis revenez.",
+ "ab855c11f4": "Cette partie d'Orca a rencontré une erreur."
+ }
+ }
+ },
+ "emulator": {
+ "pane": {
+ "EmulatorPane": {
+ "59b08fa031": "Aucun émulateur connecté"
+ },
+ "emulator": {
+ "device": {
+ "frame": {
+ "9406c15775": "Écran de l'émulateur",
+ "8f25ffaf8a": "Écran de l'émulateur, clavier capturé. Appuyez sur Échap pour le libérer.",
+ "0022420df0": "téléphone"
+ }
+ },
+ "pane": {
+ "toolbar": {
+ "06e10d7356": "Éteindre l'émulateur",
+ "e7a0d1897e": "Home",
+ "6bd8dff42a": "Pivoter",
+ "3d836b879c": "Choisir un émulateur",
+ "81b3571a07": "Se connecter",
+ "868c0f2938": "Traitement…"
+ }
+ },
+ "screen": {
+ "stream": {
+ "content": {
+ "8b1a0d8694": "Aperçu de l'émulateur",
+ "36841af608": "Flux déconnecté",
+ "5f818f12ab": "Connexion à l'émulateur…",
+ "5ee64cd44e": "Écran de l'émulateur"
+ }
+ }
+ },
+ "unavailable": {
+ "pane": {
+ "f630b9ca9f": "L'émulateur mobile nécessite un Mac avec Xcode et le runtime iOS Simulator. Sous Linux ou Windows, utilisez un appareil physique ou un hôte de build Mac distant.",
+ "b2c268a0b9": "L'émulateur mobile est disponible uniquement sur macOS"
+ }
+ }
+ },
+ "use": {
+ "emulator": {
+ "frame": {
+ "stream": {
+ "f1c0179002": "Le flux ne produit aucune image."
+ }
+ }
+ },
+ "mobile": {
+ "emulator": {
+ "agent": {
+ "setup": {
+ "state": {
+ "fdcca1ec75": "Enregistrement...",
+ "69fb2c2289": "Activé",
+ "c6705092ba": "Corriger PATH",
+ "7c1b6bdb1e": "Activer",
+ "51074ccb05": "Échec du chargement de l'état de la CLI.",
+ "35dea1ae12": "Le contrôle des agents est prêt.",
+ "9dff3a6338": "La skill est installée. Activez l'Orca CLI pour terminer la configuration.",
+ "15986a1080": "L'Orca CLI est prêt. Installez la skill pour terminer la configuration.",
+ "4c26913def": "Toujours pas configuré. Terminez les deux étapes pour activer le contrôle des agents.",
+ "c94ff11e91": "Impossible de revérifier l'état de la configuration.",
+ "2b519eed94": "Orca CLI enregistré dans PATH."
+ }
+ }
+ },
+ "tab": {
+ "intro": {
+ "actions": {
+ "68a5dc6604": "Impossible de masquer l'émulateur mobile."
+ }
+ }
+ }
+ }
+ }
+ },
+ "MobileEmulatorAgentSetupGuide": {
+ "2fda9ff015": "Configurer le contrôle des agents",
+ "0ac0fef514": "Le contrôle des agents est prêt.",
+ "2bdfff8763": "Contrôle des agents (facultatif).",
+ "72736b051f": "Configurez Orca CLI + skill quand vous voulez que des agents pilotent ce simulateur.",
+ "d10ae98046": "Terminé",
+ "3756cbeca7": "Pas maintenant",
+ "6d950431d2": "Masquer",
+ "ebceac65a4": "Configurer",
+ "3f003507f4": "Ouvrir la configuration complète dans les paramètres"
+ },
+ "MobileEmulatorAgentSetupGuideSteps": {
+ "9b49d892e3": "Activer Orca CLI",
+ "3d8dc52c93": "Enregistre la commande orca de contrôle de l'émulateur dans les shells des agents.",
+ "21f5687c07": "Compétence CLI Orca",
+ "64fb057667": "Apprend aux agents les commandes orca de l'émulateur pour ce worktree.",
+ "5c59ea96ca": "Configuration de la skill Orca CLI de l'émulateur mobile",
+ "bff5341ac3": "Terminal d'installation de la skill Orca CLI de l'émulateur mobile",
+ "3d34423e88": "Enregistrement de la CLI Orca",
+ "3be27641c9": "afin que les commandes d'émulateur puissent s'exécuter depuis les shells des agents.",
+ "3941719a56": "Vérification de la CLI Orca avant d'ouvrir la configuration du skill."
+ },
+ "MobileEmulatorTabIntroCallout": {
+ "1924982130": "Ignorer",
+ "5789936d9a": "Prévisualisez les simulateurs iOS pendant que des agents pilotent l'écran.",
+ "8014b4b80b": "Conserver",
+ "6e051a40b7": "Masquer"
+ },
+ "mobile": {
+ "emulator": {
+ "hidden": {
+ "toast": {
+ "e8f098a870": "Émulateur mobile masqué",
+ "c46c979c1d": "Réactivez l'émulateur mobile à tout moment dans",
+ "600f9a745a": "Paramètres › Émulateur mobile"
+ }
+ }
+ }
+ },
+ "useEmulatorScreenKeyboard": {
+ "pasteTooLarge": "Le collage est trop volumineux pour la saisie clavier de l'émulateur.",
+ "unsupportedPasteText": "Le collage via le clavier de l'émulateur prend uniquement en charge le texte de clavier US.",
+ "pasteTargetUnavailable": "Le collage via le clavier de l'émulateur a échoué car l'appareil n'est pas prêt."
+ }
+ }
+ },
+ "editor": {
+ "ChangesModeView": {
+ "ef25ae2d09": "Aucune modification non commitée.",
+ "052c184f24": "Le diff texte est indisponible pour ce fichier.",
+ "7dffb0f563": "Fichier binaire",
+ "54e0035b15": "Chargement du diff..."
+ },
+ "CodeBlockCopyButton": {
+ "28921f5bf9": "Copied",
+ "1f9f4def45": "Copier le code"
+ },
+ "CombinedDiffFileTree": {
+ "f984289373": "Aucun fichier ne correspond aux filtres actuels.",
+ "eafe1aeb53": "Réinitialiser les filtres",
+ "be119cb9d1": "Fichiers consultés",
+ "c00020f081": "Extensions de fichiers",
+ "cd0e0ed79e": "Filtrer les fichiers du diff",
+ "4cc7b83ffe": "Filtrer les fichiers...",
+ "21783df79f": "Réduire l'arborescence des fichiers",
+ "481e63ca52": "Fichiers",
+ "resizeFileTree": "Redimensionner l'arborescence des fichiers",
+ "d5ac717d65": "non committé",
+ "39b6b9e4e4": "Committé sur la branche"
+ },
+ "CombinedDiffViewer": {
+ "35cc27aeb2": "dans le contrôle de code source",
+ "e3b9a6ce02": "de plus",
+ "1da745c551": "Envoyé",
+ "84898c548d": "Effacer",
+ "88b70d0ef5": "Copier",
+ "bb84b4c374": "Notes IA",
+ "948a5fd6c8": "Effacer les notes",
+ "0f806a2ab1": "Annuler",
+ "80a286d8f5": "de ce worktree ?",
+ "7e7ca60816": "fichiers modifiés",
+ "b6c3b84476": "Afficher l'arborescence des fichiers",
+ "39f8007549": "Examiner les conflits",
+ "39e73e7181": "ont été exclus de cette vue de diff.",
+ "689b99f8ad": "conflit non résolu",
+ "820ec01f24": "Les fichiers en conflit sont examinés séparément",
+ "fd8892b120": "Aucune modification à afficher",
+ "eb5f40e49c": "Cette vue de diff exclut les conflits non résolus, car le pipeline de diff bidirectionnel habituel n'est pas conçu pour gérer les conflits.",
+ "45cf23b418": "Échec de l'effacement des notes.",
+ "0fb870a0fe": "notes",
+ "8ab3248fd8": "note",
+ "ec5053c7f5": "Côte à côte",
+ "f786fd54e1": "En ligne",
+ "ea08dae15b": "Tout réduire",
+ "19c45cfdc0": "Tout développer",
+ "982d14bfa5": "Ouvrir toutes les modifications",
+ "3d909843bb": "Ouvrir le diff de branche",
+ "8368d256ec": "combined-branch",
+ "8f68ad9ca9": "Afficher {{value0}} IA {{value1}}",
+ "724a13568d": " dans {{value0}}",
+ "6094135eec": " vs {{value0}}",
+ "a4420ca1f7": "Retour à la ligne activé",
+ "dde325ddfe": "Retour à la ligne désactivé"
+ },
+ "ConflictComponents": {
+ "f338288514": "Chargement du contenu des conflits...",
+ "90d576adb2": "Actualiser",
+ "a1ce36f77d": "Instantané capturé le",
+ "4be41eaafc": "conflit non résolu",
+ "c8ca989aea": "Afficher l'arborescence des fichiers",
+ "58ad5ad431": "Ignorer",
+ "28e7db4a90": "Contrôle de code source",
+ "31931dec46": "Cet instantané de revue ne contient plus aucun conflit non résolu actif.",
+ "992145ff5a": "Tous les conflits sont résolus",
+ "d5edd81755": "Renommé depuis",
+ "6e459867ad": "État de continuité valable pour la session. Git ne signale plus ce fichier comme non fusionné.",
+ "9c2901ef8a": "Conflit suivant",
+ "41d9af2e7a": "Conflit précédent",
+ "55d61a0ccd": "conflit ·",
+ "da539359b6": "Aucun fichier de l'arbre de travail n'est modifiable pour ce conflit."
+ },
+ "ConflictReviewFileTree": {
+ "3449521a8c": "Aucun conflit dans cet instantané.",
+ "a54551c5a6": "Réduire l'arborescence des fichiers",
+ "99496bab6e": "Fichiers",
+ "496e28a932": "Disparu",
+ "8528a5eaf5": "Résolu",
+ "69d4e210bb": "Non résolu"
+ },
+ "CsvViewer": {
+ "eedd0d37a7": "colonnes",
+ "ac31d2cd60": "lignes",
+ "a233d55b77": "Fichier vide"
+ },
+ "DiffNotesSendMenu": {
+ "f1aa04b5cf": "Ce fichier",
+ "8b87612461": "Toutes les notes non envoyées"
+ },
+ "DiffSectionBody": {
+ "35d6afb5be": "Fichier binaire modifié",
+ "cef4cf0ff5": "Réessayer",
+ "f5cf81cec2": "Chargement du diff...",
+ "72f71f52eb": "Le diff texte est indisponible pour ce fichier.",
+ "7ce8436458": "Le diff texte est indisponible pour ce fichier dans la comparaison de branches.",
+ "bdbf02d5df": "binaire",
+ "b5675b0694": "Enregistrer",
+ "593f2193f6": "Ce brouillon dépasse la limite d'affichage sécurisée, mais il peut toujours être enregistré."
+ },
+ "DiffSectionHeader": {
+ "8915726e93": "Copier le chemin"
+ },
+ "EditorContent": {
+ "56dba34e1a": "(modifier en mode source)",
+ "e4b074749d": "Front Matter",
+ "9640d1d3db": "Aperçu de la version modifiée de ce diff. Passez en mode source pour examiner les modifications.",
+ "78541e254e": "Fichier binaire modifié",
+ "c88c73a0d3": "Chargement du diff...",
+ "b9de81ba52": "Fichier binaire — impossible d'afficher",
+ "b2735221f5": "Chargement...",
+ "8608ce4cb1": "L'aperçu Markdown est indisponible pour les fichiers binaires.",
+ "37a0e81fa6": "Chargement de l'aperçu...",
+ "8b1a605bae": "Ce fichier est en état de conflit, mais aucun fichier de l'arbre de travail n'est disponible à l'édition.",
+ "2a512bb46a": "Réessayer",
+ "39f018b052": "Impossible de charger le fichier",
+ "8a0898ae4c": "Le diff texte est indisponible pour ce fichier.",
+ "3c6e71df22": "Le diff texte est indisponible pour ce fichier dans la comparaison de branches.",
+ "d07e4b8553": "branche",
+ "d16e037f40": "enrichi",
+ "6c4f1a8d2e": "Les détails de la vérification sont indisponibles."
+ },
+ "EditorPanelHeader": {
+ "fb8331694e": "Ouvrir l'aperçu sur le côté",
+ "4157f3cbf3": "Ouvrir l'aperçu Markdown",
+ "269ce4842b": "Copier le chemin relatif",
+ "7c08a1f990": "Copier le chemin",
+ "84cdc0794b": "Renommer",
+ "1bb1e226ec": "Renommer le fichier {{value0}}",
+ "5447c4f68f": "Table des matières",
+ "146cb5473c": "La table des matières est disponible en mode enrichi ou aperçu",
+ "e836faacfa": "Passer au diff côte à côte",
+ "94756f08ba": "Passer au diff en ligne",
+ "c98ce191da": "Ce diff n'a aucun fichier du côté modifié à ouvrir",
+ "9b80bbe1de": "Ouvrir un onglet de fichier",
+ "f0fd4174b5": "Ouvrez un onglet de fichier pour utiliser l'édition Markdown enrichie",
+ "a10d9b8337": "Ouvrir le fichier",
+ "2076ecfc9c": "Modification précédente",
+ "631dab0df3": "Modification suivante"
+ },
+ "EditorPanelMarkdownActionsMenu": {
+ "3e0ce48c24": "Exporter en PDF",
+ "561251019a": "Plus d'actions",
+ "8c8b7f5ff5": "Afficher le front matter",
+ "10c39d58c1": "Masquer le front matter",
+ "1eef809708": "Retour à la ligne"
+ },
+ "EditorPanelShell": {
+ "e2c4dec350": "Chargement de l'éditeur..."
+ },
+ "EditorViewToggle": {
+ "b3410cd5e0": "Notebook",
+ "e408aa9cd5": "Tableau",
+ "167f45888c": "Modifications non validées",
+ "4837f3f578": "Modifications",
+ "ac3bb87913": "Édition",
+ "0d193dc03c": "Aperçu",
+ "aff15f94f5": "Éditeur enrichi",
+ "4d6ccb7ba6": "Source"
+ },
+ "ImageDiffViewer": {
+ "a651be62b0": "Modifié",
+ "57aac3979a": "Original",
+ "fb0ae4f3c0": "Aucun aperçu"
+ },
+ "ImageViewer": {
+ "3c9217f5a6": "Zoom avant",
+ "6c89c73d9f": "Réinitialiser le zoom",
+ "be27304574": "Zoom arrière",
+ "77bfc9b35a": "Ouvrir l'image dans une fenêtre indépendante",
+ "3ef9551ba2": "Chargement de l'aperçu...",
+ "d9d2944855": "Échec du chargement de l'aperçu du fichier"
+ },
+ "ImageViewerPopup": {
+ "0ef78475e7": "Appuyez sur Échap pour fermer",
+ "535f4e2b56": "Fermer",
+ "9e27b2ecaf": "Aperçu de l'image en taille réelle"
+ },
+ "IpynbViewer": {
+ "859bf9fc21": "Exécuter la cellule",
+ "7f0d7077c6": "Annuler",
+ "10ed04a685": "Les cellules du notebook exécutent du Python en local sur cette machine depuis le dossier du notebook. N'exécutez que des cellules issues de fichiers de confiance.",
+ "9e06ae5d36": "Exécuter le code du notebook ?",
+ "d6f37a640b": "Notebook vide",
+ "8c3b21369a": "nbformat",
+ "329764e9fc": "BÊTA",
+ "15ec40a735": "Enregistrer le notebook",
+ "07e7d96612": "cellules",
+ "c1601b23b2": "Impossible d'afficher le notebook",
+ "66a3f7d330": "Sortie HTML du notebook",
+ "781abd6926": "Supprimer la cellule",
+ "b42f6a9547": "Insérer une cellule Markdown ci-dessous",
+ "ffc1ac2699": "Insérer une cellule Markdown ci-dessus",
+ "b4208cad7e": "Insérer une cellule de code ci-dessous",
+ "53b839b8a0": "Insérer une cellule de code ci-dessus",
+ "27e064e2db": "Déplacer la cellule vers le bas",
+ "fd8ac707bc": "Déplacer la cellule vers le haut",
+ "3e4cbf15ea": "Brut",
+ "1833dbbc43": "Markdown",
+ "7005960d73": "Code",
+ "59b6cd874b": "code",
+ "ba149053d5": "markdown"
+ },
+ "MarkdownPreview": {
+ "e4683f70c4": "Annuler",
+ "d737791433": "Ajouter une note pour l'IA",
+ "b1bfc04034": "Texte sélectionné",
+ "f37b98999e": "Cette note",
+ "2b2b31382c": "Front Matter",
+ "bb629de58a": "Copier les notes pour l'agent",
+ "322afab6ff": "Notes de revue",
+ "0f9969a159": "Aller à la première note de revue",
+ "12052c639c": "Fermer la recherche",
+ "b42c41bd0d": "Correspondance suivante",
+ "1febd97f5c": "Correspondance précédente",
+ "ec77985138": "Rechercher dans l'aperçu Markdown",
+ "517aea303b": "Rechercher dans l'aperçu",
+ "f961e94057": "Copier la note pour l'agent",
+ "94b520a96a": "Note copiée",
+ "13f94d760c": "Ajouter une note",
+ "ddf087d12e": "Toutes les notes non envoyées",
+ "d652c87c91": "Enregistrement…",
+ "c5dc92cfe3": "Aucun résultat",
+ "6c043947ae": "Fichier introuvable : {{value0}}",
+ "759463a221": "Impossible d'ouvrir le répertoire : {{value0}}"
+ },
+ "MarkdownTableOfContentsPanel": {
+ "de3928b6e4": "Aucun titre",
+ "bbe8369097": "Fermer la table des matières",
+ "4680a4b808": "Réduire jusqu'à H{{value0}}",
+ "a5daadd68b": "Tout développer",
+ "111e66b85d": "Réduire au niveau de titre {{value0}}",
+ "f3de856175": "Développer tous les niveaux de titre",
+ "0dc7b2f05a": "Réduire par niveau",
+ "06357eea60": "Table des matières",
+ "27d0a9c49a": "Table des matières",
+ "65b036a6c8": "Développer {{value0}}",
+ "97ad46f11f": "Réduire {{value0}}",
+ "8f4d2c1a9b": "Redimensionner la table des matières"
+ },
+ "MarkdownTemplatePicker": {
+ "22cd94426f": "untitled.md",
+ "6e2e6c04ad": "Markdown vierge",
+ "df667919ca": "Aucun modèle correspondant.",
+ "22fd4890ad": "Rechercher des modèles...",
+ "7b458e0b7f": "Choisissez un modèle Markdown.",
+ "1829437fce": "Nouveau Markdown"
+ },
+ "MermaidBlock": {
+ "dcc132e691": "Erreur de diagramme :"
+ },
+ "MonacoEditor": {
+ "68cb83f4a7": "Ajouter une note sur le texte sélectionné",
+ "fd68ae03b3": "Rechercher dans les fichiers",
+ "largePasteTooLarge": "Le collage est trop volumineux."
+ },
+ "MonacoGutterContextMenu": {
+ "7b57b1b468": "Copier l'URL du remote",
+ "2e0b1cdc05": "Copier le chemin relatif jusqu'à la ligne",
+ "4eaa991bde": "Copier le chemin jusqu'à la ligne"
+ },
+ "NotesSendMenu": {
+ "44dc5e60a6": "Envoyer les notes",
+ "433928cd9f": "Envoyer {{value0}} à un agent"
+ },
+ "PdfFind": {
+ "cd65b1d6b0": "Fermer",
+ "eeba2547a1": "Correspondance suivante",
+ "30de726ad0": "Correspondance précédente",
+ "2fc3ba0ea8": "Rechercher dans la page...",
+ "d080ab37d6": "Aucun résultat",
+ "db56fcd6d2": "{{value0}} sur {{value1}}"
+ },
+ "PdfViewer": {
+ "3e98d500d2": "Aperçu PDF",
+ "069ff59932": "Rechercher dans le PDF ({{value0}})",
+ "2b6eb1ccd6": "Zoom avant",
+ "c0119616d6": "Ajuster à la largeur",
+ "fa5d096b00": "Zoom arrière"
+ },
+ "ReviewNotesSendMenuContent": {
+ "a49800405b": "Nouvel agent",
+ "e84705f223": "Session d'agent active",
+ "03378aea75": "Envoyer les notes à",
+ "f5096c6e4e": "Impossible d'envoyer les notes à l'agent actif.",
+ "bb9c69a0c9": "Notes envoyées à l'agent actif.",
+ "50f7e753ea": "Envoi des notes à l'agent actif..."
+ },
+ "RichMarkdownAnnotationOverlay": {
+ "069b5677b8": "Texte sélectionné",
+ "6f2f3a6001": "Ajouter une note de revue"
+ },
+ "RichMarkdownCodeBlock": {
+ "232d9ed853": "Copied",
+ "c72beafc0f": "Copier le code",
+ "74eab1d9b2": "YAML",
+ "5ef5605cb7": "XML",
+ "88d777bc07": "TypeScript",
+ "9e384d48dc": "Swift",
+ "3009f722b9": "SQL",
+ "d01f55be57": "Shell",
+ "5af8251002": "SCSS",
+ "e72e6b03f4": "Rust",
+ "96182a2f64": "Ruby",
+ "2391f9cda9": "Python",
+ "89d6cc14fb": "Mermaid",
+ "983b9576b4": "Markdown",
+ "bcb236e2d8": "Kotlin",
+ "78eba32de4": "JSON",
+ "a209c57063": "JavaScript",
+ "36536ad539": "Java",
+ "8c4a3fa02d": "HTML",
+ "706fd85738": "GraphQL",
+ "edfcc64182": "Go",
+ "bf6ee5caaa": "Diff",
+ "026653f21f": "CSS",
+ "4daed43ae3": "C++",
+ "4227cf50fe": "Bash",
+ "13822cdfda": "Texte brut"
+ },
+ "RichMarkdownDocLinkMenu": {
+ "e17b987473": "↑↓ naviguer ↵ sélectionner esc fermer",
+ "90c5f0e1e4": "sur",
+ "2aaf7d9678": "Affichage de",
+ "63ced7cb9b": "Aucun document trouvé",
+ "0e8489bc11": "Liens de documents Markdown",
+ "142a7d51cd": "document"
+ },
+ "RichMarkdownErrorBoundary": {
+ "aad0998127": "Réessayer",
+ "4a5de9f2f0": "Passez en mode source, ou cliquez sur Réessayer pour recharger la vue enrichie.",
+ "dfdf1cacd4": "L'éditeur Markdown enrichi a rencontré une erreur inattendue et a été réinitialisé pour que le reste d'Orca reste réactif."
+ },
+ "RichMarkdownLinkBubble": {
+ "1c99b726e0": "Supprimer le lien",
+ "cdfe166f6f": "Modifier le lien",
+ "bfc813e909": "Ouvrir le lien",
+ "7b0b945fdc": "Collez ou saisissez un lien…",
+ "copyLink": "Copier le lien"
+ },
+ "RichMarkdownReviewNoteLayer": {
+ "f3ef92952b": "Cette note",
+ "9cde7ad994": "Copier la note pour l'agent",
+ "117432e2c6": "Note copiée",
+ "3ababd949d": "Notes de revue"
+ },
+ "RichMarkdownReviewRailActions": {
+ "636394af72": "Copier les notes pour l'agent",
+ "a807596997": "Notes copiées",
+ "8aaf2c4c69": "Afficher les notes de revue",
+ "af02dc2456": "Masquer les notes de revue"
+ },
+ "RichMarkdownSearchBar": {
+ "de68b75bde": "Fermer la recherche",
+ "f7bcecbe26": "Correspondance suivante",
+ "32ae8d7d57": "Correspondance précédente",
+ "158c645829": "Rechercher dans l'éditeur Markdown enrichi",
+ "98b89276f3": "Rechercher dans l'éditeur enrichi",
+ "a86958d508": "Aucun résultat",
+ "e8c147435f": "Masquer le remplacement",
+ "9cdc38be33": "Afficher/masquer le remplacement",
+ "482b637099": "Respecter la casse",
+ "68d090241d": "Mot entier uniquement",
+ "fd97c7e585": "Remplacer",
+ "44682b4159": "Remplacer dans l'éditeur Markdown enrichi",
+ "c2884f5e95": "Tout remplacer",
+ "preservedRichContentReadOnly": "Le contenu enrichi préservé est en lecture seule en mode enrichi."
+ },
+ "RichMarkdownSlashMenu": {
+ "82c6816ff8": "Aucun bloc trouvé",
+ "dbdd2ad15f": "Rechercher des blocs...",
+ "550189b06c": "Rechercher des blocs",
+ "2e0400b958": "Commandes slash",
+ "e2e12b0e98": "composant"
+ },
+ "RichMarkdownToolbar": {
+ "e935c6b61e": "Image",
+ "6d52624712": "Lien",
+ "f6a51cb9af": "Citation",
+ "f97031be09": "Liste de tâches",
+ "31630ed66e": "Liste numérotée",
+ "5d1539e5a9": "Liste à puces",
+ "0bea19a988": "Barré",
+ "6b4ccf9493": "Italique",
+ "4f9e789fe0": "Gras",
+ "cf5817d827": "Titre 3",
+ "d34a2021c8": "Titre 2",
+ "abb5100a3d": "Titre 1",
+ "b462641ed2": "Texte courant",
+ "91a843fb43": "Plus de blocs",
+ "2cd9e0bbb3": "Titres",
+ "b05e14620d": "Titre 4",
+ "6bbf827ef5": "Titre 5",
+ "d1bbf9a835": "Section repliable"
+ },
+ "UntitledFileRenameDialog": {
+ "a7dd27b0bc": "Enregistrer",
+ "949711deb4": "Annuler",
+ "725868c75d": "Parcourir les dossiers",
+ "5e7f0d8a80": "Sélecteur de dossiers indisponible pour les fichiers distants",
+ "30099dca46": "Dossier",
+ "2d7d39dc63": ".md",
+ "c8ac7868e6": "nom de fichier",
+ "b6ed807cc6": "Nom",
+ "e365f3c638": "Nommez votre fichier Markdown et choisissez un dossier.",
+ "674b046582": "Enregistrer sous"
+ },
+ "export": {
+ "active": {
+ "markdown": {
+ "51c4244904": "Exporté vers {{value0}}",
+ "d4a901e0ad": "Export du PDF...",
+ "eda2cea3ad": "Échec de l'export du PDF"
+ }
+ }
+ },
+ "markdown": {
+ "rich": {
+ "mode": {
+ "7a8ce7c7da": "Modifiable uniquement en mode code, car ce fichier contient des notes de bas de page.",
+ "2fd2b44073": "Modifiable uniquement en mode code, car ce fichier contient des liens par référence.",
+ "57128b73e1": "Modifiable uniquement en mode code, car ce fichier contient du HTML, du JSX ou du MDX."
+ }
+ }
+ },
+ "rich": {
+ "markdown": {
+ "editor": {
+ "click": {
+ "routing": {
+ "2d5fb9335d": "Fichier introuvable : {{value0}}"
+ }
+ }
+ },
+ "slash": {
+ "commands": {
+ "07e1b32396": "Insérer un emoji Unicode simple.",
+ "8a30cbaeca": "Émoji",
+ "3324eb391a": "Insérer une image depuis votre ordinateur.",
+ "572be8e524": "Image",
+ "ae7d0f3f37": "Insérer une formule LaTeX en bloc.",
+ "6993a38ad1": "Bloc mathématique",
+ "565907cf7a": "Insérer une formule LaTeX en ligne.",
+ "2bf5544faf": "Maths en ligne",
+ "0ed9a7b38c": "Insérer un bloc de code Mermaid.",
+ "e516d3f6e3": "Diagramme Mermaid",
+ "67faab829b": "Insérer un tableau Markdown 3x3.",
+ "19ea597868": "Tableau",
+ "fae45ef4d3": "Insérer une ligne horizontale.",
+ "ae8377cf6b": "Séparateur",
+ "89e327e054": "Insérer un bloc de code délimité.",
+ "624b50cf25": "Bloc de code",
+ "972ef9aeea": "Créer une section de texte repliable.",
+ "f82c78a2ee": "Texte repliable",
+ "9a7fe896dc": "Commencer un paragraphe normal.",
+ "58abdb9d41": "Paragraphe",
+ "d766f44867": "Créer une liste de cases à cocher.",
+ "d0d2cdfbdb": "Liste de cases à cocher",
+ "c9b9e826b8": "Créer une liste à puces.",
+ "56ff3237e7": "Liste à puces",
+ "8e00aba296": "Créer une liste ordonnée.",
+ "ed4cf0ebce": "Liste numérotée",
+ "6a3def14de": "Insérer un bloc de citation.",
+ "c4c775778b": "Citation",
+ "4920740259": "Petit titre de section.",
+ "30566ee962": "Titre 3",
+ "45cf7ceb3f": "Titre de section moyen.",
+ "c209a116b7": "Titre 2",
+ "3294a2c0cc": "Créer une section repliable avec un grand titre de résumé.",
+ "41482b15ce": "Titre repliable H1",
+ "570611864e": "Grand titre de section.",
+ "e66e7f04c6": "Titre 1",
+ "5f9a0ed7c4": "Titre 4",
+ "01a71dbbdd": "Titre de section imbriqué.",
+ "8440fa4acf": "Titre 5",
+ "b287b93c66": "Titre de section profond.",
+ "7a2c1f9b04": "Titre repliable H2",
+ "b3e5d8a1c6": "Créer une section repliable avec un titre de résumé moyen.",
+ "2f9d6b4e10": "Titre repliable H3",
+ "8c1a3e7d52": "Créer une section repliable avec un petit titre de résumé.",
+ "5e0b9c2a71": "Titre repliable H4",
+ "d4f16a8b39": "Créer une section repliable avec un titre de résumé imbriqué.",
+ "21d8c463e5": "Titre repliable H5",
+ "dc239b41ad": "Créer une section repliable avec un titre de résumé profond."
+ }
+ }
+ }
+ },
+ "useContextualCopySetup": {
+ "059bfb0d94": "Contexte copié"
+ },
+ "useLocalImagePick": {
+ "175cb8b8ce": "Échec de l'insertion de l'image.",
+ "91d835dc88": "Chemin du worktree indisponible."
+ },
+ "useRichMarkdownReviewData": {
+ "f9d2acd6b0": "Toutes les notes non envoyées"
+ },
+ "LargeDiffFallback": {
+ "a3c74f8a21": "nombre de lignes supérieur à la limite d'affichage sécurisée",
+ "fd92fbde46": "nombre de caractères supérieur à la limite d'affichage sécurisée",
+ "7d424bb761": "Ce diff est trop volumineux pour être affiché sans risque.",
+ "28aa2cc90b": "Lignes d'origine",
+ "20857938dd": "Lignes modifiées",
+ "e5f0d2182e": "Caractères",
+ "877c25a02f": "Raison",
+ "5fca073b72": "Limites",
+ "f1d136a163": "lignes par côté",
+ "23433fcdea": "caractères cumulés",
+ "7944ed9fb8": "Non compté"
+ },
+ "DiffViewer": {
+ "b5675b0694": "Enregistrer",
+ "593f2193f6": "Ce brouillon dépasse la limite d'affichage sécurisée, mais il peut toujours être enregistré."
+ },
+ "CheckRunDetailsPanel": {
+ "8f2d0f5a91": "Réussi",
+ "4c8e1b2d73": "Échec",
+ "91a4c7e2b0": "Annulé",
+ "2f6d8a1c45": "Délai dépassé",
+ "7b3e9d4f12": "Ignoré",
+ "5a1c8e3d67": "Neutre",
+ "3d9f2b8e14": "En attente",
+ "b7f5e2c91a": "Actualiser",
+ "a54ae21c6f": "Statut :",
+ "fd46a70f1a": "Démarré",
+ "00e1c1658a": "Terminé",
+ "aa8494ae3c": "vérification #",
+ "2dd5ddabc4": "workflow #",
+ "1f2b980522": "Chargement des détails de la vérification…",
+ "d098e5529a": "Sortie",
+ "f2fe8a4e8f": "Annotations",
+ "cdbfda4dec": "Annotation",
+ "5e2a9c3f88": "Ouvrir le fichier à cette ligne",
+ "066fedd446": "Jobs en échec",
+ "49731703ea": "Jobs",
+ "ee07b33924": "unknown",
+ "07eccfa397": "Aucun détail disponible pour cette vérification.",
+ "a916648574": "Ouvrir les détails",
+ "834cb3f23d": "Corriger avec l'IA",
+ "c8f1a2d4e7": "Choisissez l'agent et modifiez la commande complète avant le lancement.",
+ "b3e7f9a1c2": "Contexte de correction de la vérification indisponible",
+ "d5a8c2f1b9": "Démarrer l'agent IA par défaut pour corriger cette vérification",
+ "e2b4d7c8a1": "Choisir un agent pour cette vérification",
+ "f1c9e3a6d4": "Choisir un agent pour corriger la vérification",
+ "actionRequired": "Action requise"
+ },
+ "CheckRunJobs": {
+ "1c0a4d7e02": "réussi",
+ "2d3b8f1a55": "ignoré",
+ "3e6c9a2b71": "en attente",
+ "4f7d0c3e88": "étapes en échec",
+ "5a8e1d4f23": " · "
+ },
+ "check": {
+ "run": {
+ "details": {
+ "fix": {
+ "with": {
+ "ai": {
+ "1a8c4e2b90": "Sélectionnez un espace de travail avant de lancer une action IA.",
+ "4f2d9a8c17": "Sélectionnez un dépôt avant de lancer une action IA.",
+ "7c3e1b5d42": "Ouvrez une pull request ou une MR avant de lancer une correction IA.",
+ "9b2f6d4a81": "Cette vérification n'est pas en échec.",
+ "2ef90c9819": "Un agent IA a été démarré pour cette vérification."
+ }
+ }
+ }
+ }
+ }
+ },
+ "richMarkdownLargeTextPaste": {
+ "tooLarge": "Le collage est trop volumineux."
+ },
+ "ExternalFileChangeBanner": {
+ "7c41e90d12": "Ce fichier a été modifié sur le disque alors que vous avez des modifications non enregistrées. L'enregistrement écrasera le contenu plus récent présent sur le disque.",
+ "3fa2b8d417": "Recharger depuis le disque",
+ "a95d02c644": "Garder mes modifications",
+ "5c02de9b31": "Rechargé depuis le disque",
+ "d1e830fa22": "Annuler",
+ "90b2ce7d43": "Comparer"
+ },
+ "ExternalFileChangeCompareDialog": {
+ "4b8de20a11": "Fichier modifié sur le disque",
+ "90cc31e4d7": "Version du disque à gauche, vos modifications non enregistrées à droite.",
+ "8fe30ab254": "Lecture du fichier depuis le disque...",
+ "e2b1cd0393": "Impossible de lire le fichier depuis le disque : {{value0}}",
+ "b6cf20d514": "Le fichier sur le disque est binaire — aucune comparaison de texte disponible.",
+ "3fa2b8d417": "Recharger depuis le disque",
+ "a95d02c644": "Garder mes modifications",
+ "2c8f1e07b9": "Chargement de la comparaison..."
+ },
+ "RichMarkdownEditor": {
+ "citationLinkAvailable": "{{value0}}, lien vers {{value1}}. Appuyez sur Entrée pour ouvrir, ou Tab pour les actions du lien.",
+ "citationFallbackLabel": "Citation",
+ "citationLinkUnavailable": "{{value0}}, lien de citation indisponible. {{value1}}",
+ "tabForCitationActions": "Tab pour les actions disponibles.",
+ "noCitationActions": "Aucune action de lien disponible."
+ },
+ "richMarkdownHtmlSuperscriptLink": {
+ "availableAriaLabel": "{{value0}}, lien vers {{value1}}",
+ "unavailableAriaLabel": "{{value0}}, lien de citation indisponible"
+ },
+ "richMarkdownLinkClipboard": {
+ "copiedLink": "Lien copié",
+ "copyLinkFailed": "Échec de la copie du lien"
+ },
+ "richMarkdownSourceOwningCutFeedback": {
+ "selectLessContent": "Sélectionnez moins de contenu ou utilisez le mode code pour couper les citations HTML préservées."
+ },
+ "editor": {
+ "save": {
+ "failure": {
+ "notice": {
+ "8c59ce5075": "Échec de l'enregistrement du fichier. Veuillez réessayer."
+ }
+ }
+ }
+ },
+ "RichMarkdownTableControls": {
+ "deleteTable": "Supprimer le tableau",
+ "tableActions": "Actions du tableau",
+ "addColumn": "Ajouter une colonne",
+ "addRow": "Ajouter une ligne",
+ "rowActions": "Actions de ligne",
+ "columnActions": "Actions de colonne",
+ "insertRowAbove": "Insérer une ligne au-dessus",
+ "insertColumnLeft": "Insérer une colonne à gauche",
+ "insertRowBelow": "Insérer une ligne en dessous",
+ "insertColumnRight": "Insérer une colonne à droite",
+ "deleteRow": "Supprimer la ligne",
+ "deleteColumn": "Supprimer la colonne"
+ }
+ },
+ "diff": {
+ "comments": {
+ "DiffCommentCard": {
+ "109a791e7b": "Enregistrer",
+ "bb0a55f856": "Enregistrement…",
+ "0203bed775": "Annuler",
+ "6978871a3d": "Ouvert",
+ "cce596969e": "Supprimer la note",
+ "cad3384faa": "Modifier la note",
+ "508ee678a5": "Ouvrir dans le navigateur"
+ },
+ "DiffCommentPopover": {
+ "2b3ce6d394": "Annuler",
+ "e05063cfc1": "Ligne {{value0}}",
+ "c845170b3b": "Lignes {{value0}}-{{value1}}",
+ "commentTooLarge": "Le commentaire est trop volumineux pour être envoyé sans risque."
+ },
+ "useDiffCommentDecorator": {
+ "995fa28b50": "Cette note"
+ }
+ }
+ },
+ "dictation": {
+ "DictationController": {
+ "de136f1199": "Erreur vocale : {{value0}}",
+ "7afff43472": "La dictée est terminée, mais aucun champ de texte n'avait le focus.",
+ "55127a3706": "Échec de la dictée : {{value0}}",
+ "bb7f599ee7": "Ouvrir les paramètres",
+ "2d5b9fabf9": "Accès au micro refusé. Accordez l'autorisation dans les réglages système, puis redémarrez Orca.",
+ "5d2c3e7ae3": "Aucune parole détectée.",
+ "micFallback": "Micro sélectionné indisponible. Utilisation du micro par défaut du système.",
+ "micDisconnected": "Micro déconnecté. Dictée arrêtée."
+ },
+ "DictationIndicator": {
+ "335e1bc6cb": "Arrêter la dictée",
+ "7f3660a7ba": "Démarrage du micro…",
+ "f082d0cb9d": "Traitement…",
+ "3de5a129e7": "Écoute en cours",
+ "4977162383": "Trop fort",
+ "25f2b7a6a5": "Vous parlez"
+ }
+ },
+ "dashboard": {
+ "DashboardAgentChildDisclosure": {
+ "1b57ce9fa4": "{{value0}} {{value1}} enfant {{value2}}"
+ },
+ "DashboardAgentRow": {
+ "912e136cd9": "Envoyer",
+ "a743da52ff": "Développer les détails",
+ "a41fb5376e": "Réduire les détails",
+ "5ae84475cc": "Ignorer",
+ "b06e13fcf7": "Rejeter l'agent",
+ "0272969e28": "Envoyer à cet agent",
+ "92a7017987": "envoi",
+ "019b74d93a": "éligible"
+ },
+ "DashboardAgentRowMessage": {
+ "0a01046763": "interrompu",
+ "1ec01cef03": "Interrompu par l'utilisateur"
+ }
+ },
+ "crash": {
+ "report": {
+ "CrashReportDialog": {
+ "b4951cd27c": "Envoyer le rapport",
+ "88fea8e84e": "Ne pas envoyer",
+ "50b00dc327": "Copier les détails",
+ "6d3ebe216a": "Texte de diagnostic",
+ "835037edc9": "· Orca",
+ "56a3dfa283": "Échec de l'envoi du rapport de plantage.",
+ "8e24fe4f75": "Rapport de plantage envoyé.",
+ "8b8473c544": "Rapport de plantage copié.",
+ "b175e90213": "Aucun rapport de plantage disponible.",
+ "765591798d": "Recherche de rapports de plantage...",
+ "b2e36f53a1": "Échec de l'envoi du rapport de plantage. Le ticket de diagnostic {{value0}} a été téléversé, mais pas associé.",
+ "ead6fc0510": "Aucun rapport de plantage automatique n'a été capturé. Vous pouvez toujours envoyer des détails et inclure les journaux de diagnostic récents lorsqu'ils sont disponibles.",
+ "b082f27490": "Joindre les journaux de diagnostic récents",
+ "e59f0b9427": "Envoie avec le rapport un lot de journaux anonymisés et de taille limitée."
+ },
+ "submit": {
+ "notice": {
+ "unknownError": "La demande de rapport de plantage a échoué avant de renvoyer une raison.",
+ "ticketUploaded": "Le ticket de diagnostic {{value0}} a été téléversé, mais pas associé.",
+ "uncheckDiagnostics": "Décochez « Joindre les journaux de diagnostic récents » et réessayez, ou copiez les détails.",
+ "checkConnection": "Vérifiez votre connexion et réessayez, ou copiez les détails.",
+ "notSent": "Le rapport de plantage n'a pas été envoyé",
+ "copyDetails": "Copier les détails",
+ "sentWithoutDiagnostics": "Rapport de plantage envoyé sans journaux de diagnostic",
+ "diagnosticsReason": "Journaux de diagnostic non joints : {{value0}}"
+ }
+ },
+ "copy": {
+ "copyFailed": "Impossible de copier les détails du rapport de plantage."
+ }
+ }
+ },
+ "contextual": {
+ "tours": {
+ "ContextualTourControl": {
+ "186eecc34f": "Nommer automatiquement l'espace de travail d'après le premier message de l'agent",
+ "02e8373219": "Génère automatiquement un nouveau nom lorsque vous laissez cette zone de texte vide.",
+ "731c5573df": "Nom automatique d'après le premier message"
+ },
+ "ContextualTourOverlaySurface": {
+ "4a9568f773": "Retour",
+ "4f86e2a10b": "Passer la visite guidée",
+ "d974f32a83": "Fermer la visite guidée",
+ "ffa4412b66": "suivant",
+ "complete": "Terminé"
+ },
+ "ContextualTourProgressDots": {
+ "7734cb8ad3": "sur",
+ "dcd6e6b03e": "Étape {{value0}} sur {{value1}}"
+ },
+ "contextual": {
+ "tour": {
+ "overlay": {
+ "measurement": {
+ "38b3155418": "Suivant",
+ "automations": {
+ "intro": {
+ "title": "Qu'est-ce qu'une automatisation ?",
+ "body": "Les automatisations exécutent le travail des agents de façon planifiée. Ajoutez une automatisation en cliquant sur ce bouton."
+ },
+ "results": {
+ "title": "Trouver les résultats",
+ "body": "Les exécutions montrent quand les automatisations ont tourné, ce qui s'est passé et où inspecter leur sortie."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "cmd": {
+ "j": {
+ "quick": {
+ "actions": {
+ "c884a6398e": "Créer une commande de terminal enregistrée.",
+ "a43ab56fc1": "Ajouter une commande rapide",
+ "54853d52a2": "Supprimer le worktree actuel.",
+ "9537b910fe": "Supprimer le worktree",
+ "0b1f25f796": "Démarrer un nouveau worktree.",
+ "52ac9da671": "Créer un worktree",
+ "f70812764a": "Ouvrir un onglet de terminal dans l'espace de travail actif.",
+ "34980395d4": "Nouvel onglet de terminal",
+ "f2a1b33f8d": "Créer un fichier markdown sans titre dans l'espace de travail actif.",
+ "25349b66fc": "Nouveau fichier markdown",
+ "784812ca24": "Ouvrir un onglet de navigateur dans l'espace de travail actif.",
+ "892bfa9339": "Nouvel onglet de navigateur",
+ "verbs": {
+ "newBrowser": "nouveau navigateur",
+ "newBrowserTab": "nouvel onglet de navigateur",
+ "openBrowser": "ouvrir le navigateur",
+ "browserTab": "onglet de navigateur",
+ "newMarkdown": "nouveau markdown",
+ "newMarkdownFile": "nouveau fichier markdown",
+ "newMark": "nouveau mark",
+ "newFile": "nouveau fichier",
+ "markdownFile": "fichier markdown",
+ "newTerminal": "nouveau terminal",
+ "newTerminalTab": "nouvel onglet de terminal",
+ "newShell": "nouveau shell",
+ "terminalTab": "onglet de terminal",
+ "createWorktree": "créer un worktree",
+ "addWorktree": "ajouter un worktree",
+ "newWorktree": "nouveau worktree",
+ "deleteWorktree": "supprimer le worktree",
+ "deleteCurrentWorktree": "supprimer le worktree actuel",
+ "removeWorktree": "retirer le worktree",
+ "trashWorktree": "mettre le worktree à la corbeille",
+ "addQuickCommand": "ajouter une commande rapide",
+ "newQuickCommand": "nouvelle commande rapide"
+ }
+ }
+ },
+ "palette": {
+ "project": {
+ "results": {
+ "repoGroup": "Groupe de dépôts",
+ "project": "Projet"
+ }
+ }
+ },
+ "pluginQuickActions": {
+ "description": "commande du plugin {{value0}}",
+ "keyword": "commande de plugin"
+ }
+ }
+ },
+ "browser": {
+ "pane": {
+ "BrowserFind": {
+ "c9d5f63fdc": "Fermer",
+ "5c0c02ae76": "Correspondance suivante",
+ "ca7aebbd7f": "Correspondance précédente",
+ "636a69cd66": "Rechercher dans la page...",
+ "7baca7b1b8": "Aucun résultat",
+ "fc63f336aa": "{{value0}} sur {{value1}}"
+ },
+ "BrowserImportHintButton": {
+ "05e675fe96": "Masquer l'astuce",
+ "77351d22f5": "Paramètres du navigateur",
+ "e0e125e074": "Depuis un fichier…",
+ "0c6d254eca": "Depuis {{value0}}",
+ "244266c122": "Importer…",
+ "e52a955e6f": "Vous retrouverez toujours cette option dans Paramètres > Navigateur.",
+ "4f5ffaa6a1": "Importer les données du navigateur",
+ "b24fef25be": "Importer",
+ "02e89014c5": "{{value0}} cookies importés depuis {{value1}}{{value2}}.",
+ "d40d584769": "{{value0}} cookies importés depuis un fichier."
+ },
+ "BrowserMobileDriverOverlay": {
+ "a6914ee43f": "Reprendre",
+ "f4ecd61552": "Cet onglet est contrôlé depuis votre téléphone. Reprenez-le pour l'utiliser sur ordinateur.",
+ "d9768ec642": "Saisie du navigateur en pause",
+ "20539eca03": "Le mobile contrôle ce navigateur",
+ "7c31b0da94": "Impossible de reprendre cet onglet. Vérifiez la session sur le téléphone, puis réessayez."
+ },
+ "BrowserPane": {
+ "1ded0d3168": "Copier la capture d'écran",
+ "f30d2d35a7": "Capture effectuée",
+ "fa6ea61de3": "Annuler",
+ "c2ef0359b9": "Copier le contenu",
+ "f2d0c22d67": "Supprimer l'annotation {{value0}}",
+ "11c5084aa2": "Effacer les annotations",
+ "734e4343ec": "Effacer les annotations du navigateur",
+ "95af781091": "Envoyer un retour à un agent",
+ "ac39b9366b": "Envoyer",
+ "a3508d7e6e": "{{value0}} annotation{{value1}} prête. Sélectionnez un autre élément ou copiez tous les retours.",
+ "f796c774a4": "Saisissez une URL ci-dessus pour commencer à naviguer.",
+ "366bf5d62c": "Nouvel onglet",
+ "1c78adc73d": "Ouvrir en externe",
+ "da68d35f7b": "Ouvrir la page en erreur dans le navigateur par défaut",
+ "93be92f8d1": "Copier l'adresse",
+ "3c085f638d": "Copier l'URL de la page en erreur",
+ "c6be71329e": "Actualiser",
+ "781d6459ad": "Réessayer",
+ "2fdca7df09": "Ignorer",
+ "8b6fab9ffa": "Enregistrer",
+ "0f41bf80c7": "Ouvrir dans le navigateur par défaut",
+ "ec75d0c412": "Ouvrir les devtools du navigateur",
+ "fc9be38f6f": "Annoter un élément de la page",
+ "fdfc7fe0ef": "Capturer un élément de la page",
+ "a8f37f70c3": "Inspecter la page",
+ "1b179ab561": "Copier l'URL de la page",
+ "f7ab83f7ed": "Ouvrir la page dans le navigateur par défaut",
+ "0e080d820e": "Recharger",
+ "a1f3c2e4b5": "Rechargement forcé",
+ "b7e4d9c1a2": "Arrêter",
+ "250a9b3e42": "Transférer",
+ "40edfa75cb": "Retour",
+ "efb0e8f7f3": "Copier l'adresse du lien",
+ "8ce4f6b12e": "Ouvrir le lien dans le navigateur par défaut",
+ "b5b87d6cbb": "Ouvrir le lien dans le navigateur Orca",
+ "87eb75f7d2": "Saisissez une URL http(s) ou localhost valide.",
+ "27d863542c": "Annotations du navigateur",
+ "e48569ac6d": "Impossible d'accéder à ce site.",
+ "bbe8f15e83": "Ce volet est rendu depuis le serveur runtime actif.",
+ "8b7e6d1f5a": "Les annotations du navigateur ne sont disponibles que dans les onglets locaux.",
+ "deb5293610": "Annotations du navigateur indisponibles dans un runtime distant",
+ "90d021f2ad": "Ajouter",
+ "0cb3bd6221": "Intention d'annotation",
+ "8f87e6c2e5": "Intention",
+ "532bac48c5": "Décrivez ce que l'agent doit modifier ici...",
+ "d2a7092e6e": "Commentaire d'annotation",
+ "b472c5fe03": "Ajouter une annotation au navigateur",
+ "b5ba6085de": "Question",
+ "143204e423": "Modifier",
+ "b71dc3d930": "Reconnecter",
+ "e7ca5a098c": "succès",
+ "d51ef37351": "Copier",
+ "6f4ab3592b": "Copied",
+ "b2856516e2": "Impossible de charger cette page",
+ "db325a7eeb": "Impossible d'accéder à {{value0}}",
+ "499b31b84e": "Tout copier",
+ "e72dfa268a": "annoter",
+ "168350ae6a": "Cliquez ou survolez un élément, puis appuyez sur C pour copier ou S pour capturer.",
+ "e852e20cea": "Copié — appuyez sur S pour capturer, ou sélectionnez un autre élément",
+ "a5dcd0fd1d": "confirmation",
+ "777b5bc4ec": "Cliquez sur un élément pour ajouter un retour pour l'agent.",
+ "b733a91bd9": "Ajouter un retour pour l'élément sélectionné.",
+ "4328a0a062": "Échec de la capture : {{value0}}",
+ "26615e116b": "error",
+ "8aec5bc044": "idle",
+ "759f32af29": "Téléchargement",
+ "c8bc7f1f9e": "demandé",
+ "4300f38145": "Téléchargement depuis {{value0}}{{value1}}",
+ "31375046b7": "Télécharger depuis {{value0}}",
+ "acbe79fd01": "Capturer un élément de la page ({{value0}})",
+ "572046436a": "Navigateur distant",
+ "b313a7275b": "Ouverture du navigateur distant",
+ "5f66313863": "annotation",
+ "ea6af700da": "{{value0}} annotation",
+ "c13693fe27": "{{value0}} annotations",
+ "074f0ed10b": "{{value0}} annotation prête. Sélectionnez un autre élément ou copiez tous les retours.",
+ "a2164a6e5a": "{{value0}} annotations prêtes. Sélectionnez un autre élément ou copiez tous les retours.",
+ "9f6f2e8c19": "Le chemin du fichier téléchargé est indisponible.",
+ "0c79b7634d": "Impossible d'ouvrir le fichier téléchargé. Il a peut-être été déplacé ou supprimé.",
+ "397d9dc923": "Impossible d'afficher le fichier téléchargé. Il a peut-être été déplacé ou supprimé.",
+ "39c04fed61": "Téléchargement en pause",
+ "5c3d530a68": "Téléchargé",
+ "4bb7424d6b": "Annulé",
+ "6e776f9ef9": "Échec du téléchargement",
+ "756bfc25c9": "Ouvert",
+ "09a9489aa5": "Afficher",
+ "2a4c4b8e1f": "Copier"
+ },
+ "BrowserToolbarMenu": {
+ "429ef481f9": "Annuler",
+ "64f448fb6e": "Nom du profil",
+ "67e9b9fcd6": "Nouveau profil de navigateur",
+ "58f2c81542": "Basculer",
+ "a38f217b46": "Changer de profil rechargera cette page. Toutes les données de formulaire non enregistrées seront perdues.",
+ "fe683eb3b4": "Changer de profil",
+ "a771c2b6c8": "Paramètres du navigateur…",
+ "ed8f54509d": "Par défaut",
+ "e5d31de1a9": "Taille de la zone d'affichage",
+ "56f94f4ffa": "Depuis un fichier…",
+ "eb280bfb11": "Depuis {{value0}}",
+ "2293adf620": "Importer des cookies",
+ "cf7cdc67ef": "Nouveau profil…",
+ "7b838540c7": "Menu du navigateur",
+ "6aa42813e4": "{{value0}} cookies importés depuis {{value1}}{{value2}}.",
+ "a7a86702b3": "Profil {{value0}} créé et activé",
+ "4d2f9f13a7": "Échec de la création du profil.",
+ "3ccd29d771": "Profil {{value0}} activé",
+ "569bce8eb1": "Créer",
+ "bf648471c5": "Création…",
+ "53bbe3dab4": "{{value0}} cookies importés depuis un fichier.",
+ "c5f0e4d3b2a1": "{{value0}} cookies importés depuis {{value1}} ({{value2}}).",
+ "d6a1f5e4c3b2": "{{value0}} cookies importés depuis {{value1}}."
+ },
+ "GrabConfirmationSheet": {
+ "314a0aaa5b": "Joindre à l'IA",
+ "7095e98362": "Copier la capture d'écran",
+ "26fd87f4df": "Copier",
+ "87d97bdd6d": "Annuler",
+ "effd75e330": "Contexte proche",
+ "7d1480fbf1": "HTML",
+ "9098b118ab": "Page",
+ "eb98a0971a": "\"",
+ "d053db279d": "role=",
+ "a759d8f866": "Élément sélectionné",
+ "9c6ce0632a": "Capture d'écran de l'élément sélectionné",
+ "50f7114f99": "Vérifiez avant de joindre. Le contexte de page capturé peut inclure du contenu visible du site.",
+ "f3575229df": "Capturer",
+ "405bb315da": "Sans titre"
+ },
+ "browser": {
+ "address": {
+ "bar": {
+ "suggestions": {
+ "87fcdd0da9": "Recherche {{value0}}"
+ }
+ }
+ }
+ },
+ "annotate": {
+ "use": {
+ "browser": {
+ "page": {
+ "grab": {
+ "annotations": {
+ "0c7b9b2b7a": "Copied",
+ "c937229f19": "Capture effectuée",
+ "1f5cb19034": "Annotation ajoutée"
+ }
+ }
+ }
+ }
+ }
+ },
+ "navigate": {
+ "use": {
+ "browser": {
+ "page": {
+ "navigation": {
+ "downloads": {
+ "8683b84b9e": "La page du navigateur n'est pas prête pour le dépôt de fichiers.",
+ "22272f2784": "Déposez les fichiers sur la page du navigateur, pas sur la barre d'outils."
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "profile": {
+ "user": {
+ "agent": {
+ "option": {
+ "04af3dc12b": "Utiliser le user agent non modifié",
+ "5bf47a3c91": "Peut améliorer la connexion à Google, mais réduire la compatibilité avec les sites protégés contre les bots."
+ }
+ }
+ }
+ },
+ "webauthn": {
+ "account": {
+ "fallback": "Clé d'accès",
+ "title": "Choisir une clé d'accès",
+ "description": "Choisissez le compte à utiliser avec cette clé de sécurité.",
+ "site": "Site",
+ "cancel": "Annuler"
+ }
+ }
+ },
+ "automations": {
+ "AutomationCustomCronPanel": {
+ "3e3b2c369f": "Expression cron",
+ "e81a02d61b": "Saisissez une expression cron à cinq champs valide avant d'enregistrer.",
+ "968e66d686": "Saisissez une expression cron à cinq champs.",
+ "cadb7b0bc9": "invalide",
+ "f6ca30da23": "Cron personnalisé valide",
+ "a226dbdd40": "Minute",
+ "ec9c1e35df": "Heure",
+ "2d82246d23": "Jour",
+ "0e1de0358b": "Mois",
+ "77e96bded6": "Jour de la semaine"
+ },
+ "AutomationPromptDisclosure": {
+ "showLess": "Afficher moins",
+ "showMore": "Afficher plus"
+ },
+ "AutomationDetail": {
+ "007c8ad874": "Prompt",
+ "a1d52c2189": "Couverture d'utilisation",
+ "449fc83bf7": "Tokens",
+ "401f40ae79": "Dépense est.",
+ "a7c312430d": "Dernière exécution",
+ "2df8970cd5": "Agent",
+ "e353ab9516": "Pré-vérification",
+ "620b22145e": "Marge",
+ "15ea446b93": "Session",
+ "5405a09b1f": "Lieu d'exécution",
+ "2f8baf5360": "Créer depuis",
+ "578ff46987": "Prochaine exécution",
+ "18763ded26": "Planification",
+ "dbef8dc110": "Cette automatisation SSH ne s'exécute que si Orca peut joindre l'hôte SSH. Si la reconnexion exige des identifiants interactifs ou si l'hôte est indisponible, l'exécution est enregistrée comme ignorée.",
+ "1f6026358e": "Supprimer l'automatisation",
+ "d79452fb30": "Reprendre l'automatisation",
+ "91a4155e95": "Mettre l'automatisation en pause",
+ "4b1ea02d2e": "Modifier l'automatisation",
+ "2fb1605beb": "Exécuter maintenant",
+ "221916d93c": "Créez une automatisation pour planifier le travail des agents.",
+ "de0fedac06": "new_per_run",
+ "51a470b966": "ssh",
+ "b09b2384fd": "En pause",
+ "eaa02014f8": "Activé",
+ "29baf8f4c2": "Source"
+ },
+ "AutomationEditorDialog": {
+ "fb1896a5e7": "Annuler",
+ "57b722cbba": "Agent",
+ "6ff66f9012": "Nouvelle exécution",
+ "a2e688226d": "Worktree",
+ "6f9610e667": "Les exécutions se font dans un worktree de l'espace de travail sélectionné. Chaque nouvelle exécution crée un espace de travail neuf depuis la branche sélectionnée.",
+ "2c3fd9bfa1": "Aide sur le mode espace de travail",
+ "b28b140eaf": "Espace de travail",
+ "0d17f4ca8f": "Sélectionner un projet",
+ "02d351877e": "Projet",
+ "a4ac8fcc62": "/goal",
+ "827b25a81e": "Prend en charge les skills, les chemins de fichiers et les commandes intégrées comme",
+ "6d778190b7": "Exécuter l'audit hebdomadaire des dépendances et résumer les changements à risque.",
+ "058c23cb3f": "Prompt",
+ "c4b19094c2": "Planification",
+ "e46c1aa9ad": "Créer",
+ "a9d9dccf77": "Enregistrer",
+ "777548c2d6": "Enregistrer les modifications",
+ "e8c2a14f70": "Une fois enregistrée, s'exécute automatiquement jusqu'à sa mise en pause.",
+ "ff5db28639": "existant"
+ },
+ "AutomationEditorDialogHeader": {
+ "31f9253920": "Utiliser un modèle",
+ "7e35393632": "Hermes",
+ "6f309eef8d": "Orca",
+ "58f56b73d9": "Nom de l'automatisation",
+ "1d9826933e": "Audit du dépôt en semaine",
+ "4133d33862": "Créer une automatisation",
+ "0a75e5e2fa": "Créer une automatisation Hermes",
+ "03142e7721": "Modifier une automatisation Hermes",
+ "17086b48ee": "Modifier l'automatisation",
+ "4c8e1a72b9": "Une tâche d'agent récurrente"
+ },
+ "AutomationMissedRunGraceField": {
+ "0f4459e91d": "48 heures",
+ "adbab51feb": "24 heures",
+ "ba50e2a230": "12 heures",
+ "2dc9ee84d0": "3 heures",
+ "521f77cd58": "1 heure",
+ "e5ad263ae5": "30 minutes",
+ "529dc6c0b7": "Pas de marge",
+ "3d70c185c8": "Si Orca ou l'hôte d'exécution était indisponible à l'heure prévue, Orca exécute une occurrence manquée dès qu'il redevient disponible dans cette fenêtre. Les exécutions manquées plus anciennes sont ignorées.",
+ "3df53d554a": "Aide sur la marge des exécutions manquées",
+ "fc089e5fde": "Marge"
+ },
+ "AutomationEditorPromptSection": {
+ "a7c3e91b04": "Modifier le nom"
+ },
+ "AutomationPrecheckFields": {
+ "d2a2ac89ac": "10 min",
+ "bf49585b3c": "5 min",
+ "d84d3765fd": "2 min",
+ "c820119736": "1 min",
+ "51e28cdad9": "30 s",
+ "bb2dfb3629": "Délai d'expiration",
+ "99a577306c": "gh pr list --json number -q '.[0].number'",
+ "c2a762a180": "Pré-vérification"
+ },
+ "AutomationRunHistory": {
+ "402651bfb6": "Aucune exécution pour l'instant.",
+ "9974a2b429": "Statut",
+ "13988187b3": "Tokens",
+ "86a248187e": "Dépense",
+ "149c0b49c7": "Espace de travail",
+ "8faaa00726": "Exécution",
+ "53fc5f07ab": "Historique des exécutions",
+ "a00e38d1a3": "n/d",
+ "fdb3caa8fb": "connu"
+ },
+ "AutomationRunPageFrame": {
+ "40a511bed4": "Contexte d'exécution",
+ "33741dd973": "Retour aux exécutions"
+ },
+ "AutomationSchedulePicker": {
+ "9e677335b0": "Minute",
+ "d90981f766": "Heure",
+ "6b914c5fbb": "Jour",
+ "233b8c94b6": "Cadence",
+ "55b2ef82a4": "Toutes les heures",
+ "f0202f3a89": "Chaque jour",
+ "57e83307d0": "En semaine",
+ "837d902bba": "Hebdomadaire",
+ "ddba78647e": "Cron personnalisé"
+ },
+ "AutomationTimeField": {
+ "39ec1383f6": "AM ou PM",
+ "32a5e4e35e": "Minute",
+ "aa593eb5e2": "Heure"
+ },
+ "AutomationSessionField": {
+ "f3c76dce51": "Réutiliser",
+ "c90888ee94": "Nouvelle",
+ "b675112193": "Réutiliser envoie les futures exécutions vers la précédente session d'automatisation encore vivante. Si cette session n'existe plus, Orca en démarre une nouvelle.",
+ "4bdce31f37": "Aide sur la réutilisation de session",
+ "5ad314118e": "Session"
+ },
+ "AutomationsPage": {
+ "2695883141": "supprimer",
+ "c3a28c9793": "Sélectionnez une automatisation pour voir ses exécutions.",
+ "295698292f": "Relancer",
+ "0e110a3469": "Exécutions",
+ "bb1b2cd31e": "Vue d'ensemble",
+ "97ff587ee3": "Connectez cette source pour détecter les automatisations Hermes dans le profil distant.",
+ "aaa007846f": "source indisponible",
+ "25060635c6": "Ajouter",
+ "d207ab4c25": "Partir d'un modèle",
+ "15e0bfb13b": "Supprimer",
+ "f4612e3f78": "Édition",
+ "2faecab10b": "Exécuter maintenant",
+ "82eb6cb933": "source",
+ "13118faadf": "Projet inconnu",
+ "587a4b205c": "Suivant",
+ "761a35834d": "Automatisation",
+ "73f630b49d": "Annuler",
+ "1b586f0e2b": "activé",
+ "02a33e3204": "de",
+ "9adfab2596": "Supprimer l'automatisation externe",
+ "1e2e41392f": "Ne plus demander",
+ "b264564427": "et son historique d'exécution. Les espaces de travail créés par les exécutions précédentes ne sont pas supprimés.",
+ "080dcb5fbb": "Supprimer l'automatisation",
+ "19a6e30eae": "Actualiser les automatisations",
+ "8d1afa8269": "Ajouter une automatisation",
+ "77c2778945": "Automatisations",
+ "0329f9bef1": "Fermer · Esc",
+ "67c7ff795b": "Fermer les automatisations",
+ "e1bf9b1512": "L'espace de travail n'est pas disponible.",
+ "3e42a5cc1b": "Échec de la connexion SSH.",
+ "9f2855677c": "SSH connecté.",
+ "126d726546": "L'action sur l'automatisation externe a échoué.",
+ "37288942f0": "Automatisation externe reprise.",
+ "77c518a34b": "Automatisation externe mise en pause.",
+ "4d7878402c": "Automatisation externe en file d'attente.",
+ "4c22bc9913": "Automatisation externe supprimée.",
+ "3a4c476aa0": "Échec de la nouvelle exécution de l'automatisation.",
+ "a1bdb57008": "Exécution de l'automatisation en file d'attente.",
+ "8a3226f172": "Ouvrir les paramètres",
+ "d2a01b0b6f": "Vous pouvez changer cela dans les paramètres.",
+ "690b94da54": "Cette confirmation sera ignorée la prochaine fois.",
+ "b11170a008": "Échec de l'enregistrement de l'automatisation.",
+ "2a20596d6b": "Automatisation enregistrée.",
+ "244727e655": "Automatisation mise à jour.",
+ "77b81bc4ac": "Automatisation Hermes créée.",
+ "08efc3ae12": "Automatisation Hermes mise à jour.",
+ "e431bb85d4": "Choisissez un espace de travail sur le même hôte que cette automatisation Hermes.",
+ "32534e7c9c": "Choisissez un espace de travail disponible avant d'enregistrer.",
+ "2360ffc956": "Choisissez un agent activé avant d'enregistrer.",
+ "6e91dab317": "Saisissez une planification avancée valide avant d'enregistrer.",
+ "64bdb2304f": "Choisissez une planification prise en charge avant d'enregistrer.",
+ "2430fecf53": "Choisissez un lieu d'exécution et saisissez un prompt avant d'enregistrer.",
+ "7934ee0d81": "Connecter SSH",
+ "f93ed7a6f8": "Connexion...",
+ "8705757e27": "tâche",
+ "376631ef2b": "Reprendre",
+ "b457436d6a": "Pause",
+ "0ae52dd760": "hermes",
+ "e059042585": "Lecture seule",
+ "aecdc3681f": "Gérable",
+ "8500baacb4": "source externe",
+ "36f71740a7": "Espace de travail sélectionné",
+ "cd8397cc32": "Nouvel espace de travail à chaque exécution",
+ "dd0bc7a1ba": "new_per_run",
+ "7b2e285552": "Les connexions SSH sont indisponibles dans ce client.",
+ "d441032f7e": "pause",
+ "5918020edc": "exécuter",
+ "a21f6c33ad": "Source des automatisations actualisée.",
+ "53f06f0ad5": "Réessayer la source",
+ "pendingAutomationMissing": "Cette automatisation n'est plus disponible.",
+ "pendingAutomationRunMissing": "L'historique d'exécution n'est plus disponible.",
+ "noSearchMatches": "Aucune automatisation ne correspond à votre recherche.",
+ "paused": "En pause",
+ "runCount": "{{count}} exécutions",
+ "runCount_one": "{{count}} exécution",
+ "runCount_other": "{{count}} exécutions",
+ "projectDefaultBaseRef": "défaut du projet",
+ "createFromBaseRef": "Créer depuis {{baseRef}}",
+ "missingWorkspace": "Espace de travail manquant",
+ "runUsageSummary": "{{cost}} est. · {{tokens}} tokens",
+ "usageUnavailable": "Utilisation indisponible",
+ "noRunUsageYet": "Aucune utilisation d'exécution pour l'instant",
+ "noWorkspace": "Aucun espace de travail",
+ "latestSavedOutput": "Dernière sortie enregistrée",
+ "backToList": "Toutes les automatisations",
+ "tableName": "Nom",
+ "tableProject": "Projet",
+ "tableStatus": "Statut",
+ "tableActions": "Actions",
+ "newAutomation": "Nouvelle automatisation",
+ "rowActions": "Actions d'automatisation",
+ "tableLastRun": "Dernière exécution",
+ "noListMatches": "Aucune automatisation ne correspond."
+ },
+ "AutomationsPageSkeleton": {
+ "55527b7bcf": "Chargement des automatisations"
+ },
+ "CreateFromPicker": {
+ "f061f49e3f": "Rechercher des branches du dépôt...",
+ "dd3841b442": "Brancher depuis",
+ "ef6d762538": "Défaut du projet",
+ "e53d306056": "{{value0}} (par défaut)",
+ "79512f22a7": "Aucune branche trouvée.",
+ "9ce96621f4": "Recherche des branches..."
+ },
+ "ExternalAutomationManagers": {
+ "e02f970595": "Aucun gestionnaire d'automatisations externes trouvé.",
+ "6da3bfba4b": "automatisations trouvées.",
+ "3d58d5b67d": "Non",
+ "a42bf2b27e": "Supprimer l'automatisation externe",
+ "1c3bfd38fe": "Reprendre l'automatisation externe",
+ "0def1693bb": "Mettre l'automatisation externe en pause",
+ "1df491fd00": "Modifier l'automatisation externe",
+ "cc77ba88ff": "Exécuter l'automatisation externe",
+ "5820648765": "Dernière",
+ "844f1acb72": "trouvé(s)",
+ "20fd7a3a15": "suivant",
+ "c6695e6fbd": "Automatisations externes",
+ "5524365227": "OpenClaw",
+ "766abf833c": "Hermes",
+ "bf5f67b590": "hermes",
+ "e66091daf4": "exécutions",
+ "8e9165af08": "exécuter",
+ "2b0adbce21": "En pause",
+ "b3feba84c7": "Actif",
+ "92405f1431": "Indisponible",
+ "dbdcec22bd": "Lecture seule",
+ "0a2d4359a8": "Gérable",
+ "330b3c32e8": "disponible",
+ "e2532150ed": "automatisations",
+ "701515f010": "automatisation"
+ },
+ "ExternalAutomationRunTable": {
+ "0ba9c0a95c": "Page d'exécutions suivante",
+ "52d468a0b8": "Page d'exécutions précédente",
+ "7475c0ce96": "sur",
+ "be551397ca": "Statut",
+ "a813df9808": "Aperçu",
+ "d4b34feb66": "Heure d'exécution",
+ "2d4388a908": "Exécutions",
+ "9c080765ff": "Aucune exécution Hermes trouvée pour l'instant.",
+ "8ea934cacf": "Chargement des exécutions...",
+ "d5527d8fe7": "exécutions",
+ "872d032d05": "exécuter"
+ },
+ "HermesCronOutputView": {
+ "e27c716b43": "Prompt",
+ "4557213074": "Réponse",
+ "05affc68e3": "Erreur",
+ "88d48157fc": "default"
+ },
+ "WorkspaceCombobox": {
+ "ee5b280eba": "Aucun espace de travail trouvé.",
+ "8e9c8cc6b5": "Rechercher des espaces de travail...",
+ "66a0cd9628": "Sélectionner un espace de travail"
+ },
+ "automation": {
+ "templates": {
+ "37571fcb16": "Rechercher les travaux bloqués, les fichiers générés obsolètes et les validations locales en échec.",
+ "8a0228bea3": "Vérification horaire de la file d'attente",
+ "3b7281c75f": "Analyser le travail récent et signaler les risques de justesse, d'UX et de couverture de tests.",
+ "6023075b27": "Revue quotidienne des changements",
+ "513401db93": "Préparer un résumé hebdomadaire des risques de release à partir de l'état actuel du projet.",
+ "39ed39280a": "Préparation de la release",
+ "a7fbd32ddb": "Vérifier chaque jour de semaine les dépendances, les tests en échec et les changements ouverts à risque.",
+ "b84757677d": "Audit du dépôt en semaine",
+ "repoHealth": {
+ "category": "Santé du dépôt",
+ "name": "Audit du dépôt en semaine",
+ "prompt": "Passer en revue la santé du dépôt. Vérifier les mises à jour de dépendances, les tests en échec, l'état du lint/typecheck et les changements ouverts à risque. Résumer les constats et suggérer l'action suivante."
+ },
+ "releasePrep": {
+ "category": "Préparation de release",
+ "name": "Revue de préparation de release",
+ "prompt": "Préparer un résumé de préparation de release. Rechercher les bloqueurs, les changements à risque non mergés, les validations manquantes et les lacunes de documentation. Terminer par une recommandation concise : release ou pas."
+ },
+ "recurringReview": {
+ "category": "Revue récurrente",
+ "name": "Revue quotidienne des changements",
+ "prompt": "Passer en revue les changements récents de cet espace de travail. Se concentrer sur les risques de justesse, les régressions UX, les tests manquants et les tâches de suivi. Garder le rapport court et actionnable."
+ },
+ "maintenance": {
+ "category": "Maintenance",
+ "name": "Vérification de maintenance horaire",
+ "prompt": "Rechercher les travaux bloqués, les fichiers générés obsolètes, les validations en échec et tout ce qui requiert une attention humaine. Ne signaler que les problèmes actionnables."
+ }
+ },
+ "list": {
+ "last": {
+ "run": {
+ "done": "Terminé",
+ "failed": "Échec"
+ }
+ }
+ },
+ "schedule": {
+ "label": {
+ "086a5a9fe2": "Planification invalide",
+ "ba20c92073": "Planification personnalisée",
+ "a95afb7483": "Toutes les heures à :{{minute}}",
+ "280ccd2701": "Chaque jour à {{time}}",
+ "3f1422adc1": "En semaine à {{time}}",
+ "cc71e252ba": "{{day}}s à {{time}}"
+ }
+ }
+ },
+ "external": {
+ "automation": {
+ "schedule": {
+ "display": {
+ "a8e92b815a": "Planification indisponible"
+ }
+ }
+ }
+ },
+ "AutomationProjectCombobox": {
+ "search": "Rechercher des projets/dossiers…",
+ "empty": "Aucun projet/dossier ne correspond à votre recherche.",
+ "chooseHost": "Choisir l'hôte d'automatisation",
+ "adding": "Ajout du projet…",
+ "addProject": "Ajouter un projet"
+ },
+ "AutomationSetupDecisionField": {
+ "5a7863909c": "Exécuter le setup pour chaque nouvel espace de travail",
+ "18f000ad4e": "Avancé",
+ "874b72195b": "Quand cette automatisation crée un espace de travail, le prépare comme lors de la création manuelle d'un worktree — exécute le setup du projet et ouvre ses onglets de terminal."
+ },
+ "AutomationListSearchField": {
+ "tooLong": "Le texte recherché est trop long — liste non filtrée",
+ "label": "Rechercher des automatisations",
+ "placeholder": "Rechercher...",
+ "tooLongShort": "Trop long",
+ "clear": "Effacer la recherche"
+ },
+ "AutomationListLocalRows": {
+ "c92c9463c6": "Actions d'automatisation"
+ },
+ "AutomationListFilterMenu": {
+ "926e785e4d": "Rechercher des agents...",
+ "491043ee45": "Aucun agent ne correspond à votre recherche.",
+ "removeFilter": "Retirer le filtre {{value0}}",
+ "failed": "Échec",
+ "succeeded": "Réussie",
+ "neverRan": "Jamais exécutée",
+ "filters": "Filtres",
+ "all": "Tous",
+ "clear": "Réinitialiser les filtres",
+ "agent": "Agent",
+ "lastRun": "Dernière exécution"
+ },
+ "AutomationListSortHeader": {
+ "sortedAscending": "{{value0}}, tri croissant",
+ "sortedDescending": "{{value0}}, tri décroissant"
+ }
+ },
+ "agent": {
+ "AgentCombobox": {
+ "19522e25ee": "Gérer les agents",
+ "986f946354": "Terminal vierge",
+ "579c768bde": "Aucun agent ne correspond à votre recherche.",
+ "48c6a5a9b4": "Rechercher des agents...",
+ "9c6b59fe58": "Définir par défaut",
+ "1b0d6965fa": "Défaut actuel"
+ },
+ "AgentSettingsDialog": {
+ "50cdb57c03": "Gérez les agents IA, définissez-en un par défaut et personnalisez les commandes.",
+ "fc0268e4ed": "Agents"
+ }
+ },
+ "activity": {
+ "ActivityPrototypePage": {
+ "cf780197a1": "Sélectionnez un agent pour voir son activité",
+ "e3db9892f6": "Aucune activité pour l'instant.",
+ "1b633f5c1e": "Connexion au terminal...",
+ "8de7c5beaa": "Terminal indisponible",
+ "866083500b": "Faire glisser pour redimensionner",
+ "443690186e": "Redimensionner la liste des fils d'activité",
+ "7cd632006b": "Aucune activité d'agent ne correspond à ces filtres.",
+ "a2b4437bfb": "Activité de {{value0}}",
+ "023ff75afe": "Tout marquer comme lu",
+ "f70e4bec47": "Mode compact",
+ "a472a14700": "Plus d'options",
+ "db8a1878b5": "Options de la liste des fils",
+ "d1a88df9a8": "Afficher uniquement les fils non lus",
+ "f6396e1f85": "Agent",
+ "b29191b3e0": "Worktree",
+ "8c3b621ddf": "Projet",
+ "4a3986b200": "Statut",
+ "770d458144": "Grouper l'activité des agents par",
+ "795cbf26e2": "Filtrer...",
+ "4616ea39fd": "Aller à l'espace de travail",
+ "59b131fbd9": "Marquer le fil comme non lu",
+ "beb2c19173": "Non lus",
+ "5651b216c6": "Projet inconnu",
+ "22b22034bc": "Terminal autonome indisponible dans Activité.",
+ "afdc2139a8": "Terminal de l'agent fermé. Ouvrez un nouveau terminal dans cet espace de travail pour continuer."
+ },
+ "ActivityTitlebarControls": {
+ "f915168c8e": "non lu",
+ "d6a8de3934": "agents",
+ "dc708f3eff": "Fermer les agents"
+ }
+ },
+ "confirmation": {
+ "dialog": {
+ "8490e5d36a": "Confirmer",
+ "56f5c60e0c": "Annuler",
+ "92bac3217e": "Ne plus demander"
+ },
+ "skip": {
+ "saved": "Cette confirmation sera ignorée la prochaine fois.",
+ "savedDescription": "Vous pouvez changer cela dans les paramètres.",
+ "openSettings": "Ouvrir les paramètres",
+ "preference": {
+ "0b0cb6e3f9": "Impossible d'enregistrer la préférence de confirmation."
+ }
+ }
+ },
+ "jira": {
+ "connect": {
+ "dialog": {
+ "63ce735809": "Se connecter",
+ "4a2ab52781": "Vérification…",
+ "79e7aaed39": "Annuler",
+ "fdd26d81cc": "Paramètres du compte Atlassian",
+ "8090504a3e": "Créez un token dans",
+ "7b3967c12f": "Token API Atlassian",
+ "3d81bf3ab3": "Token API",
+ "e91b9a4073": "you@example.com",
+ "2849ddb295": "E-mail Atlassian",
+ "70fcd360c4": "https://example.atlassian.net",
+ "e176f9d0c5": "URL du site Jira Cloud",
+ "d785c42b8b": "Utilisez l'URL d'un site Jira Cloud, un e-mail Atlassian et un token API pour parcourir les tickets.",
+ "8388bdea2b": "Connecter le site Jira",
+ "2e2b69e48e": "Utilisez une URL de base Jira auto-hébergée et un jeton d'accès personnel pour parcourir les tickets.",
+ "b67e919bd5": "Type d'instance Jira",
+ "17787d6e4b": "Atlassian Cloud",
+ "bc7a831773": "Auto-hébergé",
+ "3489e186d6": "URL du site Jira",
+ "cbc27fa599": "https://jira.example.com",
+ "730d973bae": "Jeton d'accès personnel",
+ "8b9c7b9e7b": "Jeton d'accès personnel Jira",
+ "ccfb086d3e": "Créez un jeton d'accès personnel dans votre profil Jira, rubrique Personal Access Tokens.",
+ "1d947a07ab": "Utilisez une URL de base Jira auto-hébergée, un nom d'utilisateur et un mot de passe pour parcourir les tickets.",
+ "f49708c369": "Méthode d'authentification Jira",
+ "84a810dd0e": "Nom d'utilisateur et mot de passe",
+ "8d1223fa5c": "Nom d'utilisateur",
+ "be9eba0a1b": "nom d'utilisateur",
+ "70035652d7": "Mot de passe",
+ "c50abbf340": "Mot de passe du compte Jira",
+ "d8737db691": "Utilisez le nom d'utilisateur et le mot de passe de votre compte Jira Server ou Data Center."
+ }
+ }
+ },
+ "rightSidebar": {
+ "FolderWorkspaceWorktreesPanel": {
+ "unavailable": "Les espaces de travail ne sont affichés que pour les espaces de travail de type dossier.",
+ "label": "Espaces de travail",
+ "description": "Affiche les worktrees attachés à cet espace de travail de type dossier.",
+ "countOne": "1 worktree attaché",
+ "countMany": "{{value0}} worktrees attachés",
+ "emptyTitle": "Aucun worktree attaché pour l'instant",
+ "emptyCopy": "Les worktrees créés depuis cet espace de travail apparaîtront ici."
+ },
+ "FolderWorkspacePrChecksPanel": {
+ "unavailable": "Les vérifications PR ne sont affichées que pour les espaces de travail de type dossier.",
+ "refresh": "Actualiser les vérifications PR",
+ "emptyTitle": "Aucun worktree attaché pour l'instant",
+ "emptyCopy": "Les vérifications PR apparaîtront ici après l'attachement de worktrees à cet espace de travail de type dossier.",
+ "openChecksTab": "Ouvrir l'onglet Vérifications de {{value0}}",
+ "openReviewExternally": "Ouvrir {{value0}} en externe",
+ "summary": "{{value0}} attachés · {{value1}} avec PR/MR · {{value2}} attention requise · {{value3}} en attente · {{value4}} réussies · {{value5}} sans PR · {{value6}} inconnu",
+ "showDetails": "Afficher les détails des vérifications PR de {{value0}}",
+ "hideDetails": "Masquer les détails des vérifications PR de {{value0}}",
+ "reviewChecks": "Vérifications de revue",
+ "allChecksPassing": "toutes les vérifications passent",
+ "oneWorktree": "1 worktree",
+ "worktreeCount": "{{value0}} worktrees",
+ "oneFailing": "1 en échec",
+ "failingCount": "{{value0}} en échec",
+ "onePending": "1 en attente",
+ "pendingCount": "{{value0}} en attente"
+ },
+ "parentPrChecks": {
+ "rowSummary": {
+ "failingCount": "{{value0}} en échec",
+ "pendingCount": "{{value0}} en attente",
+ "checksFailing": "Vérifications en échec",
+ "mergeConflicts": "Conflits de fusion",
+ "checksPending": "Vérifications en attente",
+ "checksPassing": "Vérifications réussies",
+ "merged": "Fusionnés",
+ "closedWithoutMerge": "Fermée sans merge",
+ "draftReview": "Revue de brouillon",
+ "noCheckSignal": "Aucun signal de vérification",
+ "reviewUnavailable": "Statut de revue indisponible",
+ "noPrLinked": "Aucune PR liée",
+ "detailsUnavailable": "Détails de revue indisponibles",
+ "refreshFailed": "Échec de l'actualisation",
+ "checking": "Vérification du statut de revue…",
+ "notFetched": "Statut pas encore récupéré",
+ "unavailableWorktree": "Indisponible pour ce worktree"
+ },
+ "groups": {
+ "needsAttention": "Attention requise",
+ "pending": "En attente",
+ "merged": "Fusionnés",
+ "passing": "Réussies",
+ "draftOrNoChecks": "Brouillon / sans vérification",
+ "noPr": "Sans PR",
+ "unavailable": "Indisponible"
+ }
+ },
+ "pluginPanelBridgeHost": {
+ "actionsUnavailable": "Les actions de plugin ne sont pas disponibles dans ce client.",
+ "messageTooLarge": "Le message dépasse la taille limite.",
+ "tooManyRequests": "Trop de requêtes."
+ }
+ },
+ "link": {
+ "routing": {
+ "preference": {
+ "dialog": {
+ "badge": "Lien de terminal",
+ "preview": "Aperçu",
+ "keep": {
+ "title": "Ouvrir les liens de terminal dans le navigateur d'Orca ?",
+ "description": "Ou utilisez votre navigateur système par défaut.",
+ "orca": {
+ "button": "Garder Orca"
+ }
+ },
+ "title": "Ouvrir les liens du terminal dans le navigateur d'Orca ?",
+ "description": "Utilisez le navigateur d'Orca pour les liens du terminal, ou gardez votre navigateur système.",
+ "link": {
+ "label": "Lien"
+ },
+ "orca": {
+ "note": "Orca peut utiliser les cookies importés pour les sites où vous êtes connecté.",
+ "button": "Ouvrir dans Orca"
+ },
+ "settings": {
+ "note": "Modifiable plus tard dans Paramètres → Navigateur."
+ },
+ "shortcut": {
+ "note": {
+ "prefix": "Quand les liens s'ouvrent dans Orca,",
+ "suffix": "un clic ouvre le navigateur système une seule fois."
+ }
+ },
+ "system": {
+ "button": "Utiliser le navigateur système"
+ }
+ }
+ }
+ }
+ },
+ "task": {
+ "project": {
+ "source": {
+ "combobox": {
+ "noProjects": "Aucun projet",
+ "allProjects": "Tous les projets",
+ "hostCount": "{{value0}} hôtes",
+ "searchProjects": "Rechercher des projets...",
+ "noMatches": "Aucun projet ne correspond à votre recherche.",
+ "chooseSource": "Choisir la source des tâches"
+ }
+ }
+ }
+ },
+ "taskPageEmptyState": {
+ "noProjectSourcesTitle": "Aucune source de projet sélectionnée",
+ "noProjectSourcesDescription": "Sélectionnez au moins une source de projet pour qu'Orca sache sur quel hôte/compte récupérer les tâches.",
+ "noMatchingGitHubWorkTitle": "Aucun travail GitHub correspondant",
+ "changeQueryDescription": "Modifiez la requête ou effacez-la.",
+ "noGitLabIssuesTitle": "Aucun ticket GitLab",
+ "noGitLabIssuesDescription": "Aucune issue GitLab ne correspond à ce filtre.",
+ "noGitLabMrsTitle": "Aucune merge request GitLab",
+ "noGitLabMrsDescription": "Aucune MR GitLab ne correspond à ce filtre.",
+ "noGitLabWorkTitle": "Aucun travail GitLab",
+ "noGitLabWorkDescription": "Aucun work GitLab ne correspond à ce filtre."
+ },
+ "taskSourceContextSummary": {
+ "sourceUnavailable": "Source {{value0}} indisponible : {{value1}}",
+ "someSourceHostsUnavailable": "Certains hôtes de la source {{value0}} sont indisponibles : {{value1}}",
+ "reconnectOrUpdateTitle": "Reconnectez ou mettez à jour {{value0}} pour charger cette source."
+ },
+ "ShortcutKeyCombo": {
+ "07eb4985a1": "Appuyez deux fois sur {{value0}}"
+ },
+ "star": {
+ "nag": {
+ "StarNagToastHost": {
+ "starredThanks": "Étoile ajoutée — merci !",
+ "githubOpened": "GitHub ouvert",
+ "opening": "Ouverture…",
+ "starring": "Ajout de l'étoile…",
+ "openGithub": "Ouvrir GitHub",
+ "starOnGithub": "Mettre une étoile sur GitHub",
+ "onboardingCompleted": "Prise en main terminée !",
+ "body": "Si Orca vous plaît jusqu'ici, une étoile GitHub aide d'autres développeurs à le découvrir.",
+ "dismiss": "Ignorer",
+ "later": "Plus tard"
+ }
+ }
+ },
+ "nativeChat": {
+ "contextMenu": {
+ "copy": "Copier"
+ }
+ },
+ "orca": {
+ "profiles": {
+ "signout": {
+ "confirm": {
+ "title": "Se déconnecter d'Orca ?",
+ "description": "Les artifacts et Orca Relay seront indisponibles jusqu'à votre reconnexion. Vos projets et worktrees locaux ne seront pas affectés.",
+ "cancel": "Annuler",
+ "action": "Se déconnecter"
+ }
+ }
+ }
+ },
+ "linear-issue-attribute-filter-dropdowns": {
+ "removeFilter": "Retirer le filtre {{value0}}",
+ "teamRequired": "Sélectionnez une équipe pour charger les statuts, responsables et étiquettes de cet espace de travail.",
+ "filters": "Filtres",
+ "allWorkspacesTitle": "Sélectionnez un espace de travail",
+ "allWorkspacesBody": "Les filtres de statut, responsable et étiquette utilisent des identifiants issus d'un seul espace de travail Linear. Choisissez-en un pour filtrer selon ces attributs.",
+ "optionsFromTeam": "Options de {{team}}",
+ "clearAll": "Effacer tous les filtres"
+ },
+ "linear-issue-attribute-filter-sections": {
+ "status": "Statut",
+ "priority": "Priorité",
+ "assignee": "Assigné",
+ "unassigned": "Non assigné",
+ "labels": "Étiquettes",
+ "countSelected": "{{count}} sélectionnés",
+ "selected": "sélectionnés",
+ "searchPriority": "Filtrer par priorité…",
+ "searchStatus": "Filtrer par statut…",
+ "searchLabels": "Filtrer par étiquette…",
+ "searchAssignee": "Filtrer par responsable…",
+ "back": "Retour"
+ },
+ "browser-pane": {
+ "markup": {
+ "cancel": "Annuler",
+ "clear": "Tout effacer",
+ "copiedToast": "Markup copié — collez-le dans votre agent ({{value0}})",
+ "copy": "Copier le markup",
+ "drawButton": "Dessiner sur la capture d'écran",
+ "drawHint": "Dessinez sur la page, puis copiez le markup pour le coller dans votre agent.",
+ "drawHintBadge": "Nouveau",
+ "drawHintDismiss": "Compris",
+ "drawHintTry": "Essayer",
+ "errorAttach": "Impossible de joindre la capture d'écran annotée.",
+ "errorCapture": "Impossible de capturer la page à annoter.",
+ "errorUnavailable": "Le markup de capture d'écran n'est pas disponible sur cette page.",
+ "fontSize": "Taille de police",
+ "hint": "Dessinez sur la page, puis copiez le markup pour le coller dans votre agent.",
+ "redo": "Rétablir",
+ "style": "Couleur et épaisseur",
+ "textInput": "Texte d'annotation",
+ "tool": {
+ "arrow": "Flèche",
+ "ellipse": "Ellipse",
+ "highlight": "Surligneur",
+ "pen": "Stylo",
+ "rect": "Rectangle",
+ "text": "Texte"
+ },
+ "undo": "Annuler",
+ "widthOption": "{{value0}} px"
+ },
+ "grab": {
+ "errorNotReady": "Cette page n'est pas encore prête pour la sélection d'éléments.",
+ "errorNotAuthorized": "L'accès au navigateur a été refusé.",
+ "errorAlreadyActive": "La sélection d'éléments est déjà active.",
+ "errorInjectionFailed": "Impossible de démarrer la sélection d'éléments sur cette page."
+ }
+ },
+ "pluginCatalog": {
+ "PluginCatalogLayout": {
+ "title": "Plugins",
+ "filter": "Filtre de plugins",
+ "all": "Tous",
+ "installed": "Installés",
+ "searchLabel": "Rechercher des plugins",
+ "searchPlaceholder": "Rechercher des plugins, catégories ou éditeurs"
+ }
+ },
+ "terminalPane": {
+ "useManualTerminalWorktreeParking": {
+ "cannotPark": "Ces terminaux ne peuvent pas être mis en attente en toute sécurité."
+ }
+ },
+ "WorktreeBaseFallbackDialog": {
+ "title": "Espace de travail créé depuis une base locale",
+ "description": "La ref de suivi distante « {{value0}} » était indisponible ; Orca a donc utilisé la locale « {{value1}} ». Cet espace de travail pourrait ne pas inclure les derniers changements distants.",
+ "dismiss": "Compris"
+ },
+ "LinuxPackageInstallRecoveryCard": {
+ "e3de29c86a": "Afficher le paquet",
+ "3da99454c6": "Réessayer l'installation automatique",
+ "55c86654b7": "Copier la commande d'installation",
+ "53e1559f99": "Échec de l'installation automatique",
+ "a7ac6ec78b": "Orca a téléchargé la mise à jour mais n'a pas pu installer le paquet système automatiquement.",
+ "82c6dbea00": "Copiez la commande et exécutez-la dans un terminal système sur l'ordinateur où Orca est installé. Une fois terminée, quittez puis rouvrez Orca pour utiliser la nouvelle version.",
+ "53c4b8e148": "Aucun agent d'authentification exploitable n'a répondu à la demande d'installation privilégiée.",
+ "c732bcbf8f": "Vérification du paquet...",
+ "aa57fa4f80": "Commande copiée. Exécutez-la dans un terminal système pour installer {{value0}}, puis quittez et rouvrez Orca.",
+ "b7e7c5bc95": "Orca vérifie le fichier téléchargé par rapport aux métadonnées de version au moment où il construit cette commande. Le paquet système lui-même n'est pas vérifié par signature, et Orca ne peut pas garantir le fichier au-delà de ce point."
+ },
+ "pr-check-counts": {
+ "passingChip": "{{value0}} réussis",
+ "failingChip": "{{value0}} en échec",
+ "pendingChip": "{{value0}} en attente",
+ "needsActionChip": "{{value0}} action requise",
+ "unresolvedChip": "{{value0}} non résolus"
+ },
+ "BrowserPane": {
+ "streamConnectionLost": "Connexion au serveur distant perdue.",
+ "streamConnectionUnreachable": "Impossible de joindre le serveur distant.",
+ "streamRestartFailed": "Échec du redémarrage du flux du navigateur distant.",
+ "streamCapabilityUnsupported": "Le runtime sélectionné ne prend pas en charge le streaming du navigateur distant."
+ },
+ "artifacts": {
+ "ArtifactsPage": {
+ "signInAgain": "Reconnectez-vous à Orca pour charger les artifacts.",
+ "loadFailed": "Impossible de charger les artifacts.",
+ "deleteTitle": "Supprimer l'artifact ?",
+ "deleteDescription": "« {{name}} » ne sera plus disponible via son lien public.",
+ "delete": "Supprimer",
+ "deleteFailed": "Impossible de supprimer l'artifact.",
+ "title": "Artefacts",
+ "refresh": "Actualiser",
+ "signInHeading": "Se connecter à Orca",
+ "signInCopy": "Connectez-vous pour voir et gérer les artifacts partagés via votre compte.",
+ "signingIn": "Connexion…",
+ "signIn": "Se connecter à Orca",
+ "empty": "Aucun artifact partagé",
+ "emptyCopy": "Ouvrez un fichier HTML ou Markdown et choisissez Partager comme artifact, ou demandez à votre agent de le partager.",
+ "openArtifact": "Ouvrir l'artifact",
+ "deleteArtifact": "Supprimer l'artifact",
+ "moreAvailable": "Davantage d'artifacts sont disponibles",
+ "moreAvailableCopy": "Chargez la page suivante pour continuer.",
+ "loadMoreFailed": "Impossible de charger davantage d'artifacts.",
+ "publishingOff": "La publication est désactivée",
+ "publishingOffCopy": "Rien sur cet appareil ne peut encore créer de lien d'artifact public. Autorisez la publication dans Paramètres → Artifacts, puis partagez depuis un fichier HTML ou Markdown ouvert, ou demandez à votre agent.",
+ "openArtifactsSettings": "Ouvrir Paramètres → Artifacts",
+ "retry": "Réessayer",
+ "reconnectHeading": "Se reconnecter à Orca",
+ "reconnectCopy": "Reconnectez-vous pour voir et gérer les artifacts partagés via votre compte.",
+ "signInAgainAction": "Se reconnecter",
+ "unconfiguredCopy": "La connexion au compte Orca n'est pas encore configurée sur cette machine.",
+ "openAccountSettings": "Ouvrir les paramètres du compte"
+ },
+ "copySuccess": "Lien de l'artifact copié",
+ "copyFailed": "Impossible de copier le lien de l'artifact",
+ "copyLink": "Copier le lien",
+ "openInBrowser": "Ouvrir dans le navigateur",
+ "preview": "Aperçu de l'artifact",
+ "previewUnavailable": "Aperçu indisponible",
+ "previewUnavailableDescription": "Ouvrez cet artifact dans votre navigateur pour le consulter.",
+ "actions": "Actions de l'artifact",
+ "ArtifactActions": {
+ "more": "Plus d'actions sur l'artifact"
+ },
+ "ArtifactCollection": {
+ "loadMore": "Charger plus",
+ "noMatches": "Aucun résultat"
+ },
+ "ArtifactDetailHeader": {
+ "publicLink": "Toute personne disposant de ce lien peut le consulter",
+ "close": "Fermer"
+ },
+ "updatedAt": "Mis à jour {{when}}",
+ "updatedRecently": "récemment",
+ "expiryUnknown": "Expiration inconnue",
+ "expired": "Lien expiré",
+ "expires": "Le lien expire {{when}}",
+ "ArtifactPublishButton": {
+ "a4a49da6af": "Partager comme artifact",
+ "confirmTitle": "Partager comme artifact",
+ "confirmDescription": "Ceci publie le fichier actuel à un lien consultable par toute personne disposant de l'URL.",
+ "accountTitle": "Compte Orca",
+ "accountDescription": "Connectez-vous pour créer et gérer ce lien.",
+ "signingIn": "Connexion…",
+ "signInAgain": "Se reconnecter",
+ "signIn": "Se connecter",
+ "publishingOffTitle": "Le partage d'artifacts est désactivé",
+ "publishingOffDescription": "Découvrez les liens publics et activez le partage dans les paramètres.",
+ "openSettings": "Ouvrir les paramètres Artifacts",
+ "sharing": "Partage…",
+ "sharePublicLink": "Partager le lien public",
+ "publishedDescription": "Toute personne disposant de ce lien peut consulter le fichier partagé.",
+ "checkingLink": "Recherche d'un lien existant…",
+ "checkFailed": "Impossible de rechercher un lien existant.",
+ "tryAgain": "Réessayer"
+ },
+ "artifact-publish-flow": {
+ "9a078a0c65": "Le partage d'artifacts est indisponible",
+ "bba20daa6d": "Connectez-vous à Orca puis réessayez.",
+ "54b1805328": "Impossible de partager l'artifact",
+ "430019efd0": "Artifact partagé",
+ "2fc727c831": "Artifact mis à jour",
+ "5cb4f5ec36": "Copier le lien",
+ "fbb5018602": "Ce fichier est vide.",
+ "6112db5a1c": "Les artifacts partagés depuis Orca doivent peser moins de 800 Ko.",
+ "e2ed5acd8c": "Orca n'a pas pu lire ce fichier. Ouvrez-le depuis un espace de travail et réessayez.",
+ "6d475e9b25": "Seuls les fichiers HTML et Markdown locaux peuvent être partagés comme artifacts.",
+ "29a406be09": "Les artifacts doivent contenir du texte."
+ },
+ "ArtifactPublishedLinkPanel": {
+ "copyLink": "Copier le lien",
+ "openLink": "Ouvrir le lien",
+ "updating": "Mise à jour…",
+ "update": "Mettre à jour le contenu partagé"
+ },
+ "ArtifactDetailDrawer": {
+ "description": "Prévisualisez et gérez cet artifact partagé."
+ },
+ "ArtifactListSearchField": {
+ "label": "Rechercher des artifacts",
+ "placeholder": "Rechercher...",
+ "clear": "Effacer la recherche"
+ },
+ "ArtifactListTableHeader": {
+ "name": "Nom",
+ "type": "Tapez",
+ "size": "Taille",
+ "updated": "Mis à jour",
+ "expires": "Expiration",
+ "actions": "Actions"
+ },
+ "ArtifactsPageSkeleton": {
+ "loading": "Chargement des artifacts"
+ },
+ "expiredCompact": "Expiré",
+ "typeMarkdown": "Markdown",
+ "typeHtml": "HTML"
+ }
+ },
+ "i18n": {
+ "hostedReview": {
+ "copy": {
+ "f0a4b8c2d1": "PR",
+ "e9f3a7b1c0": "pull request",
+ "d8e2f6a0b9": "Pull Request",
+ "c7d1e5f9a8": "GitHub",
+ "c4e8f1a2b9": "MR",
+ "b3d7e0f1a8": "merge request",
+ "a2c6d9e0f7": "Merge Request",
+ "91b5c8d7e6": "GitLab"
+ }
+ }
+ },
+ "runtime": {
+ "remoteServerUpdateErrors": {
+ "manualRequired": "Ce serveur doit être mis à jour manuellement via son gestionnaire de services.",
+ "notAvailable": "Le serveur ne signale plus de mise à jour disponible. Revérifiez.",
+ "notDownloaded": "Le téléchargement de la mise à jour du serveur n'est pas terminé.",
+ "legacyServer": "Mettez à jour ce serveur une fois manuellement pour activer les mises à jour à distance.",
+ "updaterTimeout": "Délai dépassé en attendant le programme de mise à jour du serveur.",
+ "requestedVersionUnavailable": "Le programme de mise à jour du serveur n'a pas proposé la version d'Orca demandée.",
+ "updateUnavailable": "Le serveur n'a pas signalé de mise à jour disponible.",
+ "downloadIncomplete": "La mise à jour du serveur n'a pas fini de se télécharger.",
+ "reconnectTimeout": "Le serveur ne s'est pas reconnecté avec la version mise à jour."
+ },
+ "webRuntimeSession": {
+ "remoteHostDisconnected": "L'espace de travail n'est pas connecté à un hôte Orca distant."
+ },
+ "gitlabJobTraceClient": {
+ "loadFailed": "Échec du chargement du journal du job GitLab.",
+ "emptyTrace": "Aucun journal disponible pour ce job GitLab.",
+ "timedOut": "Délai dépassé lors du chargement du journal du job GitLab."
+ },
+ "githubCheckDetailsTimeout": {
+ "timedOut": "Délai dépassé lors du chargement des détails des vérifications."
+ }
+ },
+ "ssh": {
+ "sshConnectVerb": {
+ "reconnect": "Reconnecter",
+ "retry": "Réessayer",
+ "connect": "Se connecter",
+ "connecting": "Connexion…"
+ }
+ },
+ "main": {
+ "window": {
+ "editableContextMenu": {
+ "table": "Tableau",
+ "insertRowAbove": "Insérer une ligne au-dessus",
+ "insertRowBelow": "Insérer une ligne en dessous",
+ "deleteRow": "Supprimer la ligne",
+ "insertColumnLeft": "Insérer une colonne à gauche",
+ "insertColumnRight": "Insérer une colonne à droite",
+ "deleteColumn": "Supprimer la colonne",
+ "deleteTable": "Supprimer le tableau"
+ }
+ }
+ }
+ },
+ "components": {
+ "native-chat": {
+ "composer": {
+ "imageUnsupported": "Le collage d'image n'est pas pris en charge pour cet agent.",
+ "send": "Envoyer",
+ "noPty": "Aucun terminal actif — rebasculez pour vous reconnecter.",
+ "locked": "La saisie est détenue par un autre appareil.",
+ "placeholder": "Envoyer un message…",
+ "mentionHint": "Fichier référencé :",
+ "attach": "Joindre un fichier",
+ "startDictation": "Démarrer la dictée",
+ "stopDictation": "Arrêter la dictée",
+ "noSkills": "Aucune skill correspondante",
+ "noCommandsOrSkills": "Aucune commande ou skill correspondante",
+ "noCommands": "Aucune commande correspondante",
+ "commands": "Commandes",
+ "skills": "Skills",
+ "loadingSkills": "Chargement des skills...",
+ "skillsLoaded": "Skills chargées",
+ "skillsUnavailableHost": "Les skills sont indisponibles pour cet hôte",
+ "skillsLoadFailed": "Impossible de charger les skills depuis cet hôte",
+ "retrySkills": "Réessayer",
+ "skillCommandCollision": "Aussi un nom de skill - l'agent décide",
+ "skillMultipleSources": "{{sourceCount}} sources - l'agent décide",
+ "skillScopeProject": "Projet",
+ "skillScopePersonal": "Personnel",
+ "skillScopeBuiltIn": "Intégrée",
+ "skillScopePlugin": "Plugin",
+ "localAttachmentUnsupported": "Les pièces jointes locales ne sont pas disponibles pour les sessions distantes.",
+ "removeAttachment": "Retirer la pièce jointe",
+ "pastedImageLabel": "Image collée",
+ "imagePasteFailed": "Échec du collage de l'image.",
+ "worktreeNotReady": "Worktree pas encore prêt — réessayez dans un instant.",
+ "uploadingAttachments": "Envoi de {{value0}} fichier(s) vers le distant…",
+ "model": "Modèle",
+ "effort": "Effort",
+ "fastMode": "Mode rapide",
+ "thinking": "Réflexion",
+ "options": "Options",
+ "sessionOptions": "Options de session",
+ "chooseInAgentPicker": "Choisir dans le sélecteur d'agents…",
+ "toggleOption": "Basculer {{value0}}",
+ "pillAccessibleName": "{{value0}} {{value1}}",
+ "valueUnknown": "Valeur actuelle inconnue — choisissez Activé ou Désactivé",
+ "sentNotConfirmed": "Envoyé à l'agent — non confirmé",
+ "setWhenSessionStarts": "Défini au démarrage de la session.",
+ "availableAfterSessionStarts": "Disponible après le démarrage de la session.",
+ "optionUpdateFailed": "Impossible de mettre à jour l'option",
+ "optionValue": {
+ "fast": "Rapide",
+ "minimal": "Minimal",
+ "low": "Basse",
+ "medium": "Moyenne",
+ "high": "Haute",
+ "xhigh": "Extra élevé",
+ "max": "Max",
+ "ultra": "Ultra",
+ "on": "Activé",
+ "off": "Désactivé"
+ }
+ },
+ "tool": {
+ "running": "En cours…",
+ "result": "Résultat",
+ "countOne": "1 appel d'outil",
+ "countN": "{{value0}} appels d'outils"
+ },
+ "status": {
+ "responding": "L'agent répond"
+ },
+ "jumpToLatest": "Aller au dernier message",
+ "toggle": {
+ "showTerminal": "Afficher le terminal",
+ "showChat": "Afficher la vue chat"
+ },
+ "state": {
+ "loading": {
+ "title": "Chargement de la conversation…",
+ "subtitle": "Lecture de la transcription de l'agent."
+ },
+ "error": {
+ "title": "Impossible de charger la conversation",
+ "subtitle": "La transcription n'a pas pu être lue. Rebasculez sur le terminal pour continuer à travailler."
+ },
+ "pairHost": "Associez un hôte pour consulter l'historique de chat des agents.",
+ "notAgent": {
+ "title": "Aucune conversation ici",
+ "subtitle": "Ce terminal n'exécute aucun agent de code reconnu."
+ },
+ "empty": {
+ "title": "Démarrer un chat avec {{value0}}",
+ "subtitle": "Demandez à {{value0}} d'inspecter du code, d'expliquer une sortie ou de faire une modification."
+ }
+ },
+ "stop": "Arrêter l'agent",
+ "copyMessage": {
+ "copied": "Copied",
+ "copy": "Copier le message"
+ },
+ "scrollMessageToTop": "Revenir au début du message",
+ "loadingEarlier": "Chargement…",
+ "loadEarlier": "Charger les messages précédents",
+ "question": {
+ "step": "Étape {{value0}}",
+ "other": "Autre…",
+ "otherPlaceholder": "Saisissez votre réponse",
+ "cancel": "Annuler",
+ "send": "Valider",
+ "next": "Suivant",
+ "skip": "Ignorer",
+ "sending": "Envoi…"
+ },
+ "approval": {
+ "title": "Autoriser {{value0}} ?",
+ "allow": "Autoriser",
+ "deny": "Refuser"
+ },
+ "launchPromptNotDelivered": "Non livré — vérifiez le terminal"
+ },
+ "tab": {
+ "bar": {
+ "SortableTabContextMenu": {
+ "switchToTerminalView": "Passer à la vue terminal",
+ "switchToChatView": "Passer à la vue chat",
+ "closeTabsToLeft": "Fermer les onglets à gauche"
+ },
+ "BrowserTab": {
+ "closeOthers": "Fermer les autres",
+ "closeTabsToLeft": "Fermer les onglets à gauche"
+ },
+ "EditorFileTabContextMenu": {
+ "closeOthers": "Fermer les autres",
+ "closeTabsToLeft": "Fermer les onglets à gauche"
+ }
+ }
+ },
+ "workspace": {
+ "cleanup": {
+ "presentationFixtures": {
+ "reviewAlphaCleanup": "Nettoyage alpha de la revue"
+ },
+ "presentation": {
+ "gitlabMergeRequestNumber": "MR #{{value0}}",
+ "githubPullRequestNumber": "PR #{{value0}}"
+ },
+ "browse": {
+ "noSizes": "Aucune taille d'espace de travail mesurée pour l'instant.",
+ "noSizesDescription": "Les tailles apparaissent une fois l'utilisation disque mesurée.",
+ "measureSizes": "Analyser",
+ "measureSizesTitle": "Analyser les tailles des espaces de travail",
+ "measureSizesDescription": "Analysez l'utilisation disque pour comparer, trier et filtrer les espaces de travail par taille.",
+ "sizeOnDisk": "Taille sur disque : {{value0}}",
+ "checkingGitRow": "Vérification de l'état git",
+ "searchPlaceholder": "Rechercher par nom, dépôt, branche, chemin, hôte",
+ "searchLabel": "Rechercher des espaces de travail",
+ "filters": "Filtres",
+ "showingCount": "Affichage de {{value0}} sur {{value1}}",
+ "clearFilters": "Réinitialiser les filtres",
+ "checkingGit": "Vérification de l'état git : {{value0}} restants",
+ "facet": {
+ "git": "Git",
+ "review": "À examiner",
+ "ticket": "Tickets",
+ "location": "Emplacement",
+ "safety": "Sécurité",
+ "activity": "Activité",
+ "size": "Taille sur disque",
+ "status": "Statut de l'espace de travail",
+ "agent": "Agent",
+ "context": "Contexte local"
+ },
+ "minAhead": "Commits d'avance ≥",
+ "minBehind": "Commits de retard ≥",
+ "branchQuery": "La branche contient",
+ "prunable": "Nettoyable",
+ "locked": "Verrouillé",
+ "reviewPresence": "PR / MR",
+ "reviewProvider": "Fournisseur",
+ "ticketPresence": "Ticket lié",
+ "host": "Hôte",
+ "repo": "Dépôt",
+ "pathPrefix": "Chemin commençant par",
+ "tier": {
+ "ready": "Prêt",
+ "review": "En attente de relecture",
+ "protected": "Protégé"
+ },
+ "blockerMode": "Correspondance de bloqueur",
+ "blockerModeAny": "Au moins un",
+ "blockerModeNone": "Aucun",
+ "blockers": "Bloqueurs",
+ "dismissed": "Ignoré",
+ "selectableOnly": "Uniquement les espaces de travail que je peux supprimer maintenant",
+ "idleSignal": {
+ "lastVisited": "Non ouvert",
+ "lastActivity": "Aucune activité",
+ "created": "Créé avant"
+ },
+ "idleMinDays": "Inactif depuis au moins",
+ "days": "jours",
+ "neverVisited": "Jamais ouvert",
+ "minSize": "Au moins",
+ "maxSize": "Au plus",
+ "includeUnsized": "Inclure les espaces de travail non mesurés",
+ "matchStatusless": "Inclure les espaces de travail sans statut",
+ "archived": "Archivé",
+ "pinned": "Épinglés",
+ "unread": "Non lus",
+ "comment": "A un commentaire",
+ "retainedDoneAgents": "Transcriptions d'agents terminés",
+ "contextPresence": "Onglets, terminaux, commentaires ouverts",
+ "completelyEmpty": "Plus rien à perdre",
+ "noWorkspaces": "Aucun espace de travail trouvé.",
+ "noMatches": "Aucun espace de travail ne correspond à ces filtres.",
+ "noMatchesDescription": "Tous les espaces de travail sont dans une même liste — élargissez une facette ou effacez les filtres.",
+ "selectAll": "Sélectionner tous les espaces de travail correspondants",
+ "sortBy": "Trier par",
+ "sortByField": "Trier par {{value0}}",
+ "sort": {
+ "lastActivity": "Dernière activité",
+ "lastVisited": "Dernière ouverture",
+ "created": "Créé",
+ "size": "Taille",
+ "name": "Espace de travail",
+ "repo": "Dépôt",
+ "path": "Chemin",
+ "host": "Hôte",
+ "workspaceStatus": "Statut",
+ "agent": "Agent",
+ "git": "Git",
+ "ahead": "En avance",
+ "behind": "En retard",
+ "branch": "Branche",
+ "review": "À examiner",
+ "ticket": "Ticket",
+ "localContext": "Contexte ouvert",
+ "tier": "Sécurité",
+ "blockerCount": "Bloqueurs"
+ },
+ "git": {
+ "clean": "Propre",
+ "dirty": "Modifications non validées",
+ "unpushed": "Commits non poussés",
+ "unknown": "Non vérifié"
+ },
+ "agent": {
+ "working": "En activité",
+ "permission": "Vous attend",
+ "idle": "Inactif"
+ },
+ "review": {
+ "open": "Ouvert",
+ "draft": "Brouillon",
+ "merged": "Fusionnés",
+ "closed": "Fermé",
+ "unknown": "Inconnu",
+ "otherProvider": "Autre"
+ },
+ "ticket": {
+ "workItem": "Élément de travail",
+ "linear": "Linear",
+ "issue": "Issue"
+ },
+ "triState": {
+ "any": "Tous",
+ "only": "Uniquement",
+ "exclude": "Exclure"
+ },
+ "presence": {
+ "any": "Tous",
+ "some": "Un",
+ "none": "Aucun"
+ },
+ "tierField": "Palier de nettoyage",
+ "idleSignalField": "Signal d'inactivité",
+ "selectionVanishedOne": "1 espace de travail sélectionné n'existe plus.",
+ "selectionVanished": "{{value0}} espaces de travail sélectionnés n'existent plus.",
+ "updatedAgo": "{{value0}} mis à jour",
+ "refreshing": "Actualisation…",
+ "refreshingProgress": "Actualisation {{value0}}/{{value1}}",
+ "notMeasured": "Non mesuré",
+ "openWorkspaceNamed": "Ouvrir {{value0}}",
+ "openWorkspace": "Ouvrir l'espace de travail",
+ "workspaceStatus": "Statut de l'espace de travail : {{value0}}",
+ "measuringSizesProgress": "Analyse {{value0}}/{{value1}}",
+ "measuringSizes": "Analyse des tailles",
+ "measureSizesFailed": "Impossible d'analyser les tailles des espaces de travail",
+ "selectedCount": "{{value0}} sélectionnés",
+ "gitStatusCheckFailed": "Échec de la vérification de l'état git",
+ "gitStatusUnverified": "L'état git n'a pas pu être vérifié",
+ "deleteAnyway": "Supprimer quand même",
+ "forceDeleteProjectionOne": "{{count}} espace de travail présente actuellement un risque et peut nécessiter une suppression forcée",
+ "forceDeleteProjectionMany": "{{count}} espaces de travail présentent actuellement un risque et peuvent nécessiter une suppression forcée",
+ "selectionWithheldOne": "1 espace de travail sélectionné est masqué par les filtres actuels et a été désélectionné.",
+ "selectionWithheld": "{{value0}} espaces de travail sélectionnés sont masqués par les filtres actuels et ont été désélectionnés.",
+ "selectAllCountOne": "Sélectionner 1 espace de travail vérifié côté sécurité",
+ "selectAllCount": "Sélectionner les {{value0}} espaces de travail vérifiés côté sécurité",
+ "appliedFilters": "Filtres appliqués",
+ "removeFilter": "Supprimer le filtre {{value0}}",
+ "chip": {
+ "idleDays": "Inactif {{value0}} j+",
+ "neverVisited": "Jamais visité",
+ "minSize": "Au moins {{value0}} Mo",
+ "maxSize": "Au plus {{value0}} Mo",
+ "excludesUnsized": "Mesurés uniquement",
+ "excludesStatusless": "A un statut",
+ "list": "{{value0}} : {{value1}}",
+ "triState": "{{value0}} : {{value1}}",
+ "only": "Uniquement",
+ "exclude": "Exclus",
+ "minAhead": "{{value0}}+ d'avance",
+ "minBehind": "{{value0}}+ de retard",
+ "branchQuery": "Branche : {{value0}}",
+ "pathPrefix": "Chemin : {{value0}}",
+ "presence": "{{value0}} : {{value1}}",
+ "has": "A",
+ "none": "Aucun",
+ "completelyEmpty": "Rien à perdre",
+ "selectableOnly": "Supprimables uniquement",
+ "kind": {
+ "status": "Statut",
+ "agent": "Agent",
+ "git": "Git",
+ "review": "À examiner",
+ "reviewState": "État de revue",
+ "reviewProvider": "Fournisseur",
+ "ticket": "Ticket",
+ "ticketSource": "Source des tickets",
+ "context": "Contexte",
+ "host": "Hôte",
+ "repo": "Dépôt",
+ "blocker": "Bloqueur",
+ "tier": "Sécurité",
+ "dismissed": "Ignoré",
+ "archived": "Archivé",
+ "pinned": "Épinglés",
+ "unread": "Non lus",
+ "comment": "Commentaire",
+ "prunable": "Nettoyable",
+ "locked": "Verrouillé",
+ "retainedAgents": "Agents terminés"
+ }
+ }
+ },
+ "scan": {
+ "progress": "Analyse des espaces de travail ({{value0}}/{{value1}})",
+ "singleError": "Impossible de vérifier {{value0}} : {{value1}}. Certains espaces de travail peuvent manquer. Actualisez pour réessayer.",
+ "moreErrors": ", +{{value0}} autres",
+ "multipleErrors": "Impossible de vérifier les dépôts {{value0}} ({{value1}}{{value2}}). Certains espaces de travail peuvent manquer. Actualisez pour réessayer.",
+ "noWorkspaces": "Aucun espace de travail trouvé.",
+ "readyOneOne": "1 espace de travail trouvé, avec 1 suggestion de nettoyage.",
+ "readyOneMany": "1 espace de travail trouvé, avec {{value0}} suggestions de nettoyage.",
+ "readyManyOne": "{{value0}} espaces de travail trouvés, avec 1 suggestion de nettoyage.",
+ "readyManyMany": "{{value0}} espaces de travail trouvés, avec {{value1}} suggestions de nettoyage.",
+ "fallbackRepository": "un dépôt",
+ "gitListFailed": "Git n'a pas pu lister les worktrees",
+ "readyOne": "1 espace de travail trouvé.",
+ "readyMany": "{{value0}} espaces de travail trouvés."
+ },
+ "relativeTime": {
+ "never": "Jamais",
+ "justNow": "À l'instant",
+ "minutesAgo": "il y a {{value0}} min",
+ "hoursAgo": "il y a {{value0}} h",
+ "daysAgo": "il y a {{value0}} j"
+ },
+ "host": {
+ "label": "Hôte : {{value0}}",
+ "unknown": "Hôte inconnu",
+ "candidateName": "{{value0}} sur {{value1}}"
+ }
+ }
+ },
+ "agentSessionContinuation": {
+ "continueInNewSession": "Continuer dans une nouvelle session…",
+ "dialogTitle": "Continuer dans une nouvelle session",
+ "dialogDescription": "Démarrez une nouvelle session d'Agent depuis ce point d'arrêt. La session d'origine reste inchangée.",
+ "untitledSession": "Session actuelle",
+ "originalAgent": "Agent d'origine : {{agent}}",
+ "agent": "Agent",
+ "selectAgent": "Sélectionner un Agent",
+ "detectingAgents": "Détection des Agents sur cet hôte d'espace de travail…",
+ "detectionFailed": "Impossible de détecter des Agents sur cet hôte d'espace de travail.",
+ "noAgents": "Aucun Agent activé n'a été détecté sur cet hôte d'espace de travail.",
+ "context": "Contexte",
+ "modeFocused": "Transfert ciblé (recommandé)",
+ "modeFocusedDescription": "Utilise le statut le plus récent et l'espace de travail actuel, en ne lisant les détails anciens de la transcription qu'en cas de besoin.",
+ "modeFull": "Transcription complète de la session",
+ "modeFullDescription": "Demande au nouvel Agent de lire la session sauvegardée complète avant de continuer. Cela peut prendre plus de temps et consommer beaucoup de contexte, d'utilisation du plan ou de crédits API.",
+ "startsIn": "Démarre dans :",
+ "startSession": "Démarrer une nouvelle session",
+ "starting": "Démarrage…",
+ "noContext": "Aucun contexte de session disponible pour continuer dans une nouvelle session.",
+ "agentDisabled": "{{agent}} est désactivé dans les paramètres des Agents.",
+ "agentUnavailable": "{{agent}} n'a pas été détecté sur cet hôte d'espace de travail.",
+ "sent": "Contexte de session envoyé à {{agent}} dans une nouvelle session.",
+ "deliveryFailed": "La nouvelle session {{agent}} a démarré, mais son contexte n'a pas pu être envoyé.",
+ "launchFailed": "Impossible de démarrer une nouvelle session {{agent}}."
+ },
+ "status": {
+ "bar": {
+ "workspaceSpace": {
+ "otherTopLevelItems": "Autres éléments de premier niveau ({{value0}})"
+ }
+ }
+ },
+ "cmd-j": {
+ "paletteSessionAge": {
+ "minutes": "{{value0}} min",
+ "hours": "{{value0}} h",
+ "days": "{{value0}} j",
+ "underOneMinute": "<1 min"
+ }
+ }
+ },
+ "dashboardPopout": {
+ "view": {
+ "label": "Vue tableau de bord",
+ "board": "Tableau de bord",
+ "map": "Carte des agents"
+ },
+ "map": {
+ "host": {
+ "local": "Local",
+ "ssh": "SSH",
+ "wsl": "WSL",
+ "remote": "Distant"
+ },
+ "filters": {
+ "showStates": "États des agents",
+ "agentlessWorkspaces": "Espaces de travail sans agents",
+ "orchestrationLinks": "Liens d'orchestration",
+ "orchestrationLinksHidden": "Liens d'orchestration masqués",
+ "workspaceVisibility": "Contenu de la carte",
+ "title": "Contrôles de la carte",
+ "reset": "Réinitialiser",
+ "ofTotalAgents": "sur {{total}} agents affichés",
+ "quickViews": "Vues rapides",
+ "agents": "Agents",
+ "time": "Heure",
+ "lifespan": "Durée de vie de la session",
+ "sinceMessage": "Depuis le dernier message",
+ "timeInState": "Temps dans l'état actuel",
+ "timeMinimum": "{{label}} minimum",
+ "timeMaximum": "{{label}} maximum",
+ "timeAny": "tous",
+ "timeRangeCount": "{{count}} plages",
+ "resetRanges": "Réinitialiser les plages",
+ "summaryAll": "Tous",
+ "summaryCount": "{{shown}} sur {{total}}",
+ "summarySelected": "{{count}} sélectionnés",
+ "workspace": "Espace de travail",
+ "stateChip": "État : {{states}}"
+ },
+ "liveContainmentMap": "Carte d'imbrication en temps réel",
+ "empty": "Aucun agent ne correspond aux filtres actuels.",
+ "canvasLabel": "Carte imbriquée des projets, espaces de travail et agents",
+ "zoomOut": "Zoom arrière",
+ "zoomIn": "Zoom avant",
+ "fit": "Ajuster",
+ "openWorktree": "Ouvrir les détails du worktree {{worktree}}",
+ "openFolderWorkspace": "Ouvrir les détails de l'espace de travail de type dossier {{workspace}}",
+ "worktreeSummary": "{{total}} agents · {{active}} actifs · {{done}} terminés",
+ "worktreeSummary_one": "{{total}} agent · {{active}} actif · {{done}} terminé",
+ "worktreeSummary_other": "{{total}} agents · {{active}} actifs · {{done}} terminés",
+ "runningAgents": "Agents",
+ "spawnAgent": "Démarrer un nouvel agent",
+ "noLaunchableAgents": "Aucun agent activé détecté.",
+ "sleepWorkspace": "Veille",
+ "projectCount": "{{agents}} agents · {{workspaces}} workspaces",
+ "agentCount": "{{count}} agents",
+ "agentCount_one": "{{count}} agent",
+ "agentCount_other": "{{count}} agents",
+ "noWorkspaceAgents": "Aucun agent dans cet espace de travail.",
+ "status": {
+ "doneSeen": "Terminé, vu"
+ },
+ "quickView": {
+ "everything": "Tout",
+ "attention": "Me concerne",
+ "stuck": "Bloqué",
+ "unread": "Non lus",
+ "recent": "30 dernières minutes",
+ "longRunning": "Exécutions longues",
+ "stale": "Inactif > 3 j",
+ "orchestration": "Orchestration"
+ }
+ },
+ "placeholder": {
+ "title": "Tableau de bord des agents",
+ "description": "C'est ici que tous vos agents s'afficheront d'un coup d'œil. Le tableau arrive bientôt."
+ },
+ "recoverableError": {
+ "title": "Le tableau de bord Orca a rencontré une erreur.",
+ "description": "Le tableau de bord n'a pas pu terminer son rendu. Réessayez pour le recharger, ou rouvrez-le."
+ },
+ "bucket": {
+ "attention": "Vous concerne",
+ "working": "En activité",
+ "idle": "Inactif",
+ "empty": "Aucun",
+ "done": "Terminé"
+ },
+ "title": "Agents",
+ "total": "{{count}} au total",
+ "close": "Fermer le tableau de bord",
+ "settings": {
+ "showIdle": "Afficher les agents inactifs",
+ "showIdleCopy": "Inclure les agents restés silencieux pendant 30 minutes sans signaler leur achèvement. Masqués par défaut."
+ },
+ "settingsTooltip": "Paramètres du tableau",
+ "card": {
+ "you": "Vous",
+ "time": {
+ "justNow": "à l'instant",
+ "minutes": "{{count}} m",
+ "hours": "{{count}} h",
+ "days": "{{count}} j"
+ },
+ "review": {
+ "open": "Ouvrir la review",
+ "draft": "Revue de brouillon",
+ "merged": "Review fusionnée",
+ "closed": "Review fermée"
+ },
+ "subagents_one": "{{count}} sous-agent",
+ "subagents_other": "{{count}} sous-agents"
+ },
+ "terminal": {
+ "closed": "Pas de terminal en direct — le volet de cet agent est fermé.",
+ "focusWorktree": "Ouvrir le worktree",
+ "close": "Fermer"
+ },
+ "filters": {
+ "remove": "Retirer le filtre {{label}}",
+ "active": "Filtres",
+ "clear": "Effacer",
+ "review": {
+ "open": "Ouvert",
+ "draft": "Brouillon",
+ "merged": "Fusionnés",
+ "closed": "Fermé",
+ "none": "Pas de review"
+ },
+ "reviewChip": "Review : {{state}}",
+ "label": "Filtre",
+ "project": "Projet",
+ "workspaceStatus": "Statut de l'espace de travail",
+ "reviewStatus": "Statut PR / MR",
+ "clearAll": "Effacer tous les filtres",
+ "removeChip": "Retirer {{filter}}"
+ },
+ "search": {
+ "placeholder": "Rechercher un worktree, un projet ou un agent…",
+ "label": "Rechercher des agents",
+ "clear": "Effacer la recherche",
+ "results": "{{shown}} sur {{total}} affichés"
+ },
+ "settingsLabel": "Paramètres du tableau de bord des agents",
+ "host": {
+ "sshNamed": "Hôte SSH · {{host}}",
+ "ssh": "Hôte SSH",
+ "remoteNamed": "Hôte Orca distant · {{host}}",
+ "remote": "Hôte Orca distant"
+ }
+ },
+ "dashboard": {
+ "sidebar": {
+ "label": "Tableau de bord des agents"
+ }
+ },
+ "runtimeRpc": {
+ "startupFailure": {
+ "unknownCause": "Aucun détail supplémentaire sur l'erreur n'était disponible.",
+ "continueButton": "Continuer sans CLI",
+ "title": "CLI Orca indisponible",
+ "message": "Orca n'a pas pu démarrer son transport de commandes local.",
+ "detail": "Orca continuera de fonctionner, mais les commandes telles que orca status, orca terminal et l'orchestration sont indisponibles pour cette session.\n\n{{guidance}}\n\nCause : {{cause}}",
+ "guidance": {
+ "permissionDenied": "Orca n'a pas pu écrire son fichier runtime. Vérifiez les permissions du dossier de données d'Orca, puis redémarrez.",
+ "storageUnavailable": "Votre disque est peut-être plein ou en lecture seule. Libérez de l'espace, puis redémarrez Orca.",
+ "invalidPath": "Le dossier de données d'Orca est peut-être manquant, déplacé, ou situé dans un chemin trop long. Restaurez-le ou utilisez un chemin plus court, puis redémarrez Orca.",
+ "addressInUse": "Un autre processus occupe peut-être le port. Redémarrez Orca pour réessayer.",
+ "unknown": "Redémarrez Orca pour réessayer."
+ }
+ }
+ },
+ "featureTips": {
+ "voice": {
+ "demoPrompt": "Relisez ce diff à la recherche de cas limites et ajoutez des tests pour tout ce que vous trouvez.",
+ "startDictation": "Démarrer la dictée",
+ "agentPromptTitle": "Prompt d'agent",
+ "listening": "Écoute...",
+ "focusPaneInstruction": "Placez le focus sur un terminal, un éditeur ou un prompt d'agent, puis appuyez sur",
+ "startInstruction": "pour lancer la dictée vocale. Appuyez de nouveau sur",
+ "stopInstruction": "pour arrêter.",
+ "unassignedInstruction": "Assignez un raccourci de dictée avant de lancer la dictée vocale dans un volet actif.",
+ "settingsInstruction": "Modifiez le modèle, le mode de dictée ou le raccourci à tout moment dans",
+ "settingsLink": "Paramètres → Voix"
+ }
+ },
+ "quickOpen": {
+ "moreMatchesAvailable": "D'autres correspondances sont peut-être disponibles. Affinez votre recherche pour réduire les résultats."
+ }
+}
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index d3af25ad697..b3c30da9446 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -20,7 +20,8 @@
"chinese": "中文(简体)",
"korean": "한국어",
"japanese": "日本語",
- "spanish": "Español"
+ "spanish": "Español",
+ "french": "Français"
},
"statusBar": {
"claudeToggleDescription": "アクティブなワークスペースの Claude トークンとコストの使用状況を表示します。",
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index f494ff29a58..ac75cca2209 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -20,7 +20,8 @@
"chinese": "中文(简体)",
"korean": "한국어",
"japanese": "日本語",
- "spanish": "Español"
+ "spanish": "Español",
+ "french": "Français"
},
"statusBar": {
"claudeToggleDescription": "활성 워크스페이스에 대한 Claude 토큰 및 비용 사용량을 표시합니다.",
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 2bbaee9d1bb..7a3b47c8f2a 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -20,7 +20,8 @@
"chinese": "中文(简体)",
"korean": "한국어",
"japanese": "日本語",
- "spanish": "Español"
+ "spanish": "Español",
+ "french": "Français"
},
"statusBar": {
"claudeToggleDescription": "显示当前工作区的 Claude Token 与费用消耗情况。",
diff --git a/src/renderer/src/i18n/supported-languages.ts b/src/renderer/src/i18n/supported-languages.ts
index 446a3a0419c..eee26df0e5f 100644
--- a/src/renderer/src/i18n/supported-languages.ts
+++ b/src/renderer/src/i18n/supported-languages.ts
@@ -2,6 +2,7 @@ import { DEFAULT_UI_LOCALE, resolveRendererUiLocale } from '../../../shared/ui-l
import {
UI_LANGUAGE_CHINESE,
UI_LANGUAGE_ENGLISH,
+ UI_LANGUAGE_FRENCH,
UI_LANGUAGE_JAPANESE,
UI_LANGUAGE_KOREAN,
UI_LANGUAGE_SPANISH,
@@ -25,7 +26,8 @@ export const UI_LANGUAGE_CHOICES: UiLanguageChoice[] = [
{ value: UI_LANGUAGE_CHINESE, labelKey: 'settings.appearance.language.chinese' },
{ value: UI_LANGUAGE_KOREAN, labelKey: 'settings.appearance.language.korean' },
{ value: UI_LANGUAGE_JAPANESE, labelKey: 'settings.appearance.language.japanese' },
- { value: UI_LANGUAGE_SPANISH, labelKey: 'settings.appearance.language.spanish' }
+ { value: UI_LANGUAGE_SPANISH, labelKey: 'settings.appearance.language.spanish' },
+ { value: UI_LANGUAGE_FRENCH, labelKey: 'settings.appearance.language.french' }
]
const UI_LANGUAGE_CHOICE_FALLBACKS: Record = {
@@ -34,7 +36,8 @@ const UI_LANGUAGE_CHOICE_FALLBACKS: Record = {
[UI_LANGUAGE_CHINESE]: '中文(简体)',
[UI_LANGUAGE_KOREAN]: '한국어',
[UI_LANGUAGE_JAPANESE]: '日本語',
- [UI_LANGUAGE_SPANISH]: 'Español'
+ [UI_LANGUAGE_SPANISH]: 'Español',
+ [UI_LANGUAGE_FRENCH]: 'Français'
}
export function getUiLanguageChoiceLabel(
diff --git a/src/shared/ui-language.test.ts b/src/shared/ui-language.test.ts
index b27ab0c8312..cad48bcf396 100644
--- a/src/shared/ui-language.test.ts
+++ b/src/shared/ui-language.test.ts
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import {
UI_LANGUAGE_CHINESE,
UI_LANGUAGE_ENGLISH,
+ UI_LANGUAGE_FRENCH,
UI_LANGUAGE_JAPANESE,
UI_LANGUAGE_KOREAN,
UI_LANGUAGE_SPANISH,
@@ -18,10 +19,11 @@ describe('normalizeUiLanguage', () => {
expect(normalizeUiLanguage(UI_LANGUAGE_KOREAN)).toBe('ko')
expect(normalizeUiLanguage(UI_LANGUAGE_JAPANESE)).toBe('ja')
expect(normalizeUiLanguage(UI_LANGUAGE_SPANISH)).toBe('es')
+ expect(normalizeUiLanguage(UI_LANGUAGE_FRENCH)).toBe('fr')
})
it('falls back unknown values to system', () => {
- expect(normalizeUiLanguage('fr')).toBe('system')
+ expect(normalizeUiLanguage('de')).toBe('system')
expect(normalizeUiLanguage(null)).toBe('system')
})
})
diff --git a/src/shared/ui-language.ts b/src/shared/ui-language.ts
index 77002e81f89..0f60b9aaba5 100644
--- a/src/shared/ui-language.ts
+++ b/src/shared/ui-language.ts
@@ -4,6 +4,7 @@ export const UI_LANGUAGE_CHINESE = 'zh'
export const UI_LANGUAGE_KOREAN = 'ko'
export const UI_LANGUAGE_JAPANESE = 'ja'
export const UI_LANGUAGE_SPANISH = 'es'
+export const UI_LANGUAGE_FRENCH = 'fr'
export type BuiltInUiLanguage =
| typeof UI_LANGUAGE_SYSTEM
@@ -12,6 +13,7 @@ export type BuiltInUiLanguage =
| typeof UI_LANGUAGE_KOREAN
| typeof UI_LANGUAGE_JAPANESE
| typeof UI_LANGUAGE_SPANISH
+ | typeof UI_LANGUAGE_FRENCH
export type PluginUiLanguage = `plugin:${string}`
export type UiLanguage = BuiltInUiLanguage | PluginUiLanguage
@@ -22,7 +24,8 @@ const UI_LANGUAGE_VALUES = new Set([
UI_LANGUAGE_CHINESE,
UI_LANGUAGE_KOREAN,
UI_LANGUAGE_JAPANESE,
- UI_LANGUAGE_SPANISH
+ UI_LANGUAGE_SPANISH,
+ UI_LANGUAGE_FRENCH
])
const PLUGIN_UI_LANGUAGE_RE =
diff --git a/src/shared/ui-locale.test.ts b/src/shared/ui-locale.test.ts
index 272506e26a9..3c2de4a262d 100644
--- a/src/shared/ui-locale.test.ts
+++ b/src/shared/ui-locale.test.ts
@@ -4,6 +4,7 @@ import { normalizeSupportedUiLocale, resolveUiLocale, resolveRendererUiLocale }
import {
UI_LANGUAGE_CHINESE,
UI_LANGUAGE_ENGLISH,
+ UI_LANGUAGE_FRENCH,
UI_LANGUAGE_JAPANESE,
UI_LANGUAGE_KOREAN,
UI_LANGUAGE_SPANISH,
@@ -34,8 +35,14 @@ describe('ui-locale', () => {
expect(normalizeSupportedUiLocale('es')).toBe('es')
})
+ it('normalizes French locale prefixes', () => {
+ expect(normalizeSupportedUiLocale('fr-FR')).toBe('fr')
+ expect(normalizeSupportedUiLocale('fr-CA')).toBe('fr')
+ expect(normalizeSupportedUiLocale('fr')).toBe('fr')
+ })
+
it('falls back unsupported locales to English', () => {
- expect(normalizeSupportedUiLocale('fr-FR')).toBe('en')
+ expect(normalizeSupportedUiLocale('de-DE')).toBe('en')
})
it('does not map Traditional Chinese to Simplified yet', () => {
@@ -64,6 +71,10 @@ describe('ui-locale', () => {
expect(resolveUiLocale(UI_LANGUAGE_SPANISH, 'en-US')).toBe('es')
})
+ it('resolves explicit French independently of system locale', () => {
+ expect(resolveUiLocale(UI_LANGUAGE_FRENCH, 'en-US')).toBe('fr')
+ })
+
it('preserves a selected plugin language bundle id', () => {
expect(resolveUiLocale('plugin:orca-samples.portuguese/pt-BR')).toBe(
'plugin:orca-samples.portuguese/pt-BR'
@@ -76,7 +87,7 @@ describe('ui-locale', () => {
expect(resolveUiLocale(UI_LANGUAGE_SYSTEM, 'ko-KR')).toBe('ko')
expect(resolveUiLocale(UI_LANGUAGE_SYSTEM, 'ja-JP')).toBe('ja')
expect(resolveUiLocale(UI_LANGUAGE_SYSTEM, 'es-MX')).toBe('es')
- expect(resolveUiLocale(UI_LANGUAGE_SYSTEM, 'fr-FR')).toBe('en')
+ expect(resolveUiLocale(UI_LANGUAGE_SYSTEM, 'fr-FR')).toBe('fr')
})
it('uses renderer system locale only for the system setting', () => {
@@ -85,5 +96,6 @@ describe('ui-locale', () => {
expect(resolveRendererUiLocale(UI_LANGUAGE_KOREAN)).toBe('ko')
expect(resolveRendererUiLocale(UI_LANGUAGE_JAPANESE)).toBe('ja')
expect(resolveRendererUiLocale(UI_LANGUAGE_SPANISH)).toBe('es')
+ expect(resolveRendererUiLocale(UI_LANGUAGE_FRENCH)).toBe('fr')
})
})
diff --git a/src/shared/ui-locale.ts b/src/shared/ui-locale.ts
index bc84491b7b2..70bffcca8c8 100644
--- a/src/shared/ui-locale.ts
+++ b/src/shared/ui-locale.ts
@@ -1,6 +1,7 @@
import {
UI_LANGUAGE_CHINESE,
UI_LANGUAGE_ENGLISH,
+ UI_LANGUAGE_FRENCH,
UI_LANGUAGE_JAPANESE,
UI_LANGUAGE_KOREAN,
UI_LANGUAGE_SPANISH,
@@ -9,7 +10,7 @@ import {
type UiLanguage
} from './ui-language'
-export const SUPPORTED_UI_LOCALES = ['en', 'zh', 'ko', 'ja', 'es'] as const
+export const SUPPORTED_UI_LOCALES = ['en', 'zh', 'ko', 'ja', 'es', 'fr'] as const
export type SupportedUiLocale = (typeof SUPPORTED_UI_LOCALES)[number]
export const DEFAULT_UI_LOCALE: SupportedUiLocale = 'en'
@@ -54,6 +55,9 @@ export function resolveUiLocale(
if (language === UI_LANGUAGE_SPANISH) {
return 'es'
}
+ if (language === UI_LANGUAGE_FRENCH) {
+ return 'fr'
+ }
return normalizeSupportedUiLocale(systemLocale)
}
From 963839aa4f3cbd932dac3229364b115a77bfb620 Mon Sep 17 00:00:00 2001
From: Ihor
Date: Fri, 4 Sep 2026 03:33:02 +0300
Subject: [PATCH 11/43] docs: add Ukrainian README translation
iho <4000375+iho@users.noreply.github.com>
---
README.md | 2 +-
docs/readme/README.es.md | 2 +-
docs/readme/README.fr.md | 2 +-
docs/readme/README.ja.md | 2 +-
docs/readme/README.ko.md | 2 +-
docs/readme/README.pt.md | 2 +-
docs/readme/README.uk.md | 269 ++++++++++++++++++++++++++++++++++++
docs/readme/README.zh-CN.md | 2 +-
8 files changed, 276 insertions(+), 7 deletions(-)
create mode 100644 docs/readme/README.uk.md
diff --git a/README.md b/README.md
index 7a3cbe2360c..805c94295cc 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
- 中文 · 日本語 · 한국어 · Español · Français · Português
+ 中文 · 日本語 · 한국어 · Español · Français · Português · Українська
diff --git a/docs/readme/README.es.md b/docs/readme/README.es.md
index f2247e0900d..c8333ec31c9 100644
--- a/docs/readme/README.es.md
+++ b/docs/readme/README.es.md
@@ -12,7 +12,7 @@
- English · 中文 · 日本語 · 한국어 · Français · Português
+ English · 中文 · 日本語 · 한국어 · Français · Português · Українська
diff --git a/docs/readme/README.fr.md b/docs/readme/README.fr.md
index 97c78d4e713..7a69e1acffe 100644
--- a/docs/readme/README.fr.md
+++ b/docs/readme/README.fr.md
@@ -12,7 +12,7 @@
- English · 中文 · 日本語 · 한국어 · Español · Português
+ English · 中文 · 日本語 · 한국어 · Español · Português · Українська
diff --git a/docs/readme/README.ja.md b/docs/readme/README.ja.md
index cce2032a67c..a256f5239c4 100644
--- a/docs/readme/README.ja.md
+++ b/docs/readme/README.ja.md
@@ -12,7 +12,7 @@
- English · 中文 · 한국어 · Español · Français · Português
+ English · 中文 · 한국어 · Español · Français · Português · Українська
diff --git a/docs/readme/README.ko.md b/docs/readme/README.ko.md
index 4a75722ff8c..d2dd2a62747 100644
--- a/docs/readme/README.ko.md
+++ b/docs/readme/README.ko.md
@@ -12,7 +12,7 @@
- English · 中文 · 日本語 · Español · Français · Português
+ English · 中文 · 日本語 · Español · Français · Português · Українська
diff --git a/docs/readme/README.pt.md b/docs/readme/README.pt.md
index 86d998a4e5f..667cebb685f 100644
--- a/docs/readme/README.pt.md
+++ b/docs/readme/README.pt.md
@@ -12,7 +12,7 @@
- English · 中文 · 日本語 · 한국어 · Español · Français
+ English · 中文 · 日本語 · 한국어 · Español · Français · Українська
diff --git a/docs/readme/README.uk.md b/docs/readme/README.uk.md
new file mode 100644
index 00000000000..b3e2a42b868
--- /dev/null
+++ b/docs/readme/README.uk.md
@@ -0,0 +1,269 @@
+
+ Orca
+
+
+
+
+
+
+
+
+
+
+
+
+ English · 中文 · 日本語 · 한국어 · Español · Français · Português
+
+
+
+ AI-оркестратор для розробників рівня 100x.
+ Запускайте Codex, Claude Code, OpenCode або Pi паралельно — кожен у власному worktree, усі під контролем в одному місці.
+
+
+
+
+
+
+
+
+## Можливості
+
+
+
+
+
+### Супутній мобільний застосунок
+
+Стежте за агентами та керуйте ними з телефону — отримуйте сповіщення про завершення роботи агента та надсилайте подальші вказівки, де б ви не були.
+
+[App Store для iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.44](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk) · [Документація →](https://www.onorca.dev/docs/mobile)
+
+
+
+
+
+
+
+
+
+### Паралельні worktree
+
+Надішліть один промпт одразу п’ятьом агентам, кожен із яких працюватиме у власному ізольованому git worktree, — порівняйте результати та виконайте злиття найкращого з них.
+
+[Документація →](https://www.onorca.dev/docs/model/worktrees)
+
+
+
+
+
+
+
+
+
+### Розділені термінали
+
+Термінали рівня Ghostty з рендерингом на WebGL, необмеженою кількістю розділень і буфером прокручування, який зберігається після перезапуску.
+
+[Документація →](https://www.onorca.dev/docs/terminal)
+
+
+
+
+
+
+
+
+
+### Режим дизайну
+
+Клацніть на будь-якому елементі інтерфейсу у справжньому вікні Chromium, щоб надіслати його HTML, CSS і обрізаний скриншот прямо в промпт агента.
+
+[Документація →](https://www.onorca.dev/docs/browser/design-mode)
+
+
+
+
+
+
+
+
+
+### GitHub і Linear, нативно
+
+Переглядайте PR, issue та дошки проєктів прямо в застосунку — відкривайте worktree з будь-якої задачі та рев'юйте без перемикання контексту.
+
+[Документація →](https://www.onorca.dev/docs/review/linear)
+
+
+
+
+
+
+
+
+
+### SSH worktree
+
+Запускайте агентів на потужній віддаленій машині з повноцінним редагуванням файлів, git і терміналами — з автоперепідключенням і прокиданням портів.
+
+[Документація →](https://www.onorca.dev/docs/ssh)
+
+
+
+
+
+
+
+
+
+### Анотуйте diff-и агентів
+
+Залишайте коментарі на будь-якому рядку diff-у й надсилайте їх агенту — рев'юйте, редагуйте та комітьте, не виходячи з Orca.
+
+[Документація →](https://www.onorca.dev/docs/review/annotate-ai-diff)
+
+
+
+
+
+
+
+
+
+### Перетягуйте файли агентам
+
+Редактор на базі VS Code з автозбереженням усюди — перетягуйте файли чи зображення прямо в промпт агента.
+
+[Документація →](https://www.onorca.dev/docs/editing/file-explorer)
+
+
+
+
+
+
+
+
+
+### Orca CLI
+
+Агенти теж керують Orca — автоматизуйте будь-який робочий процес командами `orca worktree create`, `snapshot`, `click` і `fill`.
+
+[Документація →](https://www.onorca.dev/docs/cli/overview)
+
+
+
+
+
+
+
+
+**Також у комплекті:**
+
+- **[Швидкий пошук](https://www.onorca.dev/docs/model/quick-open)** — Шукайте серед worktree, файлів, агентів, команд і контексту репозиторію, не відриваючись від роботи.
+- **[Перемикач акаунтів і відстеження використання](https://www.onorca.dev/docs/agents/usage-tracking)** — Стежте за використанням Claude і Codex та скиданням лімітів, перемикайте акаунти на льоту без повторного входу.
+- **[Розширені перегляди репозиторію](https://www.onorca.dev/docs/editing/markdown)** — Переглядайте Markdown, зображення, PDF та документацію репозиторію прямо в робочому просторі.
+- **[Computer Use](https://www.onorca.dev/docs/cli/computer-use)** — Дозвольте агентам керувати десктопними застосунками та видимим інтерфейсом, коли робочий процес потребує реальної взаємодії.
+- **[Сповіщення та статус непрочитаного](https://www.onorca.dev/docs/notifications)** — Дізнавайтеся, коли агент завершив роботу або потребує уваги, і позначайте треди як непрочитані, щоб повернутися пізніше.
+- **І багато іншого** — ми випускаємо оновлення щодня, тож цей список завжди відстає. Справжній перелік можливостей — це [changelog](https://github.com/stablyai/orca/releases).
+
+---
+
+## Підтримувані агенти
+
+Працює з **будь-яким CLI-агентом** — якщо він запускається в терміналі, він запуститься і в Orca.
+
+
+ Claude Code
+ Codex
+ Grok
+ Cursor
+ GitHub Copilot
+ OpenCode
+ MiMo Code
+ Amp
+ OpenClaude
+ Antigravity
+ Pi
+ oh-my-pi
+ Hermes Agent
+ Devin
+ Goose
+ Auggie
+ Autohand Code
+ Charm
+ Cline
+ Codebuff
+ Command Code
+ Continue
+ Droid
+ Kilocode
+ Kimi
+ Kiro
+ Mistral Vibe
+ Qwen Code
+ Rovo Dev
+ + будь-який CLI-агент
+
+
+---
+
+## Встановлення
+
+### Десктоп — macOS, Windows, Linux
+
+- **[Завантажити з onOrca.dev](https://onorca.dev/download)**
+- Або завантажте білд напряму: [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) · [Усі білди](https://github.com/stablyai/orca/releases/latest)
+- Запускаєте `orca serve` на headless Linux-сервері? Дивіться [посібник із headless Linux-сервера](../reference/headless-linux-server.md).
+
+_Або через пакетний менеджер:_
+
+```bash
+# macOS (Homebrew)
+brew install --cask stablyai/orca/orca
+
+# Arch Linux (AUR) — або stably-orca-git для збірки з джерела
+yay -S stably-orca-bin
+```
+
+### Супутній мобільний застосунок — iOS, Android
+
+Під’єднайте мобільний застосунок до десктопного, щоб стежити за агентами та керувати ними з телефону.
+
+- **iOS:** [Завантажити з App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) або [приєднатися до TestFlight](https://testflight.apple.com/join/YjeGMQBA)
+- **Android:** [Завантажити APK 0.0.44](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.44/app-release.apk) · [Інструкція зі встановлення](https://www.onorca.dev/docs/android-apk)
+
+---
+
+## Спільнота та підтримка
+
+- **Discord:** Приєднуйтеся до спільноти в **[Discord](https://discord.gg/fzjDKHxv8Q)**.
+- **Twitter / X:** Стежте за **[@orca_build](https://x.com/orca_build)**, щоб бути в курсі оновлень і анонсів.
+- **WeChat:** Відскануйте QR-код, щоб приєднатися до групи № 7 спільноти Orca у WeChat. Якщо вона заповнена, приєднайтеся до групи № 8.
+
+
+
+
+- **Зворотний зв'язок та ідеї:** Ми випускаємо оновлення швидко. Чогось бракує? [Запропонуйте нову функцію](https://github.com/stablyai/orca/issues).
+- **Конфіденційність:** Перегляньте [документацію про конфіденційність і телеметрію](https://www.onorca.dev/docs/telemetry), щоб дізнатися, які анонімні дані про використання збирає Orca і як від цього відмовитися.
+- **Підтримайте нас:** Поставте [зірку](https://github.com/stablyai/orca) цьому репозиторію, щоб стежити за нашими щоденними релізами.
+
+---
+
+## Розробка
+
+Хочете зробити внесок або запустити проєкт локально? Перегляньте наш посібник [CONTRIBUTING.md](../../.github/CONTRIBUTING.md).
+
+
+
+
+
+
+
+
+
+## Підписані білди
+Підписання коду для Windows надано за підтримки [SignPath.io](https://signpath.io), сертифікат надано [SignPath Foundation](https://signpath.org).
+
+## Ліцензія
+
+Orca — безкоштовний проєкт із відкритим кодом за ліцензією [MIT](../../LICENSE).
diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md
index d7bae3fba9e..160f5b05611 100644
--- a/docs/readme/README.zh-CN.md
+++ b/docs/readme/README.zh-CN.md
@@ -12,7 +12,7 @@
- English · 日本語 · 한국어 · Español · Français · Português
+ English · 日本語 · 한국어 · Español · Français · Português · Українська
From 8463dcb7b97614a75d91e6d71c9cd83c9712daa2 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Thu, 3 Sep 2026 17:59:16 -0700
Subject: [PATCH 12/43] fix(terminal): make wrapped-line search rewind
iterative and bound its scans (#18402)
Patches @xterm/addon-search so one very long un-newlined line no longer overflows the stack, freezes the renderer, or goes unsearched. Submitted upstream as xtermjs/xterm.js#6149 (issue #6148); drop the patch once a release ships it. See the PR for measurements and the differential fuzz.
---
.github/workflows/pr.yml | 2 +-
...@xterm__addon-search@0.17.0-beta.300.patch | 275 +++++++++++++
...rm__addon-search@0.17.0-beta.300.src.patch | 231 +++++++++++
config/patches/xterm-upstream.json | 27 ++
docs/reference/xterm-patch-regeneration.md | 10 +-
pnpm-lock.yaml | 5 +-
pnpm-workspace.yaml | 1 +
.../terminal-search-long-wrapped-line.test.ts | 362 ++++++++++++++++++
8 files changed, 905 insertions(+), 8 deletions(-)
create mode 100644 config/patches/@xterm__addon-search@0.17.0-beta.300.patch
create mode 100644 config/patches/xterm-src/@xterm__addon-search@0.17.0-beta.300.src.patch
create mode 100644 src/renderer/src/components/terminal-search-long-wrapped-line.test.ts
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index d21b1a23784..ed37cefb434 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -380,7 +380,7 @@ jobs:
- uses: ./.github/actions/install-node-dependencies
# Why: the check rebuilds every package in the manifest from a pinned upstream
- # commit — @xterm/xterm and the two addons, each built twice (once unmodified to
+ # commit — @xterm/xterm and its three addons, each built twice (once unmodified to
# prove the toolchain still reproduces the published bundles, once patched). Caching
# the npm metadata and the shallow clone keeps the repeated cost to the builds
# themselves; the key is the manifest, so a commit, package or toolchain bump
diff --git a/config/patches/@xterm__addon-search@0.17.0-beta.300.patch b/config/patches/@xterm__addon-search@0.17.0-beta.300.patch
new file mode 100644
index 00000000000..c2843ec6b3d
--- /dev/null
+++ b/config/patches/@xterm__addon-search@0.17.0-beta.300.patch
@@ -0,0 +1,275 @@
+diff --git a/lib/addon-search.js b/lib/addon-search.js
+index d939cf1a65f3de449059efbf4fc5c3c3515f533f..8d2b66d265c256d4ebd507624d6a29b8075929ce 100644
+--- a/lib/addon-search.js
++++ b/lib/addon-search.js
+@@ -1,2 +1,2 @@
+-!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SearchAddon=t():e.SearchAddon=t()}(globalThis,()=>(()=>{"use strict";var e={578(e,t,s){Object.defineProperty(t,"__esModule",{value:!0}),t.IntervalTimer=t.MicrotaskTimer=t.TimeoutTimer=void 0,t.timeout=function(e){return new Promise(t=>setTimeout(t,e))},t.disposableTimeout=function(e,t=0,s){const r=setTimeout(()=>{e(),s&&n.dispose()},t),n=(0,i.toDisposable)(()=>{clearTimeout(r)});return s?.add(n),n};const i=s(426);t.TimeoutTimer=class{constructor(){this._token=-1,this._isDisposed=!1}dispose(){this.cancel(),this._isDisposed=!0}cancel(){-1!==this._token&&(clearTimeout(this._token),this._token=-1)}cancelAndSet(e,t){if(this._isDisposed)throw new Error("Calling cancelAndSet on a disposed TimeoutTimer");this.cancel(),this._token=setTimeout(()=>{this._token=-1,e()},t)}setIfNotSet(e,t){if(this._isDisposed)throw new Error("Calling setIfNotSet on a disposed TimeoutTimer");-1===this._token&&(this._token=setTimeout(()=>{this._token=-1,e()},t))}},t.MicrotaskTimer=class{constructor(){this._isScheduled=!1,this._isDisposed=!1}dispose(){this.cancel(),this._isDisposed=!0}cancel(){this._isScheduled=!1}set(e){if(this._isDisposed)throw new Error("Calling set on a disposed MicrotaskTimer");this._isScheduled||(this._isScheduled=!0,queueMicrotask(()=>{this._isScheduled&&(this._isScheduled=!1,e())}))}},t.IntervalTimer=class{constructor(){this._isDisposed=!1}cancel(){this._disposable?.dispose(),this._disposable=void 0}cancelAndSet(e,t,s=globalThis){if(this._isDisposed)throw new Error("Calling cancelAndSet on a disposed IntervalTimer");this.cancel();const i=s.setInterval(()=>{e()},t);this._disposable={dispose:()=>{s.clearInterval(i),this._disposable=void 0}}}dispose(){this.cancel(),this._isDisposed=!0}}},414(e,t,s){Object.defineProperty(t,"__esModule",{value:!0}),t.EventUtils=t.Emitter=void 0;const i=s(426);var r;t.Emitter=class{constructor(){this._listeners=[],this._disposed=!1}get event(){return this._event||(this._event=(e,t,s)=>{if(this._disposed)return(0,i.toDisposable)(()=>{});const r={fn:e,thisArgs:t};this._listeners=this._listeners.slice(),this._listeners.push(r);const n=(0,i.toDisposable)(()=>{const e=this._listeners.indexOf(r);-1!==e&&(this._listeners=this._listeners.slice(),this._listeners.splice(e,1))});return s&&(Array.isArray(s)?s.push(n):s.add(n)),n}),this._event}fire(e){if(this._disposed||!this._listeners.length)return;if(1===this._listeners.length)return void this._listeners[0].fn.call(this._listeners[0].thisArgs,e);const t=this._listeners;for(let s=0,i=t.length;st.fire(e))},e.map=function(e,t){return(s,i,r)=>e(e=>s.call(i,t(e)),void 0,r)},e.any=function(...e){return(t,s,r)=>{const n=new i.DisposableStore;for(const i of e)n.add(i(e=>t.call(s,e)));return r&&(Array.isArray(r)?r.push(n):r.add(n)),n}},e.runAndSubscribe=function(e,t,s){return t(s),e(e=>t(e))}}(r||(t.EventUtils=r={}))},426(e,t){function s(e){return{dispose:e}}function i(e){if(!e)return e;if(Array.isArray(e)){for(const t of e)t.dispose();return[]}return e.dispose(),e}Object.defineProperty(t,"__esModule",{value:!0}),t.MutableDisposable=t.Disposable=t.DisposableStore=void 0,t.toDisposable=s,t.dispose=i,t.combinedDisposable=function(...e){return s(()=>i(e))};class r{constructor(){this._disposables=new Set,this._isDisposed=!1}get isDisposed(){return this._isDisposed}add(e){return this._isDisposed?e.dispose():this._disposables.add(e),e}dispose(){if(!this._isDisposed){this._isDisposed=!0;for(const e of this._disposables)e.dispose();this._disposables.clear()}}clear(){for(const e of this._disposables)e.dispose();this._disposables.clear()}}t.DisposableStore=r;class n{constructor(){this._store=new r}dispose(){this._store.dispose()}_register(e){return this._store.add(e)}}t.Disposable=n,n.None=Object.freeze({dispose(){}}),t.MutableDisposable=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(e){this._isDisposed||e===this._value||(this._value?.dispose(),this._value=e)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,this._value?.dispose(),this._value=void 0}}},864(e,t,s){Object.defineProperty(t,"__esModule",{value:!0}),t.DecorationManager=void 0;const i=s(426);class r extends i.Disposable{constructor(e){super(),this._terminal=e,this._highlightDecorations=[],this._highlightedLines=new Set,this._register((0,i.toDisposable)(()=>this.clearHighlightDecorations()))}createHighlightDecorations(e,t){this.clearHighlightDecorations();for(const s of e){const e=this._createResultDecorations(s,t,!1);if(e)for(const t of e)this._storeDecoration(t,s)}}createActiveDecoration(e,t){const s=this._createResultDecorations(e,t,!0);if(s)return{decorations:s,match:e,dispose(){(0,i.dispose)(s)}}}clearHighlightDecorations(){(0,i.dispose)(this._highlightDecorations),this._highlightDecorations=[],this._highlightedLines.clear()}_storeDecoration(e,t){this._highlightedLines.add(e.marker.line),this._highlightDecorations.push({decoration:e,match:t,dispose(){e.dispose()}})}_applyStyles(e,t,s){e.classList.contains("xterm-find-result-decoration")||(e.classList.add("xterm-find-result-decoration"),t&&(e.style.outline=`1px solid ${t}`)),s&&e.classList.add("xterm-find-active-result-decoration")}_createResultDecorations(e,t,s){const r=[];let n=e.col,o=e.size,a=-this._terminal.buffer.active.baseY-this._terminal.buffer.active.cursorY+e.row;for(;o>0;){const e=Math.min(this._terminal.cols-n,o);r.push([a,n,e]),n=0,o-=e,a++}const h=[];for(const e of r){const r=this._terminal.registerMarker(e[0]),n=this._terminal.registerDecoration({marker:r,x:e[1],width:e[2],layer:s?"top":"bottom",backgroundColor:s?t.activeMatchBackground:t.matchBackground,overviewRulerOptions:this._highlightedLines.has(r.line)?void 0:{color:s?t.activeMatchColorOverviewRuler:t.matchOverviewRuler,position:"center"}});if(n){const e=[];e.push(r),e.push(n.onRender(e=>this._applyStyles(e,s?t.activeMatchBorder:t.matchBorder,!1))),e.push(n.onDispose(()=>(0,i.dispose)(e))),h.push(n)}}return 0===h.length?void 0:h}}t.DecorationManager=r},615(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.SearchEngine=void 0,t.SearchEngine=class{constructor(e,t){this._terminal=e,this._lineCache=t}find(e,t,s,i){if(!e||0===e.length)return void this._terminal.clearSelection();if(s>=this._terminal.cols)throw new Error(`Invalid col: ${s} to search in terminal of ${this._terminal.cols} cols`);this._lineCache.initLinesCache();const r={startRow:t,startCol:s};let n=this._findInLine(e,r,i);if(!n)for(let s=t+1;s=0&&(a.startRow=s,h=this._findInLine(e,a,t,o),!h);s--);}if(!h&&r!==this._terminal.buffer.active.baseY+this._terminal.rows-1)for(let s=this._terminal.buffer.active.baseY+this._terminal.rows-1;s>=r&&(a.startRow=s,h=this._findInLine(e,a,t,o),!h);s--);return h}_isWholeWord(e,t,s){return(0===e||" ~!@#$%^&*()+`-=[]{}|\\;:\"',./<>?".includes(t[e-1]))&&(e+s.length===t.length||" ~!@#$%^&*()+`-=[]{}|\\;:\"',./<>?".includes(t[e+s.length]))}_findInLine(e,t,s={},i=!1){const r=t.startRow,n=t.startCol,o=this._terminal.buffer.active.getLine(r);if(o?.isWrapped)return i?void(t.startCol+=this._terminal.cols):(t.startRow--,t.startCol+=this._terminal.cols,this._findInLine(e,t,s));let a=this._lineCache.getLineFromCache(r);a||(a=this._lineCache.translateBufferLineToStringWithWrap(r,!0),this._lineCache.setLineInCache(r,a));const[h,l]=a,c=this._bufferColsToStringOffset(r,n);let d=e,_=h;s.regex||(d=s.caseSensitive?e:e.toLowerCase(),_=s.caseSensitive?h:h.toLowerCase());let u=-1;if(s.regex){const t=RegExp(d,s.caseSensitive?"g":"gi");let r;if(i)for(;r=t.exec(_.slice(0,c));)u=t.lastIndex-r[0].length,e=r[0],t.lastIndex-=e.length-1;else r=t.exec(_.slice(c)),r&&r[0].length>0&&(u=c+(t.lastIndex-r[0].length),e=r[0])}else i?c-d.length>=0&&(u=_.lastIndexOf(d,c-d.length)):u=_.indexOf(d,c);if(u>=0){if(s.wholeWord&&!this._isWholeWord(u,_,e))return;let t=0;for(;t=l[t+1];)t++;let i=t;for(;i=l[i+1];)i++;const n=u-l[t],o=u+e.length-l[i],a=this._stringLengthToBufferSize(r+t,n);return{term:e,col:a,row:r+t,size:this._stringLengthToBufferSize(r+i,o)-a+this._terminal.cols*(i-t)}}}_stringLengthToBufferSize(e,t){const s=this._terminal.buffer.active.getLine(e);if(!s)return 0;for(let e=0;e1&&(t-=r.length-1);const n=s.getCell(e+1);n&&0===n.getWidth()&&t++}return t}_bufferColsToStringOffset(e,t){let s=e,i=0,r=this._terminal.buffer.active.getLine(s);for(;t>0&&r;){for(let e=0;ethis._destroyLinesCache()))}initLinesCache(){this._linesCache||(this._linesCache=new Array(this._terminal.buffer.active.length),this._linesCacheDisposables.value=(0,i.combinedDisposable)(this._terminal.onLineFeed(()=>this._destroyLinesCache()),this._terminal.onCursorMove(()=>this._destroyLinesCache()),this._terminal.onResize(()=>this._destroyLinesCache()))),this._lastAccessTimestamp=Date.now(),this._linesCacheTimeout.value||this._scheduleLinesCacheTimeout(15e3)}_destroyLinesCache(){this._linesCache=void 0,this._lastAccessTimestamp=0,this._linesCacheDisposables.clear(),this._linesCacheTimeout.clear()}_scheduleLinesCacheTimeout(e){this._linesCacheTimeout.value=(0,r.disposableTimeout)(()=>{if(!this._linesCache)return;const e=Date.now()-this._lastAccessTimestamp;e>=15e3?this._destroyLinesCache():this._scheduleLinesCacheTimeout(15e3-e)},e)}getLineFromCache(e){return this._linesCache?.[e]}setLineInCache(e,t){this._linesCache&&(this._linesCache[e]=t)}translateBufferLineToStringWithWrap(e,t){const s=[],i=[0];let r=this._terminal.buffer.active.getLine(e);for(;r;){const n=this._terminal.buffer.active.getLine(e+1),o=!!n&&n.isWrapped;let a=r.translateToString(!o&&t);if(o&&n){const e=r.getCell(r.length-1);e&&0===e.getCode()&&1===e.getWidth()&&2===n.getCell(0)?.getWidth()&&(a=a.slice(0,-1))}if(s.push(a),!o)break;i.push(i[i.length-1]+a.length),e++,r=n}return[s.join(""),i]}}t.SearchLineCache=n},438(e,t,s){Object.defineProperty(t,"__esModule",{value:!0}),t.SearchResultTracker=void 0;const i=s(414),r=s(426);class n extends r.Disposable{constructor(){super(...arguments),this._searchResults=[],this._onDidChangeResults=this._register(new i.Emitter)}get onDidChangeResults(){return this._onDidChangeResults.event}get searchResults(){return this._searchResults}get selectedDecoration(){return this._selectedDecoration}set selectedDecoration(e){this._selectedDecoration=e}updateResults(e,t){this._searchResults=e.slice(0,t)}clearResults(){this._searchResults=[]}clearSelectedDecoration(){this._selectedDecoration&&(this._selectedDecoration.dispose(),this._selectedDecoration=void 0)}findResultIndex(e){for(let t=0;t0)}didOptionsChange(e){return!this._lastSearchOptions||!!e&&(this._lastSearchOptions.caseSensitive!==e.caseSensitive||this._lastSearchOptions.regex!==e.regex||this._lastSearchOptions.wholeWord!==e.wholeWord)}shouldUpdateHighlighting(e,t){return!!t?.decorations&&(void 0===this._cachedSearchTerm||e!==this._cachedSearchTerm||this.didOptionsChange(t))}clearCachedTerm(){this._cachedSearchTerm=void 0}reset(){this._cachedSearchTerm=void 0,this._lastSearchOptions=void 0}}}},t={};function s(i){var r=t[i];if(void 0!==r)return r.exports;var n=t[i]={exports:{}};return e[i](n,n.exports,s),n.exports}var i={};return(()=>{var e=i;Object.defineProperty(e,"__esModule",{value:!0}),e.SearchAddon=void 0;const t=s(414),r=s(426),n=s(578),o=s(149),a=s(772),h=s(615),l=s(864),c=s(438);class d extends r.Disposable{get onDidChangeResults(){return this._resultTracker.onDidChangeResults}constructor(e){super(),this._highlightTimeout=this._register(new r.MutableDisposable),this._lineCache=this._register(new r.MutableDisposable),this._state=new a.SearchState,this._resultTracker=this._register(new c.SearchResultTracker),this._onAfterSearch=this._register(new t.Emitter),this.onAfterSearch=this._onAfterSearch.event,this._onBeforeSearch=this._register(new t.Emitter),this.onBeforeSearch=this._onBeforeSearch.event,this._highlightLimit=e?.highlightLimit??1e3}activate(e){this._terminal=e,this._lineCache.value=new o.SearchLineCache(e),this._engine=new h.SearchEngine(e,this._lineCache.value),this._decorationManager=new l.DecorationManager(e),this._register(this._terminal.onWriteParsed(()=>this._updateMatches())),this._register(this._terminal.onResize(()=>this._updateMatches())),this._register((0,r.toDisposable)(()=>this.clearDecorations()))}_updateMatches(){this._highlightTimeout.clear(),this._state.cachedSearchTerm&&this._state.lastSearchOptions?.decorations&&(this._highlightTimeout.value=(0,n.disposableTimeout)(()=>{const e=this._state.cachedSearchTerm;this._state.clearCachedTerm(),this.findPrevious(e,{...this._state.lastSearchOptions,incremental:!0},{noScroll:!0})},200))}clearDecorations(e){this._resultTracker.clearSelectedDecoration(),this._decorationManager?.clearHighlightDecorations(),this._resultTracker.clearResults(),e||this._state.clearCachedTerm()}clearActiveDecoration(){this._resultTracker.clearSelectedDecoration()}findNext(e,t,s){if(!this._terminal||!this._engine)throw new Error("Cannot use addon until it has been loaded");this._onBeforeSearch.fire(),this._state.lastSearchOptions=t,this._state.shouldUpdateHighlighting(e,t)&&this._highlightAllMatches(e,t);const i=this._findNextAndSelect(e,t,s);return this._fireResults(t),this._state.cachedSearchTerm=e,this._onAfterSearch.fire(),i}_highlightAllMatches(e,t){if(!this._terminal||!this._engine||!this._decorationManager)throw new Error("Cannot use addon until it has been loaded");if(!this._state.isValidSearchTerm(e))return void this.clearDecorations();this.clearDecorations(!0);const s=[];let i,r=this._engine.find(e,0,0,t);for(;r&&(i?.row!==r.row||i?.col!==r.col)&&!(s.length>=this._highlightLimit);){i=r,s.push(i);const n=this._terminal.cols;let o=i.col+i.size,a=i.row;o>=n&&(a+=Math.floor(o/n),o%=n),r=this._engine.find(e,a,o,t)}this._resultTracker.updateResults(s,this._highlightLimit),t.decorations&&this._decorationManager.createHighlightDecorations(s,t.decorations)}_findNextAndSelect(e,t,s){if(!this._terminal||!this._engine)return!1;if(!this._state.isValidSearchTerm(e))return this._terminal.clearSelection(),this.clearDecorations(),!1;const i=this._engine.findNextWithSelection(e,t,this._state.cachedSearchTerm);return this._selectResult(i,t?.decorations,s?.noScroll)}findPrevious(e,t,s){if(!this._terminal||!this._engine)throw new Error("Cannot use addon until it has been loaded");this._onBeforeSearch.fire(),this._state.lastSearchOptions=t,this._state.shouldUpdateHighlighting(e,t)&&this._highlightAllMatches(e,t);const i=this._findPreviousAndSelect(e,t,s);return this._fireResults(t),this._state.cachedSearchTerm=e,this._onAfterSearch.fire(),i}_fireResults(e){this._resultTracker.fireResultsChanged(!!e?.decorations)}_findPreviousAndSelect(e,t,s){if(!this._terminal||!this._engine)return!1;if(!this._state.isValidSearchTerm(e))return this._terminal.clearSelection(),this.clearDecorations(),!1;const i=this._engine.findPreviousWithSelection(e,t,this._state.cachedSearchTerm);return this._selectResult(i,t?.decorations,s?.noScroll)}_selectResult(e,t,s){if(!this._terminal||!this._decorationManager)return!1;if(this._resultTracker.clearSelectedDecoration(),!e)return this._terminal.clearSelection(),!1;if(this._terminal.select(e.col,e.row,e.size),t){const s=this._decorationManager.createActiveDecoration(e,t);s&&(this._resultTracker.selectedDecoration=s)}if(!s&&(e.row>=this._terminal.buffer.active.viewportY+this._terminal.rows||e.row(()=>{"use strict";var e={578(e,t,s){Object.defineProperty(t,"__esModule",{value:!0}),t.IntervalTimer=t.MicrotaskTimer=t.TimeoutTimer=void 0,t.timeout=function(e){return new Promise(t=>setTimeout(t,e))},t.disposableTimeout=function(e,t=0,s){const r=setTimeout(()=>{e(),s&&o.dispose()},t),o=(0,i.toDisposable)(()=>{clearTimeout(r)});return s?.add(o),o};const i=s(426);t.TimeoutTimer=class{constructor(){this._token=-1,this._isDisposed=!1}dispose(){this.cancel(),this._isDisposed=!0}cancel(){-1!==this._token&&(clearTimeout(this._token),this._token=-1)}cancelAndSet(e,t){if(this._isDisposed)throw new Error("Calling cancelAndSet on a disposed TimeoutTimer");this.cancel(),this._token=setTimeout(()=>{this._token=-1,e()},t)}setIfNotSet(e,t){if(this._isDisposed)throw new Error("Calling setIfNotSet on a disposed TimeoutTimer");-1===this._token&&(this._token=setTimeout(()=>{this._token=-1,e()},t))}},t.MicrotaskTimer=class{constructor(){this._isScheduled=!1,this._isDisposed=!1}dispose(){this.cancel(),this._isDisposed=!0}cancel(){this._isScheduled=!1}set(e){if(this._isDisposed)throw new Error("Calling set on a disposed MicrotaskTimer");this._isScheduled||(this._isScheduled=!0,queueMicrotask(()=>{this._isScheduled&&(this._isScheduled=!1,e())}))}},t.IntervalTimer=class{constructor(){this._isDisposed=!1}cancel(){this._disposable?.dispose(),this._disposable=void 0}cancelAndSet(e,t,s=globalThis){if(this._isDisposed)throw new Error("Calling cancelAndSet on a disposed IntervalTimer");this.cancel();const i=s.setInterval(()=>{e()},t);this._disposable={dispose:()=>{s.clearInterval(i),this._disposable=void 0}}}dispose(){this.cancel(),this._isDisposed=!0}}},414(e,t,s){Object.defineProperty(t,"__esModule",{value:!0}),t.EventUtils=t.Emitter=void 0;const i=s(426);var r;t.Emitter=class{constructor(){this._listeners=[],this._disposed=!1}get event(){return this._event||(this._event=(e,t,s)=>{if(this._disposed)return(0,i.toDisposable)(()=>{});const r={fn:e,thisArgs:t};this._listeners=this._listeners.slice(),this._listeners.push(r);const o=(0,i.toDisposable)(()=>{const e=this._listeners.indexOf(r);-1!==e&&(this._listeners=this._listeners.slice(),this._listeners.splice(e,1))});return s&&(Array.isArray(s)?s.push(o):s.add(o)),o}),this._event}fire(e){if(this._disposed||!this._listeners.length)return;if(1===this._listeners.length)return void this._listeners[0].fn.call(this._listeners[0].thisArgs,e);const t=this._listeners;for(let s=0,i=t.length;st.fire(e))},e.map=function(e,t){return(s,i,r)=>e(e=>s.call(i,t(e)),void 0,r)},e.any=function(...e){return(t,s,r)=>{const o=new i.DisposableStore;for(const i of e)o.add(i(e=>t.call(s,e)));return r&&(Array.isArray(r)?r.push(o):r.add(o)),o}},e.runAndSubscribe=function(e,t,s){return t(s),e(e=>t(e))}}(r||(t.EventUtils=r={}))},426(e,t){function s(e){return{dispose:e}}function i(e){if(!e)return e;if(Array.isArray(e)){for(const t of e)t.dispose();return[]}return e.dispose(),e}Object.defineProperty(t,"__esModule",{value:!0}),t.MutableDisposable=t.Disposable=t.DisposableStore=void 0,t.toDisposable=s,t.dispose=i,t.combinedDisposable=function(...e){return s(()=>i(e))};class r{constructor(){this._disposables=new Set,this._isDisposed=!1}get isDisposed(){return this._isDisposed}add(e){return this._isDisposed?e.dispose():this._disposables.add(e),e}dispose(){if(!this._isDisposed){this._isDisposed=!0;for(const e of this._disposables)e.dispose();this._disposables.clear()}}clear(){for(const e of this._disposables)e.dispose();this._disposables.clear()}}t.DisposableStore=r;class o{constructor(){this._store=new r}dispose(){this._store.dispose()}_register(e){return this._store.add(e)}}t.Disposable=o,o.None=Object.freeze({dispose(){}}),t.MutableDisposable=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(e){this._isDisposed||e===this._value||(this._value?.dispose(),this._value=e)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,this._value?.dispose(),this._value=void 0}}},864(e,t,s){Object.defineProperty(t,"__esModule",{value:!0}),t.DecorationManager=void 0;const i=s(426);class r extends i.Disposable{constructor(e){super(),this._terminal=e,this._highlightDecorations=[],this._highlightedLines=new Set,this._register((0,i.toDisposable)(()=>this.clearHighlightDecorations()))}createHighlightDecorations(e,t){this.clearHighlightDecorations();for(const s of e){const e=this._createResultDecorations(s,t,!1);if(e)for(const t of e)this._storeDecoration(t,s)}}createActiveDecoration(e,t){const s=this._createResultDecorations(e,t,!0);if(s)return{decorations:s,match:e,dispose(){(0,i.dispose)(s)}}}clearHighlightDecorations(){(0,i.dispose)(this._highlightDecorations),this._highlightDecorations=[],this._highlightedLines.clear()}_storeDecoration(e,t){this._highlightedLines.add(e.marker.line),this._highlightDecorations.push({decoration:e,match:t,dispose(){e.dispose()}})}_applyStyles(e,t,s){e.classList.contains("xterm-find-result-decoration")||(e.classList.add("xterm-find-result-decoration"),t&&(e.style.outline=`1px solid ${t}`)),s&&e.classList.add("xterm-find-active-result-decoration")}_createResultDecorations(e,t,s){const r=[];let o=e.col,n=e.size,a=-this._terminal.buffer.active.baseY-this._terminal.buffer.active.cursorY+e.row;for(;n>0;){const e=Math.min(this._terminal.cols-o,n);r.push([a,o,e]),o=0,n-=e,a++}const h=[];for(const e of r){const r=this._terminal.registerMarker(e[0]),o=this._terminal.registerDecoration({marker:r,x:e[1],width:e[2],layer:s?"top":"bottom",backgroundColor:s?t.activeMatchBackground:t.matchBackground,overviewRulerOptions:this._highlightedLines.has(r.line)?void 0:{color:s?t.activeMatchColorOverviewRuler:t.matchOverviewRuler,position:"center"}});if(o){const e=[];e.push(r),e.push(o.onRender(e=>this._applyStyles(e,s?t.activeMatchBorder:t.matchBorder,!1))),e.push(o.onDispose(()=>(0,i.dispose)(e))),h.push(o)}}return 0===h.length?void 0:h}}t.DecorationManager=r},615(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.SearchEngine=void 0,t.SearchEngine=class{constructor(e,t){this._terminal=e,this._lineCache=t}find(e,t,s,i){if(!e||0===e.length)return void this._terminal.clearSelection();if(s>=this._terminal.cols)throw new Error(`Invalid col: ${s} to search in terminal of ${this._terminal.cols} cols`);this._lineCache.initLinesCache();const r={startRow:t,startCol:s};let o=this._findInLine(e,r,i);if(!o)for(let s=t+1;s0&&this._isRowCoveredByEarlierSearch(s)||(n.startRow=s,n.startCol=0,a=this._findInLine(e,n,t),!a));s++);return!a&&i&&(n.startRow=i.start.y,n.startCol=0,a=this._findInLine(e,n,t)),a}findPreviousWithSelection(e,t,s){if(!e||0===e.length)return void this._terminal.clearSelection();const i=this._terminal.getSelectionPosition();this._terminal.clearSelection();let r=this._terminal.buffer.active.baseY+this._terminal.rows-1;const o=this._terminal.cols,n=!0;this._lineCache.initLinesCache();const a={startRow:r,startCol:o};let h;if(i&&(a.startRow=r=i.start.y,a.startCol=i.start.x,s!==e&&(h=this._findInLine(e,a,t,!1),h||(a.startRow=r=i.end.y,a.startCol=i.end.x))),h??=this._findInLine(e,a,t,n),!h){a.startCol=Math.max(a.startCol,this._terminal.cols);for(let s=r-1;s>=0&&(a.startRow=s,h=this._findInLine(e,a,t,n),!h);s--);}if(!h&&r!==this._terminal.buffer.active.baseY+this._terminal.rows-1)for(let s=this._terminal.buffer.active.baseY+this._terminal.rows-1;s>=r&&(a.startRow=s,h=this._findInLine(e,a,t,n),!h);s--);return h}_isWholeWord(e,t,s){return(0===e||" ~!@#$%^&*()+`-=[]{}|\\;:\"',./<>?".includes(t[e-1]))&&(e+s.length===t.length||" ~!@#$%^&*()+`-=[]{}|\\;:\"',./<>?".includes(t[e+s.length]))}_satisfiesWholeWord(e,t,s,i){return!i.wholeWord||this._isWholeWord(e,t,s)}_isRowCoveredByEarlierSearch(e){return!0===this._terminal.buffer.active.getLine(e)?.isWrapped}_findInLine(e,t,s={},i=!1){if(i){if(t.startRow>0&&this._terminal.buffer.active.getLine(t.startRow)?.isWrapped)return void(t.startCol+=this._terminal.cols)}else for(;t.startRow>0&&this._terminal.buffer.active.getLine(t.startRow)?.isWrapped;)t.startRow--,t.startCol+=this._terminal.cols;const r=t.startRow,o=t.startCol;let n=this._lineCache.getLineFromCache(r);n||(n=this._lineCache.translateBufferLineToStringWithWrap(r,!0),this._lineCache.setLineInCache(r,n));const[a,h]=n,l=this._bufferColsToStringOffset(r,o,h);let c=e,d=a;s.regex||(c=s.caseSensitive?e:e.toLowerCase(),d=s.caseSensitive?a:a.toLowerCase());let _=-1;if(s.regex){const t=RegExp(c,s.caseSensitive?"g":"gi");let r;if(i)for(;r=t.exec(d.slice(0,l));){const i=t.lastIndex-r[0].length;r[0].length>0&&this._satisfiesWholeWord(i,d,r[0],s)&&(_=i,e=r[0]),t.lastIndex=i+1}else for(t.lastIndex=l;r=t.exec(d);){const i=t.lastIndex-r[0].length;if(r[0].length>0&&this._satisfiesWholeWord(i,d,r[0],s)){_=i,e=r[0];break}t.lastIndex=i+1}}else if(i){let e=l-c.length>=0?d.lastIndexOf(c,l-c.length):-1;for(;e>=0&&!this._satisfiesWholeWord(e,d,c,s);)e=e>0?d.lastIndexOf(c,e-1):-1;_=e}else{let e=d.indexOf(c,l);for(;e>=0&&!this._satisfiesWholeWord(e,d,c,s);)e=d.indexOf(c,e+1);_=e}if(_>=0){let t=0;for(;t=h[t+1];)t++;let s=t;for(;s=h[s+1];)s++;const i=_-h[t],o=_+e.length-h[s],n=this._stringLengthToBufferSize(r+t,i);return{term:e,col:n,row:r+t,size:this._stringLengthToBufferSize(r+s,o)-n+this._terminal.cols*(s-t)}}}_stringLengthToBufferSize(e,t){const s=this._terminal.buffer.active.getLine(e);if(!s)return 0;for(let e=0;e1&&(t-=r.length-1);const o=s.getCell(e+1);o&&0===o.getWidth()&&t++}return t}_bufferColsToStringOffset(e,t,s){const i=Math.min(Math.floor(t/this._terminal.cols),s.length-1);let r=s[i];const o=this._terminal.buffer.active.getLine(e+i);if(o){const e=Math.min(t-i*this._terminal.cols,this._terminal.cols);for(let t=0;tthis._destroyLinesCache()))}initLinesCache(){this._linesCache||(this._linesCache=new Array(this._terminal.buffer.active.length),this._linesCacheDisposables.value=(0,i.combinedDisposable)(this._terminal.onLineFeed(()=>this._destroyLinesCache()),this._terminal.onCursorMove(()=>this._destroyLinesCache()),this._terminal.onResize(()=>this._destroyLinesCache()))),this._lastAccessTimestamp=Date.now(),this._linesCacheTimeout.value||this._scheduleLinesCacheTimeout(15e3)}_destroyLinesCache(){this._linesCache=void 0,this._lastAccessTimestamp=0,this._linesCacheDisposables.clear(),this._linesCacheTimeout.clear()}_scheduleLinesCacheTimeout(e){this._linesCacheTimeout.value=(0,r.disposableTimeout)(()=>{if(!this._linesCache)return;const e=Date.now()-this._lastAccessTimestamp;e>=15e3?this._destroyLinesCache():this._scheduleLinesCacheTimeout(15e3-e)},e)}getLineFromCache(e){return this._linesCache?.[e]}setLineInCache(e,t){this._linesCache&&(this._linesCache[e]=t)}translateBufferLineToStringWithWrap(e,t){const s=[],i=[0],r=this._terminal.buffer.active.length;let o=this._terminal.buffer.active.getLine(e);for(;o;){const n=e+10)}didOptionsChange(e){return!this._lastSearchOptions||!!e&&(this._lastSearchOptions.caseSensitive!==e.caseSensitive||this._lastSearchOptions.regex!==e.regex||this._lastSearchOptions.wholeWord!==e.wholeWord)}shouldUpdateHighlighting(e,t){return!!t?.decorations&&(void 0===this._cachedSearchTerm||e!==this._cachedSearchTerm||this.didOptionsChange(t))}clearCachedTerm(){this._cachedSearchTerm=void 0}reset(){this._cachedSearchTerm=void 0,this._lastSearchOptions=void 0}}}},t={};function s(i){var r=t[i];if(void 0!==r)return r.exports;var o=t[i]={exports:{}};return e[i](o,o.exports,s),o.exports}var i={};return(()=>{var e=i;Object.defineProperty(e,"__esModule",{value:!0}),e.SearchAddon=void 0;const t=s(414),r=s(426),o=s(578),n=s(149),a=s(772),h=s(615),l=s(864),c=s(438);class d extends r.Disposable{get onDidChangeResults(){return this._resultTracker.onDidChangeResults}constructor(e){super(),this._highlightTimeout=this._register(new r.MutableDisposable),this._lineCache=this._register(new r.MutableDisposable),this._state=new a.SearchState,this._resultTracker=this._register(new c.SearchResultTracker),this._onAfterSearch=this._register(new t.Emitter),this.onAfterSearch=this._onAfterSearch.event,this._onBeforeSearch=this._register(new t.Emitter),this.onBeforeSearch=this._onBeforeSearch.event,this._highlightLimit=e?.highlightLimit??1e3}activate(e){this._terminal=e,this._lineCache.value=new n.SearchLineCache(e),this._engine=new h.SearchEngine(e,this._lineCache.value),this._decorationManager=new l.DecorationManager(e),this._register(this._terminal.onWriteParsed(()=>this._updateMatches())),this._register(this._terminal.onResize(()=>this._updateMatches())),this._register((0,r.toDisposable)(()=>this.clearDecorations()))}_updateMatches(){this._highlightTimeout.clear(),this._state.cachedSearchTerm&&this._state.lastSearchOptions?.decorations&&(this._highlightTimeout.value=(0,o.disposableTimeout)(()=>{const e=this._state.cachedSearchTerm;this._state.clearCachedTerm(),this.findPrevious(e,{...this._state.lastSearchOptions,incremental:!0},{noScroll:!0})},200))}clearDecorations(e){this._resultTracker.clearSelectedDecoration(),this._decorationManager?.clearHighlightDecorations(),this._resultTracker.clearResults(),e||this._state.clearCachedTerm()}clearActiveDecoration(){this._resultTracker.clearSelectedDecoration()}findNext(e,t,s){if(!this._terminal||!this._engine)throw new Error("Cannot use addon until it has been loaded");this._onBeforeSearch.fire(),this._state.lastSearchOptions=t,this._state.shouldUpdateHighlighting(e,t)&&this._highlightAllMatches(e,t);const i=this._findNextAndSelect(e,t,s);return this._fireResults(t),this._state.cachedSearchTerm=e,this._onAfterSearch.fire(),i}_highlightAllMatches(e,t){if(!this._terminal||!this._engine||!this._decorationManager)throw new Error("Cannot use addon until it has been loaded");if(!this._state.isValidSearchTerm(e))return void this.clearDecorations();this.clearDecorations(!0);const s=[];let i,r=this._engine.find(e,0,0,t);for(;r&&(i?.row!==r.row||i?.col!==r.col)&&!(s.length>=this._highlightLimit);){i=r,s.push(i);const o=this._terminal.cols;let n=i.col+i.size,a=i.row;n>=o&&(a+=Math.floor(n/o),n%=o),r=this._engine.find(e,a,n,t)}this._resultTracker.updateResults(s,this._highlightLimit),t.decorations&&this._decorationManager.createHighlightDecorations(s,t.decorations)}_findNextAndSelect(e,t,s){if(!this._terminal||!this._engine)return!1;if(!this._state.isValidSearchTerm(e))return this._terminal.clearSelection(),this.clearDecorations(),!1;const i=this._engine.findNextWithSelection(e,t,this._state.cachedSearchTerm);return this._selectResult(i,t?.decorations,s?.noScroll)}findPrevious(e,t,s){if(!this._terminal||!this._engine)throw new Error("Cannot use addon until it has been loaded");this._onBeforeSearch.fire(),this._state.lastSearchOptions=t,this._state.shouldUpdateHighlighting(e,t)&&this._highlightAllMatches(e,t);const i=this._findPreviousAndSelect(e,t,s);return this._fireResults(t),this._state.cachedSearchTerm=e,this._onAfterSearch.fire(),i}_fireResults(e){this._resultTracker.fireResultsChanged(!!e?.decorations)}_findPreviousAndSelect(e,t,s){if(!this._terminal||!this._engine)return!1;if(!this._state.isValidSearchTerm(e))return this._terminal.clearSelection(),this.clearDecorations(),!1;const i=this._engine.findPreviousWithSelection(e,t,this._state.cachedSearchTerm);return this._selectResult(i,t?.decorations,s?.noScroll)}_selectResult(e,t,s){if(!this._terminal||!this._decorationManager)return!1;if(this._resultTracker.clearSelectedDecoration(),!e)return this._terminal.clearSelection(),!1;if(this._terminal.select(e.col,e.row,e.size),t){const s=this._decorationManager.createActiveDecoration(e,t);s&&(this._resultTracker.selectedDecoration=s)}if(!s&&(e.row>=this._terminal.buffer.active.viewportY+this._terminal.rows||e.row {\nreturn ","/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal async helpers for xterm.js core.\n */\n\nimport { DisposableStore, IDisposable, toDisposable } from './Lifecycle';\n\nexport function timeout(millis: number): Promise {\n return new Promise(resolve => setTimeout(resolve, millis));\n}\n\n/**\n * Creates a timeout that can be disposed using its returned value.\n * @param handler The timeout handler.\n * @param timeout An optional timeout in milliseconds.\n * @param store An optional {@link DisposableStore} that will have the timeout disposable managed\n * automatically.\n */\nexport function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable {\n const timer = setTimeout(() => {\n handler();\n if (store) {\n disposable.dispose();\n }\n }, timeout);\n const disposable = toDisposable(() => {\n clearTimeout(timer);\n });\n store?.add(disposable);\n return disposable;\n}\n\nexport class TimeoutTimer implements IDisposable {\n private _token: any = -1;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n if (this._token !== -1) {\n clearTimeout(this._token);\n this._token = -1;\n }\n }\n\n public cancelAndSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed TimeoutTimer');\n }\n this.cancel();\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n\n public setIfNotSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling setIfNotSet on a disposed TimeoutTimer');\n }\n if (this._token !== -1) {\n return;\n }\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n}\n\n/**\n * Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled\n * microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not\n * run yet.\n */\nexport class MicrotaskTimer implements IDisposable {\n private _isScheduled = false;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n this._isScheduled = false;\n }\n\n public set(runner: () => void): void {\n if (this._isDisposed) {\n throw new Error('Calling set on a disposed MicrotaskTimer');\n }\n if (this._isScheduled) {\n return;\n }\n this._isScheduled = true;\n queueMicrotask(() => {\n if (!this._isScheduled) {\n return;\n }\n this._isScheduled = false;\n runner();\n });\n }\n}\n\nexport class IntervalTimer implements IDisposable {\n private _disposable: IDisposable | undefined;\n private _isDisposed = false;\n\n public cancel(): void {\n this._disposable?.dispose();\n this._disposable = undefined;\n }\n\n public cancelAndSet(runner: () => void, interval: number, context: Window | typeof globalThis = globalThis): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed IntervalTimer');\n }\n this.cancel();\n const handle = context.setInterval(() => {\n runner();\n }, interval);\n this._disposable = {\n dispose: () => {\n context.clearInterval(handle as any);\n this._disposable = undefined;\n }\n };\n }\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n}\n","/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal event utilities for xterm.js core.\n * Simplified from VS Code's event.ts - no leak detection/profiling.\n */\n\nimport { IDisposable, DisposableStore, toDisposable } from './Lifecycle';\n\nexport interface IEvent {\n (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;\n}\n\nexport class Emitter {\n private _listeners: { fn: (e: T) => any, thisArgs: any }[] = [];\n private _disposed = false;\n private _event: IEvent | undefined;\n\n public get event(): IEvent {\n if (this._event) {\n return this._event;\n }\n this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n if (this._disposed) {\n return toDisposable(() => {});\n }\n\n const entry = { fn: listener, thisArgs };\n this._listeners = this._listeners.slice();\n this._listeners.push(entry);\n\n const result = toDisposable(() => {\n const idx = this._listeners.indexOf(entry);\n if (idx !== -1) {\n this._listeners = this._listeners.slice();\n this._listeners.splice(idx, 1);\n }\n });\n\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(result);\n } else {\n disposables.add(result);\n }\n }\n\n return result;\n };\n return this._event;\n }\n\n public fire(event: T): void {\n if (this._disposed || !this._listeners.length) {\n return;\n }\n if (this._listeners.length === 1) {\n this._listeners[0].fn.call(this._listeners[0].thisArgs, event);\n return;\n }\n const listeners = this._listeners;\n for (let i = 0, len = listeners.length; i < len; ++i) {\n listeners[i].fn.call(listeners[i].thisArgs, event);\n }\n }\n\n public dispose(): void {\n if (this._disposed) {\n return;\n }\n this._disposed = true;\n this._listeners.length = 0;\n }\n}\n\nexport namespace EventUtils {\n export function forward(from: IEvent, to: Emitter): IDisposable {\n return from(e => to.fire(e));\n }\n\n export function map(event: IEvent, map: (i: I) => O): IEvent {\n return (listener: (e: O) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n return event(i => listener.call(thisArgs, map(i)), undefined, disposables);\n };\n }\n\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent {\n return (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n const store = new DisposableStore();\n for (const event of events) {\n store.add(event(e => listener.call(thisArgs, e)));\n }\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(store);\n } else {\n disposables.add(store);\n }\n }\n return store;\n };\n }\n\n export function runAndSubscribe(event: IEvent, handler: (e: T) => void, initial: T): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void, initial?: T): IDisposable {\n handler(initial);\n return event(e => handler(e));\n }\n}\n","/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal lifecycle utilities for xterm.js core.\n * Simplified from VS Code's lifecycle.ts - no tracking/leak detection.\n */\n\nexport interface IDisposable {\n dispose(): void;\n}\n\nexport function toDisposable(fn: () => void): IDisposable {\n return { dispose: fn };\n}\n\nexport function dispose(disposable: T): T;\nexport function dispose(disposable: T | undefined): T | undefined;\nexport function dispose(disposables: T[]): T[];\nexport function dispose(arg: T | T[] | undefined): T | T[] | undefined {\n if (!arg) {\n return arg;\n }\n if (Array.isArray(arg)) {\n for (const d of arg) {\n d.dispose();\n }\n return [];\n }\n arg.dispose();\n return arg;\n}\n\nexport function combinedDisposable(...disposables: IDisposable[]): IDisposable {\n return toDisposable(() => dispose(disposables));\n}\n\nexport class DisposableStore implements IDisposable {\n private readonly _disposables = new Set();\n private _isDisposed = false;\n\n public get isDisposed(): boolean {\n return this._isDisposed;\n }\n\n public add(o: T): T {\n if (this._isDisposed) {\n o.dispose();\n } else {\n this._disposables.add(o);\n }\n return o;\n }\n\n public dispose(): void {\n if (this._isDisposed) {\n return;\n }\n this._isDisposed = true;\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n\n public clear(): void {\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n}\n\nexport abstract class Disposable implements IDisposable {\n public static readonly None: IDisposable = Object.freeze({ dispose() { } });\n\n protected readonly _store = new DisposableStore();\n\n public dispose(): void {\n this._store.dispose();\n }\n\n protected _register(o: T): T {\n return this._store.add(o);\n }\n}\n\nexport class MutableDisposable implements IDisposable {\n private _value: T | undefined;\n private _isDisposed = false;\n\n public get value(): T | undefined {\n return this._isDisposed ? undefined : this._value;\n }\n\n public set value(value: T | undefined) {\n if (this._isDisposed || value === this._value) {\n return;\n }\n this._value?.dispose();\n this._value = value;\n }\n\n public clear(): void {\n this.value = undefined;\n }\n\n public dispose(): void {\n this._isDisposed = true;\n this._value?.dispose();\n this._value = undefined;\n }\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';\nimport type { ISearchDecorationOptions } from '@xterm/addon-search';\nimport { dispose, Disposable, toDisposable } from 'common/Lifecycle';\nimport type { ISearchResult } from './SearchEngine';\n\n/**\n * Interface for managing a highlight decoration.\n */\ninterface IHighlight extends IDisposable {\n decoration: IDecoration;\n match: ISearchResult;\n}\n\n/**\n * Interface for managing multiple decorations for a single match.\n */\ninterface IMultiHighlight extends IDisposable {\n decorations: IDecoration[];\n match: ISearchResult;\n}\n\n/**\n * Manages visual decorations for search results including highlighting and active selection\n * indicators. This class handles the creation, styling, and disposal of search-related decorations.\n */\nexport class DecorationManager extends Disposable {\n private _highlightDecorations: IHighlight[] = [];\n private _highlightedLines: Set = new Set();\n\n constructor(private readonly _terminal: Terminal) {\n super();\n this._register(toDisposable(() => this.clearHighlightDecorations()));\n }\n\n /**\n * Creates decorations for all provided search results.\n * @param results The search results to create decorations for.\n * @param options The decoration options.\n */\n public createHighlightDecorations(results: ISearchResult[], options: ISearchDecorationOptions): void {\n this.clearHighlightDecorations();\n\n for (const match of results) {\n const decorations = this._createResultDecorations(match, options, false);\n if (decorations) {\n for (const decoration of decorations) {\n this._storeDecoration(decoration, match);\n }\n }\n }\n }\n\n /**\n * Creates decorations for the currently active search result.\n * @param result The active search result.\n * @param options The decoration options.\n * @returns The multi-highlight decoration or undefined if creation failed.\n */\n public createActiveDecoration(result: ISearchResult, options: ISearchDecorationOptions): IMultiHighlight | undefined {\n const decorations = this._createResultDecorations(result, options, true);\n if (decorations) {\n return { decorations, match: result, dispose() { dispose(decorations); } };\n }\n return undefined;\n }\n\n /**\n * Clears all highlight decorations.\n */\n public clearHighlightDecorations(): void {\n dispose(this._highlightDecorations);\n this._highlightDecorations = [];\n this._highlightedLines.clear();\n }\n\n /**\n * Stores a decoration and tracks it for management.\n * @param decoration The decoration to store.\n * @param match The search result this decoration represents.\n */\n private _storeDecoration(decoration: IDecoration, match: ISearchResult): void {\n this._highlightedLines.add(decoration.marker.line);\n this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });\n }\n\n /**\n * Applies styles to the decoration when it is rendered.\n * @param element The decoration's element.\n * @param borderColor The border color to apply.\n * @param isActiveResult Whether the element is part of the active search result.\n */\n private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {\n if (!element.classList.contains('xterm-find-result-decoration')) {\n element.classList.add('xterm-find-result-decoration');\n if (borderColor) {\n element.style.outline = `1px solid ${borderColor}`;\n }\n }\n if (isActiveResult) {\n element.classList.add('xterm-find-active-result-decoration');\n }\n }\n\n /**\n * Creates a decoration for the result and applies styles\n * @param result the search result for which to create the decoration\n * @param options the options for the decoration\n * @param isActiveResult whether this is the currently active result\n * @returns the decorations or undefined if the marker has already been disposed of\n */\n private _createResultDecorations(result: ISearchResult, options: ISearchDecorationOptions, isActiveResult: boolean): IDecoration[] | undefined {\n // Gather decoration ranges for this match as it could wrap\n const decorationRanges: [number, number, number][] = [];\n let currentCol = result.col;\n let remainingSize = result.size;\n let markerOffset = -this._terminal.buffer.active.baseY - this._terminal.buffer.active.cursorY + result.row;\n while (remainingSize > 0) {\n const amountThisRow = Math.min(this._terminal.cols - currentCol, remainingSize);\n decorationRanges.push([markerOffset, currentCol, amountThisRow]);\n currentCol = 0;\n remainingSize -= amountThisRow;\n markerOffset++;\n }\n\n // Create the decorations\n const decorations: IDecoration[] = [];\n for (const range of decorationRanges) {\n const marker = this._terminal.registerMarker(range[0]);\n const decoration = this._terminal.registerDecoration({\n marker,\n x: range[1],\n width: range[2],\n layer: isActiveResult ? 'top' : 'bottom',\n backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,\n overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {\n color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,\n position: 'center'\n }\n });\n if (decoration) {\n const disposables: IDisposable[] = [];\n disposables.push(marker);\n disposables.push(decoration.onRender((e) => this._applyStyles(e, isActiveResult ? options.activeMatchBorder : options.matchBorder, false)));\n disposables.push(decoration.onDispose(() => dispose(disposables)));\n decorations.push(decoration);\n }\n }\n\n return decorations.length === 0 ? undefined : decorations;\n }\n}\n\n\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal } from '@xterm/xterm';\nimport type { ISearchOptions } from '@xterm/addon-search';\nimport type { SearchLineCache } from './SearchLineCache';\n\n/**\n * Represents the position to start a search from.\n */\ninterface ISearchPosition {\n startCol: number;\n startRow: number;\n}\n\n/**\n * Represents a search result with its position and content.\n */\nexport interface ISearchResult {\n term: string;\n col: number;\n row: number;\n size: number;\n}\n\n/**\n * Configuration constants for the search engine functionality.\n */\nconst enum Constants {\n /**\n * Characters that are considered non-word characters for search boundary detection. These\n * characters are used to determine word boundaries when performing whole-word searches. Includes\n * common punctuation, symbols, and whitespace characters.\n */\n NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\\\;:\"\\',./<>?'\n}\n\n/**\n * Core search engine that handles finding text within terminal content.\n * This class is responsible for the actual search algorithms and position calculations.\n */\nexport class SearchEngine {\n constructor(\n private readonly _terminal: Terminal,\n private readonly _lineCache: SearchLineCache\n ) {}\n\n /**\n * Find the first occurrence of a term starting from a specific position.\n * @param term The search term.\n * @param startRow The row to start searching from.\n * @param startCol The column to start searching from.\n * @param searchOptions Search options.\n * @returns The search result if found, undefined otherwise.\n */\n public find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n if (startCol >= this._terminal.cols) {\n throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);\n }\n\n this._lineCache.initLinesCache();\n\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n // Search startRow\n let result = this._findInLine(term, searchPosition, searchOptions);\n // Search from startRow + 1 to end\n if (!result) {\n for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n return result;\n }\n\n /**\n * Find the next occurrence of a term with wrapping and selection management.\n * @param term The search term.\n * @param searchOptions Search options.\n * @param cachedSearchTerm The cached search term to determine incremental behavior.\n * @returns The search result if found, undefined otherwise.\n */\n public findNextWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n\n const prevSelectedPos = this._terminal.getSelectionPosition();\n this._terminal.clearSelection();\n\n let startCol = 0;\n let startRow = 0;\n if (prevSelectedPos) {\n if (cachedSearchTerm === term) {\n startCol = prevSelectedPos.end.x;\n startRow = prevSelectedPos.end.y;\n } else {\n startCol = prevSelectedPos.start.x;\n startRow = prevSelectedPos.start.y;\n }\n }\n\n this._lineCache.initLinesCache();\n\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n // Search startRow\n let result = this._findInLine(term, searchPosition, searchOptions);\n // Search from startRow + 1 to end\n if (!result) {\n for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n // If we hit the bottom and didn't search from the very top wrap back up\n if (!result && startRow !== 0) {\n for (let y = 0; y < startRow; y++) {\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n\n // If there is only one result, wrap back and return selection if it exists.\n if (!result && prevSelectedPos) {\n searchPosition.startRow = prevSelectedPos.start.y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n }\n\n return result;\n }\n\n /**\n * Find the previous occurrence of a term with wrapping and selection management.\n * @param term The search term.\n * @param searchOptions Search options.\n * @param cachedSearchTerm The cached search term to determine if expansion should occur.\n * @returns The search result if found, undefined otherwise.\n */\n public findPreviousWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n\n const prevSelectedPos = this._terminal.getSelectionPosition();\n this._terminal.clearSelection();\n\n let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;\n const startCol = this._terminal.cols;\n const isReverseSearch = true;\n\n this._lineCache.initLinesCache();\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n let result: ISearchResult | undefined;\n if (prevSelectedPos) {\n searchPosition.startRow = startRow = prevSelectedPos.start.y;\n searchPosition.startCol = prevSelectedPos.start.x;\n if (cachedSearchTerm !== term) {\n // Try to expand selection to right first.\n result = this._findInLine(term, searchPosition, searchOptions, false);\n if (!result) {\n // If selection was not able to be expanded to the right, then try reverse search\n searchPosition.startRow = startRow = prevSelectedPos.end.y;\n searchPosition.startCol = prevSelectedPos.end.x;\n }\n }\n }\n\n result ??= this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n\n // Search from startRow - 1 to top\n if (!result) {\n searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols);\n for (let y = startRow - 1; y >= 0; y--) {\n searchPosition.startRow = y;\n result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n if (result) {\n break;\n }\n }\n }\n // If we hit the top and didn't search from the very bottom wrap back down\n if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {\n for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {\n searchPosition.startRow = y;\n result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n if (result) {\n break;\n }\n }\n }\n\n return result;\n }\n\n /**\n * A found substring is a whole word if it doesn't have an alphanumeric character directly\n * adjacent to it.\n * @param searchIndex starting index of the potential whole word substring\n * @param line entire string in which the potential whole word was found\n * @param term the substring that starts at searchIndex\n */\n private _isWholeWord(searchIndex: number, line: string, term: string): boolean {\n return ((searchIndex === 0) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&\n (((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));\n }\n\n /**\n * Searches a line for a search term. Takes the provided terminal line and searches the text line,\n * which may contain subsequent terminal lines if the text is wrapped. If the provided line number\n * is part of a wrapped text line that started on an earlier line then it is skipped since it will\n * be properly searched when the terminal line that the text starts on is searched.\n * @param term The search term.\n * @param searchPosition The position to start the search.\n * @param searchOptions Search options.\n * @param isReverseSearch Whether the search should start from the right side of the terminal and\n * search to the left.\n * @returns The search result if it was found.\n */\n private _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {\n const row = searchPosition.startRow;\n const col = searchPosition.startCol;\n\n // Ignore wrapped lines, only consider on unwrapped line (first row of command string).\n const firstLine = this._terminal.buffer.active.getLine(row);\n if (firstLine?.isWrapped) {\n if (isReverseSearch) {\n searchPosition.startCol += this._terminal.cols;\n return;\n }\n\n // This will iterate until we find the line start.\n // When we find it, we will search using the calculated start column.\n searchPosition.startRow--;\n searchPosition.startCol += this._terminal.cols;\n return this._findInLine(term, searchPosition, searchOptions);\n }\n let cache = this._lineCache.getLineFromCache(row);\n if (!cache) {\n cache = this._lineCache.translateBufferLineToStringWithWrap(row, true);\n this._lineCache.setLineInCache(row, cache);\n }\n const [stringLine, offsets] = cache;\n\n const offset = this._bufferColsToStringOffset(row, col);\n let searchTerm = term;\n let searchStringLine = stringLine;\n if (!searchOptions.regex) {\n searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();\n searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();\n }\n\n let resultIndex = -1;\n if (searchOptions.regex) {\n const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');\n let foundTerm: RegExpExecArray | null;\n if (isReverseSearch) {\n // This loop will get the resultIndex of the _last_ regex match in the range 0..offset\n while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {\n resultIndex = searchRegex.lastIndex - foundTerm[0].length;\n term = foundTerm[0];\n searchRegex.lastIndex -= (term.length - 1);\n }\n } else {\n foundTerm = searchRegex.exec(searchStringLine.slice(offset));\n if (foundTerm && foundTerm[0].length > 0) {\n resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);\n term = foundTerm[0];\n }\n }\n } else {\n if (isReverseSearch) {\n if (offset - searchTerm.length >= 0) {\n resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length);\n }\n } else {\n resultIndex = searchStringLine.indexOf(searchTerm, offset);\n }\n }\n\n if (resultIndex >= 0) {\n if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {\n return;\n }\n\n // Adjust the row number and search index if needed since a \"line\" of text can span multiple\n // rows\n let startRowOffset = 0;\n while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {\n startRowOffset++;\n }\n let endRowOffset = startRowOffset;\n while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) {\n endRowOffset++;\n }\n const startColOffset = resultIndex - offsets[startRowOffset];\n const endColOffset = resultIndex + term.length - offsets[endRowOffset];\n const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset);\n const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset);\n const size = endColIndex - startColIndex + this._terminal.cols * (endRowOffset - startRowOffset);\n\n return {\n term,\n col: startColIndex,\n row: row + startRowOffset,\n size\n };\n }\n }\n\n private _stringLengthToBufferSize(row: number, offset: number): number {\n const line = this._terminal.buffer.active.getLine(row);\n if (!line) {\n return 0;\n }\n for (let i = 0; i < offset; i++) {\n const cell = line.getCell(i);\n if (!cell) {\n break;\n }\n // Adjust the searchIndex to normalize emoji into single chars\n const char = cell.getChars();\n if (char.length > 1) {\n offset -= char.length - 1;\n }\n // Adjust the searchIndex for empty characters following wide unicode\n // chars (eg. CJK)\n const nextCell = line.getCell(i + 1);\n if (nextCell && nextCell.getWidth() === 0) {\n offset++;\n }\n }\n return offset;\n }\n\n private _bufferColsToStringOffset(startRow: number, cols: number): number {\n let lineIndex = startRow;\n let offset = 0;\n let line = this._terminal.buffer.active.getLine(lineIndex);\n while (cols > 0 && line) {\n for (let i = 0; i < cols && i < this._terminal.cols; i++) {\n const cell = line.getCell(i);\n if (!cell) {\n break;\n }\n if (cell.getWidth()) {\n // Treat null characters as whitespace to align with the translateToString API\n offset += cell.getCode() === 0 ? 1 : cell.getChars().length;\n }\n }\n lineIndex++;\n line = this._terminal.buffer.active.getLine(lineIndex);\n if (line && !line.isWrapped) {\n break;\n }\n cols -= this._terminal.cols;\n }\n return offset;\n }\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal } from '@xterm/xterm';\nimport { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';\nimport { disposableTimeout } from 'common/Async';\n\nexport type LineCacheEntry = [\n /**\n * The string representation of a line (as opposed to the buffer cell representation).\n */\n lineAsString: string,\n /**\n * The offsets where each line starts when the entry describes a wrapped line.\n */\n lineOffsets: number[]\n];\n\n/**\n * Configuration constants for the search line cache functionality.\n */\nconst enum Constants {\n /**\n * Time-to-live for cached search results in milliseconds. After this duration, cached search\n * results will be invalidated to ensure they remain consistent with terminal content changes.\n */\n LINES_CACHE_TIME_TO_LIVE = 15000\n}\n\nexport class SearchLineCache extends Disposable {\n /**\n * translateBufferLineToStringWithWrap is a fairly expensive call.\n * We memoize the calls into an array that has a time based ttl.\n * _linesCache is also invalidated when the terminal cursor moves.\n */\n private _linesCache: LineCacheEntry[] | undefined;\n private _linesCacheTimeout = this._register(new MutableDisposable());\n private _linesCacheDisposables = this._register(new MutableDisposable());\n // Track access to avoid recreating a timeout on every init call which occurs once per search\n // result (findNext/findPrevious -> _highlightAllMatches -> find loop).\n private _lastAccessTimestamp = 0;\n\n constructor(private readonly _terminal: Terminal) {\n super();\n this._register(toDisposable(() => this._destroyLinesCache()));\n }\n\n /**\n * Sets up a line cache with a ttl\n */\n public initLinesCache(): void {\n if (!this._linesCache) {\n this._linesCache = new Array(this._terminal.buffer.active.length);\n this._linesCacheDisposables.value = combinedDisposable(\n this._terminal.onLineFeed(() => this._destroyLinesCache()),\n this._terminal.onCursorMove(() => this._destroyLinesCache()),\n this._terminal.onResize(() => this._destroyLinesCache())\n );\n }\n\n this._lastAccessTimestamp = Date.now();\n if (!this._linesCacheTimeout.value) {\n this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE);\n }\n }\n\n private _destroyLinesCache(): void {\n this._linesCache = undefined;\n this._lastAccessTimestamp = 0;\n this._linesCacheDisposables.clear();\n this._linesCacheTimeout.clear();\n }\n\n private _scheduleLinesCacheTimeout(delay: number): void {\n this._linesCacheTimeout.value = disposableTimeout(() => {\n if (!this._linesCache) {\n return;\n }\n const now = Date.now();\n const elapsed = now - this._lastAccessTimestamp;\n if (elapsed >= Constants.LINES_CACHE_TIME_TO_LIVE) {\n this._destroyLinesCache();\n return;\n }\n this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE - elapsed);\n }, delay);\n }\n\n public getLineFromCache(row: number): LineCacheEntry | undefined {\n return this._linesCache?.[row];\n }\n\n public setLineInCache(row: number, entry: LineCacheEntry): void {\n if (this._linesCache) {\n this._linesCache[row] = entry;\n }\n }\n\n /**\n * Translates a buffer line to a string, including subsequent lines if they are wraps.\n * Wide characters will count as two columns in the resulting string. This\n * function is useful for getting the actual text underneath the raw selection\n * position.\n * @param lineIndex The index of the line being translated.\n * @param trimRight Whether to trim whitespace to the right.\n */\n public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {\n const strings = [];\n const lineOffsets = [0];\n let line = this._terminal.buffer.active.getLine(lineIndex);\n while (line) {\n const nextLine = this._terminal.buffer.active.getLine(lineIndex + 1);\n const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;\n let string = line.translateToString(!lineWrapsToNext && trimRight);\n if (lineWrapsToNext && nextLine) {\n const lastCell = line.getCell(line.length - 1);\n const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;\n // a wide character wrapped to the next line\n if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {\n string = string.slice(0, -1);\n }\n }\n strings.push(string);\n if (lineWrapsToNext) {\n lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);\n } else {\n break;\n }\n lineIndex++;\n line = nextLine;\n }\n return [strings.join(''), lineOffsets];\n }\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ISearchResultChangeEvent } from '@xterm/addon-search';\nimport type { IDisposable } from '@xterm/xterm';\nimport { Emitter, type IEvent } from 'common/Event';\nimport { Disposable } from 'common/Lifecycle';\nimport type { ISearchResult } from './SearchEngine';\n\n/**\n * Interface for managing a currently selected decoration.\n */\ninterface ISelectedDecoration extends IDisposable {\n match: ISearchResult;\n}\n\n/**\n * Tracks search results, manages result indexing, and fires events when results change.\n * This class provides centralized management of search result state and notifications.\n */\nexport class SearchResultTracker extends Disposable {\n private _searchResults: ISearchResult[] = [];\n private _selectedDecoration: ISelectedDecoration | undefined;\n\n private readonly _onDidChangeResults = this._register(new Emitter());\n public get onDidChangeResults(): IEvent { return this._onDidChangeResults.event; }\n\n /**\n * Gets the current search results.\n */\n public get searchResults(): ReadonlyArray {\n return this._searchResults;\n }\n\n /**\n * Gets the currently selected decoration.\n */\n public get selectedDecoration(): ISelectedDecoration | undefined {\n return this._selectedDecoration;\n }\n\n /**\n * Sets the currently selected decoration.\n */\n public set selectedDecoration(decoration: ISelectedDecoration | undefined) {\n this._selectedDecoration = decoration;\n }\n\n /**\n * Updates the search results with a new set of results.\n * @param results The new search results.\n * @param maxResults The maximum number of results to track.\n */\n public updateResults(results: ISearchResult[], maxResults: number): void {\n this._searchResults = results.slice(0, maxResults);\n }\n\n /**\n * Clears all search results.\n */\n public clearResults(): void {\n this._searchResults = [];\n }\n\n /**\n * Clears the selected decoration.\n */\n public clearSelectedDecoration(): void {\n if (this._selectedDecoration) {\n this._selectedDecoration.dispose();\n this._selectedDecoration = undefined;\n }\n }\n\n /**\n * Finds the index of a result in the current results array.\n * @param result The result to find.\n * @returns The index of the result, or -1 if not found.\n */\n public findResultIndex(result: ISearchResult): number {\n for (let i = 0; i < this._searchResults.length; i++) {\n const match = this._searchResults[i];\n if (match.row === result.row && match.col === result.col && match.size === result.size) {\n return i;\n }\n }\n return -1;\n }\n\n /**\n * Fires a result change event with the current state.\n * @param hasDecorations Whether decorations are enabled.\n */\n public fireResultsChanged(hasDecorations: boolean): void {\n if (!hasDecorations) {\n return;\n }\n\n let resultIndex = -1;\n if (this._selectedDecoration) {\n resultIndex = this.findResultIndex(this._selectedDecoration.match);\n }\n\n this._onDidChangeResults.fire({\n resultIndex,\n resultCount: this._searchResults.length\n });\n }\n\n /**\n * Resets all state.\n */\n public reset(): void {\n this.clearSelectedDecoration();\n this.clearResults();\n }\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ISearchOptions } from '@xterm/addon-search';\n\n/**\n * Manages search state including cached search terms, options tracking, and validation.\n * This class provides a centralized way to handle search state consistency and option changes.\n */\nexport class SearchState {\n private _cachedSearchTerm: string | undefined;\n private _lastSearchOptions: ISearchOptions | undefined;\n\n /**\n * Gets the currently cached search term.\n */\n public get cachedSearchTerm(): string | undefined {\n return this._cachedSearchTerm;\n }\n\n /**\n * Sets the cached search term.\n */\n public set cachedSearchTerm(term: string | undefined) {\n this._cachedSearchTerm = term;\n }\n\n /**\n * Gets the last search options used.\n */\n public get lastSearchOptions(): ISearchOptions | undefined {\n return this._lastSearchOptions;\n }\n\n /**\n * Sets the last search options used.\n */\n public set lastSearchOptions(options: ISearchOptions | undefined) {\n this._lastSearchOptions = options;\n }\n\n /**\n * Validates a search term to ensure it's not empty or invalid.\n * @param term The search term to validate.\n * @returns true if the term is valid for searching.\n */\n public isValidSearchTerm(term: string): boolean {\n return !!(term && term.length > 0);\n }\n\n /**\n * Determines if search options have changed compared to the last search.\n * @param newOptions The new search options to compare.\n * @returns true if the options have changed.\n */\n public didOptionsChange(newOptions?: ISearchOptions): boolean {\n if (!this._lastSearchOptions) {\n return true;\n }\n if (!newOptions) {\n return false;\n }\n if (this._lastSearchOptions.caseSensitive !== newOptions.caseSensitive) {\n return true;\n }\n if (this._lastSearchOptions.regex !== newOptions.regex) {\n return true;\n }\n if (this._lastSearchOptions.wholeWord !== newOptions.wholeWord) {\n return true;\n }\n return false;\n }\n\n /**\n * Determines if a new search should trigger highlighting updates.\n * @param term The search term.\n * @param options The search options.\n * @returns true if highlighting should be updated.\n */\n public shouldUpdateHighlighting(term: string, options?: ISearchOptions): boolean {\n if (!options?.decorations) {\n return false;\n }\n return this._cachedSearchTerm === undefined ||\n term !== this._cachedSearchTerm ||\n this.didOptionsChange(options);\n }\n\n /**\n * Clears the cached search term.\n */\n public clearCachedTerm(): void {\n this._cachedSearchTerm = undefined;\n }\n\n /**\n * Resets all state.\n */\n public reset(): void {\n this._cachedSearchTerm = undefined;\n this._lastSearchOptions = undefined;\n }\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';\nimport type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';\nimport { Emitter, type IEvent } from 'common/Event';\nimport { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';\nimport { disposableTimeout } from 'common/Async';\nimport { SearchLineCache } from './SearchLineCache';\nimport { SearchState } from './SearchState';\nimport { SearchEngine, type ISearchResult } from './SearchEngine';\nimport { DecorationManager } from './DecorationManager';\nimport { SearchResultTracker } from './SearchResultTracker';\n\ninterface IInternalSearchOptions {\n noScroll: boolean;\n}\n\n/**\n * Configuration constants for the search addon functionality.\n */\nconst enum Constants {\n /**\n * Default maximum number of search results to highlight simultaneously. This limit prevents\n * performance degradation when searching for very common terms that would result in excessive\n * highlighting decorations.\n */\n DEFAULT_HIGHLIGHT_LIMIT = 1000\n}\n\nexport class SearchAddon extends Disposable implements ITerminalAddon, ISearchApi {\n private _terminal: Terminal | undefined;\n private _highlightLimit: number;\n private _highlightTimeout = this._register(new MutableDisposable());\n private _lineCache = this._register(new MutableDisposable());\n\n // Component instances\n private _state = new SearchState();\n private _engine: SearchEngine | undefined;\n private _decorationManager: DecorationManager | undefined;\n private _resultTracker = this._register(new SearchResultTracker());\n\n private readonly _onAfterSearch = this._register(new Emitter());\n public readonly onAfterSearch = this._onAfterSearch.event;\n private readonly _onBeforeSearch = this._register(new Emitter());\n public readonly onBeforeSearch = this._onBeforeSearch.event;\n\n public get onDidChangeResults(): IEvent {\n return this._resultTracker.onDidChangeResults;\n }\n\n constructor(options?: Partial) {\n super();\n\n this._highlightLimit = options?.highlightLimit ?? Constants.DEFAULT_HIGHLIGHT_LIMIT;\n }\n\n public activate(terminal: Terminal): void {\n this._terminal = terminal;\n this._lineCache.value = new SearchLineCache(terminal);\n this._engine = new SearchEngine(terminal, this._lineCache.value);\n this._decorationManager = new DecorationManager(terminal);\n this._register(this._terminal.onWriteParsed(() => this._updateMatches()));\n this._register(this._terminal.onResize(() => this._updateMatches()));\n this._register(toDisposable(() => this.clearDecorations()));\n }\n\n private _updateMatches(): void {\n this._highlightTimeout.clear();\n if (this._state.cachedSearchTerm && this._state.lastSearchOptions?.decorations) {\n this._highlightTimeout.value = disposableTimeout(() => {\n const term = this._state.cachedSearchTerm;\n this._state.clearCachedTerm();\n this.findPrevious(term!, { ...this._state.lastSearchOptions, incremental: true }, { noScroll: true });\n }, 200);\n }\n }\n\n public clearDecorations(retainCachedSearchTerm?: boolean): void {\n this._resultTracker.clearSelectedDecoration();\n this._decorationManager?.clearHighlightDecorations();\n this._resultTracker.clearResults();\n if (!retainCachedSearchTerm) {\n this._state.clearCachedTerm();\n }\n }\n\n public clearActiveDecoration(): void {\n this._resultTracker.clearSelectedDecoration();\n }\n\n /**\n * Find the next instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options.\n * @returns Whether a result was found.\n */\n public findNext(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n\n this._onBeforeSearch.fire();\n\n this._state.lastSearchOptions = searchOptions;\n\n if (this._state.shouldUpdateHighlighting(term, searchOptions)) {\n this._highlightAllMatches(term, searchOptions!);\n }\n\n const found = this._findNextAndSelect(term, searchOptions, internalSearchOptions);\n this._fireResults(searchOptions);\n this._state.cachedSearchTerm = term;\n\n this._onAfterSearch.fire();\n\n return found;\n }\n\n private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {\n if (!this._terminal || !this._engine || !this._decorationManager) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n if (!this._state.isValidSearchTerm(term)) {\n this.clearDecorations();\n return;\n }\n\n // new search, clear out the old decorations\n this.clearDecorations(true);\n\n const results: ISearchResult[] = [];\n let prevResult: ISearchResult | undefined = undefined;\n let result = this._engine.find(term, 0, 0, searchOptions);\n\n while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {\n if (results.length >= this._highlightLimit) {\n break;\n }\n prevResult = result;\n results.push(prevResult);\n const cols = this._terminal.cols;\n let nextCol = prevResult.col + prevResult.size;\n let nextRow = prevResult.row;\n if (nextCol >= cols) {\n nextRow += Math.floor(nextCol / cols);\n nextCol = nextCol % cols;\n }\n result = this._engine.find(term, nextRow, nextCol, searchOptions);\n }\n\n this._resultTracker.updateResults(results, this._highlightLimit);\n if (searchOptions.decorations) {\n this._decorationManager.createHighlightDecorations(results, searchOptions.decorations);\n }\n }\n\n private _findNextAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n return false;\n }\n if (!this._state.isValidSearchTerm(term)) {\n this._terminal.clearSelection();\n this.clearDecorations();\n return false;\n }\n\n const result = this._engine.findNextWithSelection(term, searchOptions, this._state.cachedSearchTerm);\n return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);\n }\n\n /**\n * Find the previous instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options.\n * @returns Whether a result was found.\n */\n public findPrevious(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n\n this._onBeforeSearch.fire();\n\n this._state.lastSearchOptions = searchOptions;\n\n if (this._state.shouldUpdateHighlighting(term, searchOptions)) {\n this._highlightAllMatches(term, searchOptions!);\n }\n\n const found = this._findPreviousAndSelect(term, searchOptions, internalSearchOptions);\n this._fireResults(searchOptions);\n this._state.cachedSearchTerm = term;\n\n this._onAfterSearch.fire();\n\n return found;\n }\n\n private _fireResults(searchOptions?: ISearchOptions): void {\n this._resultTracker.fireResultsChanged(!!searchOptions?.decorations);\n }\n\n private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n return false;\n }\n if (!this._state.isValidSearchTerm(term)) {\n this._terminal.clearSelection();\n this.clearDecorations();\n return false;\n }\n\n const result = this._engine.findPreviousWithSelection(term, searchOptions, this._state.cachedSearchTerm);\n return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);\n }\n\n /**\n * Selects and scrolls to a result.\n * @param result The result to select.\n * @returns Whether a result was selected.\n */\n private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {\n if (!this._terminal || !this._decorationManager) {\n return false;\n }\n\n this._resultTracker.clearSelectedDecoration();\n if (!result) {\n this._terminal.clearSelection();\n return false;\n }\n\n this._terminal.select(result.col, result.row, result.size);\n if (options) {\n const activeDecoration = this._decorationManager.createActiveDecoration(result, options);\n if (activeDecoration) {\n this._resultTracker.selectedDecoration = activeDecoration;\n }\n }\n\n if (!noScroll) {\n // If it is not in the viewport then we scroll else it just gets selected\n if (result.row >= (this._terminal.buffer.active.viewportY + this._terminal.rows) || result.row < this._terminal.buffer.active.viewportY) {\n let scroll = result.row - this._terminal.buffer.active.viewportY;\n scroll -= Math.floor(this._terminal.rows / 2);\n this._terminal.scrollLines(scroll);\n }\n }\n return true;\n }\n}\n"],"names":["root","factory","exports","module","define","amd","globalThis","millis","Promise","resolve","setTimeout","handler","timeout","store","timer","disposable","dispose","Lifecycle_1","toDisposable","clearTimeout","add","__webpack_require__","constructor","this","_token","_isDisposed","cancel","cancelAndSet","runner","Error","setIfNotSet","_isScheduled","set","queueMicrotask","_disposable","undefined","interval","context","handle","setInterval","clearInterval","EventUtils","_listeners","_disposed","event","_event","listener","thisArgs","disposables","entry","fn","slice","push","result","idx","indexOf","splice","Array","isArray","fire","length","call","listeners","i","len","forward","from","to","e","map","any","events","DisposableStore","runAndSubscribe","initial","arg","d","_disposables","Set","isDisposed","o","clear","Disposable","_store","_register","None","Object","freeze","value","_value","DecorationManager","_terminal","super","_highlightDecorations","_highlightedLines","clearHighlightDecorations","createHighlightDecorations","results","options","match","decorations","_createResultDecorations","decoration","_storeDecoration","createActiveDecoration","marker","line","_applyStyles","element","borderColor","isActiveResult","classList","contains","style","outline","decorationRanges","currentCol","col","remainingSize","size","markerOffset","buffer","active","baseY","cursorY","row","amountThisRow","Math","min","cols","range","registerMarker","registerDecoration","x","width","layer","backgroundColor","activeMatchBackground","matchBackground","overviewRulerOptions","has","color","activeMatchColorOverviewRuler","matchOverviewRuler","position","onRender","activeMatchBorder","matchBorder","onDispose","_lineCache","find","term","startRow","startCol","searchOptions","clearSelection","initLinesCache","searchPosition","_findInLine","y","rows","findNextWithSelection","cachedSearchTerm","prevSelectedPos","getSelectionPosition","end","start","findPreviousWithSelection","isReverseSearch","max","_isWholeWord","searchIndex","includes","firstLine","getLine","isWrapped","cache","getLineFromCache","translateBufferLineToStringWithWrap","setLineInCache","stringLine","offsets","offset","_bufferColsToStringOffset","searchTerm","searchStringLine","regex","caseSensitive","toLowerCase","resultIndex","searchRegex","RegExp","foundTerm","exec","lastIndex","lastIndexOf","wholeWord","startRowOffset","endRowOffset","startColOffset","endColOffset","startColIndex","_stringLengthToBufferSize","cell","getCell","char","getChars","nextCell","getWidth","lineIndex","getCode","Async_1","SearchLineCache","_linesCacheTimeout","MutableDisposable","_linesCacheDisposables","_lastAccessTimestamp","_destroyLinesCache","_linesCache","combinedDisposable","onLineFeed","onCursorMove","onResize","Date","now","_scheduleLinesCacheTimeout","delay","disposableTimeout","elapsed","trimRight","strings","lineOffsets","nextLine","lineWrapsToNext","string","translateToString","lastCell","join","Event_1","SearchResultTracker","_searchResults","_onDidChangeResults","Emitter","onDidChangeResults","searchResults","selectedDecoration","_selectedDecoration","updateResults","maxResults","clearResults","clearSelectedDecoration","findResultIndex","fireResultsChanged","hasDecorations","resultCount","reset","_cachedSearchTerm","lastSearchOptions","_lastSearchOptions","isValidSearchTerm","didOptionsChange","newOptions","shouldUpdateHighlighting","clearCachedTerm","__webpack_module_cache__","moduleId","cachedModule","__webpack_modules__","SearchLineCache_1","SearchState_1","SearchEngine_1","DecorationManager_1","SearchResultTracker_1","SearchAddon","_resultTracker","_highlightTimeout","_state","SearchState","_onAfterSearch","onAfterSearch","_onBeforeSearch","onBeforeSearch","_highlightLimit","highlightLimit","activate","terminal","_engine","SearchEngine","_decorationManager","onWriteParsed","_updateMatches","clearDecorations","findPrevious","incremental","noScroll","retainCachedSearchTerm","clearActiveDecoration","findNext","internalSearchOptions","_highlightAllMatches","found","_findNextAndSelect","_fireResults","prevResult","nextCol","nextRow","floor","_selectResult","_findPreviousAndSelect","select","activeDecoration","viewportY","scroll","scrollLines"],"sourceRoot":""}
+\ No newline at end of file
++{"version":3,"file":"addon-search.js","mappings":"CAAA,SAAAA,EAAAC,GACA,iBAAAC,SAAA,iBAAAC,OACAA,OAAAD,QAAAD,IACA,mBAAAG,QAAAA,OAAAC,IACAD,OAAA,GAAAH,GACA,iBAAAC,QACAA,QAAA,YAAAD,IAEAD,EAAA,YAAAC,GACC,CATD,CASCK,WAAA,2JCAD,SAAwBC,GACtB,OAAO,IAAIC,QAAQC,GAAWC,WAAWD,EAASF,GACpD,sBASA,SAAkCI,EAAqBC,EAAU,EAAGC,GAClE,MAAMC,EAAQJ,WAAW,KACvBC,IACIE,GACFE,EAAWC,WAEZJ,GACGG,GAAa,EAAAE,EAAAC,cAAa,KAC9BC,aAAaL,KAGf,OADAD,GAAOO,IAAIL,GACJA,CACT,EAzBA,MAAAE,EAAAI,EAAA,oBA2BA,iBAAAC,GACUC,KAAAC,QAAe,EACfD,KAAAE,aAAc,CAqCxB,CAnCS,OAAAT,GACLO,KAAKG,SACLH,KAAKE,aAAc,CACrB,CAEO,MAAAC,IACgB,IAAjBH,KAAKC,SACPL,aAAaI,KAAKC,QAClBD,KAAKC,QAAU,EAEnB,CAEO,YAAAG,CAAaC,EAAoBhB,GACtC,GAAIW,KAAKE,YACP,MAAM,IAAII,MAAM,mDAElBN,KAAKG,SACLH,KAAKC,OAASd,WAAW,KACvBa,KAAKC,QAAU,EACfI,KACChB,EACL,CAEO,WAAAkB,CAAYF,EAAoBhB,GACrC,GAAIW,KAAKE,YACP,MAAM,IAAII,MAAM,mDAEG,IAAjBN,KAAKC,SAGTD,KAAKC,OAASd,WAAW,KACvBa,KAAKC,QAAU,EACfI,KACChB,GACL,oBAQF,iBAAAU,GACUC,KAAAQ,cAAe,EACfR,KAAAE,aAAc,CA2BxB,CAzBS,OAAAT,GACLO,KAAKG,SACLH,KAAKE,aAAc,CACrB,CAEO,MAAAC,GACLH,KAAKQ,cAAe,CACtB,CAEO,GAAAC,CAAIJ,GACT,GAAIL,KAAKE,YACP,MAAM,IAAII,MAAM,4CAEdN,KAAKQ,eAGTR,KAAKQ,cAAe,EACpBE,eAAe,KACRV,KAAKQ,eAGVR,KAAKQ,cAAe,EACpBH,OAEJ,mBAGF,iBAAAN,GAEUC,KAAAE,aAAc,CA2BxB,CAzBS,MAAAC,GACLH,KAAKW,aAAalB,UAClBO,KAAKW,iBAAcC,CACrB,CAEO,YAAAR,CAAaC,EAAoBQ,EAAkBC,EAAsC/B,YAC9F,GAAIiB,KAAKE,YACP,MAAM,IAAII,MAAM,oDAElBN,KAAKG,SACL,MAAMY,EAASD,EAAQE,YAAY,KACjCX,KACCQ,GACHb,KAAKW,YAAc,CACjBlB,QAAS,KACPqB,EAAQG,cAAcF,GACtBf,KAAKW,iBAAcC,GAGzB,CAEO,OAAAnB,GACLO,KAAKG,SACLH,KAAKE,aAAc,CACrB,8FCnIF,MAAAR,EAAAI,EAAA,KAoEA,IAAiBoB,YA9DjB,iBAAAnB,GACUC,KAAAmB,WAAqD,GACrDnB,KAAAoB,WAAY,CA0DtB,CAvDE,SAAWC,GACT,OAAIrB,KAAKsB,SAGTtB,KAAKsB,OAAS,CAACC,EAAyBC,EAAgBC,KACtD,GAAIzB,KAAKoB,UACP,OAAO,EAAA1B,EAAAC,cAAa,QAGtB,MAAM+B,EAAQ,CAAEC,GAAIJ,EAAUC,YAC9BxB,KAAKmB,WAAanB,KAAKmB,WAAWS,QAClC5B,KAAKmB,WAAWU,KAAKH,GAErB,MAAMI,GAAS,EAAApC,EAAAC,cAAa,KAC1B,MAAMoC,EAAM/B,KAAKmB,WAAWa,QAAQN,IACvB,IAATK,IACF/B,KAAKmB,WAAanB,KAAKmB,WAAWS,QAClC5B,KAAKmB,WAAWc,OAAOF,EAAK,MAYhC,OARIN,IACES,MAAMC,QAAQV,GAChBA,EAAYI,KAAKC,GAEjBL,EAAY5B,IAAIiC,IAIbA,IA3BA9B,KAAKsB,MA8BhB,CAEO,IAAAc,CAAKf,GACV,GAAIrB,KAAKoB,YAAcpB,KAAKmB,WAAWkB,OACrC,OAEF,GAA+B,IAA3BrC,KAAKmB,WAAWkB,OAElB,YADArC,KAAKmB,WAAW,GAAGQ,GAAGW,KAAKtC,KAAKmB,WAAW,GAAGK,SAAUH,GAG1D,MAAMkB,EAAYvC,KAAKmB,WACvB,IAAK,IAAIqB,EAAI,EAAGC,EAAMF,EAAUF,OAAQG,EAAIC,IAAOD,EACjDD,EAAUC,GAAGb,GAAGW,KAAKC,EAAUC,GAAGhB,SAAUH,EAEhD,CAEO,OAAA5B,GACDO,KAAKoB,YAGTpB,KAAKoB,WAAY,EACjBpB,KAAKmB,WAAWkB,OAAS,EAC3B,GAGF,SAAiBnB,GACCA,EAAAwB,QAAhB,SAA2BC,EAAiBC,GAC1C,OAAOD,EAAKE,GAAKD,EAAGR,KAAKS,GAC3B,EAEgB3B,EAAA4B,IAAhB,SAA0BzB,EAAkByB,GAC1C,MAAO,CAACvB,EAAyBC,EAAgBC,IACxCJ,EAAMmB,GAAKjB,EAASe,KAAKd,EAAUsB,EAAIN,SAAK5B,EAAWa,EAElE,EAIgBP,EAAA6B,IAAhB,YAA0BC,GACxB,MAAO,CAACzB,EAAyBC,EAAgBC,KAC/C,MAAMnC,EAAQ,IAAII,EAAAuD,gBAClB,IAAK,MAAM5B,KAAS2B,EAClB1D,EAAMO,IAAIwB,EAAMwB,GAAKtB,EAASe,KAAKd,EAAUqB,KAS/C,OAPIpB,IACES,MAAMC,QAAQV,GAChBA,EAAYI,KAAKvC,GAEjBmC,EAAY5B,IAAIP,IAGbA,EAEX,EAIgB4B,EAAAgC,gBAAhB,SAAmC7B,EAAkBjC,EAAqC+D,GAExF,OADA/D,EAAQ+D,GACD9B,EAAMwB,GAAKzD,EAAQyD,GAC5B,CACD,CApCD,CAAiB3B,IAAUvC,EAAAuC,WAAVA,EAAU,eChE3B,SAAAvB,EAA6BgC,GAC3B,MAAO,CAAElC,QAASkC,EACpB,CAKA,SAAAlC,EAA+C2D,GAC7C,IAAKA,EACH,OAAOA,EAET,GAAIlB,MAAMC,QAAQiB,GAAM,CACtB,IAAK,MAAMC,KAAKD,EACdC,EAAE5D,UAEJ,MAAO,EACT,CAEA,OADA2D,EAAI3D,UACG2D,CACT,8JAEA,YAAsC3B,GACpC,OAAO9B,EAAa,IAAMF,EAAQgC,GACpC,EAEA,MAAAwB,EAAA,WAAAlD,GACmBC,KAAAsD,aAAe,IAAIC,IAC5BvD,KAAAE,aAAc,CAgCxB,CA9BE,cAAWsD,GACT,OAAOxD,KAAKE,WACd,CAEO,GAAAL,CAA2B4D,GAMhC,OALIzD,KAAKE,YACPuD,EAAEhE,UAEFO,KAAKsD,aAAazD,IAAI4D,GAEjBA,CACT,CAEO,OAAAhE,GACL,IAAIO,KAAKE,YAAT,CAGAF,KAAKE,aAAc,EACnB,IAAK,MAAMmD,KAAKrD,KAAKsD,aACnBD,EAAE5D,UAEJO,KAAKsD,aAAaI,OALlB,CAMF,CAEO,KAAAA,GACL,IAAK,MAAML,KAAKrD,KAAKsD,aACnBD,EAAE5D,UAEJO,KAAKsD,aAAaI,OACpB,sBAGF,MAAAC,EAAA,WAAA5D,GAGqBC,KAAA4D,OAAS,IAAIX,CASlC,CAPS,OAAAxD,GACLO,KAAK4D,OAAOnE,SACd,CAEU,SAAAoE,CAAiCJ,GACzC,OAAOzD,KAAK4D,OAAO/D,IAAI4D,EACzB,iBAVuBE,EAAAG,KAAoBC,OAAOC,OAAO,CAAE,OAAAvE,GAAY,wBAazE,iBAAAM,GAEUC,KAAAE,aAAc,CAuBxB,CArBE,SAAW+D,GACT,OAAOjE,KAAKE,iBAAcU,EAAYZ,KAAKkE,MAC7C,CAEA,SAAWD,CAAMA,GACXjE,KAAKE,aAAe+D,IAAUjE,KAAKkE,SAGvClE,KAAKkE,QAAQzE,UACbO,KAAKkE,OAASD,EAChB,CAEO,KAAAP,GACL1D,KAAKiE,WAAQrD,CACf,CAEO,OAAAnB,GACLO,KAAKE,aAAc,EACnBF,KAAKkE,QAAQzE,UACbO,KAAKkE,YAAStD,CAChB,2FCxGF,MAAAlB,EAAAI,EAAA,KAuBA,MAAAqE,UAAuCzE,EAAAiE,WAIrC,WAAA5D,CAA6BqE,GAC3BC,QAD2BrE,KAAAoE,UAAAA,EAHrBpE,KAAAsE,sBAAsC,GACtCtE,KAAAuE,kBAAiC,IAAIhB,IAI3CvD,KAAK6D,WAAU,EAAAnE,EAAAC,cAAa,IAAMK,KAAKwE,6BACzC,CAOO,0BAAAC,CAA2BC,EAA0BC,GAC1D3E,KAAKwE,4BAEL,IAAK,MAAMI,KAASF,EAAS,CAC3B,MAAMG,EAAc7E,KAAK8E,yBAAyBF,EAAOD,GAAS,GAClE,GAAIE,EACF,IAAK,MAAME,KAAcF,EACvB7E,KAAKgF,iBAAiBD,EAAYH,EAGxC,CACF,CAQO,sBAAAK,CAAuBnD,EAAuB6C,GACnD,MAAME,EAAc7E,KAAK8E,yBAAyBhD,EAAQ6C,GAAS,GACnE,GAAIE,EACF,MAAO,CAAEA,cAAaD,MAAO9C,EAAQ,OAAArC,IAAY,EAAAC,EAAAD,SAAQoF,EAAc,EAG3E,CAKO,yBAAAL,IACL,EAAA9E,EAAAD,SAAQO,KAAKsE,uBACbtE,KAAKsE,sBAAwB,GAC7BtE,KAAKuE,kBAAkBb,OACzB,CAOQ,gBAAAsB,CAAiBD,EAAyBH,GAChD5E,KAAKuE,kBAAkB1E,IAAIkF,EAAWG,OAAOC,MAC7CnF,KAAKsE,sBAAsBzC,KAAK,CAAEkD,aAAYH,QAAO,OAAAnF,GAAYsF,EAAWtF,SAAW,GACzF,CAQQ,YAAA2F,CAAaC,EAAsBC,EAAiCC,GACrEF,EAAQG,UAAUC,SAAS,kCAC9BJ,EAAQG,UAAU3F,IAAI,gCAClByF,IACFD,EAAQK,MAAMC,QAAU,aAAaL,MAGrCC,GACFF,EAAQG,UAAU3F,IAAI,sCAE1B,CASQ,wBAAAiF,CAAyBhD,EAAuB6C,EAAmCY,GAEzF,MAAMK,EAA+C,GACrD,IAAIC,EAAa/D,EAAOgE,IACpBC,EAAgBjE,EAAOkE,KACvBC,GAAgBjG,KAAKoE,UAAU8B,OAAOC,OAAOC,MAAQpG,KAAKoE,UAAU8B,OAAOC,OAAOE,QAAUvE,EAAOwE,IACvG,KAAOP,EAAgB,GAAG,CACxB,MAAMQ,EAAgBC,KAAKC,IAAIzG,KAAKoE,UAAUsC,KAAOb,EAAYE,GACjEH,EAAiB/D,KAAK,CAACoE,EAAcJ,EAAYU,IACjDV,EAAa,EACbE,GAAiBQ,EACjBN,GACF,CAGA,MAAMpB,EAA6B,GACnC,IAAK,MAAM8B,KAASf,EAAkB,CACpC,MAAMV,EAASlF,KAAKoE,UAAUwC,eAAeD,EAAM,IAC7C5B,EAAa/E,KAAKoE,UAAUyC,mBAAmB,CACnD3B,SACA4B,EAAGH,EAAM,GACTI,MAAOJ,EAAM,GACbK,MAAOzB,EAAiB,MAAQ,SAChC0B,gBAAiB1B,EAAiBZ,EAAQuC,sBAAwBvC,EAAQwC,gBAC1EC,qBAAsBpH,KAAKuE,kBAAkB8C,IAAInC,EAAOC,WAAQvE,EAAY,CAC1E0G,MAAO/B,EAAiBZ,EAAQ4C,8BAAgC5C,EAAQ6C,mBACxEC,SAAU,YAGd,GAAI1C,EAAY,CACd,MAAMtD,EAA6B,GACnCA,EAAYI,KAAKqD,GACjBzD,EAAYI,KAAKkD,EAAW2C,SAAU7E,GAAM7C,KAAKoF,aAAavC,EAAG0C,EAAiBZ,EAAQgD,kBAAoBhD,EAAQiD,aAAa,KACnInG,EAAYI,KAAKkD,EAAW8C,UAAU,KAAM,EAAAnI,EAAAD,SAAQgC,KACpDoD,EAAYhD,KAAKkD,EACnB,CACF,CAEA,OAA8B,IAAvBF,EAAYxC,YAAezB,EAAYiE,CAChD,wHC/GF,MACE,WAAA9E,CACmBqE,EACA0D,kBADA1D,kBACA0D,CAChB,CAUI,IAAAC,CAAKC,EAAcC,EAAkBC,EAAkBC,GAC5D,IAAKH,GAAwB,IAAhBA,EAAK3F,OAEhB,YADArC,KAAKoE,UAAUgE,iBAGjB,GAAIF,GAAYlI,KAAKoE,UAAUsC,KAC7B,MAAM,IAAIpG,MAAM,gBAAgB4H,8BAAqClI,KAAKoE,UAAUsC,aAGtF1G,KAAK8H,WAAWO,iBAEhB,MAAMC,EAAkC,CACtCL,WACAC,YAIF,IAAIpG,EAAS9B,KAAKuI,YAAYP,EAAMM,EAAgBH,GAEpD,IAAKrG,EACH,IAAK,IAAI0G,EAAIP,EAAW,EAAGO,EAAIxI,KAAKoE,UAAU8B,OAAOC,OAAOC,MAAQpG,KAAKoE,UAAUqE,OAC7EzI,KAAK0I,6BAA6BF,KAGtCF,EAAeL,SAAWO,EAC1BF,EAAeJ,SAAW,EAC1BpG,EAAS9B,KAAKuI,YAAYP,EAAMM,EAAgBH,IAC5CrG,IAPmF0G,KAY3F,OAAO1G,CACT,CASO,qBAAA6G,CAAsBX,EAAcG,EAAgCS,GACzE,IAAKZ,GAAwB,IAAhBA,EAAK3F,OAEhB,YADArC,KAAKoE,UAAUgE,iBAIjB,MAAMS,EAAkB7I,KAAKoE,UAAU0E,uBACvC9I,KAAKoE,UAAUgE,iBAEf,IAAIF,EAAW,EACXD,EAAW,EACXY,IACED,IAAqBZ,GACvBE,EAAWW,EAAgBE,IAAIjC,EAC/BmB,EAAWY,EAAgBE,IAAIP,IAE/BN,EAAWW,EAAgBG,MAAMlC,EACjCmB,EAAWY,EAAgBG,MAAMR,IAIrCxI,KAAK8H,WAAWO,iBAEhB,MAAMC,EAAkC,CACtCL,WACAC,YAIF,IAAIpG,EAAS9B,KAAKuI,YAAYP,EAAMM,EAAgBH,GAEpD,IAAKrG,EACH,IAAK,IAAI0G,EAAIP,EAAW,EAAGO,EAAIxI,KAAKoE,UAAU8B,OAAOC,OAAOC,MAAQpG,KAAKoE,UAAUqE,OAC7EzI,KAAK0I,6BAA6BF,KAGtCF,EAAeL,SAAWO,EAC1BF,EAAeJ,SAAW,EAC1BpG,EAAS9B,KAAKuI,YAAYP,EAAMM,EAAgBH,IAC5CrG,IAPmF0G,KAa3F,IAAK1G,GAAuB,IAAbmG,EACb,IAAK,IAAIO,EAAI,EAAGA,EAAIP,IAGdO,EAAI,GAAKxI,KAAK0I,6BAA6BF,KAG/CF,EAAeL,SAAWO,EAC1BF,EAAeJ,SAAW,EAC1BpG,EAAS9B,KAAKuI,YAAYP,EAAMM,EAAgBH,IAC5CrG,IATwB0G,KAsBhC,OANK1G,GAAU+G,IACbP,EAAeL,SAAWY,EAAgBG,MAAMR,EAChDF,EAAeJ,SAAW,EAC1BpG,EAAS9B,KAAKuI,YAAYP,EAAMM,EAAgBH,IAG3CrG,CACT,CASO,yBAAAmH,CAA0BjB,EAAcG,EAAgCS,GAC7E,IAAKZ,GAAwB,IAAhBA,EAAK3F,OAEhB,YADArC,KAAKoE,UAAUgE,iBAIjB,MAAMS,EAAkB7I,KAAKoE,UAAU0E,uBACvC9I,KAAKoE,UAAUgE,iBAEf,IAAIH,EAAWjI,KAAKoE,UAAU8B,OAAOC,OAAOC,MAAQpG,KAAKoE,UAAUqE,KAAO,EAC1E,MAAMP,EAAWlI,KAAKoE,UAAUsC,KAC1BwC,GAAkB,EAExBlJ,KAAK8H,WAAWO,iBAChB,MAAMC,EAAkC,CACtCL,WACAC,YAGF,IAAIpG,EAkBJ,GAjBI+G,IACFP,EAAeL,SAAWA,EAAWY,EAAgBG,MAAMR,EAC3DF,EAAeJ,SAAWW,EAAgBG,MAAMlC,EAC5C8B,IAAqBZ,IAEvBlG,EAAS9B,KAAKuI,YAAYP,EAAMM,EAAgBH,GAAe,GAC1DrG,IAEHwG,EAAeL,SAAWA,EAAWY,EAAgBE,IAAIP,EACzDF,EAAeJ,SAAWW,EAAgBE,IAAIjC,KAKpDhF,IAAW9B,KAAKuI,YAAYP,EAAMM,EAAgBH,EAAee,IAG5DpH,EAAQ,CACXwG,EAAeJ,SAAW1B,KAAK2C,IAAIb,EAAeJ,SAAUlI,KAAKoE,UAAUsC,MAC3E,IAAK,IAAI8B,EAAIP,EAAW,EAAGO,GAAK,IAC9BF,EAAeL,SAAWO,EAC1B1G,EAAS9B,KAAKuI,YAAYP,EAAMM,EAAgBH,EAAee,IAC3DpH,GAH6B0G,KAOrC,CAEA,IAAK1G,GAAUmG,IAAcjI,KAAKoE,UAAU8B,OAAOC,OAAOC,MAAQpG,KAAKoE,UAAUqE,KAAO,EACtF,IAAK,IAAID,EAAKxI,KAAKoE,UAAU8B,OAAOC,OAAOC,MAAQpG,KAAKoE,UAAUqE,KAAO,EAAID,GAAKP,IAChFK,EAAeL,SAAWO,EAC1B1G,EAAS9B,KAAKuI,YAAYP,EAAMM,EAAgBH,EAAee,IAC3DpH,GAHsF0G,KAS9F,OAAO1G,CACT,CASQ,YAAAsH,CAAaC,EAAqBlE,EAAc6C,GACtD,OAAyB,IAAhBqB,GAAuB,qCAA8BC,SAASnE,EAAKkE,EAAc,OACrFA,EAAcrB,EAAK3F,SAAY8C,EAAK9C,QAAY,qCAA8BiH,SAASnE,EAAKkE,EAAcrB,EAAK3F,SACtH,CAGQ,mBAAAkH,CAAoBF,EAAqBlE,EAAc6C,EAAcG,GAC3E,OAAQA,EAAcqB,WAAaxJ,KAAKoJ,aAAaC,EAAalE,EAAM6C,EAC1E,CASQ,4BAAAU,CAA6BpC,GACnC,OAAgE,IAAzDtG,KAAKoE,UAAU8B,OAAOC,OAAOsD,QAAQnD,IAAMoD,SACpD,CAcQ,WAAAnB,CAAYP,EAAcM,EAAiCH,EAAgC,GAAIe,GAA2B,GAEhI,GAAIA,GAGF,GAAIZ,EAAeL,SAAW,GAAKjI,KAAKoE,UAAU8B,OAAOC,OAAOsD,QAAQnB,EAAeL,WAAWyB,UAEhG,YADApB,EAAeJ,UAAYlI,KAAKoE,UAAUsC,WAO5C,KAAO4B,EAAeL,SAAW,GAAKjI,KAAKoE,UAAU8B,OAAOC,OAAOsD,QAAQnB,EAAeL,WAAWyB,WACnGpB,EAAeL,WACfK,EAAeJ,UAAYlI,KAAKoE,UAAUsC,KAG9C,MAAMJ,EAAMgC,EAAeL,SACrBnC,EAAMwC,EAAeJ,SAE3B,IAAIyB,EAAQ3J,KAAK8H,WAAW8B,iBAAiBtD,GACxCqD,IACHA,EAAQ3J,KAAK8H,WAAW+B,oCAAoCvD,GAAK,GACjEtG,KAAK8H,WAAWgC,eAAexD,EAAKqD,IAEtC,MAAOI,EAAYC,GAAWL,EAExBM,EAASjK,KAAKkK,0BAA0B5D,EAAKR,EAAKkE,GACxD,IAAIG,EAAanC,EACboC,EAAmBL,EAClB5B,EAAckC,QACjBF,EAAahC,EAAcmC,cAAgBtC,EAAOA,EAAKuC,cACvDH,EAAmBjC,EAAcmC,cAAgBP,EAAaA,EAAWQ,eAG3E,IAAIC,GAAe,EACnB,GAAIrC,EAAckC,MAAO,CACvB,MAAMI,EAAcC,OAAOP,EAAYhC,EAAcmC,cAAgB,IAAM,MAC3E,IAAIK,EACJ,GAAIzB,EAEF,KAAOyB,EAAYF,EAAYG,KAAKR,EAAiBxI,MAAM,EAAGqI,KAAU,CACtE,MAAMY,EAAaJ,EAAYK,UAAYH,EAAU,GAAGtI,OACpDsI,EAAU,GAAGtI,OAAS,GAAKrC,KAAKuJ,oBAAoBsB,EAAYT,EAAkBO,EAAU,GAAIxC,KAClGqC,EAAcK,EACd7C,EAAO2C,EAAU,IAEnBF,EAAYK,UAAYD,EAAa,CACvC,MAOA,IADAJ,EAAYK,UAAYb,EACjBU,EAAYF,EAAYG,KAAKR,IAAmB,CACrD,MAAMS,EAAaJ,EAAYK,UAAYH,EAAU,GAAGtI,OACxD,GAAIsI,EAAU,GAAGtI,OAAS,GAAKrC,KAAKuJ,oBAAoBsB,EAAYT,EAAkBO,EAAU,GAAIxC,GAAgB,CAClHqC,EAAcK,EACd7C,EAAO2C,EAAU,GACjB,KACF,CAEAF,EAAYK,UAAYD,EAAa,CACvC,CAEJ,MAAO,GAAI3B,EAAiB,CAC1B,IAAI2B,EAAaZ,EAASE,EAAW9H,QAAU,EAAI+H,EAAiBW,YAAYZ,EAAYF,EAASE,EAAW9H,SAAW,EAE3H,KAAOwI,GAAc,IAAM7K,KAAKuJ,oBAAoBsB,EAAYT,EAAkBD,EAAYhC,IAC5F0C,EAAaA,EAAa,EAAIT,EAAiBW,YAAYZ,EAAYU,EAAa,IAAM,EAE5FL,EAAcK,CAChB,KAAO,CACL,IAAIA,EAAaT,EAAiBpI,QAAQmI,EAAYF,GACtD,KAAOY,GAAc,IAAM7K,KAAKuJ,oBAAoBsB,EAAYT,EAAkBD,EAAYhC,IAC5F0C,EAAaT,EAAiBpI,QAAQmI,EAAYU,EAAa,GAEjEL,EAAcK,CAChB,CAEA,GAAIL,GAAe,EAAG,CAGpB,IAAIQ,EAAiB,EACrB,KAAOA,EAAiBhB,EAAQ3H,OAAS,GAAKmI,GAAeR,EAAQgB,EAAiB,IACpFA,IAEF,IAAIC,EAAeD,EACnB,KAAOC,EAAejB,EAAQ3H,OAAS,GAAKmI,EAAcxC,EAAK3F,QAAU2H,EAAQiB,EAAe,IAC9FA,IAEF,MAAMC,EAAiBV,EAAcR,EAAQgB,GACvCG,EAAeX,EAAcxC,EAAK3F,OAAS2H,EAAQiB,GACnDG,EAAgBpL,KAAKqL,0BAA0B/E,EAAM0E,EAAgBE,GAI3E,MAAO,CACLlD,OACAlC,IAAKsF,EACL9E,IAAKA,EAAM0E,EACXhF,KAPkBhG,KAAKqL,0BAA0B/E,EAAM2E,EAAcE,GAC5CC,EAAgBpL,KAAKoE,UAAUsC,MAAQuE,EAAeD,GAQnF,CACF,CAEQ,yBAAAK,CAA0B/E,EAAa2D,GAC7C,MAAM9E,EAAOnF,KAAKoE,UAAU8B,OAAOC,OAAOsD,QAAQnD,GAClD,IAAKnB,EACH,OAAO,EAET,IAAK,IAAI3C,EAAI,EAAGA,EAAIyH,EAAQzH,IAAK,CAC/B,MAAM8I,EAAOnG,EAAKoG,QAAQ/I,GAC1B,IAAK8I,EACH,MAGF,MAAME,EAAOF,EAAKG,WACdD,EAAKnJ,OAAS,IAChB4H,GAAUuB,EAAKnJ,OAAS,GAI1B,MAAMqJ,EAAWvG,EAAKoG,QAAQ/I,EAAI,GAC9BkJ,GAAoC,IAAxBA,EAASC,YACvB1B,GAEJ,CACA,OAAOA,CACT,CAUQ,yBAAAC,CAA0BjC,EAAkBvB,EAAckF,GAChE,MAAMC,EAAWrF,KAAKC,IAAID,KAAKsF,MAAMpF,EAAO1G,KAAKoE,UAAUsC,MAAOkF,EAAYvJ,OAAS,GACvF,IAAI4H,EAAS2B,EAAYC,GACzB,MAAM1G,EAAOnF,KAAKoE,UAAU8B,OAAOC,OAAOsD,QAAQxB,EAAW4D,GAC7D,GAAI1G,EAAM,CACR,MAAM4G,EAAYvF,KAAKC,IAAIC,EAAOmF,EAAW7L,KAAKoE,UAAUsC,KAAM1G,KAAKoE,UAAUsC,MACjF,IAAK,IAAIlE,EAAI,EAAGA,EAAIuJ,EAAWvJ,IAAK,CAClC,MAAM8I,EAAOnG,EAAKoG,QAAQ/I,GAC1B,IAAK8I,EACH,MAEEA,EAAKK,aAEP1B,GAA6B,IAAnBqB,EAAKU,UAAkB,EAAIV,EAAKG,WAAWpJ,OAEzD,CACF,CACA,OAAO4H,CACT,yFC/aF,MAAAvK,EAAAI,EAAA,KACAmM,EAAAnM,EAAA,KAwBA,MAAAoM,UAAqCxM,EAAAiE,WAanC,WAAA5D,CAA6BqE,GAC3BC,QAD2BrE,KAAAoE,UAAAA,EANrBpE,KAAAmM,mBAAqBnM,KAAK6D,UAAU,IAAInE,EAAA0M,mBACxCpM,KAAAqM,uBAAyBrM,KAAK6D,UAAU,IAAInE,EAAA0M,mBAG5CpM,KAAAsM,qBAAuB,EAI7BtM,KAAK6D,WAAU,EAAAnE,EAAAC,cAAa,IAAMK,KAAKuM,sBACzC,CAKO,cAAAlE,GACArI,KAAKwM,cACRxM,KAAKwM,YAAc,IAAItK,MAAMlC,KAAKoE,UAAU8B,OAAOC,OAAO9D,QAC1DrC,KAAKqM,uBAAuBpI,OAAQ,EAAAvE,EAAA+M,oBAClCzM,KAAKoE,UAAUsI,WAAW,IAAM1M,KAAKuM,sBACrCvM,KAAKoE,UAAUuI,aAAa,IAAM3M,KAAKuM,sBACvCvM,KAAKoE,UAAUwI,SAAS,IAAM5M,KAAKuM,wBAIvCvM,KAAKsM,qBAAuBO,KAAKC,MAC5B9M,KAAKmM,mBAAmBlI,OAC3BjE,KAAK+M,2BAA0B,KAEnC,CAEQ,kBAAAR,GACNvM,KAAKwM,iBAAc5L,EACnBZ,KAAKsM,qBAAuB,EAC5BtM,KAAKqM,uBAAuB3I,QAC5B1D,KAAKmM,mBAAmBzI,OAC1B,CAEQ,0BAAAqJ,CAA2BC,GACjChN,KAAKmM,mBAAmBlI,OAAQ,EAAAgI,EAAAgB,mBAAkB,KAChD,IAAKjN,KAAKwM,YACR,OAEF,MACMU,EADML,KAAKC,MACK9M,KAAKsM,qBACvBY,GAAO,KACTlN,KAAKuM,qBAGPvM,KAAK+M,2BAA2B,KAAqCG,IACpEF,EACL,CAEO,gBAAApD,CAAiBtD,GACtB,OAAOtG,KAAKwM,cAAclG,EAC5B,CAEO,cAAAwD,CAAexD,EAAa5E,GAC7B1B,KAAKwM,cACPxM,KAAKwM,YAAYlG,GAAO5E,EAE5B,CAUO,mCAAAmI,CAAoCsD,EAAmBC,GAC5D,MAAMC,EAAU,GACVzB,EAAc,CAAC,GAIf0B,EAAetN,KAAKoE,UAAU8B,OAAOC,OAAO9D,OAClD,IAAI8C,EAAOnF,KAAKoE,UAAU8B,OAAOC,OAAOsD,QAAQ0D,GAChD,KAAOhI,GAAM,CACX,MAAMoI,EAAWJ,EAAY,EAAIG,EAAetN,KAAKoE,UAAU8B,OAAOC,OAAOsD,QAAQ0D,EAAY,QAAKvM,EAChG4M,IAAkBD,GAAWA,EAAS7D,UAC5C,IAAI+D,EAAStI,EAAKuI,mBAAmBF,GAAmBJ,GACxD,GAAII,GAAmBD,EAAU,CAC/B,MAAMI,EAAWxI,EAAKoG,QAAQpG,EAAK9C,OAAS,GACrBsL,GAAmC,IAAvBA,EAAS3B,WAA2C,IAAxB2B,EAAShC,YAEd,IAApC4B,EAAShC,QAAQ,IAAII,aACzC8B,EAASA,EAAO7L,MAAM,GAAI,GAE9B,CAEA,GADAyL,EAAQxL,KAAK4L,IACTD,EAGF,MAFA5B,EAAY/J,KAAK+J,EAAYA,EAAYvJ,OAAS,GAAKoL,EAAOpL,QAIhE8K,IACAhI,EAAOoI,CACT,CACA,MAAO,CAACF,EAAQO,KAAK,IAAKhC,EAC5B,gHCnIF,MAAAiC,EAAA/N,EAAA,KACAJ,EAAAI,EAAA,KAcA,MAAAgO,UAAyCpO,EAAAiE,WAAzC,WAAA5D,uBACUC,KAAA+N,eAAkC,GAGzB/N,KAAAgO,oBAAsBhO,KAAK6D,UAAU,IAAIgK,EAAAI,QA4F5D,CA3FE,sBAAWC,GAAyD,OAAOlO,KAAKgO,oBAAoB3M,KAAO,CAK3G,iBAAW8M,GACT,OAAOnO,KAAK+N,cACd,CAKA,sBAAWK,GACT,OAAOpO,KAAKqO,mBACd,CAKA,sBAAWD,CAAmBrJ,GAC5B/E,KAAKqO,oBAAsBtJ,CAC7B,CAOO,aAAAuJ,CAAc5J,EAA0B6J,GAC7CvO,KAAK+N,eAAiBrJ,EAAQ9C,MAAM,EAAG2M,EACzC,CAKO,YAAAC,GACLxO,KAAK+N,eAAiB,EACxB,CAKO,uBAAAU,GACDzO,KAAKqO,sBACPrO,KAAKqO,oBAAoB5O,UACzBO,KAAKqO,yBAAsBzN,EAE/B,CAOO,eAAA8N,CAAgB5M,GACrB,IAAK,IAAIU,EAAI,EAAGA,EAAIxC,KAAK+N,eAAe1L,OAAQG,IAAK,CACnD,MAAMoC,EAAQ5E,KAAK+N,eAAevL,GAClC,GAAIoC,EAAM0B,MAAQxE,EAAOwE,KAAO1B,EAAMkB,MAAQhE,EAAOgE,KAAOlB,EAAMoB,OAASlE,EAAOkE,KAChF,OAAOxD,CAEX,CACA,OAAQ,CACV,CAMO,kBAAAmM,CAAmBC,GACxB,IAAKA,EACH,OAGF,IAAIpE,GAAe,EACfxK,KAAKqO,sBACP7D,EAAcxK,KAAK0O,gBAAgB1O,KAAKqO,oBAAoBzJ,QAG9D5E,KAAKgO,oBAAoB5L,KAAK,CAC5BoI,cACAqE,YAAa7O,KAAK+N,eAAe1L,QAErC,CAKO,KAAAyM,GACL9O,KAAKyO,0BACLzO,KAAKwO,cACP,wHC1GF,MAOE,oBAAW5F,GACT,OAAO5I,KAAK+O,iBACd,CAKA,oBAAWnG,CAAiBZ,GAC1BhI,KAAK+O,kBAAoB/G,CAC3B,CAKA,qBAAWgH,GACT,OAAOhP,KAAKiP,kBACd,CAKA,qBAAWD,CAAkBrK,GAC3B3E,KAAKiP,mBAAqBtK,CAC5B,CAOO,iBAAAuK,CAAkBlH,GACvB,SAAUA,GAAQA,EAAK3F,OAAS,EAClC,CAOO,gBAAA8M,CAAiBC,GACtB,OAAKpP,KAAKiP,sBAGLG,IAGDpP,KAAKiP,mBAAmB3E,gBAAkB8E,EAAW9E,eAGrDtK,KAAKiP,mBAAmB5E,QAAU+E,EAAW/E,OAG7CrK,KAAKiP,mBAAmBzF,YAAc4F,EAAW5F,UAIvD,CAQO,wBAAA6F,CAAyBrH,EAAcrD,GAC5C,QAAKA,GAASE,mBAGoBjE,IAA3BZ,KAAK+O,mBACL/G,IAAShI,KAAK+O,mBACd/O,KAAKmP,iBAAiBxK,GAC/B,CAKO,eAAA2K,GACLtP,KAAK+O,uBAAoBnO,CAC3B,CAKO,KAAAkO,GACL9O,KAAK+O,uBAAoBnO,EACzBZ,KAAKiP,wBAAqBrO,CAC5B,KCvGF2O,EAAA,GAGA,SAAAzP,EAAA0P,GAEA,IAAAC,EAAAF,EAAAC,GACA,QAAA5O,IAAA6O,EACA,OAAAA,EAAA9Q,QAGA,IAAAC,EAAA2Q,EAAAC,GAAA,CAGA7Q,QAAA,IAOA,OAHA+Q,EAAAF,GAAA5Q,EAAAA,EAAAD,QAAAmB,GAGAlB,EAAAD,OACA,oGCfA,MAAAkP,EAAA/N,EAAA,KACAJ,EAAAI,EAAA,KACAmM,EAAAnM,EAAA,KACA6P,EAAA7P,EAAA,KACA8P,EAAA9P,EAAA,KACA+P,EAAA/P,EAAA,KACAgQ,EAAAhQ,EAAA,KACAiQ,EAAAjQ,EAAA,KAkBA,MAAAkQ,UAAiCtQ,EAAAiE,WAiB/B,sBAAWuK,GACT,OAAOlO,KAAKiQ,eAAe/B,kBAC7B,CAEA,WAAAnO,CAAY4E,GACVN,QAnBMrE,KAAAkQ,kBAAoBlQ,KAAK6D,UAAU,IAAInE,EAAA0M,mBACvCpM,KAAA8H,WAAa9H,KAAK6D,UAAU,IAAInE,EAAA0M,mBAGhCpM,KAAAmQ,OAAS,IAAIP,EAAAQ,YAGbpQ,KAAAiQ,eAAiBjQ,KAAK6D,UAAU,IAAIkM,EAAAjC,qBAE3B9N,KAAAqQ,eAAiBrQ,KAAK6D,UAAU,IAAIgK,EAAAI,SACrCjO,KAAAsQ,cAAgBtQ,KAAKqQ,eAAehP,MACnCrB,KAAAuQ,gBAAkBvQ,KAAK6D,UAAU,IAAIgK,EAAAI,SACtCjO,KAAAwQ,eAAiBxQ,KAAKuQ,gBAAgBlP,MASpDrB,KAAKyQ,gBAAkB9L,GAAS+L,gBAAc,GAChD,CAEO,QAAAC,CAASC,GACd5Q,KAAKoE,UAAYwM,EACjB5Q,KAAK8H,WAAW7D,MAAQ,IAAI0L,EAAAzD,gBAAgB0E,GAC5C5Q,KAAK6Q,QAAU,IAAIhB,EAAAiB,aAAaF,EAAU5Q,KAAK8H,WAAW7D,OAC1DjE,KAAK+Q,mBAAqB,IAAIjB,EAAA3L,kBAAkByM,GAChD5Q,KAAK6D,UAAU7D,KAAKoE,UAAU4M,cAAc,IAAMhR,KAAKiR,mBACvDjR,KAAK6D,UAAU7D,KAAKoE,UAAUwI,SAAS,IAAM5M,KAAKiR,mBAClDjR,KAAK6D,WAAU,EAAAnE,EAAAC,cAAa,IAAMK,KAAKkR,oBACzC,CAEQ,cAAAD,GACNjR,KAAKkQ,kBAAkBxM,QACnB1D,KAAKmQ,OAAOvH,kBAAoB5I,KAAKmQ,OAAOnB,mBAAmBnK,cACjE7E,KAAKkQ,kBAAkBjM,OAAQ,EAAAgI,EAAAgB,mBAAkB,KAC/C,MAAMjF,EAAOhI,KAAKmQ,OAAOvH,iBACzB5I,KAAKmQ,OAAOb,kBACZtP,KAAKmR,aAAanJ,EAAO,IAAKhI,KAAKmQ,OAAOnB,kBAAmBoC,aAAa,GAAQ,CAAEC,UAAU,KAC7F,KAEP,CAEO,gBAAAH,CAAiBI,GACtBtR,KAAKiQ,eAAexB,0BACpBzO,KAAK+Q,oBAAoBvM,4BACzBxE,KAAKiQ,eAAezB,eACf8C,GACHtR,KAAKmQ,OAAOb,iBAEhB,CAEO,qBAAAiC,GACLvR,KAAKiQ,eAAexB,yBACtB,CASO,QAAA+C,CAASxJ,EAAcG,EAAgCsJ,GAC5D,IAAKzR,KAAKoE,YAAcpE,KAAK6Q,QAC3B,MAAM,IAAIvQ,MAAM,6CAGlBN,KAAKuQ,gBAAgBnO,OAErBpC,KAAKmQ,OAAOnB,kBAAoB7G,EAE5BnI,KAAKmQ,OAAOd,yBAAyBrH,EAAMG,IAC7CnI,KAAK0R,qBAAqB1J,EAAMG,GAGlC,MAAMwJ,EAAQ3R,KAAK4R,mBAAmB5J,EAAMG,EAAesJ,GAM3D,OALAzR,KAAK6R,aAAa1J,GAClBnI,KAAKmQ,OAAOvH,iBAAmBZ,EAE/BhI,KAAKqQ,eAAejO,OAEbuP,CACT,CAEQ,oBAAAD,CAAqB1J,EAAcG,GACzC,IAAKnI,KAAKoE,YAAcpE,KAAK6Q,UAAY7Q,KAAK+Q,mBAC5C,MAAM,IAAIzQ,MAAM,6CAElB,IAAKN,KAAKmQ,OAAOjB,kBAAkBlH,GAEjC,YADAhI,KAAKkR,mBAKPlR,KAAKkR,kBAAiB,GAEtB,MAAMxM,EAA2B,GACjC,IAAIoN,EACAhQ,EAAS9B,KAAK6Q,QAAQ9I,KAAKC,EAAM,EAAG,EAAGG,GAE3C,KAAOrG,IAAWgQ,GAAYxL,MAAQxE,EAAOwE,KAAOwL,GAAYhM,MAAQhE,EAAOgE,QACzEpB,EAAQrC,QAAUrC,KAAKyQ,kBADwD,CAInFqB,EAAahQ,EACb4C,EAAQ7C,KAAKiQ,GACb,MAAMpL,EAAO1G,KAAKoE,UAAUsC,KAC5B,IAAIqL,EAAUD,EAAWhM,IAAMgM,EAAW9L,KACtCgM,EAAUF,EAAWxL,IACrByL,GAAWrL,IACbsL,GAAWxL,KAAKsF,MAAMiG,EAAUrL,GAChCqL,GAAoBrL,GAEtB5E,EAAS9B,KAAK6Q,QAAQ9I,KAAKC,EAAMgK,EAASD,EAAS5J,EACrD,CAEAnI,KAAKiQ,eAAe3B,cAAc5J,EAAS1E,KAAKyQ,iBAC5CtI,EAActD,aAChB7E,KAAK+Q,mBAAmBtM,2BAA2BC,EAASyD,EAActD,YAE9E,CAEQ,kBAAA+M,CAAmB5J,EAAcG,EAAgCsJ,GACvE,IAAKzR,KAAKoE,YAAcpE,KAAK6Q,QAC3B,OAAO,EAET,IAAK7Q,KAAKmQ,OAAOjB,kBAAkBlH,GAGjC,OAFAhI,KAAKoE,UAAUgE,iBACfpI,KAAKkR,oBACE,EAGT,MAAMpP,EAAS9B,KAAK6Q,QAAQlI,sBAAsBX,EAAMG,EAAenI,KAAKmQ,OAAOvH,kBACnF,OAAO5I,KAAKiS,cAAcnQ,EAAQqG,GAAetD,YAAa4M,GAAuBJ,SACvF,CASO,YAAAF,CAAanJ,EAAcG,EAAgCsJ,GAChE,IAAKzR,KAAKoE,YAAcpE,KAAK6Q,QAC3B,MAAM,IAAIvQ,MAAM,6CAGlBN,KAAKuQ,gBAAgBnO,OAErBpC,KAAKmQ,OAAOnB,kBAAoB7G,EAE5BnI,KAAKmQ,OAAOd,yBAAyBrH,EAAMG,IAC7CnI,KAAK0R,qBAAqB1J,EAAMG,GAGlC,MAAMwJ,EAAQ3R,KAAKkS,uBAAuBlK,EAAMG,EAAesJ,GAM/D,OALAzR,KAAK6R,aAAa1J,GAClBnI,KAAKmQ,OAAOvH,iBAAmBZ,EAE/BhI,KAAKqQ,eAAejO,OAEbuP,CACT,CAEQ,YAAAE,CAAa1J,GACnBnI,KAAKiQ,eAAetB,qBAAqBxG,GAAetD,YAC1D,CAEQ,sBAAAqN,CAAuBlK,EAAcG,EAAgCsJ,GAC3E,IAAKzR,KAAKoE,YAAcpE,KAAK6Q,QAC3B,OAAO,EAET,IAAK7Q,KAAKmQ,OAAOjB,kBAAkBlH,GAGjC,OAFAhI,KAAKoE,UAAUgE,iBACfpI,KAAKkR,oBACE,EAGT,MAAMpP,EAAS9B,KAAK6Q,QAAQ5H,0BAA0BjB,EAAMG,EAAenI,KAAKmQ,OAAOvH,kBACvF,OAAO5I,KAAKiS,cAAcnQ,EAAQqG,GAAetD,YAAa4M,GAAuBJ,SACvF,CAOQ,aAAAY,CAAcnQ,EAAmC6C,EAAoC0M,GAC3F,IAAKrR,KAAKoE,YAAcpE,KAAK+Q,mBAC3B,OAAO,EAIT,GADA/Q,KAAKiQ,eAAexB,2BACf3M,EAEH,OADA9B,KAAKoE,UAAUgE,kBACR,EAIT,GADApI,KAAKoE,UAAU+N,OAAOrQ,EAAOgE,IAAKhE,EAAOwE,IAAKxE,EAAOkE,MACjDrB,EAAS,CACX,MAAMyN,EAAmBpS,KAAK+Q,mBAAmB9L,uBAAuBnD,EAAQ6C,GAC5EyN,IACFpS,KAAKiQ,eAAe7B,mBAAqBgE,EAE7C,CAEA,IAAKf,IAECvP,EAAOwE,KAAQtG,KAAKoE,UAAU8B,OAAOC,OAAOkM,UAAYrS,KAAKoE,UAAUqE,MAAS3G,EAAOwE,IAAMtG,KAAKoE,UAAU8B,OAAOC,OAAOkM,WAAW,CACvI,IAAIC,EAASxQ,EAAOwE,IAAMtG,KAAKoE,UAAU8B,OAAOC,OAAOkM,UACvDC,GAAU9L,KAAKsF,MAAM9L,KAAKoE,UAAUqE,KAAO,GAC3CzI,KAAKoE,UAAUmO,YAAYD,EAC7B,CAEF,OAAO,CACT","sources":["webpack://SearchAddon/webpack/universalModuleDefinition","webpack://SearchAddon/../src/common/Async.ts","webpack://SearchAddon/../src/common/Event.ts","webpack://SearchAddon/../src/common/Lifecycle.ts","webpack://SearchAddon/./src/DecorationManager.ts","webpack://SearchAddon/./src/SearchEngine.ts","webpack://SearchAddon/./src/SearchLineCache.ts","webpack://SearchAddon/./src/SearchResultTracker.ts","webpack://SearchAddon/./src/SearchState.ts","webpack://SearchAddon/webpack/bootstrap","webpack://SearchAddon/./src/SearchAddon.ts"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"SearchAddon\"] = factory();\n\telse\n\t\troot[\"SearchAddon\"] = factory();\n})(globalThis, () => {\nreturn ","/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal async helpers for xterm.js core.\n */\n\nimport { DisposableStore, IDisposable, toDisposable } from './Lifecycle';\n\nexport function timeout(millis: number): Promise {\n return new Promise(resolve => setTimeout(resolve, millis));\n}\n\n/**\n * Creates a timeout that can be disposed using its returned value.\n * @param handler The timeout handler.\n * @param timeout An optional timeout in milliseconds.\n * @param store An optional {@link DisposableStore} that will have the timeout disposable managed\n * automatically.\n */\nexport function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable {\n const timer = setTimeout(() => {\n handler();\n if (store) {\n disposable.dispose();\n }\n }, timeout);\n const disposable = toDisposable(() => {\n clearTimeout(timer);\n });\n store?.add(disposable);\n return disposable;\n}\n\nexport class TimeoutTimer implements IDisposable {\n private _token: any = -1;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n if (this._token !== -1) {\n clearTimeout(this._token);\n this._token = -1;\n }\n }\n\n public cancelAndSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed TimeoutTimer');\n }\n this.cancel();\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n\n public setIfNotSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling setIfNotSet on a disposed TimeoutTimer');\n }\n if (this._token !== -1) {\n return;\n }\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n}\n\n/**\n * Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled\n * microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not\n * run yet.\n */\nexport class MicrotaskTimer implements IDisposable {\n private _isScheduled = false;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n this._isScheduled = false;\n }\n\n public set(runner: () => void): void {\n if (this._isDisposed) {\n throw new Error('Calling set on a disposed MicrotaskTimer');\n }\n if (this._isScheduled) {\n return;\n }\n this._isScheduled = true;\n queueMicrotask(() => {\n if (!this._isScheduled) {\n return;\n }\n this._isScheduled = false;\n runner();\n });\n }\n}\n\nexport class IntervalTimer implements IDisposable {\n private _disposable: IDisposable | undefined;\n private _isDisposed = false;\n\n public cancel(): void {\n this._disposable?.dispose();\n this._disposable = undefined;\n }\n\n public cancelAndSet(runner: () => void, interval: number, context: Window | typeof globalThis = globalThis): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed IntervalTimer');\n }\n this.cancel();\n const handle = context.setInterval(() => {\n runner();\n }, interval);\n this._disposable = {\n dispose: () => {\n context.clearInterval(handle as any);\n this._disposable = undefined;\n }\n };\n }\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n}\n","/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal event utilities for xterm.js core.\n * Simplified from VS Code's event.ts - no leak detection/profiling.\n */\n\nimport { IDisposable, DisposableStore, toDisposable } from './Lifecycle';\n\nexport interface IEvent {\n (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;\n}\n\nexport class Emitter {\n private _listeners: { fn: (e: T) => any, thisArgs: any }[] = [];\n private _disposed = false;\n private _event: IEvent | undefined;\n\n public get event(): IEvent {\n if (this._event) {\n return this._event;\n }\n this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n if (this._disposed) {\n return toDisposable(() => {});\n }\n\n const entry = { fn: listener, thisArgs };\n this._listeners = this._listeners.slice();\n this._listeners.push(entry);\n\n const result = toDisposable(() => {\n const idx = this._listeners.indexOf(entry);\n if (idx !== -1) {\n this._listeners = this._listeners.slice();\n this._listeners.splice(idx, 1);\n }\n });\n\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(result);\n } else {\n disposables.add(result);\n }\n }\n\n return result;\n };\n return this._event;\n }\n\n public fire(event: T): void {\n if (this._disposed || !this._listeners.length) {\n return;\n }\n if (this._listeners.length === 1) {\n this._listeners[0].fn.call(this._listeners[0].thisArgs, event);\n return;\n }\n const listeners = this._listeners;\n for (let i = 0, len = listeners.length; i < len; ++i) {\n listeners[i].fn.call(listeners[i].thisArgs, event);\n }\n }\n\n public dispose(): void {\n if (this._disposed) {\n return;\n }\n this._disposed = true;\n this._listeners.length = 0;\n }\n}\n\nexport namespace EventUtils {\n export function forward(from: IEvent, to: Emitter): IDisposable {\n return from(e => to.fire(e));\n }\n\n export function map(event: IEvent, map: (i: I) => O): IEvent {\n return (listener: (e: O) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n return event(i => listener.call(thisArgs, map(i)), undefined, disposables);\n };\n }\n\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent {\n return (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n const store = new DisposableStore();\n for (const event of events) {\n store.add(event(e => listener.call(thisArgs, e)));\n }\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(store);\n } else {\n disposables.add(store);\n }\n }\n return store;\n };\n }\n\n export function runAndSubscribe(event: IEvent, handler: (e: T) => void, initial: T): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void, initial?: T): IDisposable {\n handler(initial);\n return event(e => handler(e));\n }\n}\n","/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal lifecycle utilities for xterm.js core.\n * Simplified from VS Code's lifecycle.ts - no tracking/leak detection.\n */\n\nexport interface IDisposable {\n dispose(): void;\n}\n\nexport function toDisposable(fn: () => void): IDisposable {\n return { dispose: fn };\n}\n\nexport function dispose(disposable: T): T;\nexport function dispose(disposable: T | undefined): T | undefined;\nexport function dispose(disposables: T[]): T[];\nexport function dispose(arg: T | T[] | undefined): T | T[] | undefined {\n if (!arg) {\n return arg;\n }\n if (Array.isArray(arg)) {\n for (const d of arg) {\n d.dispose();\n }\n return [];\n }\n arg.dispose();\n return arg;\n}\n\nexport function combinedDisposable(...disposables: IDisposable[]): IDisposable {\n return toDisposable(() => dispose(disposables));\n}\n\nexport class DisposableStore implements IDisposable {\n private readonly _disposables = new Set();\n private _isDisposed = false;\n\n public get isDisposed(): boolean {\n return this._isDisposed;\n }\n\n public add(o: T): T {\n if (this._isDisposed) {\n o.dispose();\n } else {\n this._disposables.add(o);\n }\n return o;\n }\n\n public dispose(): void {\n if (this._isDisposed) {\n return;\n }\n this._isDisposed = true;\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n\n public clear(): void {\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n}\n\nexport abstract class Disposable implements IDisposable {\n public static readonly None: IDisposable = Object.freeze({ dispose() { } });\n\n protected readonly _store = new DisposableStore();\n\n public dispose(): void {\n this._store.dispose();\n }\n\n protected _register(o: T): T {\n return this._store.add(o);\n }\n}\n\nexport class MutableDisposable implements IDisposable {\n private _value: T | undefined;\n private _isDisposed = false;\n\n public get value(): T | undefined {\n return this._isDisposed ? undefined : this._value;\n }\n\n public set value(value: T | undefined) {\n if (this._isDisposed || value === this._value) {\n return;\n }\n this._value?.dispose();\n this._value = value;\n }\n\n public clear(): void {\n this.value = undefined;\n }\n\n public dispose(): void {\n this._isDisposed = true;\n this._value?.dispose();\n this._value = undefined;\n }\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';\nimport type { ISearchDecorationOptions } from '@xterm/addon-search';\nimport { dispose, Disposable, toDisposable } from 'common/Lifecycle';\nimport type { ISearchResult } from './SearchEngine';\n\n/**\n * Interface for managing a highlight decoration.\n */\ninterface IHighlight extends IDisposable {\n decoration: IDecoration;\n match: ISearchResult;\n}\n\n/**\n * Interface for managing multiple decorations for a single match.\n */\ninterface IMultiHighlight extends IDisposable {\n decorations: IDecoration[];\n match: ISearchResult;\n}\n\n/**\n * Manages visual decorations for search results including highlighting and active selection\n * indicators. This class handles the creation, styling, and disposal of search-related decorations.\n */\nexport class DecorationManager extends Disposable {\n private _highlightDecorations: IHighlight[] = [];\n private _highlightedLines: Set = new Set();\n\n constructor(private readonly _terminal: Terminal) {\n super();\n this._register(toDisposable(() => this.clearHighlightDecorations()));\n }\n\n /**\n * Creates decorations for all provided search results.\n * @param results The search results to create decorations for.\n * @param options The decoration options.\n */\n public createHighlightDecorations(results: ISearchResult[], options: ISearchDecorationOptions): void {\n this.clearHighlightDecorations();\n\n for (const match of results) {\n const decorations = this._createResultDecorations(match, options, false);\n if (decorations) {\n for (const decoration of decorations) {\n this._storeDecoration(decoration, match);\n }\n }\n }\n }\n\n /**\n * Creates decorations for the currently active search result.\n * @param result The active search result.\n * @param options The decoration options.\n * @returns The multi-highlight decoration or undefined if creation failed.\n */\n public createActiveDecoration(result: ISearchResult, options: ISearchDecorationOptions): IMultiHighlight | undefined {\n const decorations = this._createResultDecorations(result, options, true);\n if (decorations) {\n return { decorations, match: result, dispose() { dispose(decorations); } };\n }\n return undefined;\n }\n\n /**\n * Clears all highlight decorations.\n */\n public clearHighlightDecorations(): void {\n dispose(this._highlightDecorations);\n this._highlightDecorations = [];\n this._highlightedLines.clear();\n }\n\n /**\n * Stores a decoration and tracks it for management.\n * @param decoration The decoration to store.\n * @param match The search result this decoration represents.\n */\n private _storeDecoration(decoration: IDecoration, match: ISearchResult): void {\n this._highlightedLines.add(decoration.marker.line);\n this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });\n }\n\n /**\n * Applies styles to the decoration when it is rendered.\n * @param element The decoration's element.\n * @param borderColor The border color to apply.\n * @param isActiveResult Whether the element is part of the active search result.\n */\n private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {\n if (!element.classList.contains('xterm-find-result-decoration')) {\n element.classList.add('xterm-find-result-decoration');\n if (borderColor) {\n element.style.outline = `1px solid ${borderColor}`;\n }\n }\n if (isActiveResult) {\n element.classList.add('xterm-find-active-result-decoration');\n }\n }\n\n /**\n * Creates a decoration for the result and applies styles\n * @param result the search result for which to create the decoration\n * @param options the options for the decoration\n * @param isActiveResult whether this is the currently active result\n * @returns the decorations or undefined if the marker has already been disposed of\n */\n private _createResultDecorations(result: ISearchResult, options: ISearchDecorationOptions, isActiveResult: boolean): IDecoration[] | undefined {\n // Gather decoration ranges for this match as it could wrap\n const decorationRanges: [number, number, number][] = [];\n let currentCol = result.col;\n let remainingSize = result.size;\n let markerOffset = -this._terminal.buffer.active.baseY - this._terminal.buffer.active.cursorY + result.row;\n while (remainingSize > 0) {\n const amountThisRow = Math.min(this._terminal.cols - currentCol, remainingSize);\n decorationRanges.push([markerOffset, currentCol, amountThisRow]);\n currentCol = 0;\n remainingSize -= amountThisRow;\n markerOffset++;\n }\n\n // Create the decorations\n const decorations: IDecoration[] = [];\n for (const range of decorationRanges) {\n const marker = this._terminal.registerMarker(range[0]);\n const decoration = this._terminal.registerDecoration({\n marker,\n x: range[1],\n width: range[2],\n layer: isActiveResult ? 'top' : 'bottom',\n backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,\n overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {\n color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,\n position: 'center'\n }\n });\n if (decoration) {\n const disposables: IDisposable[] = [];\n disposables.push(marker);\n disposables.push(decoration.onRender((e) => this._applyStyles(e, isActiveResult ? options.activeMatchBorder : options.matchBorder, false)));\n disposables.push(decoration.onDispose(() => dispose(disposables)));\n decorations.push(decoration);\n }\n }\n\n return decorations.length === 0 ? undefined : decorations;\n }\n}\n\n\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal } from '@xterm/xterm';\nimport type { ISearchOptions } from '@xterm/addon-search';\nimport type { SearchLineCache } from './SearchLineCache';\n\n/**\n * Represents the position to start a search from.\n */\ninterface ISearchPosition {\n startCol: number;\n startRow: number;\n}\n\n/**\n * Represents a search result with its position and content.\n */\nexport interface ISearchResult {\n term: string;\n col: number;\n row: number;\n size: number;\n}\n\n/**\n * Configuration constants for the search engine functionality.\n */\nconst enum Constants {\n /**\n * Characters that are considered non-word characters for search boundary detection. These\n * characters are used to determine word boundaries when performing whole-word searches. Includes\n * common punctuation, symbols, and whitespace characters.\n */\n NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\\\;:\"\\',./<>?'\n}\n\n/**\n * Core search engine that handles finding text within terminal content.\n * This class is responsible for the actual search algorithms and position calculations.\n */\nexport class SearchEngine {\n constructor(\n private readonly _terminal: Terminal,\n private readonly _lineCache: SearchLineCache\n ) {}\n\n /**\n * Find the first occurrence of a term starting from a specific position.\n * @param term The search term.\n * @param startRow The row to start searching from.\n * @param startCol The column to start searching from.\n * @param searchOptions Search options.\n * @returns The search result if found, undefined otherwise.\n */\n public find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n if (startCol >= this._terminal.cols) {\n throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);\n }\n\n this._lineCache.initLinesCache();\n\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n // Search startRow\n let result = this._findInLine(term, searchPosition, searchOptions);\n // Search from startRow + 1 to end\n if (!result) {\n for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {\n if (this._isRowCoveredByEarlierSearch(y)) {\n continue;\n }\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n return result;\n }\n\n /**\n * Find the next occurrence of a term with wrapping and selection management.\n * @param term The search term.\n * @param searchOptions Search options.\n * @param cachedSearchTerm The cached search term to determine incremental behavior.\n * @returns The search result if found, undefined otherwise.\n */\n public findNextWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n\n const prevSelectedPos = this._terminal.getSelectionPosition();\n this._terminal.clearSelection();\n\n let startCol = 0;\n let startRow = 0;\n if (prevSelectedPos) {\n if (cachedSearchTerm === term) {\n startCol = prevSelectedPos.end.x;\n startRow = prevSelectedPos.end.y;\n } else {\n startCol = prevSelectedPos.start.x;\n startRow = prevSelectedPos.start.y;\n }\n }\n\n this._lineCache.initLinesCache();\n\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n // Search startRow\n let result = this._findInLine(term, searchPosition, searchOptions);\n // Search from startRow + 1 to end\n if (!result) {\n for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {\n if (this._isRowCoveredByEarlierSearch(y)) {\n continue;\n }\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n // If we hit the bottom and didn't search from the very top wrap back up\n if (!result && startRow !== 0) {\n for (let y = 0; y < startRow; y++) {\n // Row 0 is never skipped: it can be a continuation whose line start was trimmed from the\n // scrollback, and nothing earlier in this loop has searched it.\n if (y > 0 && this._isRowCoveredByEarlierSearch(y)) {\n continue;\n }\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n\n // If there is only one result, wrap back and return selection if it exists.\n if (!result && prevSelectedPos) {\n searchPosition.startRow = prevSelectedPos.start.y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n }\n\n return result;\n }\n\n /**\n * Find the previous occurrence of a term with wrapping and selection management.\n * @param term The search term.\n * @param searchOptions Search options.\n * @param cachedSearchTerm The cached search term to determine if expansion should occur.\n * @returns The search result if found, undefined otherwise.\n */\n public findPreviousWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n\n const prevSelectedPos = this._terminal.getSelectionPosition();\n this._terminal.clearSelection();\n\n let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;\n const startCol = this._terminal.cols;\n const isReverseSearch = true;\n\n this._lineCache.initLinesCache();\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n let result: ISearchResult | undefined;\n if (prevSelectedPos) {\n searchPosition.startRow = startRow = prevSelectedPos.start.y;\n searchPosition.startCol = prevSelectedPos.start.x;\n if (cachedSearchTerm !== term) {\n // Try to expand selection to right first.\n result = this._findInLine(term, searchPosition, searchOptions, false);\n if (!result) {\n // If selection was not able to be expanded to the right, then try reverse search\n searchPosition.startRow = startRow = prevSelectedPos.end.y;\n searchPosition.startCol = prevSelectedPos.end.x;\n }\n }\n }\n\n result ??= this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n\n // Search from startRow - 1 to top\n if (!result) {\n searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols);\n for (let y = startRow - 1; y >= 0; y--) {\n searchPosition.startRow = y;\n result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n if (result) {\n break;\n }\n }\n }\n // If we hit the top and didn't search from the very bottom wrap back down\n if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {\n for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {\n searchPosition.startRow = y;\n result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n if (result) {\n break;\n }\n }\n }\n\n return result;\n }\n\n /**\n * A found substring is a whole word if it doesn't have an alphanumeric character directly\n * adjacent to it.\n * @param searchIndex starting index of the potential whole word substring\n * @param line entire string in which the potential whole word was found\n * @param term the substring that starts at searchIndex\n */\n private _isWholeWord(searchIndex: number, line: string, term: string): boolean {\n return ((searchIndex === 0) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&\n (((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));\n }\n\n /** `_isWholeWord` gated on the option, so a rejected hit can be stepped past instead of ending the scan. */\n private _satisfiesWholeWord(searchIndex: number, line: string, term: string, searchOptions: ISearchOptions): boolean {\n return !searchOptions.wholeWord || this._isWholeWord(searchIndex, line, term);\n }\n\n /**\n * Whether an earlier `_findInLine` in this same call already scanned this row's line from an\n * equal or lower offset, which makes rescanning it pure O(rows^2) work on one long line. Sound\n * for every option because `_findInLine` returns the first accepted match at or after its\n * offset, which is monotone in that offset. Only valid once such a search has happened — the\n * wrap-around loop starts at row 0, whose line start may have been trimmed from the scrollback.\n */\n private _isRowCoveredByEarlierSearch(row: number): boolean {\n return this._terminal.buffer.active.getLine(row)?.isWrapped === true;\n }\n\n /**\n * Searches a line for a search term. Takes the provided terminal line and searches the text line,\n * which may contain subsequent terminal lines if the text is wrapped. If the provided line number\n * is part of a wrapped text line that started on an earlier line then it is skipped since it will\n * be properly searched when the terminal line that the text starts on is searched.\n * @param term The search term.\n * @param searchPosition The position to start the search.\n * @param searchOptions Search options.\n * @param isReverseSearch Whether the search should start from the right side of the terminal and\n * search to the left.\n * @returns The search result if it was found.\n */\n private _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {\n // Ignore wrapped lines, only consider on unwrapped line (first row of command string).\n if (isReverseSearch) {\n // Reverse search never rewinds: its caller carries startCol down the rows of the line. Row 0\n // is searched even when wrapped, since its line start may have been trimmed from the scrollback.\n if (searchPosition.startRow > 0 && this._terminal.buffer.active.getLine(searchPosition.startRow)?.isWrapped) {\n searchPosition.startCol += this._terminal.cols;\n return;\n }\n } else {\n // A loop rather than recursion: one frame per wrapped row overflows the stack on a line long\n // enough to fill the scrollback. Bounded at row 0 because after a reflow the buffer's ring\n // holds stale entries at negative indices, so `getLine(-1)` answers with a wrapped line.\n while (searchPosition.startRow > 0 && this._terminal.buffer.active.getLine(searchPosition.startRow)?.isWrapped) {\n searchPosition.startRow--;\n searchPosition.startCol += this._terminal.cols;\n }\n }\n const row = searchPosition.startRow;\n const col = searchPosition.startCol;\n\n let cache = this._lineCache.getLineFromCache(row);\n if (!cache) {\n cache = this._lineCache.translateBufferLineToStringWithWrap(row, true);\n this._lineCache.setLineInCache(row, cache);\n }\n const [stringLine, offsets] = cache;\n\n const offset = this._bufferColsToStringOffset(row, col, offsets);\n let searchTerm = term;\n let searchStringLine = stringLine;\n if (!searchOptions.regex) {\n searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();\n searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();\n }\n\n let resultIndex = -1;\n if (searchOptions.regex) {\n const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');\n let foundTerm: RegExpExecArray | null;\n if (isReverseSearch) {\n // This loop will get the resultIndex of the _last_ regex match in the range 0..offset\n while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {\n const matchIndex = searchRegex.lastIndex - foundTerm[0].length;\n if (foundTerm[0].length > 0 && this._satisfiesWholeWord(matchIndex, searchStringLine, foundTerm[0], searchOptions)) {\n resultIndex = matchIndex;\n term = foundTerm[0];\n }\n searchRegex.lastIndex = matchIndex + 1;\n }\n } else {\n // Driven over the whole line from `offset` rather than over `slice(offset)`: a slice\n // re-anchors ^ and \\b at whatever column the row happened to wrap at, and only\n // first-accepted-match-at-or-after-offset is monotone in `offset`, which is what lets\n // `_isRowCoveredByEarlierSearch` skip a wrapped row an earlier scan already covered.\n searchRegex.lastIndex = offset;\n while (foundTerm = searchRegex.exec(searchStringLine)) {\n const matchIndex = searchRegex.lastIndex - foundTerm[0].length;\n if (foundTerm[0].length > 0 && this._satisfiesWholeWord(matchIndex, searchStringLine, foundTerm[0], searchOptions)) {\n resultIndex = matchIndex;\n term = foundTerm[0];\n break;\n }\n // A zero-length or rejected match would otherwise repeat forever.\n searchRegex.lastIndex = matchIndex + 1;\n }\n }\n } else if (isReverseSearch) {\n let matchIndex = offset - searchTerm.length >= 0 ? searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length) : -1;\n // `lastIndexOf` clamps a negative fromIndex to 0, so index 0 has to end the walk.\n while (matchIndex >= 0 && !this._satisfiesWholeWord(matchIndex, searchStringLine, searchTerm, searchOptions)) {\n matchIndex = matchIndex > 0 ? searchStringLine.lastIndexOf(searchTerm, matchIndex - 1) : -1;\n }\n resultIndex = matchIndex;\n } else {\n let matchIndex = searchStringLine.indexOf(searchTerm, offset);\n while (matchIndex >= 0 && !this._satisfiesWholeWord(matchIndex, searchStringLine, searchTerm, searchOptions)) {\n matchIndex = searchStringLine.indexOf(searchTerm, matchIndex + 1);\n }\n resultIndex = matchIndex;\n }\n\n if (resultIndex >= 0) {\n // Adjust the row number and search index if needed since a \"line\" of text can span multiple\n // rows\n let startRowOffset = 0;\n while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {\n startRowOffset++;\n }\n let endRowOffset = startRowOffset;\n while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) {\n endRowOffset++;\n }\n const startColOffset = resultIndex - offsets[startRowOffset];\n const endColOffset = resultIndex + term.length - offsets[endRowOffset];\n const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset);\n const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset);\n const size = endColIndex - startColIndex + this._terminal.cols * (endRowOffset - startRowOffset);\n\n return {\n term,\n col: startColIndex,\n row: row + startRowOffset,\n size\n };\n }\n }\n\n private _stringLengthToBufferSize(row: number, offset: number): number {\n const line = this._terminal.buffer.active.getLine(row);\n if (!line) {\n return 0;\n }\n for (let i = 0; i < offset; i++) {\n const cell = line.getCell(i);\n if (!cell) {\n break;\n }\n // Adjust the searchIndex to normalize emoji into single chars\n const char = cell.getChars();\n if (char.length > 1) {\n offset -= char.length - 1;\n }\n // Adjust the searchIndex for empty characters following wide unicode\n // chars (eg. CJK)\n const nextCell = line.getCell(i + 1);\n if (nextCell && nextCell.getWidth() === 0) {\n offset++;\n }\n }\n return offset;\n }\n\n /**\n * `cols` counts from the start of the logical line, so summing the cells of every row before the\n * resume point costs O(line) per call and the highlight-all pass makes one call per match.\n * `lineOffsets` already holds the string offset each wrapped row starts at — the same map used\n * above to turn a match index back into a row — so only the last, partial row needs cells. It is\n * also the map the row a match lands on is read from, which the cell sum disagreed with by one\n * for a row whose trailing cell is the null placeholder of a wide character that wrapped.\n */\n private _bufferColsToStringOffset(startRow: number, cols: number, lineOffsets: number[]): number {\n const rowsBack = Math.min(Math.floor(cols / this._terminal.cols), lineOffsets.length - 1);\n let offset = lineOffsets[rowsBack];\n const line = this._terminal.buffer.active.getLine(startRow + rowsBack);\n if (line) {\n const colsInRow = Math.min(cols - rowsBack * this._terminal.cols, this._terminal.cols);\n for (let i = 0; i < colsInRow; i++) {\n const cell = line.getCell(i);\n if (!cell) {\n break;\n }\n if (cell.getWidth()) {\n // Treat null characters as whitespace to align with the translateToString API\n offset += cell.getCode() === 0 ? 1 : cell.getChars().length;\n }\n }\n }\n return offset;\n }\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal } from '@xterm/xterm';\nimport { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';\nimport { disposableTimeout } from 'common/Async';\n\nexport type LineCacheEntry = [\n /**\n * The string representation of a line (as opposed to the buffer cell representation).\n */\n lineAsString: string,\n /**\n * The offsets where each line starts when the entry describes a wrapped line.\n */\n lineOffsets: number[]\n];\n\n/**\n * Configuration constants for the search line cache functionality.\n */\nconst enum Constants {\n /**\n * Time-to-live for cached search results in milliseconds. After this duration, cached search\n * results will be invalidated to ensure they remain consistent with terminal content changes.\n */\n LINES_CACHE_TIME_TO_LIVE = 15000\n}\n\nexport class SearchLineCache extends Disposable {\n /**\n * translateBufferLineToStringWithWrap is a fairly expensive call.\n * We memoize the calls into an array that has a time based ttl.\n * _linesCache is also invalidated when the terminal cursor moves.\n */\n private _linesCache: LineCacheEntry[] | undefined;\n private _linesCacheTimeout = this._register(new MutableDisposable());\n private _linesCacheDisposables = this._register(new MutableDisposable());\n // Track access to avoid recreating a timeout on every init call which occurs once per search\n // result (findNext/findPrevious -> _highlightAllMatches -> find loop).\n private _lastAccessTimestamp = 0;\n\n constructor(private readonly _terminal: Terminal) {\n super();\n this._register(toDisposable(() => this._destroyLinesCache()));\n }\n\n /**\n * Sets up a line cache with a ttl\n */\n public initLinesCache(): void {\n if (!this._linesCache) {\n this._linesCache = new Array(this._terminal.buffer.active.length);\n this._linesCacheDisposables.value = combinedDisposable(\n this._terminal.onLineFeed(() => this._destroyLinesCache()),\n this._terminal.onCursorMove(() => this._destroyLinesCache()),\n this._terminal.onResize(() => this._destroyLinesCache())\n );\n }\n\n this._lastAccessTimestamp = Date.now();\n if (!this._linesCacheTimeout.value) {\n this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE);\n }\n }\n\n private _destroyLinesCache(): void {\n this._linesCache = undefined;\n this._lastAccessTimestamp = 0;\n this._linesCacheDisposables.clear();\n this._linesCacheTimeout.clear();\n }\n\n private _scheduleLinesCacheTimeout(delay: number): void {\n this._linesCacheTimeout.value = disposableTimeout(() => {\n if (!this._linesCache) {\n return;\n }\n const now = Date.now();\n const elapsed = now - this._lastAccessTimestamp;\n if (elapsed >= Constants.LINES_CACHE_TIME_TO_LIVE) {\n this._destroyLinesCache();\n return;\n }\n this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE - elapsed);\n }, delay);\n }\n\n public getLineFromCache(row: number): LineCacheEntry | undefined {\n return this._linesCache?.[row];\n }\n\n public setLineInCache(row: number, entry: LineCacheEntry): void {\n if (this._linesCache) {\n this._linesCache[row] = entry;\n }\n }\n\n /**\n * Translates a buffer line to a string, including subsequent lines if they are wraps.\n * Wide characters will count as two columns in the resulting string. This\n * function is useful for getting the actual text underneath the raw selection\n * position.\n * @param lineIndex The index of the line being translated.\n * @param trimRight Whether to trim whitespace to the right.\n */\n public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {\n const strings = [];\n const lineOffsets = [0];\n // A single line longer than the whole scrollback leaves every buffer row wrapped, and the\n // buffer's ring answers an out-of-range row by cycling back to the start, so an unbounded walk\n // never reaches an unwrapped line.\n const bufferLength = this._terminal.buffer.active.length;\n let line = this._terminal.buffer.active.getLine(lineIndex);\n while (line) {\n const nextLine = lineIndex + 1 < bufferLength ? this._terminal.buffer.active.getLine(lineIndex + 1) : undefined;\n const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;\n let string = line.translateToString(!lineWrapsToNext && trimRight);\n if (lineWrapsToNext && nextLine) {\n const lastCell = line.getCell(line.length - 1);\n const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;\n // a wide character wrapped to the next line\n if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {\n string = string.slice(0, -1);\n }\n }\n strings.push(string);\n if (lineWrapsToNext) {\n lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);\n } else {\n break;\n }\n lineIndex++;\n line = nextLine;\n }\n return [strings.join(''), lineOffsets];\n }\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ISearchResultChangeEvent } from '@xterm/addon-search';\nimport type { IDisposable } from '@xterm/xterm';\nimport { Emitter, type IEvent } from 'common/Event';\nimport { Disposable } from 'common/Lifecycle';\nimport type { ISearchResult } from './SearchEngine';\n\n/**\n * Interface for managing a currently selected decoration.\n */\ninterface ISelectedDecoration extends IDisposable {\n match: ISearchResult;\n}\n\n/**\n * Tracks search results, manages result indexing, and fires events when results change.\n * This class provides centralized management of search result state and notifications.\n */\nexport class SearchResultTracker extends Disposable {\n private _searchResults: ISearchResult[] = [];\n private _selectedDecoration: ISelectedDecoration | undefined;\n\n private readonly _onDidChangeResults = this._register(new Emitter());\n public get onDidChangeResults(): IEvent { return this._onDidChangeResults.event; }\n\n /**\n * Gets the current search results.\n */\n public get searchResults(): ReadonlyArray {\n return this._searchResults;\n }\n\n /**\n * Gets the currently selected decoration.\n */\n public get selectedDecoration(): ISelectedDecoration | undefined {\n return this._selectedDecoration;\n }\n\n /**\n * Sets the currently selected decoration.\n */\n public set selectedDecoration(decoration: ISelectedDecoration | undefined) {\n this._selectedDecoration = decoration;\n }\n\n /**\n * Updates the search results with a new set of results.\n * @param results The new search results.\n * @param maxResults The maximum number of results to track.\n */\n public updateResults(results: ISearchResult[], maxResults: number): void {\n this._searchResults = results.slice(0, maxResults);\n }\n\n /**\n * Clears all search results.\n */\n public clearResults(): void {\n this._searchResults = [];\n }\n\n /**\n * Clears the selected decoration.\n */\n public clearSelectedDecoration(): void {\n if (this._selectedDecoration) {\n this._selectedDecoration.dispose();\n this._selectedDecoration = undefined;\n }\n }\n\n /**\n * Finds the index of a result in the current results array.\n * @param result The result to find.\n * @returns The index of the result, or -1 if not found.\n */\n public findResultIndex(result: ISearchResult): number {\n for (let i = 0; i < this._searchResults.length; i++) {\n const match = this._searchResults[i];\n if (match.row === result.row && match.col === result.col && match.size === result.size) {\n return i;\n }\n }\n return -1;\n }\n\n /**\n * Fires a result change event with the current state.\n * @param hasDecorations Whether decorations are enabled.\n */\n public fireResultsChanged(hasDecorations: boolean): void {\n if (!hasDecorations) {\n return;\n }\n\n let resultIndex = -1;\n if (this._selectedDecoration) {\n resultIndex = this.findResultIndex(this._selectedDecoration.match);\n }\n\n this._onDidChangeResults.fire({\n resultIndex,\n resultCount: this._searchResults.length\n });\n }\n\n /**\n * Resets all state.\n */\n public reset(): void {\n this.clearSelectedDecoration();\n this.clearResults();\n }\n}\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ISearchOptions } from '@xterm/addon-search';\n\n/**\n * Manages search state including cached search terms, options tracking, and validation.\n * This class provides a centralized way to handle search state consistency and option changes.\n */\nexport class SearchState {\n private _cachedSearchTerm: string | undefined;\n private _lastSearchOptions: ISearchOptions | undefined;\n\n /**\n * Gets the currently cached search term.\n */\n public get cachedSearchTerm(): string | undefined {\n return this._cachedSearchTerm;\n }\n\n /**\n * Sets the cached search term.\n */\n public set cachedSearchTerm(term: string | undefined) {\n this._cachedSearchTerm = term;\n }\n\n /**\n * Gets the last search options used.\n */\n public get lastSearchOptions(): ISearchOptions | undefined {\n return this._lastSearchOptions;\n }\n\n /**\n * Sets the last search options used.\n */\n public set lastSearchOptions(options: ISearchOptions | undefined) {\n this._lastSearchOptions = options;\n }\n\n /**\n * Validates a search term to ensure it's not empty or invalid.\n * @param term The search term to validate.\n * @returns true if the term is valid for searching.\n */\n public isValidSearchTerm(term: string): boolean {\n return !!(term && term.length > 0);\n }\n\n /**\n * Determines if search options have changed compared to the last search.\n * @param newOptions The new search options to compare.\n * @returns true if the options have changed.\n */\n public didOptionsChange(newOptions?: ISearchOptions): boolean {\n if (!this._lastSearchOptions) {\n return true;\n }\n if (!newOptions) {\n return false;\n }\n if (this._lastSearchOptions.caseSensitive !== newOptions.caseSensitive) {\n return true;\n }\n if (this._lastSearchOptions.regex !== newOptions.regex) {\n return true;\n }\n if (this._lastSearchOptions.wholeWord !== newOptions.wholeWord) {\n return true;\n }\n return false;\n }\n\n /**\n * Determines if a new search should trigger highlighting updates.\n * @param term The search term.\n * @param options The search options.\n * @returns true if highlighting should be updated.\n */\n public shouldUpdateHighlighting(term: string, options?: ISearchOptions): boolean {\n if (!options?.decorations) {\n return false;\n }\n return this._cachedSearchTerm === undefined ||\n term !== this._cachedSearchTerm ||\n this.didOptionsChange(options);\n }\n\n /**\n * Clears the cached search term.\n */\n public clearCachedTerm(): void {\n this._cachedSearchTerm = undefined;\n }\n\n /**\n * Resets all state.\n */\n public reset(): void {\n this._cachedSearchTerm = undefined;\n this._lastSearchOptions = undefined;\n }\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';\nimport type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';\nimport { Emitter, type IEvent } from 'common/Event';\nimport { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';\nimport { disposableTimeout } from 'common/Async';\nimport { SearchLineCache } from './SearchLineCache';\nimport { SearchState } from './SearchState';\nimport { SearchEngine, type ISearchResult } from './SearchEngine';\nimport { DecorationManager } from './DecorationManager';\nimport { SearchResultTracker } from './SearchResultTracker';\n\ninterface IInternalSearchOptions {\n noScroll: boolean;\n}\n\n/**\n * Configuration constants for the search addon functionality.\n */\nconst enum Constants {\n /**\n * Default maximum number of search results to highlight simultaneously. This limit prevents\n * performance degradation when searching for very common terms that would result in excessive\n * highlighting decorations.\n */\n DEFAULT_HIGHLIGHT_LIMIT = 1000\n}\n\nexport class SearchAddon extends Disposable implements ITerminalAddon, ISearchApi {\n private _terminal: Terminal | undefined;\n private _highlightLimit: number;\n private _highlightTimeout = this._register(new MutableDisposable());\n private _lineCache = this._register(new MutableDisposable());\n\n // Component instances\n private _state = new SearchState();\n private _engine: SearchEngine | undefined;\n private _decorationManager: DecorationManager | undefined;\n private _resultTracker = this._register(new SearchResultTracker());\n\n private readonly _onAfterSearch = this._register(new Emitter());\n public readonly onAfterSearch = this._onAfterSearch.event;\n private readonly _onBeforeSearch = this._register(new Emitter());\n public readonly onBeforeSearch = this._onBeforeSearch.event;\n\n public get onDidChangeResults(): IEvent {\n return this._resultTracker.onDidChangeResults;\n }\n\n constructor(options?: Partial) {\n super();\n\n this._highlightLimit = options?.highlightLimit ?? Constants.DEFAULT_HIGHLIGHT_LIMIT;\n }\n\n public activate(terminal: Terminal): void {\n this._terminal = terminal;\n this._lineCache.value = new SearchLineCache(terminal);\n this._engine = new SearchEngine(terminal, this._lineCache.value);\n this._decorationManager = new DecorationManager(terminal);\n this._register(this._terminal.onWriteParsed(() => this._updateMatches()));\n this._register(this._terminal.onResize(() => this._updateMatches()));\n this._register(toDisposable(() => this.clearDecorations()));\n }\n\n private _updateMatches(): void {\n this._highlightTimeout.clear();\n if (this._state.cachedSearchTerm && this._state.lastSearchOptions?.decorations) {\n this._highlightTimeout.value = disposableTimeout(() => {\n const term = this._state.cachedSearchTerm;\n this._state.clearCachedTerm();\n this.findPrevious(term!, { ...this._state.lastSearchOptions, incremental: true }, { noScroll: true });\n }, 200);\n }\n }\n\n public clearDecorations(retainCachedSearchTerm?: boolean): void {\n this._resultTracker.clearSelectedDecoration();\n this._decorationManager?.clearHighlightDecorations();\n this._resultTracker.clearResults();\n if (!retainCachedSearchTerm) {\n this._state.clearCachedTerm();\n }\n }\n\n public clearActiveDecoration(): void {\n this._resultTracker.clearSelectedDecoration();\n }\n\n /**\n * Find the next instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options.\n * @returns Whether a result was found.\n */\n public findNext(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n\n this._onBeforeSearch.fire();\n\n this._state.lastSearchOptions = searchOptions;\n\n if (this._state.shouldUpdateHighlighting(term, searchOptions)) {\n this._highlightAllMatches(term, searchOptions!);\n }\n\n const found = this._findNextAndSelect(term, searchOptions, internalSearchOptions);\n this._fireResults(searchOptions);\n this._state.cachedSearchTerm = term;\n\n this._onAfterSearch.fire();\n\n return found;\n }\n\n private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {\n if (!this._terminal || !this._engine || !this._decorationManager) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n if (!this._state.isValidSearchTerm(term)) {\n this.clearDecorations();\n return;\n }\n\n // new search, clear out the old decorations\n this.clearDecorations(true);\n\n const results: ISearchResult[] = [];\n let prevResult: ISearchResult | undefined = undefined;\n let result = this._engine.find(term, 0, 0, searchOptions);\n\n while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {\n if (results.length >= this._highlightLimit) {\n break;\n }\n prevResult = result;\n results.push(prevResult);\n const cols = this._terminal.cols;\n let nextCol = prevResult.col + prevResult.size;\n let nextRow = prevResult.row;\n if (nextCol >= cols) {\n nextRow += Math.floor(nextCol / cols);\n nextCol = nextCol % cols;\n }\n result = this._engine.find(term, nextRow, nextCol, searchOptions);\n }\n\n this._resultTracker.updateResults(results, this._highlightLimit);\n if (searchOptions.decorations) {\n this._decorationManager.createHighlightDecorations(results, searchOptions.decorations);\n }\n }\n\n private _findNextAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n return false;\n }\n if (!this._state.isValidSearchTerm(term)) {\n this._terminal.clearSelection();\n this.clearDecorations();\n return false;\n }\n\n const result = this._engine.findNextWithSelection(term, searchOptions, this._state.cachedSearchTerm);\n return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);\n }\n\n /**\n * Find the previous instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options.\n * @returns Whether a result was found.\n */\n public findPrevious(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n\n this._onBeforeSearch.fire();\n\n this._state.lastSearchOptions = searchOptions;\n\n if (this._state.shouldUpdateHighlighting(term, searchOptions)) {\n this._highlightAllMatches(term, searchOptions!);\n }\n\n const found = this._findPreviousAndSelect(term, searchOptions, internalSearchOptions);\n this._fireResults(searchOptions);\n this._state.cachedSearchTerm = term;\n\n this._onAfterSearch.fire();\n\n return found;\n }\n\n private _fireResults(searchOptions?: ISearchOptions): void {\n this._resultTracker.fireResultsChanged(!!searchOptions?.decorations);\n }\n\n private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n return false;\n }\n if (!this._state.isValidSearchTerm(term)) {\n this._terminal.clearSelection();\n this.clearDecorations();\n return false;\n }\n\n const result = this._engine.findPreviousWithSelection(term, searchOptions, this._state.cachedSearchTerm);\n return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);\n }\n\n /**\n * Selects and scrolls to a result.\n * @param result The result to select.\n * @returns Whether a result was selected.\n */\n private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {\n if (!this._terminal || !this._decorationManager) {\n return false;\n }\n\n this._resultTracker.clearSelectedDecoration();\n if (!result) {\n this._terminal.clearSelection();\n return false;\n }\n\n this._terminal.select(result.col, result.row, result.size);\n if (options) {\n const activeDecoration = this._decorationManager.createActiveDecoration(result, options);\n if (activeDecoration) {\n this._resultTracker.selectedDecoration = activeDecoration;\n }\n }\n\n if (!noScroll) {\n // If it is not in the viewport then we scroll else it just gets selected\n if (result.row >= (this._terminal.buffer.active.viewportY + this._terminal.rows) || result.row < this._terminal.buffer.active.viewportY) {\n let scroll = result.row - this._terminal.buffer.active.viewportY;\n scroll -= Math.floor(this._terminal.rows / 2);\n this._terminal.scrollLines(scroll);\n }\n }\n return true;\n }\n}\n"],"names":["root","factory","exports","module","define","amd","globalThis","millis","Promise","resolve","setTimeout","handler","timeout","store","timer","disposable","dispose","Lifecycle_1","toDisposable","clearTimeout","add","__webpack_require__","constructor","this","_token","_isDisposed","cancel","cancelAndSet","runner","Error","setIfNotSet","_isScheduled","set","queueMicrotask","_disposable","undefined","interval","context","handle","setInterval","clearInterval","EventUtils","_listeners","_disposed","event","_event","listener","thisArgs","disposables","entry","fn","slice","push","result","idx","indexOf","splice","Array","isArray","fire","length","call","listeners","i","len","forward","from","to","e","map","any","events","DisposableStore","runAndSubscribe","initial","arg","d","_disposables","Set","isDisposed","o","clear","Disposable","_store","_register","None","Object","freeze","value","_value","DecorationManager","_terminal","super","_highlightDecorations","_highlightedLines","clearHighlightDecorations","createHighlightDecorations","results","options","match","decorations","_createResultDecorations","decoration","_storeDecoration","createActiveDecoration","marker","line","_applyStyles","element","borderColor","isActiveResult","classList","contains","style","outline","decorationRanges","currentCol","col","remainingSize","size","markerOffset","buffer","active","baseY","cursorY","row","amountThisRow","Math","min","cols","range","registerMarker","registerDecoration","x","width","layer","backgroundColor","activeMatchBackground","matchBackground","overviewRulerOptions","has","color","activeMatchColorOverviewRuler","matchOverviewRuler","position","onRender","activeMatchBorder","matchBorder","onDispose","_lineCache","find","term","startRow","startCol","searchOptions","clearSelection","initLinesCache","searchPosition","_findInLine","y","rows","_isRowCoveredByEarlierSearch","findNextWithSelection","cachedSearchTerm","prevSelectedPos","getSelectionPosition","end","start","findPreviousWithSelection","isReverseSearch","max","_isWholeWord","searchIndex","includes","_satisfiesWholeWord","wholeWord","getLine","isWrapped","cache","getLineFromCache","translateBufferLineToStringWithWrap","setLineInCache","stringLine","offsets","offset","_bufferColsToStringOffset","searchTerm","searchStringLine","regex","caseSensitive","toLowerCase","resultIndex","searchRegex","RegExp","foundTerm","exec","matchIndex","lastIndex","lastIndexOf","startRowOffset","endRowOffset","startColOffset","endColOffset","startColIndex","_stringLengthToBufferSize","cell","getCell","char","getChars","nextCell","getWidth","lineOffsets","rowsBack","floor","colsInRow","getCode","Async_1","SearchLineCache","_linesCacheTimeout","MutableDisposable","_linesCacheDisposables","_lastAccessTimestamp","_destroyLinesCache","_linesCache","combinedDisposable","onLineFeed","onCursorMove","onResize","Date","now","_scheduleLinesCacheTimeout","delay","disposableTimeout","elapsed","lineIndex","trimRight","strings","bufferLength","nextLine","lineWrapsToNext","string","translateToString","lastCell","join","Event_1","SearchResultTracker","_searchResults","_onDidChangeResults","Emitter","onDidChangeResults","searchResults","selectedDecoration","_selectedDecoration","updateResults","maxResults","clearResults","clearSelectedDecoration","findResultIndex","fireResultsChanged","hasDecorations","resultCount","reset","_cachedSearchTerm","lastSearchOptions","_lastSearchOptions","isValidSearchTerm","didOptionsChange","newOptions","shouldUpdateHighlighting","clearCachedTerm","__webpack_module_cache__","moduleId","cachedModule","__webpack_modules__","SearchLineCache_1","SearchState_1","SearchEngine_1","DecorationManager_1","SearchResultTracker_1","SearchAddon","_resultTracker","_highlightTimeout","_state","SearchState","_onAfterSearch","onAfterSearch","_onBeforeSearch","onBeforeSearch","_highlightLimit","highlightLimit","activate","terminal","_engine","SearchEngine","_decorationManager","onWriteParsed","_updateMatches","clearDecorations","findPrevious","incremental","noScroll","retainCachedSearchTerm","clearActiveDecoration","findNext","internalSearchOptions","_highlightAllMatches","found","_findNextAndSelect","_fireResults","prevResult","nextCol","nextRow","_selectResult","_findPreviousAndSelect","select","activeDecoration","viewportY","scroll","scrollLines"],"sourceRoot":""}
+\ No newline at end of file
+diff --git a/lib/addon-search.mjs b/lib/addon-search.mjs
+index 5cf231a96b56284705711faebe6b0c492f133546..f2b8b804ac733d737a9bff22b4e1d24778a807c8 100644
+--- a/lib/addon-search.mjs
++++ b/lib/addon-search.mjs
+@@ -14,5 +14,5 @@
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+-function _(c){return{dispose:c}}function D(c){if(!c)return c;if(Array.isArray(c)){for(let i of c)i.dispose();return[]}return c.dispose(),c}function E(...c){return _(()=>D(c))}var S=class{constructor(){this._disposables=new Set;this._isDisposed=!1}get isDisposed(){return this._isDisposed}add(i){return this._isDisposed?i.dispose():this._disposables.add(i),i}dispose(){if(!this._isDisposed){this._isDisposed=!0;for(let i of this._disposables)i.dispose();this._disposables.clear()}}clear(){for(let i of this._disposables)i.dispose();this._disposables.clear()}},b=class{constructor(){this._store=new S}dispose(){this._store.dispose()}_register(i){return this._store.add(i)}};b.None=Object.freeze({dispose(){}});var v=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(i){this._isDisposed||i===this._value||(this._value?.dispose(),this._value=i)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,this._value?.dispose(),this._value=void 0}};var I=class{constructor(){this._listeners=[];this._disposed=!1}get event(){return this._event?this._event:(this._event=(i,e,t)=>{if(this._disposed)return _(()=>{});let r={fn:i,thisArgs:e};this._listeners=this._listeners.slice(),this._listeners.push(r);let s=_(()=>{let n=this._listeners.indexOf(r);n!==-1&&(this._listeners=this._listeners.slice(),this._listeners.splice(n,1))});return t&&(Array.isArray(t)?t.push(s):t.add(s)),s},this._event)}fire(i){if(this._disposed||!this._listeners.length)return;if(this._listeners.length===1){this._listeners[0].fn.call(this._listeners[0].thisArgs,i);return}let e=this._listeners;for(let t=0,r=e.length;t{function c(s,n){return s(a=>n.fire(a))}r.forward=c;function i(s,n){return(a,o,l)=>s(h=>a.call(o,n(h)),void 0,l)}r.map=i;function e(...s){return(n,a,o)=>{let l=new S;for(let h of s)l.add(h(p=>n.call(a,p)));return o&&(Array.isArray(o)?o.push(l):o.add(l)),l}}r.any=e;function t(s,n,a){return n(a),s(o=>n(o))}r.runAndSubscribe=t})(W||={});function T(c,i=0,e){let t=setTimeout(()=>{c(),e&&r.dispose()},i),r=_(()=>{clearTimeout(t)});return e?.add(r),r}var C=class extends b{constructor(e){super();this._terminal=e;this._linesCacheTimeout=this._register(new v);this._linesCacheDisposables=this._register(new v);this._lastAccessTimestamp=0;this._register(_(()=>this._destroyLinesCache()))}initLinesCache(){this._linesCache||(this._linesCache=new Array(this._terminal.buffer.active.length),this._linesCacheDisposables.value=E(this._terminal.onLineFeed(()=>this._destroyLinesCache()),this._terminal.onCursorMove(()=>this._destroyLinesCache()),this._terminal.onResize(()=>this._destroyLinesCache()))),this._lastAccessTimestamp=Date.now(),this._linesCacheTimeout.value||this._scheduleLinesCacheTimeout(15e3)}_destroyLinesCache(){this._linesCache=void 0,this._lastAccessTimestamp=0,this._linesCacheDisposables.clear(),this._linesCacheTimeout.clear()}_scheduleLinesCacheTimeout(e){this._linesCacheTimeout.value=T(()=>{if(!this._linesCache)return;let r=Date.now()-this._lastAccessTimestamp;if(r>=15e3){this._destroyLinesCache();return}this._scheduleLinesCacheTimeout(15e3-r)},e)}getLineFromCache(e){return this._linesCache?.[e]}setLineInCache(e,t){this._linesCache&&(this._linesCache[e]=t)}translateBufferLineToStringWithWrap(e,t){let r=[],s=[0],n=this._terminal.buffer.active.getLine(e);for(;n;){let a=this._terminal.buffer.active.getLine(e+1),o=a?a.isWrapped:!1,l=n.translateToString(!o&&t);if(o&&a){let h=n.getCell(n.length-1);h&&h.getCode()===0&&h.getWidth()===1&&a.getCell(0)?.getWidth()===2&&(l=l.slice(0,-1))}if(r.push(l),o)s.push(s[s.length-1]+l.length);else break;e++,n=a}return[r.join(""),s]}};var x=class{get cachedSearchTerm(){return this._cachedSearchTerm}set cachedSearchTerm(i){this._cachedSearchTerm=i}get lastSearchOptions(){return this._lastSearchOptions}set lastSearchOptions(i){this._lastSearchOptions=i}isValidSearchTerm(i){return!!(i&&i.length>0)}didOptionsChange(i){return this._lastSearchOptions?i?this._lastSearchOptions.caseSensitive!==i.caseSensitive||this._lastSearchOptions.regex!==i.regex||this._lastSearchOptions.wholeWord!==i.wholeWord:!1:!0}shouldUpdateHighlighting(i,e){return e?.decorations?this._cachedSearchTerm===void 0||i!==this._cachedSearchTerm||this.didOptionsChange(e):!1}clearCachedTerm(){this._cachedSearchTerm=void 0}reset(){this._cachedSearchTerm=void 0,this._lastSearchOptions=void 0}};var R=class{constructor(i,e){this._terminal=i;this._lineCache=e}find(i,e,t,r){if(!i||i.length===0){this._terminal.clearSelection();return}if(t>=this._terminal.cols)throw new Error(`Invalid col: ${t} to search in terminal of ${this._terminal.cols} cols`);this._lineCache.initLinesCache();let s={startRow:e,startCol:t},n=this._findInLine(i,s,r);if(!n)for(let a=e+1;a=0&&(o.startRow=h,l=this._findInLine(i,o,e,a),!l);h--);}if(!l&&s!==this._terminal.buffer.active.baseY+this._terminal.rows-1)for(let h=this._terminal.buffer.active.baseY+this._terminal.rows-1;h>=s&&(o.startRow=h,l=this._findInLine(i,o,e,a),!l);h--);return l}_isWholeWord(i,e,t){return(i===0||" ~!@#$%^&*()+`-=[]{}|\\;:\"',./<>?".includes(e[i-1]))&&(i+t.length===e.length||" ~!@#$%^&*()+`-=[]{}|\\;:\"',./<>?".includes(e[i+t.length]))}_findInLine(i,e,t={},r=!1){let s=e.startRow,n=e.startCol;if(this._terminal.buffer.active.getLine(s)?.isWrapped){if(r){e.startCol+=this._terminal.cols;return}return e.startRow--,e.startCol+=this._terminal.cols,this._findInLine(i,e,t)}let o=this._lineCache.getLineFromCache(s);o||(o=this._lineCache.translateBufferLineToStringWithWrap(s,!0),this._lineCache.setLineInCache(s,o));let[l,h]=o,p=this._bufferColsToStringOffset(s,n),m=i,g=l;t.regex||(m=t.caseSensitive?i:i.toLowerCase(),g=t.caseSensitive?l:l.toLowerCase());let f=-1;if(t.regex){let u=RegExp(m,t.caseSensitive?"g":"gi"),d;if(r)for(;d=u.exec(g.slice(0,p));)f=u.lastIndex-d[0].length,i=d[0],u.lastIndex-=i.length-1;else d=u.exec(g.slice(p)),d&&d[0].length>0&&(f=p+(u.lastIndex-d[0].length),i=d[0])}else r?p-m.length>=0&&(f=g.lastIndexOf(m,p-m.length)):f=g.indexOf(m,p);if(f>=0){if(t.wholeWord&&!this._isWholeWord(f,g,i))return;let u=0;for(;u=h[u+1];)u++;let d=u;for(;d=h[d+1];)d++;let O=f-h[u],k=f+i.length-h[d],y=this._stringLengthToBufferSize(s+u,O),M=this._stringLengthToBufferSize(s+d,k)-y+this._terminal.cols*(d-u);return{term:i,col:y,row:s+u,size:M}}}_stringLengthToBufferSize(i,e){let t=this._terminal.buffer.active.getLine(i);if(!t)return 0;for(let r=0;r1&&(e-=n.length-1);let a=t.getCell(r+1);a&&a.getWidth()===0&&e++}return e}_bufferColsToStringOffset(i,e){let t=i,r=0,s=this._terminal.buffer.active.getLine(t);for(;e>0&&s;){for(let n=0;nthis.clearHighlightDecorations()))}createHighlightDecorations(e,t){this.clearHighlightDecorations();for(let r of e){let s=this._createResultDecorations(r,t,!1);if(s)for(let n of s)this._storeDecoration(n,r)}}createActiveDecoration(e,t){let r=this._createResultDecorations(e,t,!0);if(r)return{decorations:r,match:e,dispose(){D(r)}}}clearHighlightDecorations(){D(this._highlightDecorations),this._highlightDecorations=[],this._highlightedLines.clear()}_storeDecoration(e,t){this._highlightedLines.add(e.marker.line),this._highlightDecorations.push({decoration:e,match:t,dispose(){e.dispose()}})}_applyStyles(e,t,r){e.classList.contains("xterm-find-result-decoration")||(e.classList.add("xterm-find-result-decoration"),t&&(e.style.outline=`1px solid ${t}`)),r&&e.classList.add("xterm-find-active-result-decoration")}_createResultDecorations(e,t,r){let s=[],n=e.col,a=e.size,o=-this._terminal.buffer.active.baseY-this._terminal.buffer.active.cursorY+e.row;for(;a>0;){let h=Math.min(this._terminal.cols-n,a);s.push([o,n,h]),n=0,a-=h,o++}let l=[];for(let h of s){let p=this._terminal.registerMarker(h[0]),m=this._terminal.registerDecoration({marker:p,x:h[1],width:h[2],layer:r?"top":"bottom",backgroundColor:r?t.activeMatchBackground:t.matchBackground,overviewRulerOptions:this._highlightedLines.has(p.line)?void 0:{color:r?t.activeMatchColorOverviewRuler:t.matchOverviewRuler,position:"center"}});if(m){let g=[];g.push(p),g.push(m.onRender(f=>this._applyStyles(f,r?t.activeMatchBorder:t.matchBorder,!1))),g.push(m.onDispose(()=>D(g))),l.push(m)}}return l.length===0?void 0:l}};var L=class extends b{constructor(){super(...arguments);this._searchResults=[];this._onDidChangeResults=this._register(new I)}get onDidChangeResults(){return this._onDidChangeResults.event}get searchResults(){return this._searchResults}get selectedDecoration(){return this._selectedDecoration}set selectedDecoration(e){this._selectedDecoration=e}updateResults(e,t){this._searchResults=e.slice(0,t)}clearResults(){this._searchResults=[]}clearSelectedDecoration(){this._selectedDecoration&&(this._selectedDecoration.dispose(),this._selectedDecoration=void 0)}findResultIndex(e){for(let t=0;tthis._updateMatches())),this._register(this._terminal.onResize(()=>this._updateMatches())),this._register(_(()=>this.clearDecorations()))}_updateMatches(){this._highlightTimeout.clear(),this._state.cachedSearchTerm&&this._state.lastSearchOptions?.decorations&&(this._highlightTimeout.value=T(()=>{let e=this._state.cachedSearchTerm;this._state.clearCachedTerm(),this.findPrevious(e,{...this._state.lastSearchOptions,incremental:!0},{noScroll:!0})},200))}clearDecorations(e){this._resultTracker.clearSelectedDecoration(),this._decorationManager?.clearHighlightDecorations(),this._resultTracker.clearResults(),e||this._state.clearCachedTerm()}clearActiveDecoration(){this._resultTracker.clearSelectedDecoration()}findNext(e,t,r){if(!this._terminal||!this._engine)throw new Error("Cannot use addon until it has been loaded");this._onBeforeSearch.fire(),this._state.lastSearchOptions=t,this._state.shouldUpdateHighlighting(e,t)&&this._highlightAllMatches(e,t);let s=this._findNextAndSelect(e,t,r);return this._fireResults(t),this._state.cachedSearchTerm=e,this._onAfterSearch.fire(),s}_highlightAllMatches(e,t){if(!this._terminal||!this._engine||!this._decorationManager)throw new Error("Cannot use addon until it has been loaded");if(!this._state.isValidSearchTerm(e)){this.clearDecorations();return}this.clearDecorations(!0);let r=[],s,n=this._engine.find(e,0,0,t);for(;n&&(s?.row!==n.row||s?.col!==n.col)&&!(r.length>=this._highlightLimit);){s=n,r.push(s);let a=this._terminal.cols,o=s.col+s.size,l=s.row;o>=a&&(l+=Math.floor(o/a),o=o%a),n=this._engine.find(e,l,o,t)}this._resultTracker.updateResults(r,this._highlightLimit),t.decorations&&this._decorationManager.createHighlightDecorations(r,t.decorations)}_findNextAndSelect(e,t,r){if(!this._terminal||!this._engine)return!1;if(!this._state.isValidSearchTerm(e))return this._terminal.clearSelection(),this.clearDecorations(),!1;let s=this._engine.findNextWithSelection(e,t,this._state.cachedSearchTerm);return this._selectResult(s,t?.decorations,r?.noScroll)}findPrevious(e,t,r){if(!this._terminal||!this._engine)throw new Error("Cannot use addon until it has been loaded");this._onBeforeSearch.fire(),this._state.lastSearchOptions=t,this._state.shouldUpdateHighlighting(e,t)&&this._highlightAllMatches(e,t);let s=this._findPreviousAndSelect(e,t,r);return this._fireResults(t),this._state.cachedSearchTerm=e,this._onAfterSearch.fire(),s}_fireResults(e){this._resultTracker.fireResultsChanged(!!e?.decorations)}_findPreviousAndSelect(e,t,r){if(!this._terminal||!this._engine)return!1;if(!this._state.isValidSearchTerm(e))return this._terminal.clearSelection(),this.clearDecorations(),!1;let s=this._engine.findPreviousWithSelection(e,t,this._state.cachedSearchTerm);return this._selectResult(s,t?.decorations,r?.noScroll)}_selectResult(e,t,r){if(!this._terminal||!this._decorationManager)return!1;if(this._resultTracker.clearSelectedDecoration(),!e)return this._terminal.clearSelection(),!1;if(this._terminal.select(e.col,e.row,e.size),t){let s=this._decorationManager.createActiveDecoration(e,t);s&&(this._resultTracker.selectedDecoration=s)}if(!r&&(e.row>=this._terminal.buffer.active.viewportY+this._terminal.rows||e.rowD(d))}var S=class{constructor(){this._disposables=new Set;this._isDisposed=!1}get isDisposed(){return this._isDisposed}add(i){return this._isDisposed?i.dispose():this._disposables.add(i),i}dispose(){if(!this._isDisposed){this._isDisposed=!0;for(let i of this._disposables)i.dispose();this._disposables.clear()}}clear(){for(let i of this._disposables)i.dispose();this._disposables.clear()}},g=class{constructor(){this._store=new S}dispose(){this._store.dispose()}_register(i){return this._store.add(i)}};g.None=Object.freeze({dispose(){}});var v=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(i){this._isDisposed||i===this._value||(this._value?.dispose(),this._value=i)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,this._value?.dispose(),this._value=void 0}};var I=class{constructor(){this._listeners=[];this._disposed=!1}get event(){return this._event?this._event:(this._event=(i,e,t)=>{if(this._disposed)return m(()=>{});let s={fn:i,thisArgs:e};this._listeners=this._listeners.slice(),this._listeners.push(s);let r=m(()=>{let l=this._listeners.indexOf(s);l!==-1&&(this._listeners=this._listeners.slice(),this._listeners.splice(l,1))});return t&&(Array.isArray(t)?t.push(r):t.add(r)),r},this._event)}fire(i){if(this._disposed||!this._listeners.length)return;if(this._listeners.length===1){this._listeners[0].fn.call(this._listeners[0].thisArgs,i);return}let e=this._listeners;for(let t=0,s=e.length;t{function d(r,l){return r(o=>l.fire(o))}s.forward=d;function i(r,l){return(o,n,a)=>r(c=>o.call(n,l(c)),void 0,a)}s.map=i;function e(...r){return(l,o,n)=>{let a=new S;for(let c of r)a.add(c(u=>l.call(o,u)));return n&&(Array.isArray(n)?n.push(a):n.add(a)),a}}s.any=e;function t(r,l,o){return l(o),r(n=>l(n))}s.runAndSubscribe=t})(k||={});function T(d,i=0,e){let t=setTimeout(()=>{d(),e&&s.dispose()},i),s=m(()=>{clearTimeout(t)});return e?.add(s),s}var C=class extends g{constructor(e){super();this._terminal=e;this._linesCacheTimeout=this._register(new v);this._linesCacheDisposables=this._register(new v);this._lastAccessTimestamp=0;this._register(m(()=>this._destroyLinesCache()))}initLinesCache(){this._linesCache||(this._linesCache=new Array(this._terminal.buffer.active.length),this._linesCacheDisposables.value=E(this._terminal.onLineFeed(()=>this._destroyLinesCache()),this._terminal.onCursorMove(()=>this._destroyLinesCache()),this._terminal.onResize(()=>this._destroyLinesCache()))),this._lastAccessTimestamp=Date.now(),this._linesCacheTimeout.value||this._scheduleLinesCacheTimeout(15e3)}_destroyLinesCache(){this._linesCache=void 0,this._lastAccessTimestamp=0,this._linesCacheDisposables.clear(),this._linesCacheTimeout.clear()}_scheduleLinesCacheTimeout(e){this._linesCacheTimeout.value=T(()=>{if(!this._linesCache)return;let s=Date.now()-this._lastAccessTimestamp;if(s>=15e3){this._destroyLinesCache();return}this._scheduleLinesCacheTimeout(15e3-s)},e)}getLineFromCache(e){return this._linesCache?.[e]}setLineInCache(e,t){this._linesCache&&(this._linesCache[e]=t)}translateBufferLineToStringWithWrap(e,t){let s=[],r=[0],l=this._terminal.buffer.active.length,o=this._terminal.buffer.active.getLine(e);for(;o;){let n=e+10)}didOptionsChange(i){return this._lastSearchOptions?i?this._lastSearchOptions.caseSensitive!==i.caseSensitive||this._lastSearchOptions.regex!==i.regex||this._lastSearchOptions.wholeWord!==i.wholeWord:!1:!0}shouldUpdateHighlighting(i,e){return e?.decorations?this._cachedSearchTerm===void 0||i!==this._cachedSearchTerm||this.didOptionsChange(e):!1}clearCachedTerm(){this._cachedSearchTerm=void 0}reset(){this._cachedSearchTerm=void 0,this._lastSearchOptions=void 0}};var w=class{constructor(i,e){this._terminal=i;this._lineCache=e}find(i,e,t,s){if(!i||i.length===0){this._terminal.clearSelection();return}if(t>=this._terminal.cols)throw new Error(`Invalid col: ${t} to search in terminal of ${this._terminal.cols} cols`);this._lineCache.initLinesCache();let r={startRow:e,startCol:t},l=this._findInLine(i,r,s);if(!l)for(let o=e+1;o0&&this._isRowCoveredByEarlierSearch(a))&&(o.startRow=a,o.startCol=0,n=this._findInLine(i,o,e),n));a++);return!n&&s&&(o.startRow=s.start.y,o.startCol=0,n=this._findInLine(i,o,e)),n}findPreviousWithSelection(i,e,t){if(!i||i.length===0){this._terminal.clearSelection();return}let s=this._terminal.getSelectionPosition();this._terminal.clearSelection();let r=this._terminal.buffer.active.baseY+this._terminal.rows-1,l=this._terminal.cols,o=!0;this._lineCache.initLinesCache();let n={startRow:r,startCol:l},a;if(s&&(n.startRow=r=s.start.y,n.startCol=s.start.x,t!==i&&(a=this._findInLine(i,n,e,!1),a||(n.startRow=r=s.end.y,n.startCol=s.end.x))),a??=this._findInLine(i,n,e,o),!a){n.startCol=Math.max(n.startCol,this._terminal.cols);for(let c=r-1;c>=0&&(n.startRow=c,a=this._findInLine(i,n,e,o),!a);c--);}if(!a&&r!==this._terminal.buffer.active.baseY+this._terminal.rows-1)for(let c=this._terminal.buffer.active.baseY+this._terminal.rows-1;c>=r&&(n.startRow=c,a=this._findInLine(i,n,e,o),!a);c--);return a}_isWholeWord(i,e,t){return(i===0||" ~!@#$%^&*()+`-=[]{}|\\;:\"',./<>?".includes(e[i-1]))&&(i+t.length===e.length||" ~!@#$%^&*()+`-=[]{}|\\;:\"',./<>?".includes(e[i+t.length]))}_satisfiesWholeWord(i,e,t,s){return!s.wholeWord||this._isWholeWord(i,e,t)}_isRowCoveredByEarlierSearch(i){return this._terminal.buffer.active.getLine(i)?.isWrapped===!0}_findInLine(i,e,t={},s=!1){if(s){if(e.startRow>0&&this._terminal.buffer.active.getLine(e.startRow)?.isWrapped){e.startCol+=this._terminal.cols;return}}else for(;e.startRow>0&&this._terminal.buffer.active.getLine(e.startRow)?.isWrapped;)e.startRow--,e.startCol+=this._terminal.cols;let r=e.startRow,l=e.startCol,o=this._lineCache.getLineFromCache(r);o||(o=this._lineCache.translateBufferLineToStringWithWrap(r,!0),this._lineCache.setLineInCache(r,o));let[n,a]=o,c=this._bufferColsToStringOffset(r,l,a),u=i,f=n;t.regex||(u=t.caseSensitive?i:i.toLowerCase(),f=t.caseSensitive?n:n.toLowerCase());let _=-1;if(t.regex){let h=RegExp(u,t.caseSensitive?"g":"gi"),p;if(s)for(;p=h.exec(f.slice(0,c));){let b=h.lastIndex-p[0].length;p[0].length>0&&this._satisfiesWholeWord(b,f,p[0],t)&&(_=b,i=p[0]),h.lastIndex=b+1}else for(h.lastIndex=c;p=h.exec(f);){let b=h.lastIndex-p[0].length;if(p[0].length>0&&this._satisfiesWholeWord(b,f,p[0],t)){_=b,i=p[0];break}h.lastIndex=b+1}}else if(s){let h=c-u.length>=0?f.lastIndexOf(u,c-u.length):-1;for(;h>=0&&!this._satisfiesWholeWord(h,f,u,t);)h=h>0?f.lastIndexOf(u,h-1):-1;_=h}else{let h=f.indexOf(u,c);for(;h>=0&&!this._satisfiesWholeWord(h,f,u,t);)h=f.indexOf(u,h+1);_=h}if(_>=0){let h=0;for(;h=a[h+1];)h++;let p=h;for(;p=a[p+1];)p++;let b=_-a[h],O=_+i.length-a[p],L=this._stringLengthToBufferSize(r+h,b),W=this._stringLengthToBufferSize(r+p,O)-L+this._terminal.cols*(p-h);return{term:i,col:L,row:r+h,size:W}}}_stringLengthToBufferSize(i,e){let t=this._terminal.buffer.active.getLine(i);if(!t)return 0;for(let s=0;s1&&(e-=l.length-1);let o=t.getCell(s+1);o&&o.getWidth()===0&&e++}return e}_bufferColsToStringOffset(i,e,t){let s=Math.min(Math.floor(e/this._terminal.cols),t.length-1),r=t[s],l=this._terminal.buffer.active.getLine(i+s);if(l){let o=Math.min(e-s*this._terminal.cols,this._terminal.cols);for(let n=0;nthis.clearHighlightDecorations()))}createHighlightDecorations(e,t){this.clearHighlightDecorations();for(let s of e){let r=this._createResultDecorations(s,t,!1);if(r)for(let l of r)this._storeDecoration(l,s)}}createActiveDecoration(e,t){let s=this._createResultDecorations(e,t,!0);if(s)return{decorations:s,match:e,dispose(){D(s)}}}clearHighlightDecorations(){D(this._highlightDecorations),this._highlightDecorations=[],this._highlightedLines.clear()}_storeDecoration(e,t){this._highlightedLines.add(e.marker.line),this._highlightDecorations.push({decoration:e,match:t,dispose(){e.dispose()}})}_applyStyles(e,t,s){e.classList.contains("xterm-find-result-decoration")||(e.classList.add("xterm-find-result-decoration"),t&&(e.style.outline=`1px solid ${t}`)),s&&e.classList.add("xterm-find-active-result-decoration")}_createResultDecorations(e,t,s){let r=[],l=e.col,o=e.size,n=-this._terminal.buffer.active.baseY-this._terminal.buffer.active.cursorY+e.row;for(;o>0;){let c=Math.min(this._terminal.cols-l,o);r.push([n,l,c]),l=0,o-=c,n++}let a=[];for(let c of r){let u=this._terminal.registerMarker(c[0]),f=this._terminal.registerDecoration({marker:u,x:c[1],width:c[2],layer:s?"top":"bottom",backgroundColor:s?t.activeMatchBackground:t.matchBackground,overviewRulerOptions:this._highlightedLines.has(u.line)?void 0:{color:s?t.activeMatchColorOverviewRuler:t.matchOverviewRuler,position:"center"}});if(f){let _=[];_.push(u),_.push(f.onRender(h=>this._applyStyles(h,s?t.activeMatchBorder:t.matchBorder,!1))),_.push(f.onDispose(()=>D(_))),a.push(f)}}return a.length===0?void 0:a}};var y=class extends g{constructor(){super(...arguments);this._searchResults=[];this._onDidChangeResults=this._register(new I)}get onDidChangeResults(){return this._onDidChangeResults.event}get searchResults(){return this._searchResults}get selectedDecoration(){return this._selectedDecoration}set selectedDecoration(e){this._selectedDecoration=e}updateResults(e,t){this._searchResults=e.slice(0,t)}clearResults(){this._searchResults=[]}clearSelectedDecoration(){this._selectedDecoration&&(this._selectedDecoration.dispose(),this._selectedDecoration=void 0)}findResultIndex(e){for(let t=0;tthis._updateMatches())),this._register(this._terminal.onResize(()=>this._updateMatches())),this._register(m(()=>this.clearDecorations()))}_updateMatches(){this._highlightTimeout.clear(),this._state.cachedSearchTerm&&this._state.lastSearchOptions?.decorations&&(this._highlightTimeout.value=T(()=>{let e=this._state.cachedSearchTerm;this._state.clearCachedTerm(),this.findPrevious(e,{...this._state.lastSearchOptions,incremental:!0},{noScroll:!0})},200))}clearDecorations(e){this._resultTracker.clearSelectedDecoration(),this._decorationManager?.clearHighlightDecorations(),this._resultTracker.clearResults(),e||this._state.clearCachedTerm()}clearActiveDecoration(){this._resultTracker.clearSelectedDecoration()}findNext(e,t,s){if(!this._terminal||!this._engine)throw new Error("Cannot use addon until it has been loaded");this._onBeforeSearch.fire(),this._state.lastSearchOptions=t,this._state.shouldUpdateHighlighting(e,t)&&this._highlightAllMatches(e,t);let r=this._findNextAndSelect(e,t,s);return this._fireResults(t),this._state.cachedSearchTerm=e,this._onAfterSearch.fire(),r}_highlightAllMatches(e,t){if(!this._terminal||!this._engine||!this._decorationManager)throw new Error("Cannot use addon until it has been loaded");if(!this._state.isValidSearchTerm(e)){this.clearDecorations();return}this.clearDecorations(!0);let s=[],r,l=this._engine.find(e,0,0,t);for(;l&&(r?.row!==l.row||r?.col!==l.col)&&!(s.length>=this._highlightLimit);){r=l,s.push(r);let o=this._terminal.cols,n=r.col+r.size,a=r.row;n>=o&&(a+=Math.floor(n/o),n=n%o),l=this._engine.find(e,a,n,t)}this._resultTracker.updateResults(s,this._highlightLimit),t.decorations&&this._decorationManager.createHighlightDecorations(s,t.decorations)}_findNextAndSelect(e,t,s){if(!this._terminal||!this._engine)return!1;if(!this._state.isValidSearchTerm(e))return this._terminal.clearSelection(),this.clearDecorations(),!1;let r=this._engine.findNextWithSelection(e,t,this._state.cachedSearchTerm);return this._selectResult(r,t?.decorations,s?.noScroll)}findPrevious(e,t,s){if(!this._terminal||!this._engine)throw new Error("Cannot use addon until it has been loaded");this._onBeforeSearch.fire(),this._state.lastSearchOptions=t,this._state.shouldUpdateHighlighting(e,t)&&this._highlightAllMatches(e,t);let r=this._findPreviousAndSelect(e,t,s);return this._fireResults(t),this._state.cachedSearchTerm=e,this._onAfterSearch.fire(),r}_fireResults(e){this._resultTracker.fireResultsChanged(!!e?.decorations)}_findPreviousAndSelect(e,t,s){if(!this._terminal||!this._engine)return!1;if(!this._state.isValidSearchTerm(e))return this._terminal.clearSelection(),this.clearDecorations(),!1;let r=this._engine.findPreviousWithSelection(e,t,this._state.cachedSearchTerm);return this._selectResult(r,t?.decorations,s?.noScroll)}_selectResult(e,t,s){if(!this._terminal||!this._decorationManager)return!1;if(this._resultTracker.clearSelectedDecoration(),!e)return this._terminal.clearSelection(),!1;if(this._terminal.select(e.col,e.row,e.size),t){let r=this._decorationManager.createActiveDecoration(e,t);r&&(this._resultTracker.selectedDecoration=r)}if(!s&&(e.row>=this._terminal.buffer.active.viewportY+this._terminal.rows||e.row void): IDisposable {\n return { dispose: fn };\n}\n\nexport function dispose(disposable: T): T;\nexport function dispose(disposable: T | undefined): T | undefined;\nexport function dispose(disposables: T[]): T[];\nexport function dispose(arg: T | T[] | undefined): T | T[] | undefined {\n if (!arg) {\n return arg;\n }\n if (Array.isArray(arg)) {\n for (const d of arg) {\n d.dispose();\n }\n return [];\n }\n arg.dispose();\n return arg;\n}\n\nexport function combinedDisposable(...disposables: IDisposable[]): IDisposable {\n return toDisposable(() => dispose(disposables));\n}\n\nexport class DisposableStore implements IDisposable {\n private readonly _disposables = new Set();\n private _isDisposed = false;\n\n public get isDisposed(): boolean {\n return this._isDisposed;\n }\n\n public add(o: T): T {\n if (this._isDisposed) {\n o.dispose();\n } else {\n this._disposables.add(o);\n }\n return o;\n }\n\n public dispose(): void {\n if (this._isDisposed) {\n return;\n }\n this._isDisposed = true;\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n\n public clear(): void {\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n}\n\nexport abstract class Disposable implements IDisposable {\n public static readonly None: IDisposable = Object.freeze({ dispose() { } });\n\n protected readonly _store = new DisposableStore();\n\n public dispose(): void {\n this._store.dispose();\n }\n\n protected _register(o: T): T {\n return this._store.add(o);\n }\n}\n\nexport class MutableDisposable implements IDisposable {\n private _value: T | undefined;\n private _isDisposed = false;\n\n public get value(): T | undefined {\n return this._isDisposed ? undefined : this._value;\n }\n\n public set value(value: T | undefined) {\n if (this._isDisposed || value === this._value) {\n return;\n }\n this._value?.dispose();\n this._value = value;\n }\n\n public clear(): void {\n this.value = undefined;\n }\n\n public dispose(): void {\n this._isDisposed = true;\n this._value?.dispose();\n this._value = undefined;\n }\n}\n", "/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal event utilities for xterm.js core.\n * Simplified from VS Code's event.ts - no leak detection/profiling.\n */\n\nimport { IDisposable, DisposableStore, toDisposable } from './Lifecycle';\n\nexport interface IEvent {\n (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;\n}\n\nexport class Emitter {\n private _listeners: { fn: (e: T) => any, thisArgs: any }[] = [];\n private _disposed = false;\n private _event: IEvent | undefined;\n\n public get event(): IEvent {\n if (this._event) {\n return this._event;\n }\n this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n if (this._disposed) {\n return toDisposable(() => {});\n }\n\n const entry = { fn: listener, thisArgs };\n this._listeners = this._listeners.slice();\n this._listeners.push(entry);\n\n const result = toDisposable(() => {\n const idx = this._listeners.indexOf(entry);\n if (idx !== -1) {\n this._listeners = this._listeners.slice();\n this._listeners.splice(idx, 1);\n }\n });\n\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(result);\n } else {\n disposables.add(result);\n }\n }\n\n return result;\n };\n return this._event;\n }\n\n public fire(event: T): void {\n if (this._disposed || !this._listeners.length) {\n return;\n }\n if (this._listeners.length === 1) {\n this._listeners[0].fn.call(this._listeners[0].thisArgs, event);\n return;\n }\n const listeners = this._listeners;\n for (let i = 0, len = listeners.length; i < len; ++i) {\n listeners[i].fn.call(listeners[i].thisArgs, event);\n }\n }\n\n public dispose(): void {\n if (this._disposed) {\n return;\n }\n this._disposed = true;\n this._listeners.length = 0;\n }\n}\n\nexport namespace EventUtils {\n export function forward(from: IEvent, to: Emitter): IDisposable {\n return from(e => to.fire(e));\n }\n\n export function map(event: IEvent, map: (i: I) => O): IEvent {\n return (listener: (e: O) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n return event(i => listener.call(thisArgs, map(i)), undefined, disposables);\n };\n }\n\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent {\n return (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n const store = new DisposableStore();\n for (const event of events) {\n store.add(event(e => listener.call(thisArgs, e)));\n }\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(store);\n } else {\n disposables.add(store);\n }\n }\n return store;\n };\n }\n\n export function runAndSubscribe(event: IEvent, handler: (e: T) => void, initial: T): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void, initial?: T): IDisposable {\n handler(initial);\n return event(e => handler(e));\n }\n}\n", "/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal async helpers for xterm.js core.\n */\n\nimport { DisposableStore, IDisposable, toDisposable } from './Lifecycle';\n\nexport function timeout(millis: number): Promise {\n return new Promise(resolve => setTimeout(resolve, millis));\n}\n\n/**\n * Creates a timeout that can be disposed using its returned value.\n * @param handler The timeout handler.\n * @param timeout An optional timeout in milliseconds.\n * @param store An optional {@link DisposableStore} that will have the timeout disposable managed\n * automatically.\n */\nexport function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable {\n const timer = setTimeout(() => {\n handler();\n if (store) {\n disposable.dispose();\n }\n }, timeout);\n const disposable = toDisposable(() => {\n clearTimeout(timer);\n });\n store?.add(disposable);\n return disposable;\n}\n\nexport class TimeoutTimer implements IDisposable {\n private _token: any = -1;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n if (this._token !== -1) {\n clearTimeout(this._token);\n this._token = -1;\n }\n }\n\n public cancelAndSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed TimeoutTimer');\n }\n this.cancel();\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n\n public setIfNotSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling setIfNotSet on a disposed TimeoutTimer');\n }\n if (this._token !== -1) {\n return;\n }\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n}\n\n/**\n * Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled\n * microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not\n * run yet.\n */\nexport class MicrotaskTimer implements IDisposable {\n private _isScheduled = false;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n this._isScheduled = false;\n }\n\n public set(runner: () => void): void {\n if (this._isDisposed) {\n throw new Error('Calling set on a disposed MicrotaskTimer');\n }\n if (this._isScheduled) {\n return;\n }\n this._isScheduled = true;\n queueMicrotask(() => {\n if (!this._isScheduled) {\n return;\n }\n this._isScheduled = false;\n runner();\n });\n }\n}\n\nexport class IntervalTimer implements IDisposable {\n private _disposable: IDisposable | undefined;\n private _isDisposed = false;\n\n public cancel(): void {\n this._disposable?.dispose();\n this._disposable = undefined;\n }\n\n public cancelAndSet(runner: () => void, interval: number, context: Window | typeof globalThis = globalThis): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed IntervalTimer');\n }\n this.cancel();\n const handle = context.setInterval(() => {\n runner();\n }, interval);\n this._disposable = {\n dispose: () => {\n context.clearInterval(handle as any);\n this._disposable = undefined;\n }\n };\n }\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal } from '@xterm/xterm';\nimport { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';\nimport { disposableTimeout } from 'common/Async';\n\nexport type LineCacheEntry = [\n /**\n * The string representation of a line (as opposed to the buffer cell representation).\n */\n lineAsString: string,\n /**\n * The offsets where each line starts when the entry describes a wrapped line.\n */\n lineOffsets: number[]\n];\n\n/**\n * Configuration constants for the search line cache functionality.\n */\nconst enum Constants {\n /**\n * Time-to-live for cached search results in milliseconds. After this duration, cached search\n * results will be invalidated to ensure they remain consistent with terminal content changes.\n */\n LINES_CACHE_TIME_TO_LIVE = 15000\n}\n\nexport class SearchLineCache extends Disposable {\n /**\n * translateBufferLineToStringWithWrap is a fairly expensive call.\n * We memoize the calls into an array that has a time based ttl.\n * _linesCache is also invalidated when the terminal cursor moves.\n */\n private _linesCache: LineCacheEntry[] | undefined;\n private _linesCacheTimeout = this._register(new MutableDisposable());\n private _linesCacheDisposables = this._register(new MutableDisposable());\n // Track access to avoid recreating a timeout on every init call which occurs once per search\n // result (findNext/findPrevious -> _highlightAllMatches -> find loop).\n private _lastAccessTimestamp = 0;\n\n constructor(private readonly _terminal: Terminal) {\n super();\n this._register(toDisposable(() => this._destroyLinesCache()));\n }\n\n /**\n * Sets up a line cache with a ttl\n */\n public initLinesCache(): void {\n if (!this._linesCache) {\n this._linesCache = new Array(this._terminal.buffer.active.length);\n this._linesCacheDisposables.value = combinedDisposable(\n this._terminal.onLineFeed(() => this._destroyLinesCache()),\n this._terminal.onCursorMove(() => this._destroyLinesCache()),\n this._terminal.onResize(() => this._destroyLinesCache())\n );\n }\n\n this._lastAccessTimestamp = Date.now();\n if (!this._linesCacheTimeout.value) {\n this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE);\n }\n }\n\n private _destroyLinesCache(): void {\n this._linesCache = undefined;\n this._lastAccessTimestamp = 0;\n this._linesCacheDisposables.clear();\n this._linesCacheTimeout.clear();\n }\n\n private _scheduleLinesCacheTimeout(delay: number): void {\n this._linesCacheTimeout.value = disposableTimeout(() => {\n if (!this._linesCache) {\n return;\n }\n const now = Date.now();\n const elapsed = now - this._lastAccessTimestamp;\n if (elapsed >= Constants.LINES_CACHE_TIME_TO_LIVE) {\n this._destroyLinesCache();\n return;\n }\n this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE - elapsed);\n }, delay);\n }\n\n public getLineFromCache(row: number): LineCacheEntry | undefined {\n return this._linesCache?.[row];\n }\n\n public setLineInCache(row: number, entry: LineCacheEntry): void {\n if (this._linesCache) {\n this._linesCache[row] = entry;\n }\n }\n\n /**\n * Translates a buffer line to a string, including subsequent lines if they are wraps.\n * Wide characters will count as two columns in the resulting string. This\n * function is useful for getting the actual text underneath the raw selection\n * position.\n * @param lineIndex The index of the line being translated.\n * @param trimRight Whether to trim whitespace to the right.\n */\n public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {\n const strings = [];\n const lineOffsets = [0];\n let line = this._terminal.buffer.active.getLine(lineIndex);\n while (line) {\n const nextLine = this._terminal.buffer.active.getLine(lineIndex + 1);\n const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;\n let string = line.translateToString(!lineWrapsToNext && trimRight);\n if (lineWrapsToNext && nextLine) {\n const lastCell = line.getCell(line.length - 1);\n const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;\n // a wide character wrapped to the next line\n if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {\n string = string.slice(0, -1);\n }\n }\n strings.push(string);\n if (lineWrapsToNext) {\n lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);\n } else {\n break;\n }\n lineIndex++;\n line = nextLine;\n }\n return [strings.join(''), lineOffsets];\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ISearchOptions } from '@xterm/addon-search';\n\n/**\n * Manages search state including cached search terms, options tracking, and validation.\n * This class provides a centralized way to handle search state consistency and option changes.\n */\nexport class SearchState {\n private _cachedSearchTerm: string | undefined;\n private _lastSearchOptions: ISearchOptions | undefined;\n\n /**\n * Gets the currently cached search term.\n */\n public get cachedSearchTerm(): string | undefined {\n return this._cachedSearchTerm;\n }\n\n /**\n * Sets the cached search term.\n */\n public set cachedSearchTerm(term: string | undefined) {\n this._cachedSearchTerm = term;\n }\n\n /**\n * Gets the last search options used.\n */\n public get lastSearchOptions(): ISearchOptions | undefined {\n return this._lastSearchOptions;\n }\n\n /**\n * Sets the last search options used.\n */\n public set lastSearchOptions(options: ISearchOptions | undefined) {\n this._lastSearchOptions = options;\n }\n\n /**\n * Validates a search term to ensure it's not empty or invalid.\n * @param term The search term to validate.\n * @returns true if the term is valid for searching.\n */\n public isValidSearchTerm(term: string): boolean {\n return !!(term && term.length > 0);\n }\n\n /**\n * Determines if search options have changed compared to the last search.\n * @param newOptions The new search options to compare.\n * @returns true if the options have changed.\n */\n public didOptionsChange(newOptions?: ISearchOptions): boolean {\n if (!this._lastSearchOptions) {\n return true;\n }\n if (!newOptions) {\n return false;\n }\n if (this._lastSearchOptions.caseSensitive !== newOptions.caseSensitive) {\n return true;\n }\n if (this._lastSearchOptions.regex !== newOptions.regex) {\n return true;\n }\n if (this._lastSearchOptions.wholeWord !== newOptions.wholeWord) {\n return true;\n }\n return false;\n }\n\n /**\n * Determines if a new search should trigger highlighting updates.\n * @param term The search term.\n * @param options The search options.\n * @returns true if highlighting should be updated.\n */\n public shouldUpdateHighlighting(term: string, options?: ISearchOptions): boolean {\n if (!options?.decorations) {\n return false;\n }\n return this._cachedSearchTerm === undefined ||\n term !== this._cachedSearchTerm ||\n this.didOptionsChange(options);\n }\n\n /**\n * Clears the cached search term.\n */\n public clearCachedTerm(): void {\n this._cachedSearchTerm = undefined;\n }\n\n /**\n * Resets all state.\n */\n public reset(): void {\n this._cachedSearchTerm = undefined;\n this._lastSearchOptions = undefined;\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal } from '@xterm/xterm';\nimport type { ISearchOptions } from '@xterm/addon-search';\nimport type { SearchLineCache } from './SearchLineCache';\n\n/**\n * Represents the position to start a search from.\n */\ninterface ISearchPosition {\n startCol: number;\n startRow: number;\n}\n\n/**\n * Represents a search result with its position and content.\n */\nexport interface ISearchResult {\n term: string;\n col: number;\n row: number;\n size: number;\n}\n\n/**\n * Configuration constants for the search engine functionality.\n */\nconst enum Constants {\n /**\n * Characters that are considered non-word characters for search boundary detection. These\n * characters are used to determine word boundaries when performing whole-word searches. Includes\n * common punctuation, symbols, and whitespace characters.\n */\n NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\\\;:\"\\',./<>?'\n}\n\n/**\n * Core search engine that handles finding text within terminal content.\n * This class is responsible for the actual search algorithms and position calculations.\n */\nexport class SearchEngine {\n constructor(\n private readonly _terminal: Terminal,\n private readonly _lineCache: SearchLineCache\n ) {}\n\n /**\n * Find the first occurrence of a term starting from a specific position.\n * @param term The search term.\n * @param startRow The row to start searching from.\n * @param startCol The column to start searching from.\n * @param searchOptions Search options.\n * @returns The search result if found, undefined otherwise.\n */\n public find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n if (startCol >= this._terminal.cols) {\n throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);\n }\n\n this._lineCache.initLinesCache();\n\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n // Search startRow\n let result = this._findInLine(term, searchPosition, searchOptions);\n // Search from startRow + 1 to end\n if (!result) {\n for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n return result;\n }\n\n /**\n * Find the next occurrence of a term with wrapping and selection management.\n * @param term The search term.\n * @param searchOptions Search options.\n * @param cachedSearchTerm The cached search term to determine incremental behavior.\n * @returns The search result if found, undefined otherwise.\n */\n public findNextWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n\n const prevSelectedPos = this._terminal.getSelectionPosition();\n this._terminal.clearSelection();\n\n let startCol = 0;\n let startRow = 0;\n if (prevSelectedPos) {\n if (cachedSearchTerm === term) {\n startCol = prevSelectedPos.end.x;\n startRow = prevSelectedPos.end.y;\n } else {\n startCol = prevSelectedPos.start.x;\n startRow = prevSelectedPos.start.y;\n }\n }\n\n this._lineCache.initLinesCache();\n\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n // Search startRow\n let result = this._findInLine(term, searchPosition, searchOptions);\n // Search from startRow + 1 to end\n if (!result) {\n for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n // If we hit the bottom and didn't search from the very top wrap back up\n if (!result && startRow !== 0) {\n for (let y = 0; y < startRow; y++) {\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n\n // If there is only one result, wrap back and return selection if it exists.\n if (!result && prevSelectedPos) {\n searchPosition.startRow = prevSelectedPos.start.y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n }\n\n return result;\n }\n\n /**\n * Find the previous occurrence of a term with wrapping and selection management.\n * @param term The search term.\n * @param searchOptions Search options.\n * @param cachedSearchTerm The cached search term to determine if expansion should occur.\n * @returns The search result if found, undefined otherwise.\n */\n public findPreviousWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n\n const prevSelectedPos = this._terminal.getSelectionPosition();\n this._terminal.clearSelection();\n\n let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;\n const startCol = this._terminal.cols;\n const isReverseSearch = true;\n\n this._lineCache.initLinesCache();\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n let result: ISearchResult | undefined;\n if (prevSelectedPos) {\n searchPosition.startRow = startRow = prevSelectedPos.start.y;\n searchPosition.startCol = prevSelectedPos.start.x;\n if (cachedSearchTerm !== term) {\n // Try to expand selection to right first.\n result = this._findInLine(term, searchPosition, searchOptions, false);\n if (!result) {\n // If selection was not able to be expanded to the right, then try reverse search\n searchPosition.startRow = startRow = prevSelectedPos.end.y;\n searchPosition.startCol = prevSelectedPos.end.x;\n }\n }\n }\n\n result ??= this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n\n // Search from startRow - 1 to top\n if (!result) {\n searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols);\n for (let y = startRow - 1; y >= 0; y--) {\n searchPosition.startRow = y;\n result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n if (result) {\n break;\n }\n }\n }\n // If we hit the top and didn't search from the very bottom wrap back down\n if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {\n for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {\n searchPosition.startRow = y;\n result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n if (result) {\n break;\n }\n }\n }\n\n return result;\n }\n\n /**\n * A found substring is a whole word if it doesn't have an alphanumeric character directly\n * adjacent to it.\n * @param searchIndex starting index of the potential whole word substring\n * @param line entire string in which the potential whole word was found\n * @param term the substring that starts at searchIndex\n */\n private _isWholeWord(searchIndex: number, line: string, term: string): boolean {\n return ((searchIndex === 0) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&\n (((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));\n }\n\n /**\n * Searches a line for a search term. Takes the provided terminal line and searches the text line,\n * which may contain subsequent terminal lines if the text is wrapped. If the provided line number\n * is part of a wrapped text line that started on an earlier line then it is skipped since it will\n * be properly searched when the terminal line that the text starts on is searched.\n * @param term The search term.\n * @param searchPosition The position to start the search.\n * @param searchOptions Search options.\n * @param isReverseSearch Whether the search should start from the right side of the terminal and\n * search to the left.\n * @returns The search result if it was found.\n */\n private _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {\n const row = searchPosition.startRow;\n const col = searchPosition.startCol;\n\n // Ignore wrapped lines, only consider on unwrapped line (first row of command string).\n const firstLine = this._terminal.buffer.active.getLine(row);\n if (firstLine?.isWrapped) {\n if (isReverseSearch) {\n searchPosition.startCol += this._terminal.cols;\n return;\n }\n\n // This will iterate until we find the line start.\n // When we find it, we will search using the calculated start column.\n searchPosition.startRow--;\n searchPosition.startCol += this._terminal.cols;\n return this._findInLine(term, searchPosition, searchOptions);\n }\n let cache = this._lineCache.getLineFromCache(row);\n if (!cache) {\n cache = this._lineCache.translateBufferLineToStringWithWrap(row, true);\n this._lineCache.setLineInCache(row, cache);\n }\n const [stringLine, offsets] = cache;\n\n const offset = this._bufferColsToStringOffset(row, col);\n let searchTerm = term;\n let searchStringLine = stringLine;\n if (!searchOptions.regex) {\n searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();\n searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();\n }\n\n let resultIndex = -1;\n if (searchOptions.regex) {\n const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');\n let foundTerm: RegExpExecArray | null;\n if (isReverseSearch) {\n // This loop will get the resultIndex of the _last_ regex match in the range 0..offset\n while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {\n resultIndex = searchRegex.lastIndex - foundTerm[0].length;\n term = foundTerm[0];\n searchRegex.lastIndex -= (term.length - 1);\n }\n } else {\n foundTerm = searchRegex.exec(searchStringLine.slice(offset));\n if (foundTerm && foundTerm[0].length > 0) {\n resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);\n term = foundTerm[0];\n }\n }\n } else {\n if (isReverseSearch) {\n if (offset - searchTerm.length >= 0) {\n resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length);\n }\n } else {\n resultIndex = searchStringLine.indexOf(searchTerm, offset);\n }\n }\n\n if (resultIndex >= 0) {\n if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {\n return;\n }\n\n // Adjust the row number and search index if needed since a \"line\" of text can span multiple\n // rows\n let startRowOffset = 0;\n while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {\n startRowOffset++;\n }\n let endRowOffset = startRowOffset;\n while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) {\n endRowOffset++;\n }\n const startColOffset = resultIndex - offsets[startRowOffset];\n const endColOffset = resultIndex + term.length - offsets[endRowOffset];\n const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset);\n const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset);\n const size = endColIndex - startColIndex + this._terminal.cols * (endRowOffset - startRowOffset);\n\n return {\n term,\n col: startColIndex,\n row: row + startRowOffset,\n size\n };\n }\n }\n\n private _stringLengthToBufferSize(row: number, offset: number): number {\n const line = this._terminal.buffer.active.getLine(row);\n if (!line) {\n return 0;\n }\n for (let i = 0; i < offset; i++) {\n const cell = line.getCell(i);\n if (!cell) {\n break;\n }\n // Adjust the searchIndex to normalize emoji into single chars\n const char = cell.getChars();\n if (char.length > 1) {\n offset -= char.length - 1;\n }\n // Adjust the searchIndex for empty characters following wide unicode\n // chars (eg. CJK)\n const nextCell = line.getCell(i + 1);\n if (nextCell && nextCell.getWidth() === 0) {\n offset++;\n }\n }\n return offset;\n }\n\n private _bufferColsToStringOffset(startRow: number, cols: number): number {\n let lineIndex = startRow;\n let offset = 0;\n let line = this._terminal.buffer.active.getLine(lineIndex);\n while (cols > 0 && line) {\n for (let i = 0; i < cols && i < this._terminal.cols; i++) {\n const cell = line.getCell(i);\n if (!cell) {\n break;\n }\n if (cell.getWidth()) {\n // Treat null characters as whitespace to align with the translateToString API\n offset += cell.getCode() === 0 ? 1 : cell.getChars().length;\n }\n }\n lineIndex++;\n line = this._terminal.buffer.active.getLine(lineIndex);\n if (line && !line.isWrapped) {\n break;\n }\n cols -= this._terminal.cols;\n }\n return offset;\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';\nimport type { ISearchDecorationOptions } from '@xterm/addon-search';\nimport { dispose, Disposable, toDisposable } from 'common/Lifecycle';\nimport type { ISearchResult } from './SearchEngine';\n\n/**\n * Interface for managing a highlight decoration.\n */\ninterface IHighlight extends IDisposable {\n decoration: IDecoration;\n match: ISearchResult;\n}\n\n/**\n * Interface for managing multiple decorations for a single match.\n */\ninterface IMultiHighlight extends IDisposable {\n decorations: IDecoration[];\n match: ISearchResult;\n}\n\n/**\n * Manages visual decorations for search results including highlighting and active selection\n * indicators. This class handles the creation, styling, and disposal of search-related decorations.\n */\nexport class DecorationManager extends Disposable {\n private _highlightDecorations: IHighlight[] = [];\n private _highlightedLines: Set = new Set();\n\n constructor(private readonly _terminal: Terminal) {\n super();\n this._register(toDisposable(() => this.clearHighlightDecorations()));\n }\n\n /**\n * Creates decorations for all provided search results.\n * @param results The search results to create decorations for.\n * @param options The decoration options.\n */\n public createHighlightDecorations(results: ISearchResult[], options: ISearchDecorationOptions): void {\n this.clearHighlightDecorations();\n\n for (const match of results) {\n const decorations = this._createResultDecorations(match, options, false);\n if (decorations) {\n for (const decoration of decorations) {\n this._storeDecoration(decoration, match);\n }\n }\n }\n }\n\n /**\n * Creates decorations for the currently active search result.\n * @param result The active search result.\n * @param options The decoration options.\n * @returns The multi-highlight decoration or undefined if creation failed.\n */\n public createActiveDecoration(result: ISearchResult, options: ISearchDecorationOptions): IMultiHighlight | undefined {\n const decorations = this._createResultDecorations(result, options, true);\n if (decorations) {\n return { decorations, match: result, dispose() { dispose(decorations); } };\n }\n return undefined;\n }\n\n /**\n * Clears all highlight decorations.\n */\n public clearHighlightDecorations(): void {\n dispose(this._highlightDecorations);\n this._highlightDecorations = [];\n this._highlightedLines.clear();\n }\n\n /**\n * Stores a decoration and tracks it for management.\n * @param decoration The decoration to store.\n * @param match The search result this decoration represents.\n */\n private _storeDecoration(decoration: IDecoration, match: ISearchResult): void {\n this._highlightedLines.add(decoration.marker.line);\n this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });\n }\n\n /**\n * Applies styles to the decoration when it is rendered.\n * @param element The decoration's element.\n * @param borderColor The border color to apply.\n * @param isActiveResult Whether the element is part of the active search result.\n */\n private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {\n if (!element.classList.contains('xterm-find-result-decoration')) {\n element.classList.add('xterm-find-result-decoration');\n if (borderColor) {\n element.style.outline = `1px solid ${borderColor}`;\n }\n }\n if (isActiveResult) {\n element.classList.add('xterm-find-active-result-decoration');\n }\n }\n\n /**\n * Creates a decoration for the result and applies styles\n * @param result the search result for which to create the decoration\n * @param options the options for the decoration\n * @param isActiveResult whether this is the currently active result\n * @returns the decorations or undefined if the marker has already been disposed of\n */\n private _createResultDecorations(result: ISearchResult, options: ISearchDecorationOptions, isActiveResult: boolean): IDecoration[] | undefined {\n // Gather decoration ranges for this match as it could wrap\n const decorationRanges: [number, number, number][] = [];\n let currentCol = result.col;\n let remainingSize = result.size;\n let markerOffset = -this._terminal.buffer.active.baseY - this._terminal.buffer.active.cursorY + result.row;\n while (remainingSize > 0) {\n const amountThisRow = Math.min(this._terminal.cols - currentCol, remainingSize);\n decorationRanges.push([markerOffset, currentCol, amountThisRow]);\n currentCol = 0;\n remainingSize -= amountThisRow;\n markerOffset++;\n }\n\n // Create the decorations\n const decorations: IDecoration[] = [];\n for (const range of decorationRanges) {\n const marker = this._terminal.registerMarker(range[0]);\n const decoration = this._terminal.registerDecoration({\n marker,\n x: range[1],\n width: range[2],\n layer: isActiveResult ? 'top' : 'bottom',\n backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,\n overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {\n color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,\n position: 'center'\n }\n });\n if (decoration) {\n const disposables: IDisposable[] = [];\n disposables.push(marker);\n disposables.push(decoration.onRender((e) => this._applyStyles(e, isActiveResult ? options.activeMatchBorder : options.matchBorder, false)));\n disposables.push(decoration.onDispose(() => dispose(disposables)));\n decorations.push(decoration);\n }\n }\n\n return decorations.length === 0 ? undefined : decorations;\n }\n}\n\n\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ISearchResultChangeEvent } from '@xterm/addon-search';\nimport type { IDisposable } from '@xterm/xterm';\nimport { Emitter, type IEvent } from 'common/Event';\nimport { Disposable } from 'common/Lifecycle';\nimport type { ISearchResult } from './SearchEngine';\n\n/**\n * Interface for managing a currently selected decoration.\n */\ninterface ISelectedDecoration extends IDisposable {\n match: ISearchResult;\n}\n\n/**\n * Tracks search results, manages result indexing, and fires events when results change.\n * This class provides centralized management of search result state and notifications.\n */\nexport class SearchResultTracker extends Disposable {\n private _searchResults: ISearchResult[] = [];\n private _selectedDecoration: ISelectedDecoration | undefined;\n\n private readonly _onDidChangeResults = this._register(new Emitter());\n public get onDidChangeResults(): IEvent { return this._onDidChangeResults.event; }\n\n /**\n * Gets the current search results.\n */\n public get searchResults(): ReadonlyArray {\n return this._searchResults;\n }\n\n /**\n * Gets the currently selected decoration.\n */\n public get selectedDecoration(): ISelectedDecoration | undefined {\n return this._selectedDecoration;\n }\n\n /**\n * Sets the currently selected decoration.\n */\n public set selectedDecoration(decoration: ISelectedDecoration | undefined) {\n this._selectedDecoration = decoration;\n }\n\n /**\n * Updates the search results with a new set of results.\n * @param results The new search results.\n * @param maxResults The maximum number of results to track.\n */\n public updateResults(results: ISearchResult[], maxResults: number): void {\n this._searchResults = results.slice(0, maxResults);\n }\n\n /**\n * Clears all search results.\n */\n public clearResults(): void {\n this._searchResults = [];\n }\n\n /**\n * Clears the selected decoration.\n */\n public clearSelectedDecoration(): void {\n if (this._selectedDecoration) {\n this._selectedDecoration.dispose();\n this._selectedDecoration = undefined;\n }\n }\n\n /**\n * Finds the index of a result in the current results array.\n * @param result The result to find.\n * @returns The index of the result, or -1 if not found.\n */\n public findResultIndex(result: ISearchResult): number {\n for (let i = 0; i < this._searchResults.length; i++) {\n const match = this._searchResults[i];\n if (match.row === result.row && match.col === result.col && match.size === result.size) {\n return i;\n }\n }\n return -1;\n }\n\n /**\n * Fires a result change event with the current state.\n * @param hasDecorations Whether decorations are enabled.\n */\n public fireResultsChanged(hasDecorations: boolean): void {\n if (!hasDecorations) {\n return;\n }\n\n let resultIndex = -1;\n if (this._selectedDecoration) {\n resultIndex = this.findResultIndex(this._selectedDecoration.match);\n }\n\n this._onDidChangeResults.fire({\n resultIndex,\n resultCount: this._searchResults.length\n });\n }\n\n /**\n * Resets all state.\n */\n public reset(): void {\n this.clearSelectedDecoration();\n this.clearResults();\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';\nimport type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';\nimport { Emitter, type IEvent } from 'common/Event';\nimport { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';\nimport { disposableTimeout } from 'common/Async';\nimport { SearchLineCache } from './SearchLineCache';\nimport { SearchState } from './SearchState';\nimport { SearchEngine, type ISearchResult } from './SearchEngine';\nimport { DecorationManager } from './DecorationManager';\nimport { SearchResultTracker } from './SearchResultTracker';\n\ninterface IInternalSearchOptions {\n noScroll: boolean;\n}\n\n/**\n * Configuration constants for the search addon functionality.\n */\nconst enum Constants {\n /**\n * Default maximum number of search results to highlight simultaneously. This limit prevents\n * performance degradation when searching for very common terms that would result in excessive\n * highlighting decorations.\n */\n DEFAULT_HIGHLIGHT_LIMIT = 1000\n}\n\nexport class SearchAddon extends Disposable implements ITerminalAddon, ISearchApi {\n private _terminal: Terminal | undefined;\n private _highlightLimit: number;\n private _highlightTimeout = this._register(new MutableDisposable());\n private _lineCache = this._register(new MutableDisposable());\n\n // Component instances\n private _state = new SearchState();\n private _engine: SearchEngine | undefined;\n private _decorationManager: DecorationManager | undefined;\n private _resultTracker = this._register(new SearchResultTracker());\n\n private readonly _onAfterSearch = this._register(new Emitter());\n public readonly onAfterSearch = this._onAfterSearch.event;\n private readonly _onBeforeSearch = this._register(new Emitter());\n public readonly onBeforeSearch = this._onBeforeSearch.event;\n\n public get onDidChangeResults(): IEvent {\n return this._resultTracker.onDidChangeResults;\n }\n\n constructor(options?: Partial) {\n super();\n\n this._highlightLimit = options?.highlightLimit ?? Constants.DEFAULT_HIGHLIGHT_LIMIT;\n }\n\n public activate(terminal: Terminal): void {\n this._terminal = terminal;\n this._lineCache.value = new SearchLineCache(terminal);\n this._engine = new SearchEngine(terminal, this._lineCache.value);\n this._decorationManager = new DecorationManager(terminal);\n this._register(this._terminal.onWriteParsed(() => this._updateMatches()));\n this._register(this._terminal.onResize(() => this._updateMatches()));\n this._register(toDisposable(() => this.clearDecorations()));\n }\n\n private _updateMatches(): void {\n this._highlightTimeout.clear();\n if (this._state.cachedSearchTerm && this._state.lastSearchOptions?.decorations) {\n this._highlightTimeout.value = disposableTimeout(() => {\n const term = this._state.cachedSearchTerm;\n this._state.clearCachedTerm();\n this.findPrevious(term!, { ...this._state.lastSearchOptions, incremental: true }, { noScroll: true });\n }, 200);\n }\n }\n\n public clearDecorations(retainCachedSearchTerm?: boolean): void {\n this._resultTracker.clearSelectedDecoration();\n this._decorationManager?.clearHighlightDecorations();\n this._resultTracker.clearResults();\n if (!retainCachedSearchTerm) {\n this._state.clearCachedTerm();\n }\n }\n\n public clearActiveDecoration(): void {\n this._resultTracker.clearSelectedDecoration();\n }\n\n /**\n * Find the next instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options.\n * @returns Whether a result was found.\n */\n public findNext(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n\n this._onBeforeSearch.fire();\n\n this._state.lastSearchOptions = searchOptions;\n\n if (this._state.shouldUpdateHighlighting(term, searchOptions)) {\n this._highlightAllMatches(term, searchOptions!);\n }\n\n const found = this._findNextAndSelect(term, searchOptions, internalSearchOptions);\n this._fireResults(searchOptions);\n this._state.cachedSearchTerm = term;\n\n this._onAfterSearch.fire();\n\n return found;\n }\n\n private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {\n if (!this._terminal || !this._engine || !this._decorationManager) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n if (!this._state.isValidSearchTerm(term)) {\n this.clearDecorations();\n return;\n }\n\n // new search, clear out the old decorations\n this.clearDecorations(true);\n\n const results: ISearchResult[] = [];\n let prevResult: ISearchResult | undefined = undefined;\n let result = this._engine.find(term, 0, 0, searchOptions);\n\n while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {\n if (results.length >= this._highlightLimit) {\n break;\n }\n prevResult = result;\n results.push(prevResult);\n const cols = this._terminal.cols;\n let nextCol = prevResult.col + prevResult.size;\n let nextRow = prevResult.row;\n if (nextCol >= cols) {\n nextRow += Math.floor(nextCol / cols);\n nextCol = nextCol % cols;\n }\n result = this._engine.find(term, nextRow, nextCol, searchOptions);\n }\n\n this._resultTracker.updateResults(results, this._highlightLimit);\n if (searchOptions.decorations) {\n this._decorationManager.createHighlightDecorations(results, searchOptions.decorations);\n }\n }\n\n private _findNextAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n return false;\n }\n if (!this._state.isValidSearchTerm(term)) {\n this._terminal.clearSelection();\n this.clearDecorations();\n return false;\n }\n\n const result = this._engine.findNextWithSelection(term, searchOptions, this._state.cachedSearchTerm);\n return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);\n }\n\n /**\n * Find the previous instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options.\n * @returns Whether a result was found.\n */\n public findPrevious(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n\n this._onBeforeSearch.fire();\n\n this._state.lastSearchOptions = searchOptions;\n\n if (this._state.shouldUpdateHighlighting(term, searchOptions)) {\n this._highlightAllMatches(term, searchOptions!);\n }\n\n const found = this._findPreviousAndSelect(term, searchOptions, internalSearchOptions);\n this._fireResults(searchOptions);\n this._state.cachedSearchTerm = term;\n\n this._onAfterSearch.fire();\n\n return found;\n }\n\n private _fireResults(searchOptions?: ISearchOptions): void {\n this._resultTracker.fireResultsChanged(!!searchOptions?.decorations);\n }\n\n private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n return false;\n }\n if (!this._state.isValidSearchTerm(term)) {\n this._terminal.clearSelection();\n this.clearDecorations();\n return false;\n }\n\n const result = this._engine.findPreviousWithSelection(term, searchOptions, this._state.cachedSearchTerm);\n return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);\n }\n\n /**\n * Selects and scrolls to a result.\n * @param result The result to select.\n * @returns Whether a result was selected.\n */\n private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {\n if (!this._terminal || !this._decorationManager) {\n return false;\n }\n\n this._resultTracker.clearSelectedDecoration();\n if (!result) {\n this._terminal.clearSelection();\n return false;\n }\n\n this._terminal.select(result.col, result.row, result.size);\n if (options) {\n const activeDecoration = this._decorationManager.createActiveDecoration(result, options);\n if (activeDecoration) {\n this._resultTracker.selectedDecoration = activeDecoration;\n }\n }\n\n if (!noScroll) {\n // If it is not in the viewport then we scroll else it just gets selected\n if (result.row >= (this._terminal.buffer.active.viewportY + this._terminal.rows) || result.row < this._terminal.buffer.active.viewportY) {\n let scroll = result.row - this._terminal.buffer.active.viewportY;\n scroll -= Math.floor(this._terminal.rows / 2);\n this._terminal.scrollLines(scroll);\n }\n }\n return true;\n }\n}\n"],
+- "mappings": ";;;;;;;;;;;;;;;;AAYO,SAASA,EAAaC,EAA6B,CACxD,MAAO,CAAE,QAASA,CAAG,CACvB,CAKO,SAASC,EAA+BC,EAA+C,CAC5F,GAAI,CAACA,EACH,OAAOA,EAET,GAAI,MAAM,QAAQA,CAAG,EAAG,CACtB,QAAWC,KAAKD,EACdC,EAAE,QAAQ,EAEZ,MAAO,CAAC,CACV,CACA,OAAAD,EAAI,QAAQ,EACLA,CACT,CAEO,SAASE,KAAsBC,EAAyC,CAC7E,OAAON,EAAa,IAAME,EAAQI,CAAW,CAAC,CAChD,CAEO,IAAMC,EAAN,KAA6C,CAA7C,cACL,KAAiB,aAAe,IAAI,IACpC,KAAQ,YAAc,GAEtB,IAAW,YAAsB,CAC/B,OAAO,KAAK,WACd,CAEO,IAA2BC,EAAS,CACzC,OAAI,KAAK,YACPA,EAAE,QAAQ,EAEV,KAAK,aAAa,IAAIA,CAAC,EAElBA,CACT,CAEO,SAAgB,CACrB,GAAI,MAAK,YAGT,MAAK,YAAc,GACnB,QAAWJ,KAAK,KAAK,aACnBA,EAAE,QAAQ,EAEZ,KAAK,aAAa,MAAM,EAC1B,CAEO,OAAc,CACnB,QAAWA,KAAK,KAAK,aACnBA,EAAE,QAAQ,EAEZ,KAAK,aAAa,MAAM,CAC1B,CACF,EAEsBK,EAAf,KAAiD,CAAjD,cAGL,KAAmB,OAAS,IAAIF,EAEzB,SAAgB,CACrB,KAAK,OAAO,QAAQ,CACtB,CAEU,UAAiCC,EAAS,CAClD,OAAO,KAAK,OAAO,IAAIA,CAAC,CAC1B,CACF,EAZsBC,EACG,KAAoB,OAAO,OAAO,CAAE,SAAU,CAAE,CAAE,CAAC,EAarE,IAAMC,EAAN,KAAsE,CAAtE,cAEL,KAAQ,YAAc,GAEtB,IAAW,OAAuB,CAChC,OAAO,KAAK,YAAc,OAAY,KAAK,MAC7C,CAEA,IAAW,MAAMC,EAAsB,CACjC,KAAK,aAAeA,IAAU,KAAK,SAGvC,KAAK,QAAQ,QAAQ,EACrB,KAAK,OAASA,EAChB,CAEO,OAAc,CACnB,KAAK,MAAQ,MACf,CAEO,SAAgB,CACrB,KAAK,YAAc,GACnB,KAAK,QAAQ,QAAQ,EACrB,KAAK,OAAS,MAChB,CACF,EClGO,IAAMC,EAAN,KAAiB,CAAjB,cACL,KAAQ,WAAqD,CAAC,EAC9D,KAAQ,UAAY,GAGpB,IAAW,OAAmB,CAC5B,OAAI,KAAK,OACA,KAAK,QAEd,KAAK,OAAS,CAACC,EAAyBC,EAAgBC,IAAkD,CACxG,GAAI,KAAK,UACP,OAAOC,EAAa,IAAM,CAAC,CAAC,EAG9B,IAAMC,EAAQ,CAAE,GAAIJ,EAAU,SAAAC,CAAS,EACvC,KAAK,WAAa,KAAK,WAAW,MAAM,EACxC,KAAK,WAAW,KAAKG,CAAK,EAE1B,IAAMC,EAASF,EAAa,IAAM,CAChC,IAAMG,EAAM,KAAK,WAAW,QAAQF,CAAK,EACrCE,IAAQ,KACV,KAAK,WAAa,KAAK,WAAW,MAAM,EACxC,KAAK,WAAW,OAAOA,EAAK,CAAC,EAEjC,CAAC,EAED,OAAIJ,IACE,MAAM,QAAQA,CAAW,EAC3BA,EAAY,KAAKG,CAAM,EAEvBH,EAAY,IAAIG,CAAM,GAInBA,CACT,EACO,KAAK,OACd,CAEO,KAAKE,EAAgB,CAC1B,GAAI,KAAK,WAAa,CAAC,KAAK,WAAW,OACrC,OAEF,GAAI,KAAK,WAAW,SAAW,EAAG,CAChC,KAAK,WAAW,CAAC,EAAE,GAAG,KAAK,KAAK,WAAW,CAAC,EAAE,SAAUA,CAAK,EAC7D,MACF,CACA,IAAMC,EAAY,KAAK,WACvB,QAASC,EAAI,EAAGC,EAAMF,EAAU,OAAQC,EAAIC,EAAK,EAAED,EACjDD,EAAUC,CAAC,EAAE,GAAG,KAAKD,EAAUC,CAAC,EAAE,SAAUF,CAAK,CAErD,CAEO,SAAgB,CACjB,KAAK,YAGT,KAAK,UAAY,GACjB,KAAK,WAAW,OAAS,EAC3B,CACF,EAEiBI,MAAV,CACE,SAASC,EAAWC,EAAiBC,EAA6B,CACvE,OAAOD,EAAKE,GAAKD,EAAG,KAAKC,CAAC,CAAC,CAC7B,CAFOJ,EAAS,QAAAC,EAIT,SAASI,EAAUT,EAAkBS,EAA6B,CACvE,MAAO,CAAChB,EAAyBC,EAAgBC,IACxCK,EAAME,GAAKT,EAAS,KAAKC,EAAUe,EAAIP,CAAC,CAAC,EAAG,OAAWP,CAAW,CAE7E,CAJOS,EAAS,IAAAK,EAQT,SAASC,KAAUC,EAAgC,CACxD,MAAO,CAAClB,EAAyBC,EAAgBC,IAAkD,CACjG,IAAMiB,EAAQ,IAAIC,EAClB,QAAWb,KAASW,EAClBC,EAAM,IAAIZ,EAAMQ,GAAKf,EAAS,KAAKC,EAAUc,CAAC,CAAC,CAAC,EAElD,OAAIb,IACE,MAAM,QAAQA,CAAW,EAC3BA,EAAY,KAAKiB,CAAK,EAEtBjB,EAAY,IAAIiB,CAAK,GAGlBA,CACT,CACF,CAfOR,EAAS,IAAAM,EAmBT,SAASI,EAAmBd,EAAkBe,EAAqCC,EAA0B,CAClH,OAAAD,EAAQC,CAAO,EACRhB,EAAMQ,GAAKO,EAAQP,CAAC,CAAC,CAC9B,CAHOJ,EAAS,gBAAAU,IAhCDV,IAAA,ICxDV,SAASa,EAAkBC,EAAqBC,EAAU,EAAGC,EAAsC,CACxG,IAAMC,EAAQ,WAAW,IAAM,CAC7BH,EAAQ,EACJE,GACFE,EAAW,QAAQ,CAEvB,EAAGH,CAAO,EACJG,EAAaC,EAAa,IAAM,CACpC,aAAaF,CAAK,CACpB,CAAC,EACD,OAAAD,GAAO,IAAIE,CAAU,EACdA,CACT,CCDO,IAAME,EAAN,cAA8BC,CAAW,CAa9C,YAA6BC,EAAqB,CAChD,MAAM,EADqB,eAAAA,EAN7B,KAAQ,mBAAqB,KAAK,UAAU,IAAIC,CAAmB,EACnE,KAAQ,uBAAyB,KAAK,UAAU,IAAIA,CAAmB,EAGvE,KAAQ,qBAAuB,EAI7B,KAAK,UAAUC,EAAa,IAAM,KAAK,mBAAmB,CAAC,CAAC,CAC9D,CAKO,gBAAuB,CACvB,KAAK,cACR,KAAK,YAAc,IAAI,MAAM,KAAK,UAAU,OAAO,OAAO,MAAM,EAChE,KAAK,uBAAuB,MAAQC,EAClC,KAAK,UAAU,WAAW,IAAM,KAAK,mBAAmB,CAAC,EACzD,KAAK,UAAU,aAAa,IAAM,KAAK,mBAAmB,CAAC,EAC3D,KAAK,UAAU,SAAS,IAAM,KAAK,mBAAmB,CAAC,CACzD,GAGF,KAAK,qBAAuB,KAAK,IAAI,EAChC,KAAK,mBAAmB,OAC3B,KAAK,2BAA2B,IAAkC,CAEtE,CAEQ,oBAA2B,CACjC,KAAK,YAAc,OACnB,KAAK,qBAAuB,EAC5B,KAAK,uBAAuB,MAAM,EAClC,KAAK,mBAAmB,MAAM,CAChC,CAEQ,2BAA2BC,EAAqB,CACtD,KAAK,mBAAmB,MAAQC,EAAkB,IAAM,CACtD,GAAI,CAAC,KAAK,YACR,OAGF,IAAMC,EADM,KAAK,IAAI,EACC,KAAK,qBAC3B,GAAIA,GAAW,KAAoC,CACjD,KAAK,mBAAmB,EACxB,MACF,CACA,KAAK,2BAA2B,KAAqCA,CAAO,CAC9E,EAAGF,CAAK,CACV,CAEO,iBAAiBG,EAAyC,CAC/D,OAAO,KAAK,cAAcA,CAAG,CAC/B,CAEO,eAAeA,EAAaC,EAA6B,CAC1D,KAAK,cACP,KAAK,YAAYD,CAAG,EAAIC,EAE5B,CAUO,oCAAoCC,EAAmBC,EAAoC,CAChG,IAAMC,EAAU,CAAC,EACXC,EAAc,CAAC,CAAC,EAClBC,EAAO,KAAK,UAAU,OAAO,OAAO,QAAQJ,CAAS,EACzD,KAAOI,GAAM,CACX,IAAMC,EAAW,KAAK,UAAU,OAAO,OAAO,QAAQL,EAAY,CAAC,EAC7DM,EAAkBD,EAAWA,EAAS,UAAY,GACpDE,EAASH,EAAK,kBAAkB,CAACE,GAAmBL,CAAS,EACjE,GAAIK,GAAmBD,EAAU,CAC/B,IAAMG,EAAWJ,EAAK,QAAQA,EAAK,OAAS,CAAC,EACtBI,GAAYA,EAAS,QAAQ,IAAM,GAAKA,EAAS,SAAS,IAAM,GAEjEH,EAAS,QAAQ,CAAC,GAAG,SAAS,IAAM,IACxDE,EAASA,EAAO,MAAM,EAAG,EAAE,EAE/B,CAEA,GADAL,EAAQ,KAAKK,CAAM,EACfD,EACFH,EAAY,KAAKA,EAAYA,EAAY,OAAS,CAAC,EAAII,EAAO,MAAM,MAEpE,OAEFP,IACAI,EAAOC,CACT,CACA,MAAO,CAACH,EAAQ,KAAK,EAAE,EAAGC,CAAW,CACvC,CACF,EC5HO,IAAMM,EAAN,KAAkB,CAOvB,IAAW,kBAAuC,CAChD,OAAO,KAAK,iBACd,CAKA,IAAW,iBAAiBC,EAA0B,CACpD,KAAK,kBAAoBA,CAC3B,CAKA,IAAW,mBAAgD,CACzD,OAAO,KAAK,kBACd,CAKA,IAAW,kBAAkBC,EAAqC,CAChE,KAAK,mBAAqBA,CAC5B,CAOO,kBAAkBD,EAAuB,CAC9C,MAAO,CAAC,EAAEA,GAAQA,EAAK,OAAS,EAClC,CAOO,iBAAiBE,EAAsC,CAC5D,OAAK,KAAK,mBAGLA,EAGD,KAAK,mBAAmB,gBAAkBA,EAAW,eAGrD,KAAK,mBAAmB,QAAUA,EAAW,OAG7C,KAAK,mBAAmB,YAAcA,EAAW,UAR5C,GAHA,EAeX,CAQO,yBAAyBF,EAAcC,EAAmC,CAC/E,OAAKA,GAAS,YAGP,KAAK,oBAAsB,QAC3BD,IAAS,KAAK,mBACd,KAAK,iBAAiBC,CAAO,EAJ3B,EAKX,CAKO,iBAAwB,CAC7B,KAAK,kBAAoB,MAC3B,CAKO,OAAc,CACnB,KAAK,kBAAoB,OACzB,KAAK,mBAAqB,MAC5B,CACF,EC9DO,IAAME,EAAN,KAAmB,CACxB,YACmBC,EACAC,EACjB,CAFiB,eAAAD,EACA,gBAAAC,CAChB,CAUI,KAAKC,EAAcC,EAAkBC,EAAkBC,EAA2D,CACvH,GAAI,CAACH,GAAQA,EAAK,SAAW,EAAG,CAC9B,KAAK,UAAU,eAAe,EAC9B,MACF,CACA,GAAIE,GAAY,KAAK,UAAU,KAC7B,MAAM,IAAI,MAAM,gBAAgBA,CAAQ,6BAA6B,KAAK,UAAU,IAAI,OAAO,EAGjG,KAAK,WAAW,eAAe,EAE/B,IAAME,EAAkC,CACtC,SAAAH,EACA,SAAAC,CACF,EAGIG,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,EAEjE,GAAI,CAACE,EACH,QAASC,EAAIL,EAAW,EAAGK,EAAI,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,OACjFF,EAAe,SAAWE,EAC1BF,EAAe,SAAW,EAC1BC,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,EACzD,CAAAE,GAJmFC,IAIvF,CAKJ,OAAOD,CACT,CASO,sBAAsBL,EAAcG,EAAgCI,EAAsD,CAC/H,GAAI,CAACP,GAAQA,EAAK,SAAW,EAAG,CAC9B,KAAK,UAAU,eAAe,EAC9B,MACF,CAEA,IAAMQ,EAAkB,KAAK,UAAU,qBAAqB,EAC5D,KAAK,UAAU,eAAe,EAE9B,IAAIN,EAAW,EACXD,EAAW,EACXO,IACED,IAAqBP,GACvBE,EAAWM,EAAgB,IAAI,EAC/BP,EAAWO,EAAgB,IAAI,IAE/BN,EAAWM,EAAgB,MAAM,EACjCP,EAAWO,EAAgB,MAAM,IAIrC,KAAK,WAAW,eAAe,EAE/B,IAAMJ,EAAkC,CACtC,SAAAH,EACA,SAAAC,CACF,EAGIG,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,EAEjE,GAAI,CAACE,EACH,QAASC,EAAIL,EAAW,EAAGK,EAAI,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,OACjFF,EAAe,SAAWE,EAC1BF,EAAe,SAAW,EAC1BC,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,EACzD,CAAAE,GAJmFC,IAIvF,CAMJ,GAAI,CAACD,GAAUJ,IAAa,EAC1B,QAASK,EAAI,EAAGA,EAAIL,IAClBG,EAAe,SAAWE,EAC1BF,EAAe,SAAW,EAC1BC,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,EACzD,CAAAE,GAJwBC,IAI5B,CAOJ,MAAI,CAACD,GAAUG,IACbJ,EAAe,SAAWI,EAAgB,MAAM,EAChDJ,EAAe,SAAW,EAC1BC,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,GAGxDE,CACT,CASO,0BAA0BL,EAAcG,EAAgCI,EAAsD,CACnI,GAAI,CAACP,GAAQA,EAAK,SAAW,EAAG,CAC9B,KAAK,UAAU,eAAe,EAC9B,MACF,CAEA,IAAMQ,EAAkB,KAAK,UAAU,qBAAqB,EAC5D,KAAK,UAAU,eAAe,EAE9B,IAAIP,EAAW,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,KAAO,EACpEC,EAAW,KAAK,UAAU,KAC1BO,EAAkB,GAExB,KAAK,WAAW,eAAe,EAC/B,IAAML,EAAkC,CACtC,SAAAH,EACA,SAAAC,CACF,EAEIG,EAkBJ,GAjBIG,IACFJ,EAAe,SAAWH,EAAWO,EAAgB,MAAM,EAC3DJ,EAAe,SAAWI,EAAgB,MAAM,EAC5CD,IAAqBP,IAEvBK,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,EAAe,EAAK,EAC/DE,IAEHD,EAAe,SAAWH,EAAWO,EAAgB,IAAI,EACzDJ,EAAe,SAAWI,EAAgB,IAAI,KAKpDH,IAAW,KAAK,YAAYL,EAAMI,EAAgBD,EAAeM,CAAe,EAG5E,CAACJ,EAAQ,CACXD,EAAe,SAAW,KAAK,IAAIA,EAAe,SAAU,KAAK,UAAU,IAAI,EAC/E,QAASE,EAAIL,EAAW,EAAGK,GAAK,IAC9BF,EAAe,SAAWE,EAC1BD,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,EAAeM,CAAe,EAC1E,CAAAJ,GAH6BC,IAGjC,CAIJ,CAEA,GAAI,CAACD,GAAUJ,IAAc,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,KAAO,EACtF,QAASK,EAAK,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,KAAO,EAAIA,GAAKL,IAChFG,EAAe,SAAWE,EAC1BD,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,EAAeM,CAAe,EAC1E,CAAAJ,GAHsFC,IAG1F,CAMJ,OAAOD,CACT,CASQ,aAAaK,EAAqBC,EAAcX,EAAuB,CAC7E,OAASU,IAAgB,GAAO,qCAA8B,SAASC,EAAKD,EAAc,CAAC,CAAC,KACvFA,EAAcV,EAAK,SAAYW,EAAK,QAAY,qCAA8B,SAASA,EAAKD,EAAcV,EAAK,MAAM,CAAC,EAC7H,CAcQ,YAAYA,EAAcI,EAAiCD,EAAgC,CAAC,EAAGM,EAA2B,GAAkC,CAClK,IAAMG,EAAMR,EAAe,SACrBS,EAAMT,EAAe,SAI3B,GADkB,KAAK,UAAU,OAAO,OAAO,QAAQQ,CAAG,GAC3C,UAAW,CACxB,GAAIH,EAAiB,CACnBL,EAAe,UAAY,KAAK,UAAU,KAC1C,MACF,CAIA,OAAAA,EAAe,WACfA,EAAe,UAAY,KAAK,UAAU,KACnC,KAAK,YAAYJ,EAAMI,EAAgBD,CAAa,CAC7D,CACA,IAAIW,EAAQ,KAAK,WAAW,iBAAiBF,CAAG,EAC3CE,IACHA,EAAQ,KAAK,WAAW,oCAAoCF,EAAK,EAAI,EACrE,KAAK,WAAW,eAAeA,EAAKE,CAAK,GAE3C,GAAM,CAACC,EAAYC,CAAO,EAAIF,EAExBG,EAAS,KAAK,0BAA0BL,EAAKC,CAAG,EAClDK,EAAalB,EACbmB,EAAmBJ,EAClBZ,EAAc,QACjBe,EAAaf,EAAc,cAAgBH,EAAOA,EAAK,YAAY,EACnEmB,EAAmBhB,EAAc,cAAgBY,EAAaA,EAAW,YAAY,GAGvF,IAAIK,EAAc,GAClB,GAAIjB,EAAc,MAAO,CACvB,IAAMkB,EAAc,OAAOH,EAAYf,EAAc,cAAgB,IAAM,IAAI,EAC3EmB,EACJ,GAAIb,EAEF,KAAOa,EAAYD,EAAY,KAAKF,EAAiB,MAAM,EAAGF,CAAM,CAAC,GACnEG,EAAcC,EAAY,UAAYC,EAAU,CAAC,EAAE,OACnDtB,EAAOsB,EAAU,CAAC,EAClBD,EAAY,WAAcrB,EAAK,OAAS,OAG1CsB,EAAYD,EAAY,KAAKF,EAAiB,MAAMF,CAAM,CAAC,EACvDK,GAAaA,EAAU,CAAC,EAAE,OAAS,IACrCF,EAAcH,GAAUI,EAAY,UAAYC,EAAU,CAAC,EAAE,QAC7DtB,EAAOsB,EAAU,CAAC,EAGxB,MACMb,EACEQ,EAASC,EAAW,QAAU,IAChCE,EAAcD,EAAiB,YAAYD,EAAYD,EAASC,EAAW,MAAM,GAGnFE,EAAcD,EAAiB,QAAQD,EAAYD,CAAM,EAI7D,GAAIG,GAAe,EAAG,CACpB,GAAIjB,EAAc,WAAa,CAAC,KAAK,aAAaiB,EAAaD,EAAkBnB,CAAI,EACnF,OAKF,IAAIuB,EAAiB,EACrB,KAAOA,EAAiBP,EAAQ,OAAS,GAAKI,GAAeJ,EAAQO,EAAiB,CAAC,GACrFA,IAEF,IAAIC,EAAeD,EACnB,KAAOC,EAAeR,EAAQ,OAAS,GAAKI,EAAcpB,EAAK,QAAUgB,EAAQQ,EAAe,CAAC,GAC/FA,IAEF,IAAMC,EAAiBL,EAAcJ,EAAQO,CAAc,EACrDG,EAAeN,EAAcpB,EAAK,OAASgB,EAAQQ,CAAY,EAC/DG,EAAgB,KAAK,0BAA0Bf,EAAMW,EAAgBE,CAAc,EAEnFG,EADc,KAAK,0BAA0BhB,EAAMY,EAAcE,CAAY,EACxDC,EAAgB,KAAK,UAAU,MAAQH,EAAeD,GAEjF,MAAO,CACL,KAAAvB,EACA,IAAK2B,EACL,IAAKf,EAAMW,EACX,KAAAK,CACF,CACF,CACF,CAEQ,0BAA0BhB,EAAaK,EAAwB,CACrE,IAAMN,EAAO,KAAK,UAAU,OAAO,OAAO,QAAQC,CAAG,EACrD,GAAI,CAACD,EACH,MAAO,GAET,QAASkB,EAAI,EAAGA,EAAIZ,EAAQY,IAAK,CAC/B,IAAMC,EAAOnB,EAAK,QAAQkB,CAAC,EAC3B,GAAI,CAACC,EACH,MAGF,IAAMC,EAAOD,EAAK,SAAS,EACvBC,EAAK,OAAS,IAChBd,GAAUc,EAAK,OAAS,GAI1B,IAAMC,EAAWrB,EAAK,QAAQkB,EAAI,CAAC,EAC/BG,GAAYA,EAAS,SAAS,IAAM,GACtCf,GAEJ,CACA,OAAOA,CACT,CAEQ,0BAA0BhB,EAAkBgC,EAAsB,CACxE,IAAIC,EAAYjC,EACZgB,EAAS,EACTN,EAAO,KAAK,UAAU,OAAO,OAAO,QAAQuB,CAAS,EACzD,KAAOD,EAAO,GAAKtB,GAAM,CACvB,QAASkB,EAAI,EAAGA,EAAII,GAAQJ,EAAI,KAAK,UAAU,KAAMA,IAAK,CACxD,IAAMC,EAAOnB,EAAK,QAAQkB,CAAC,EAC3B,GAAI,CAACC,EACH,MAEEA,EAAK,SAAS,IAEhBb,GAAUa,EAAK,QAAQ,IAAM,EAAI,EAAIA,EAAK,SAAS,EAAE,OAEzD,CAGA,GAFAI,IACAvB,EAAO,KAAK,UAAU,OAAO,OAAO,QAAQuB,CAAS,EACjDvB,GAAQ,CAACA,EAAK,UAChB,MAEFsB,GAAQ,KAAK,UAAU,IACzB,CACA,OAAOhB,CACT,CACF,ECzWO,IAAMkB,EAAN,cAAgCC,CAAW,CAIhD,YAA6BC,EAAqB,CAChD,MAAM,EADqB,eAAAA,EAH7B,KAAQ,sBAAsC,CAAC,EAC/C,KAAQ,kBAAiC,IAAI,IAI3C,KAAK,UAAUC,EAAa,IAAM,KAAK,0BAA0B,CAAC,CAAC,CACrE,CAOO,2BAA2BC,EAA0BC,EAAyC,CACnG,KAAK,0BAA0B,EAE/B,QAAWC,KAASF,EAAS,CAC3B,IAAMG,EAAc,KAAK,yBAAyBD,EAAOD,EAAS,EAAK,EACvE,GAAIE,EACF,QAAWC,KAAcD,EACvB,KAAK,iBAAiBC,EAAYF,CAAK,CAG7C,CACF,CAQO,uBAAuBG,EAAuBJ,EAAgE,CACnH,IAAME,EAAc,KAAK,yBAAyBE,EAAQJ,EAAS,EAAI,EACvE,GAAIE,EACF,MAAO,CAAE,YAAAA,EAAa,MAAOE,EAAQ,SAAU,CAAEC,EAAQH,CAAW,CAAG,CAAE,CAG7E,CAKO,2BAAkC,CACvCG,EAAQ,KAAK,qBAAqB,EAClC,KAAK,sBAAwB,CAAC,EAC9B,KAAK,kBAAkB,MAAM,CAC/B,CAOQ,iBAAiBF,EAAyBF,EAA4B,CAC5E,KAAK,kBAAkB,IAAIE,EAAW,OAAO,IAAI,EACjD,KAAK,sBAAsB,KAAK,CAAE,WAAAA,EAAY,MAAAF,EAAO,SAAU,CAAEE,EAAW,QAAQ,CAAG,CAAE,CAAC,CAC5F,CAQQ,aAAaG,EAAsBC,EAAiCC,EAA+B,CACpGF,EAAQ,UAAU,SAAS,8BAA8B,IAC5DA,EAAQ,UAAU,IAAI,8BAA8B,EAChDC,IACFD,EAAQ,MAAM,QAAU,aAAaC,CAAW,KAGhDC,GACFF,EAAQ,UAAU,IAAI,qCAAqC,CAE/D,CASQ,yBAAyBF,EAAuBJ,EAAmCQ,EAAoD,CAE7I,IAAMC,EAA+C,CAAC,EAClDC,EAAaN,EAAO,IACpBO,EAAgBP,EAAO,KACvBQ,EAAe,CAAC,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,OAAO,OAAO,QAAUR,EAAO,IACvG,KAAOO,EAAgB,GAAG,CACxB,IAAME,EAAgB,KAAK,IAAI,KAAK,UAAU,KAAOH,EAAYC,CAAa,EAC9EF,EAAiB,KAAK,CAACG,EAAcF,EAAYG,CAAa,CAAC,EAC/DH,EAAa,EACbC,GAAiBE,EACjBD,GACF,CAGA,IAAMV,EAA6B,CAAC,EACpC,QAAWY,KAASL,EAAkB,CACpC,IAAMM,EAAS,KAAK,UAAU,eAAeD,EAAM,CAAC,CAAC,EAC/CX,EAAa,KAAK,UAAU,mBAAmB,CACnD,OAAAY,EACA,EAAGD,EAAM,CAAC,EACV,MAAOA,EAAM,CAAC,EACd,MAAON,EAAiB,MAAQ,SAChC,gBAAiBA,EAAiBR,EAAQ,sBAAwBA,EAAQ,gBAC1E,qBAAsB,KAAK,kBAAkB,IAAIe,EAAO,IAAI,EAAI,OAAY,CAC1E,MAAOP,EAAiBR,EAAQ,8BAAgCA,EAAQ,mBACxE,SAAU,QACZ,CACF,CAAC,EACD,GAAIG,EAAY,CACd,IAAMa,EAA6B,CAAC,EACpCA,EAAY,KAAKD,CAAM,EACvBC,EAAY,KAAKb,EAAW,SAAUc,GAAM,KAAK,aAAaA,EAAGT,EAAiBR,EAAQ,kBAAoBA,EAAQ,YAAa,EAAK,CAAC,CAAC,EAC1IgB,EAAY,KAAKb,EAAW,UAAU,IAAME,EAAQW,CAAW,CAAC,CAAC,EACjEd,EAAY,KAAKC,CAAU,CAC7B,CACF,CAEA,OAAOD,EAAY,SAAW,EAAI,OAAYA,CAChD,CACF,ECrIO,IAAMgB,EAAN,cAAkCC,CAAW,CAA7C,kCACL,KAAQ,eAAkC,CAAC,EAG3C,KAAiB,oBAAsB,KAAK,UAAU,IAAIC,CAAmC,EAC7F,IAAW,oBAAuD,CAAE,OAAO,KAAK,oBAAoB,KAAO,CAK3G,IAAW,eAA8C,CACvD,OAAO,KAAK,cACd,CAKA,IAAW,oBAAsD,CAC/D,OAAO,KAAK,mBACd,CAKA,IAAW,mBAAmBC,EAA6C,CACzE,KAAK,oBAAsBA,CAC7B,CAOO,cAAcC,EAA0BC,EAA0B,CACvE,KAAK,eAAiBD,EAAQ,MAAM,EAAGC,CAAU,CACnD,CAKO,cAAqB,CAC1B,KAAK,eAAiB,CAAC,CACzB,CAKO,yBAAgC,CACjC,KAAK,sBACP,KAAK,oBAAoB,QAAQ,EACjC,KAAK,oBAAsB,OAE/B,CAOO,gBAAgBC,EAA+B,CACpD,QAASC,EAAI,EAAGA,EAAI,KAAK,eAAe,OAAQA,IAAK,CACnD,IAAMC,EAAQ,KAAK,eAAeD,CAAC,EACnC,GAAIC,EAAM,MAAQF,EAAO,KAAOE,EAAM,MAAQF,EAAO,KAAOE,EAAM,OAASF,EAAO,KAChF,OAAOC,CAEX,CACA,MAAO,EACT,CAMO,mBAAmBE,EAA+B,CACvD,GAAI,CAACA,EACH,OAGF,IAAIC,EAAc,GACd,KAAK,sBACPA,EAAc,KAAK,gBAAgB,KAAK,oBAAoB,KAAK,GAGnE,KAAK,oBAAoB,KAAK,CAC5B,YAAAA,EACA,YAAa,KAAK,eAAe,MACnC,CAAC,CACH,CAKO,OAAc,CACnB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,CACpB,CACF,ECtFO,IAAMC,EAAN,cAA0BC,CAAiD,CAqBhF,YAAYC,EAAwC,CAClD,MAAM,EAnBR,KAAQ,kBAAoB,KAAK,UAAU,IAAIC,CAAgC,EAC/E,KAAQ,WAAa,KAAK,UAAU,IAAIA,CAAoC,EAG5E,KAAQ,OAAS,IAAIC,EAGrB,KAAQ,eAAiB,KAAK,UAAU,IAAIC,CAAqB,EAEjE,KAAiB,eAAiB,KAAK,UAAU,IAAIC,CAAe,EACpE,KAAgB,cAAgB,KAAK,eAAe,MACpD,KAAiB,gBAAkB,KAAK,UAAU,IAAIA,CAAe,EACrE,KAAgB,eAAiB,KAAK,gBAAgB,MASpD,KAAK,gBAAkBJ,GAAS,gBAAkB,GACpD,CARA,IAAW,oBAAuD,CAChE,OAAO,KAAK,eAAe,kBAC7B,CAQO,SAASK,EAA0B,CACxC,KAAK,UAAYA,EACjB,KAAK,WAAW,MAAQ,IAAIC,EAAgBD,CAAQ,EACpD,KAAK,QAAU,IAAIE,EAAaF,EAAU,KAAK,WAAW,KAAK,EAC/D,KAAK,mBAAqB,IAAIG,EAAkBH,CAAQ,EACxD,KAAK,UAAU,KAAK,UAAU,cAAc,IAAM,KAAK,eAAe,CAAC,CAAC,EACxE,KAAK,UAAU,KAAK,UAAU,SAAS,IAAM,KAAK,eAAe,CAAC,CAAC,EACnE,KAAK,UAAUI,EAAa,IAAM,KAAK,iBAAiB,CAAC,CAAC,CAC5D,CAEQ,gBAAuB,CAC7B,KAAK,kBAAkB,MAAM,EACzB,KAAK,OAAO,kBAAoB,KAAK,OAAO,mBAAmB,cACjE,KAAK,kBAAkB,MAAQC,EAAkB,IAAM,CACrD,IAAMC,EAAO,KAAK,OAAO,iBACzB,KAAK,OAAO,gBAAgB,EAC5B,KAAK,aAAaA,EAAO,CAAE,GAAG,KAAK,OAAO,kBAAmB,YAAa,EAAK,EAAG,CAAE,SAAU,EAAK,CAAC,CACtG,EAAG,GAAG,EAEV,CAEO,iBAAiBC,EAAwC,CAC9D,KAAK,eAAe,wBAAwB,EAC5C,KAAK,oBAAoB,0BAA0B,EACnD,KAAK,eAAe,aAAa,EAC5BA,GACH,KAAK,OAAO,gBAAgB,CAEhC,CAEO,uBAA8B,CACnC,KAAK,eAAe,wBAAwB,CAC9C,CASO,SAASD,EAAcE,EAAgCC,EAAyD,CACrH,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,QAC3B,MAAM,IAAI,MAAM,2CAA2C,EAG7D,KAAK,gBAAgB,KAAK,EAE1B,KAAK,OAAO,kBAAoBD,EAE5B,KAAK,OAAO,yBAAyBF,EAAME,CAAa,GAC1D,KAAK,qBAAqBF,EAAME,CAAc,EAGhD,IAAME,EAAQ,KAAK,mBAAmBJ,EAAME,EAAeC,CAAqB,EAChF,YAAK,aAAaD,CAAa,EAC/B,KAAK,OAAO,iBAAmBF,EAE/B,KAAK,eAAe,KAAK,EAElBI,CACT,CAEQ,qBAAqBJ,EAAcE,EAAqC,CAC9E,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,SAAW,CAAC,KAAK,mBAC5C,MAAM,IAAI,MAAM,2CAA2C,EAE7D,GAAI,CAAC,KAAK,OAAO,kBAAkBF,CAAI,EAAG,CACxC,KAAK,iBAAiB,EACtB,MACF,CAGA,KAAK,iBAAiB,EAAI,EAE1B,IAAMK,EAA2B,CAAC,EAC9BC,EACAC,EAAS,KAAK,QAAQ,KAAKP,EAAM,EAAG,EAAGE,CAAa,EAExD,KAAOK,IAAWD,GAAY,MAAQC,EAAO,KAAOD,GAAY,MAAQC,EAAO,MACzE,EAAAF,EAAQ,QAAU,KAAK,kBADwD,CAInFC,EAAaC,EACbF,EAAQ,KAAKC,CAAU,EACvB,IAAME,EAAO,KAAK,UAAU,KACxBC,EAAUH,EAAW,IAAMA,EAAW,KACtCI,EAAUJ,EAAW,IACrBG,GAAWD,IACbE,GAAW,KAAK,MAAMD,EAAUD,CAAI,EACpCC,EAAUA,EAAUD,GAEtBD,EAAS,KAAK,QAAQ,KAAKP,EAAMU,EAASD,EAASP,CAAa,CAClE,CAEA,KAAK,eAAe,cAAcG,EAAS,KAAK,eAAe,EAC3DH,EAAc,aAChB,KAAK,mBAAmB,2BAA2BG,EAASH,EAAc,WAAW,CAEzF,CAEQ,mBAAmBF,EAAcE,EAAgCC,EAAyD,CAChI,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,QAC3B,MAAO,GAET,GAAI,CAAC,KAAK,OAAO,kBAAkBH,CAAI,EACrC,YAAK,UAAU,eAAe,EAC9B,KAAK,iBAAiB,EACf,GAGT,IAAMO,EAAS,KAAK,QAAQ,sBAAsBP,EAAME,EAAe,KAAK,OAAO,gBAAgB,EACnG,OAAO,KAAK,cAAcK,EAAQL,GAAe,YAAaC,GAAuB,QAAQ,CAC/F,CASO,aAAaH,EAAcE,EAAgCC,EAAyD,CACzH,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,QAC3B,MAAM,IAAI,MAAM,2CAA2C,EAG7D,KAAK,gBAAgB,KAAK,EAE1B,KAAK,OAAO,kBAAoBD,EAE5B,KAAK,OAAO,yBAAyBF,EAAME,CAAa,GAC1D,KAAK,qBAAqBF,EAAME,CAAc,EAGhD,IAAME,EAAQ,KAAK,uBAAuBJ,EAAME,EAAeC,CAAqB,EACpF,YAAK,aAAaD,CAAa,EAC/B,KAAK,OAAO,iBAAmBF,EAE/B,KAAK,eAAe,KAAK,EAElBI,CACT,CAEQ,aAAaF,EAAsC,CACzD,KAAK,eAAe,mBAAmB,CAAC,CAACA,GAAe,WAAW,CACrE,CAEQ,uBAAuBF,EAAcE,EAAgCC,EAAyD,CACpI,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,QAC3B,MAAO,GAET,GAAI,CAAC,KAAK,OAAO,kBAAkBH,CAAI,EACrC,YAAK,UAAU,eAAe,EAC9B,KAAK,iBAAiB,EACf,GAGT,IAAMO,EAAS,KAAK,QAAQ,0BAA0BP,EAAME,EAAe,KAAK,OAAO,gBAAgB,EACvG,OAAO,KAAK,cAAcK,EAAQL,GAAe,YAAaC,GAAuB,QAAQ,CAC/F,CAOQ,cAAcI,EAAmClB,EAAoCsB,EAA6B,CACxH,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,mBAC3B,MAAO,GAIT,GADA,KAAK,eAAe,wBAAwB,EACxC,CAACJ,EACH,YAAK,UAAU,eAAe,EACvB,GAIT,GADA,KAAK,UAAU,OAAOA,EAAO,IAAKA,EAAO,IAAKA,EAAO,IAAI,EACrDlB,EAAS,CACX,IAAMuB,EAAmB,KAAK,mBAAmB,uBAAuBL,EAAQlB,CAAO,EACnFuB,IACF,KAAK,eAAe,mBAAqBA,EAE7C,CAEA,GAAI,CAACD,IAECJ,EAAO,KAAQ,KAAK,UAAU,OAAO,OAAO,UAAY,KAAK,UAAU,MAASA,EAAO,IAAM,KAAK,UAAU,OAAO,OAAO,WAAW,CACvI,IAAIM,EAASN,EAAO,IAAM,KAAK,UAAU,OAAO,OAAO,UACvDM,GAAU,KAAK,MAAM,KAAK,UAAU,KAAO,CAAC,EAC5C,KAAK,UAAU,YAAYA,CAAM,CACnC,CAEF,MAAO,EACT,CACF",
+- "names": ["toDisposable", "fn", "dispose", "arg", "d", "combinedDisposable", "disposables", "DisposableStore", "o", "Disposable", "MutableDisposable", "value", "Emitter", "listener", "thisArgs", "disposables", "toDisposable", "entry", "result", "idx", "event", "listeners", "i", "len", "EventUtils", "forward", "from", "to", "e", "map", "any", "events", "store", "DisposableStore", "runAndSubscribe", "handler", "initial", "disposableTimeout", "handler", "timeout", "store", "timer", "disposable", "toDisposable", "SearchLineCache", "Disposable", "_terminal", "MutableDisposable", "toDisposable", "combinedDisposable", "delay", "disposableTimeout", "elapsed", "row", "entry", "lineIndex", "trimRight", "strings", "lineOffsets", "line", "nextLine", "lineWrapsToNext", "string", "lastCell", "SearchState", "term", "options", "newOptions", "SearchEngine", "_terminal", "_lineCache", "term", "startRow", "startCol", "searchOptions", "searchPosition", "result", "y", "cachedSearchTerm", "prevSelectedPos", "isReverseSearch", "searchIndex", "line", "row", "col", "cache", "stringLine", "offsets", "offset", "searchTerm", "searchStringLine", "resultIndex", "searchRegex", "foundTerm", "startRowOffset", "endRowOffset", "startColOffset", "endColOffset", "startColIndex", "size", "i", "cell", "char", "nextCell", "cols", "lineIndex", "DecorationManager", "Disposable", "_terminal", "toDisposable", "results", "options", "match", "decorations", "decoration", "result", "dispose", "element", "borderColor", "isActiveResult", "decorationRanges", "currentCol", "remainingSize", "markerOffset", "amountThisRow", "range", "marker", "disposables", "e", "SearchResultTracker", "Disposable", "Emitter", "decoration", "results", "maxResults", "result", "i", "match", "hasDecorations", "resultIndex", "SearchAddon", "Disposable", "options", "MutableDisposable", "SearchState", "SearchResultTracker", "Emitter", "terminal", "SearchLineCache", "SearchEngine", "DecorationManager", "toDisposable", "disposableTimeout", "term", "retainCachedSearchTerm", "searchOptions", "internalSearchOptions", "found", "results", "prevResult", "result", "cols", "nextCol", "nextRow", "noScroll", "activeDecoration", "scroll"]
++ "sourcesContent": ["/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal lifecycle utilities for xterm.js core.\n * Simplified from VS Code's lifecycle.ts - no tracking/leak detection.\n */\n\nexport interface IDisposable {\n dispose(): void;\n}\n\nexport function toDisposable(fn: () => void): IDisposable {\n return { dispose: fn };\n}\n\nexport function dispose(disposable: T): T;\nexport function dispose(disposable: T | undefined): T | undefined;\nexport function dispose(disposables: T[]): T[];\nexport function dispose(arg: T | T[] | undefined): T | T[] | undefined {\n if (!arg) {\n return arg;\n }\n if (Array.isArray(arg)) {\n for (const d of arg) {\n d.dispose();\n }\n return [];\n }\n arg.dispose();\n return arg;\n}\n\nexport function combinedDisposable(...disposables: IDisposable[]): IDisposable {\n return toDisposable(() => dispose(disposables));\n}\n\nexport class DisposableStore implements IDisposable {\n private readonly _disposables = new Set();\n private _isDisposed = false;\n\n public get isDisposed(): boolean {\n return this._isDisposed;\n }\n\n public add(o: T): T {\n if (this._isDisposed) {\n o.dispose();\n } else {\n this._disposables.add(o);\n }\n return o;\n }\n\n public dispose(): void {\n if (this._isDisposed) {\n return;\n }\n this._isDisposed = true;\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n\n public clear(): void {\n for (const d of this._disposables) {\n d.dispose();\n }\n this._disposables.clear();\n }\n}\n\nexport abstract class Disposable implements IDisposable {\n public static readonly None: IDisposable = Object.freeze({ dispose() { } });\n\n protected readonly _store = new DisposableStore();\n\n public dispose(): void {\n this._store.dispose();\n }\n\n protected _register(o: T): T {\n return this._store.add(o);\n }\n}\n\nexport class MutableDisposable implements IDisposable {\n private _value: T | undefined;\n private _isDisposed = false;\n\n public get value(): T | undefined {\n return this._isDisposed ? undefined : this._value;\n }\n\n public set value(value: T | undefined) {\n if (this._isDisposed || value === this._value) {\n return;\n }\n this._value?.dispose();\n this._value = value;\n }\n\n public clear(): void {\n this.value = undefined;\n }\n\n public dispose(): void {\n this._isDisposed = true;\n this._value?.dispose();\n this._value = undefined;\n }\n}\n", "/**\n * Copyright (c) 2024-2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal event utilities for xterm.js core.\n * Simplified from VS Code's event.ts - no leak detection/profiling.\n */\n\nimport { IDisposable, DisposableStore, toDisposable } from './Lifecycle';\n\nexport interface IEvent {\n (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable;\n}\n\nexport class Emitter {\n private _listeners: { fn: (e: T) => any, thisArgs: any }[] = [];\n private _disposed = false;\n private _event: IEvent | undefined;\n\n public get event(): IEvent {\n if (this._event) {\n return this._event;\n }\n this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n if (this._disposed) {\n return toDisposable(() => {});\n }\n\n const entry = { fn: listener, thisArgs };\n this._listeners = this._listeners.slice();\n this._listeners.push(entry);\n\n const result = toDisposable(() => {\n const idx = this._listeners.indexOf(entry);\n if (idx !== -1) {\n this._listeners = this._listeners.slice();\n this._listeners.splice(idx, 1);\n }\n });\n\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(result);\n } else {\n disposables.add(result);\n }\n }\n\n return result;\n };\n return this._event;\n }\n\n public fire(event: T): void {\n if (this._disposed || !this._listeners.length) {\n return;\n }\n if (this._listeners.length === 1) {\n this._listeners[0].fn.call(this._listeners[0].thisArgs, event);\n return;\n }\n const listeners = this._listeners;\n for (let i = 0, len = listeners.length; i < len; ++i) {\n listeners[i].fn.call(listeners[i].thisArgs, event);\n }\n }\n\n public dispose(): void {\n if (this._disposed) {\n return;\n }\n this._disposed = true;\n this._listeners.length = 0;\n }\n}\n\nexport namespace EventUtils {\n export function forward(from: IEvent, to: Emitter): IDisposable {\n return from(e => to.fire(e));\n }\n\n export function map(event: IEvent, map: (i: I) => O): IEvent {\n return (listener: (e: O) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n return event(i => listener.call(thisArgs, map(i)), undefined, disposables);\n };\n }\n\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent;\n export function any(...events: IEvent[]): IEvent {\n return (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore) => {\n const store = new DisposableStore();\n for (const event of events) {\n store.add(event(e => listener.call(thisArgs, e)));\n }\n if (disposables) {\n if (Array.isArray(disposables)) {\n disposables.push(store);\n } else {\n disposables.add(store);\n }\n }\n return store;\n };\n }\n\n export function runAndSubscribe(event: IEvent, handler: (e: T) => void, initial: T): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void): IDisposable;\n export function runAndSubscribe(event: IEvent, handler: (e: T | undefined) => void, initial?: T): IDisposable {\n handler(initial);\n return event(e => handler(e));\n }\n}\n", "/**\n * Copyright (c) 2026 The xterm.js authors. All rights reserved.\n * @license MIT\n *\n * Minimal async helpers for xterm.js core.\n */\n\nimport { DisposableStore, IDisposable, toDisposable } from './Lifecycle';\n\nexport function timeout(millis: number): Promise {\n return new Promise(resolve => setTimeout(resolve, millis));\n}\n\n/**\n * Creates a timeout that can be disposed using its returned value.\n * @param handler The timeout handler.\n * @param timeout An optional timeout in milliseconds.\n * @param store An optional {@link DisposableStore} that will have the timeout disposable managed\n * automatically.\n */\nexport function disposableTimeout(handler: () => void, timeout = 0, store?: DisposableStore): IDisposable {\n const timer = setTimeout(() => {\n handler();\n if (store) {\n disposable.dispose();\n }\n }, timeout);\n const disposable = toDisposable(() => {\n clearTimeout(timer);\n });\n store?.add(disposable);\n return disposable;\n}\n\nexport class TimeoutTimer implements IDisposable {\n private _token: any = -1;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n if (this._token !== -1) {\n clearTimeout(this._token);\n this._token = -1;\n }\n }\n\n public cancelAndSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed TimeoutTimer');\n }\n this.cancel();\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n\n public setIfNotSet(runner: () => void, timeout: number): void {\n if (this._isDisposed) {\n throw new Error('Calling setIfNotSet on a disposed TimeoutTimer');\n }\n if (this._token !== -1) {\n return;\n }\n this._token = setTimeout(() => {\n this._token = -1;\n runner();\n }, timeout);\n }\n}\n\n/**\n * Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled\n * microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not\n * run yet.\n */\nexport class MicrotaskTimer implements IDisposable {\n private _isScheduled = false;\n private _isDisposed = false;\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n\n public cancel(): void {\n this._isScheduled = false;\n }\n\n public set(runner: () => void): void {\n if (this._isDisposed) {\n throw new Error('Calling set on a disposed MicrotaskTimer');\n }\n if (this._isScheduled) {\n return;\n }\n this._isScheduled = true;\n queueMicrotask(() => {\n if (!this._isScheduled) {\n return;\n }\n this._isScheduled = false;\n runner();\n });\n }\n}\n\nexport class IntervalTimer implements IDisposable {\n private _disposable: IDisposable | undefined;\n private _isDisposed = false;\n\n public cancel(): void {\n this._disposable?.dispose();\n this._disposable = undefined;\n }\n\n public cancelAndSet(runner: () => void, interval: number, context: Window | typeof globalThis = globalThis): void {\n if (this._isDisposed) {\n throw new Error('Calling cancelAndSet on a disposed IntervalTimer');\n }\n this.cancel();\n const handle = context.setInterval(() => {\n runner();\n }, interval);\n this._disposable = {\n dispose: () => {\n context.clearInterval(handle as any);\n this._disposable = undefined;\n }\n };\n }\n\n public dispose(): void {\n this.cancel();\n this._isDisposed = true;\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal } from '@xterm/xterm';\nimport { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';\nimport { disposableTimeout } from 'common/Async';\n\nexport type LineCacheEntry = [\n /**\n * The string representation of a line (as opposed to the buffer cell representation).\n */\n lineAsString: string,\n /**\n * The offsets where each line starts when the entry describes a wrapped line.\n */\n lineOffsets: number[]\n];\n\n/**\n * Configuration constants for the search line cache functionality.\n */\nconst enum Constants {\n /**\n * Time-to-live for cached search results in milliseconds. After this duration, cached search\n * results will be invalidated to ensure they remain consistent with terminal content changes.\n */\n LINES_CACHE_TIME_TO_LIVE = 15000\n}\n\nexport class SearchLineCache extends Disposable {\n /**\n * translateBufferLineToStringWithWrap is a fairly expensive call.\n * We memoize the calls into an array that has a time based ttl.\n * _linesCache is also invalidated when the terminal cursor moves.\n */\n private _linesCache: LineCacheEntry[] | undefined;\n private _linesCacheTimeout = this._register(new MutableDisposable());\n private _linesCacheDisposables = this._register(new MutableDisposable());\n // Track access to avoid recreating a timeout on every init call which occurs once per search\n // result (findNext/findPrevious -> _highlightAllMatches -> find loop).\n private _lastAccessTimestamp = 0;\n\n constructor(private readonly _terminal: Terminal) {\n super();\n this._register(toDisposable(() => this._destroyLinesCache()));\n }\n\n /**\n * Sets up a line cache with a ttl\n */\n public initLinesCache(): void {\n if (!this._linesCache) {\n this._linesCache = new Array(this._terminal.buffer.active.length);\n this._linesCacheDisposables.value = combinedDisposable(\n this._terminal.onLineFeed(() => this._destroyLinesCache()),\n this._terminal.onCursorMove(() => this._destroyLinesCache()),\n this._terminal.onResize(() => this._destroyLinesCache())\n );\n }\n\n this._lastAccessTimestamp = Date.now();\n if (!this._linesCacheTimeout.value) {\n this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE);\n }\n }\n\n private _destroyLinesCache(): void {\n this._linesCache = undefined;\n this._lastAccessTimestamp = 0;\n this._linesCacheDisposables.clear();\n this._linesCacheTimeout.clear();\n }\n\n private _scheduleLinesCacheTimeout(delay: number): void {\n this._linesCacheTimeout.value = disposableTimeout(() => {\n if (!this._linesCache) {\n return;\n }\n const now = Date.now();\n const elapsed = now - this._lastAccessTimestamp;\n if (elapsed >= Constants.LINES_CACHE_TIME_TO_LIVE) {\n this._destroyLinesCache();\n return;\n }\n this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE - elapsed);\n }, delay);\n }\n\n public getLineFromCache(row: number): LineCacheEntry | undefined {\n return this._linesCache?.[row];\n }\n\n public setLineInCache(row: number, entry: LineCacheEntry): void {\n if (this._linesCache) {\n this._linesCache[row] = entry;\n }\n }\n\n /**\n * Translates a buffer line to a string, including subsequent lines if they are wraps.\n * Wide characters will count as two columns in the resulting string. This\n * function is useful for getting the actual text underneath the raw selection\n * position.\n * @param lineIndex The index of the line being translated.\n * @param trimRight Whether to trim whitespace to the right.\n */\n public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {\n const strings = [];\n const lineOffsets = [0];\n // A single line longer than the whole scrollback leaves every buffer row wrapped, and the\n // buffer's ring answers an out-of-range row by cycling back to the start, so an unbounded walk\n // never reaches an unwrapped line.\n const bufferLength = this._terminal.buffer.active.length;\n let line = this._terminal.buffer.active.getLine(lineIndex);\n while (line) {\n const nextLine = lineIndex + 1 < bufferLength ? this._terminal.buffer.active.getLine(lineIndex + 1) : undefined;\n const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;\n let string = line.translateToString(!lineWrapsToNext && trimRight);\n if (lineWrapsToNext && nextLine) {\n const lastCell = line.getCell(line.length - 1);\n const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;\n // a wide character wrapped to the next line\n if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {\n string = string.slice(0, -1);\n }\n }\n strings.push(string);\n if (lineWrapsToNext) {\n lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);\n } else {\n break;\n }\n lineIndex++;\n line = nextLine;\n }\n return [strings.join(''), lineOffsets];\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ISearchOptions } from '@xterm/addon-search';\n\n/**\n * Manages search state including cached search terms, options tracking, and validation.\n * This class provides a centralized way to handle search state consistency and option changes.\n */\nexport class SearchState {\n private _cachedSearchTerm: string | undefined;\n private _lastSearchOptions: ISearchOptions | undefined;\n\n /**\n * Gets the currently cached search term.\n */\n public get cachedSearchTerm(): string | undefined {\n return this._cachedSearchTerm;\n }\n\n /**\n * Sets the cached search term.\n */\n public set cachedSearchTerm(term: string | undefined) {\n this._cachedSearchTerm = term;\n }\n\n /**\n * Gets the last search options used.\n */\n public get lastSearchOptions(): ISearchOptions | undefined {\n return this._lastSearchOptions;\n }\n\n /**\n * Sets the last search options used.\n */\n public set lastSearchOptions(options: ISearchOptions | undefined) {\n this._lastSearchOptions = options;\n }\n\n /**\n * Validates a search term to ensure it's not empty or invalid.\n * @param term The search term to validate.\n * @returns true if the term is valid for searching.\n */\n public isValidSearchTerm(term: string): boolean {\n return !!(term && term.length > 0);\n }\n\n /**\n * Determines if search options have changed compared to the last search.\n * @param newOptions The new search options to compare.\n * @returns true if the options have changed.\n */\n public didOptionsChange(newOptions?: ISearchOptions): boolean {\n if (!this._lastSearchOptions) {\n return true;\n }\n if (!newOptions) {\n return false;\n }\n if (this._lastSearchOptions.caseSensitive !== newOptions.caseSensitive) {\n return true;\n }\n if (this._lastSearchOptions.regex !== newOptions.regex) {\n return true;\n }\n if (this._lastSearchOptions.wholeWord !== newOptions.wholeWord) {\n return true;\n }\n return false;\n }\n\n /**\n * Determines if a new search should trigger highlighting updates.\n * @param term The search term.\n * @param options The search options.\n * @returns true if highlighting should be updated.\n */\n public shouldUpdateHighlighting(term: string, options?: ISearchOptions): boolean {\n if (!options?.decorations) {\n return false;\n }\n return this._cachedSearchTerm === undefined ||\n term !== this._cachedSearchTerm ||\n this.didOptionsChange(options);\n }\n\n /**\n * Clears the cached search term.\n */\n public clearCachedTerm(): void {\n this._cachedSearchTerm = undefined;\n }\n\n /**\n * Resets all state.\n */\n public reset(): void {\n this._cachedSearchTerm = undefined;\n this._lastSearchOptions = undefined;\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal } from '@xterm/xterm';\nimport type { ISearchOptions } from '@xterm/addon-search';\nimport type { SearchLineCache } from './SearchLineCache';\n\n/**\n * Represents the position to start a search from.\n */\ninterface ISearchPosition {\n startCol: number;\n startRow: number;\n}\n\n/**\n * Represents a search result with its position and content.\n */\nexport interface ISearchResult {\n term: string;\n col: number;\n row: number;\n size: number;\n}\n\n/**\n * Configuration constants for the search engine functionality.\n */\nconst enum Constants {\n /**\n * Characters that are considered non-word characters for search boundary detection. These\n * characters are used to determine word boundaries when performing whole-word searches. Includes\n * common punctuation, symbols, and whitespace characters.\n */\n NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\\\;:\"\\',./<>?'\n}\n\n/**\n * Core search engine that handles finding text within terminal content.\n * This class is responsible for the actual search algorithms and position calculations.\n */\nexport class SearchEngine {\n constructor(\n private readonly _terminal: Terminal,\n private readonly _lineCache: SearchLineCache\n ) {}\n\n /**\n * Find the first occurrence of a term starting from a specific position.\n * @param term The search term.\n * @param startRow The row to start searching from.\n * @param startCol The column to start searching from.\n * @param searchOptions Search options.\n * @returns The search result if found, undefined otherwise.\n */\n public find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n if (startCol >= this._terminal.cols) {\n throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);\n }\n\n this._lineCache.initLinesCache();\n\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n // Search startRow\n let result = this._findInLine(term, searchPosition, searchOptions);\n // Search from startRow + 1 to end\n if (!result) {\n for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {\n if (this._isRowCoveredByEarlierSearch(y)) {\n continue;\n }\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n return result;\n }\n\n /**\n * Find the next occurrence of a term with wrapping and selection management.\n * @param term The search term.\n * @param searchOptions Search options.\n * @param cachedSearchTerm The cached search term to determine incremental behavior.\n * @returns The search result if found, undefined otherwise.\n */\n public findNextWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n\n const prevSelectedPos = this._terminal.getSelectionPosition();\n this._terminal.clearSelection();\n\n let startCol = 0;\n let startRow = 0;\n if (prevSelectedPos) {\n if (cachedSearchTerm === term) {\n startCol = prevSelectedPos.end.x;\n startRow = prevSelectedPos.end.y;\n } else {\n startCol = prevSelectedPos.start.x;\n startRow = prevSelectedPos.start.y;\n }\n }\n\n this._lineCache.initLinesCache();\n\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n // Search startRow\n let result = this._findInLine(term, searchPosition, searchOptions);\n // Search from startRow + 1 to end\n if (!result) {\n for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {\n if (this._isRowCoveredByEarlierSearch(y)) {\n continue;\n }\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n // If we hit the bottom and didn't search from the very top wrap back up\n if (!result && startRow !== 0) {\n for (let y = 0; y < startRow; y++) {\n // Row 0 is never skipped: it can be a continuation whose line start was trimmed from the\n // scrollback, and nothing earlier in this loop has searched it.\n if (y > 0 && this._isRowCoveredByEarlierSearch(y)) {\n continue;\n }\n searchPosition.startRow = y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n if (result) {\n break;\n }\n }\n }\n\n // If there is only one result, wrap back and return selection if it exists.\n if (!result && prevSelectedPos) {\n searchPosition.startRow = prevSelectedPos.start.y;\n searchPosition.startCol = 0;\n result = this._findInLine(term, searchPosition, searchOptions);\n }\n\n return result;\n }\n\n /**\n * Find the previous occurrence of a term with wrapping and selection management.\n * @param term The search term.\n * @param searchOptions Search options.\n * @param cachedSearchTerm The cached search term to determine if expansion should occur.\n * @returns The search result if found, undefined otherwise.\n */\n public findPreviousWithSelection(term: string, searchOptions?: ISearchOptions, cachedSearchTerm?: string): ISearchResult | undefined {\n if (!term || term.length === 0) {\n this._terminal.clearSelection();\n return undefined;\n }\n\n const prevSelectedPos = this._terminal.getSelectionPosition();\n this._terminal.clearSelection();\n\n let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;\n const startCol = this._terminal.cols;\n const isReverseSearch = true;\n\n this._lineCache.initLinesCache();\n const searchPosition: ISearchPosition = {\n startRow,\n startCol\n };\n\n let result: ISearchResult | undefined;\n if (prevSelectedPos) {\n searchPosition.startRow = startRow = prevSelectedPos.start.y;\n searchPosition.startCol = prevSelectedPos.start.x;\n if (cachedSearchTerm !== term) {\n // Try to expand selection to right first.\n result = this._findInLine(term, searchPosition, searchOptions, false);\n if (!result) {\n // If selection was not able to be expanded to the right, then try reverse search\n searchPosition.startRow = startRow = prevSelectedPos.end.y;\n searchPosition.startCol = prevSelectedPos.end.x;\n }\n }\n }\n\n result ??= this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n\n // Search from startRow - 1 to top\n if (!result) {\n searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols);\n for (let y = startRow - 1; y >= 0; y--) {\n searchPosition.startRow = y;\n result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n if (result) {\n break;\n }\n }\n }\n // If we hit the top and didn't search from the very bottom wrap back down\n if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {\n for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {\n searchPosition.startRow = y;\n result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);\n if (result) {\n break;\n }\n }\n }\n\n return result;\n }\n\n /**\n * A found substring is a whole word if it doesn't have an alphanumeric character directly\n * adjacent to it.\n * @param searchIndex starting index of the potential whole word substring\n * @param line entire string in which the potential whole word was found\n * @param term the substring that starts at searchIndex\n */\n private _isWholeWord(searchIndex: number, line: string, term: string): boolean {\n return ((searchIndex === 0) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&\n (((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));\n }\n\n /** `_isWholeWord` gated on the option, so a rejected hit can be stepped past instead of ending the scan. */\n private _satisfiesWholeWord(searchIndex: number, line: string, term: string, searchOptions: ISearchOptions): boolean {\n return !searchOptions.wholeWord || this._isWholeWord(searchIndex, line, term);\n }\n\n /**\n * Whether an earlier `_findInLine` in this same call already scanned this row's line from an\n * equal or lower offset, which makes rescanning it pure O(rows^2) work on one long line. Sound\n * for every option because `_findInLine` returns the first accepted match at or after its\n * offset, which is monotone in that offset. Only valid once such a search has happened \u2014 the\n * wrap-around loop starts at row 0, whose line start may have been trimmed from the scrollback.\n */\n private _isRowCoveredByEarlierSearch(row: number): boolean {\n return this._terminal.buffer.active.getLine(row)?.isWrapped === true;\n }\n\n /**\n * Searches a line for a search term. Takes the provided terminal line and searches the text line,\n * which may contain subsequent terminal lines if the text is wrapped. If the provided line number\n * is part of a wrapped text line that started on an earlier line then it is skipped since it will\n * be properly searched when the terminal line that the text starts on is searched.\n * @param term The search term.\n * @param searchPosition The position to start the search.\n * @param searchOptions Search options.\n * @param isReverseSearch Whether the search should start from the right side of the terminal and\n * search to the left.\n * @returns The search result if it was found.\n */\n private _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {\n // Ignore wrapped lines, only consider on unwrapped line (first row of command string).\n if (isReverseSearch) {\n // Reverse search never rewinds: its caller carries startCol down the rows of the line. Row 0\n // is searched even when wrapped, since its line start may have been trimmed from the scrollback.\n if (searchPosition.startRow > 0 && this._terminal.buffer.active.getLine(searchPosition.startRow)?.isWrapped) {\n searchPosition.startCol += this._terminal.cols;\n return;\n }\n } else {\n // A loop rather than recursion: one frame per wrapped row overflows the stack on a line long\n // enough to fill the scrollback. Bounded at row 0 because after a reflow the buffer's ring\n // holds stale entries at negative indices, so `getLine(-1)` answers with a wrapped line.\n while (searchPosition.startRow > 0 && this._terminal.buffer.active.getLine(searchPosition.startRow)?.isWrapped) {\n searchPosition.startRow--;\n searchPosition.startCol += this._terminal.cols;\n }\n }\n const row = searchPosition.startRow;\n const col = searchPosition.startCol;\n\n let cache = this._lineCache.getLineFromCache(row);\n if (!cache) {\n cache = this._lineCache.translateBufferLineToStringWithWrap(row, true);\n this._lineCache.setLineInCache(row, cache);\n }\n const [stringLine, offsets] = cache;\n\n const offset = this._bufferColsToStringOffset(row, col, offsets);\n let searchTerm = term;\n let searchStringLine = stringLine;\n if (!searchOptions.regex) {\n searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();\n searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();\n }\n\n let resultIndex = -1;\n if (searchOptions.regex) {\n const searchRegex = RegExp(searchTerm, searchOptions.caseSensitive ? 'g' : 'gi');\n let foundTerm: RegExpExecArray | null;\n if (isReverseSearch) {\n // This loop will get the resultIndex of the _last_ regex match in the range 0..offset\n while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {\n const matchIndex = searchRegex.lastIndex - foundTerm[0].length;\n if (foundTerm[0].length > 0 && this._satisfiesWholeWord(matchIndex, searchStringLine, foundTerm[0], searchOptions)) {\n resultIndex = matchIndex;\n term = foundTerm[0];\n }\n searchRegex.lastIndex = matchIndex + 1;\n }\n } else {\n // Driven over the whole line from `offset` rather than over `slice(offset)`: a slice\n // re-anchors ^ and \\b at whatever column the row happened to wrap at, and only\n // first-accepted-match-at-or-after-offset is monotone in `offset`, which is what lets\n // `_isRowCoveredByEarlierSearch` skip a wrapped row an earlier scan already covered.\n searchRegex.lastIndex = offset;\n while (foundTerm = searchRegex.exec(searchStringLine)) {\n const matchIndex = searchRegex.lastIndex - foundTerm[0].length;\n if (foundTerm[0].length > 0 && this._satisfiesWholeWord(matchIndex, searchStringLine, foundTerm[0], searchOptions)) {\n resultIndex = matchIndex;\n term = foundTerm[0];\n break;\n }\n // A zero-length or rejected match would otherwise repeat forever.\n searchRegex.lastIndex = matchIndex + 1;\n }\n }\n } else if (isReverseSearch) {\n let matchIndex = offset - searchTerm.length >= 0 ? searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length) : -1;\n // `lastIndexOf` clamps a negative fromIndex to 0, so index 0 has to end the walk.\n while (matchIndex >= 0 && !this._satisfiesWholeWord(matchIndex, searchStringLine, searchTerm, searchOptions)) {\n matchIndex = matchIndex > 0 ? searchStringLine.lastIndexOf(searchTerm, matchIndex - 1) : -1;\n }\n resultIndex = matchIndex;\n } else {\n let matchIndex = searchStringLine.indexOf(searchTerm, offset);\n while (matchIndex >= 0 && !this._satisfiesWholeWord(matchIndex, searchStringLine, searchTerm, searchOptions)) {\n matchIndex = searchStringLine.indexOf(searchTerm, matchIndex + 1);\n }\n resultIndex = matchIndex;\n }\n\n if (resultIndex >= 0) {\n // Adjust the row number and search index if needed since a \"line\" of text can span multiple\n // rows\n let startRowOffset = 0;\n while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {\n startRowOffset++;\n }\n let endRowOffset = startRowOffset;\n while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) {\n endRowOffset++;\n }\n const startColOffset = resultIndex - offsets[startRowOffset];\n const endColOffset = resultIndex + term.length - offsets[endRowOffset];\n const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset);\n const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset);\n const size = endColIndex - startColIndex + this._terminal.cols * (endRowOffset - startRowOffset);\n\n return {\n term,\n col: startColIndex,\n row: row + startRowOffset,\n size\n };\n }\n }\n\n private _stringLengthToBufferSize(row: number, offset: number): number {\n const line = this._terminal.buffer.active.getLine(row);\n if (!line) {\n return 0;\n }\n for (let i = 0; i < offset; i++) {\n const cell = line.getCell(i);\n if (!cell) {\n break;\n }\n // Adjust the searchIndex to normalize emoji into single chars\n const char = cell.getChars();\n if (char.length > 1) {\n offset -= char.length - 1;\n }\n // Adjust the searchIndex for empty characters following wide unicode\n // chars (eg. CJK)\n const nextCell = line.getCell(i + 1);\n if (nextCell && nextCell.getWidth() === 0) {\n offset++;\n }\n }\n return offset;\n }\n\n /**\n * `cols` counts from the start of the logical line, so summing the cells of every row before the\n * resume point costs O(line) per call and the highlight-all pass makes one call per match.\n * `lineOffsets` already holds the string offset each wrapped row starts at \u2014 the same map used\n * above to turn a match index back into a row \u2014 so only the last, partial row needs cells. It is\n * also the map the row a match lands on is read from, which the cell sum disagreed with by one\n * for a row whose trailing cell is the null placeholder of a wide character that wrapped.\n */\n private _bufferColsToStringOffset(startRow: number, cols: number, lineOffsets: number[]): number {\n const rowsBack = Math.min(Math.floor(cols / this._terminal.cols), lineOffsets.length - 1);\n let offset = lineOffsets[rowsBack];\n const line = this._terminal.buffer.active.getLine(startRow + rowsBack);\n if (line) {\n const colsInRow = Math.min(cols - rowsBack * this._terminal.cols, this._terminal.cols);\n for (let i = 0; i < colsInRow; i++) {\n const cell = line.getCell(i);\n if (!cell) {\n break;\n }\n if (cell.getWidth()) {\n // Treat null characters as whitespace to align with the translateToString API\n offset += cell.getCode() === 0 ? 1 : cell.getChars().length;\n }\n }\n }\n return offset;\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';\nimport type { ISearchDecorationOptions } from '@xterm/addon-search';\nimport { dispose, Disposable, toDisposable } from 'common/Lifecycle';\nimport type { ISearchResult } from './SearchEngine';\n\n/**\n * Interface for managing a highlight decoration.\n */\ninterface IHighlight extends IDisposable {\n decoration: IDecoration;\n match: ISearchResult;\n}\n\n/**\n * Interface for managing multiple decorations for a single match.\n */\ninterface IMultiHighlight extends IDisposable {\n decorations: IDecoration[];\n match: ISearchResult;\n}\n\n/**\n * Manages visual decorations for search results including highlighting and active selection\n * indicators. This class handles the creation, styling, and disposal of search-related decorations.\n */\nexport class DecorationManager extends Disposable {\n private _highlightDecorations: IHighlight[] = [];\n private _highlightedLines: Set = new Set();\n\n constructor(private readonly _terminal: Terminal) {\n super();\n this._register(toDisposable(() => this.clearHighlightDecorations()));\n }\n\n /**\n * Creates decorations for all provided search results.\n * @param results The search results to create decorations for.\n * @param options The decoration options.\n */\n public createHighlightDecorations(results: ISearchResult[], options: ISearchDecorationOptions): void {\n this.clearHighlightDecorations();\n\n for (const match of results) {\n const decorations = this._createResultDecorations(match, options, false);\n if (decorations) {\n for (const decoration of decorations) {\n this._storeDecoration(decoration, match);\n }\n }\n }\n }\n\n /**\n * Creates decorations for the currently active search result.\n * @param result The active search result.\n * @param options The decoration options.\n * @returns The multi-highlight decoration or undefined if creation failed.\n */\n public createActiveDecoration(result: ISearchResult, options: ISearchDecorationOptions): IMultiHighlight | undefined {\n const decorations = this._createResultDecorations(result, options, true);\n if (decorations) {\n return { decorations, match: result, dispose() { dispose(decorations); } };\n }\n return undefined;\n }\n\n /**\n * Clears all highlight decorations.\n */\n public clearHighlightDecorations(): void {\n dispose(this._highlightDecorations);\n this._highlightDecorations = [];\n this._highlightedLines.clear();\n }\n\n /**\n * Stores a decoration and tracks it for management.\n * @param decoration The decoration to store.\n * @param match The search result this decoration represents.\n */\n private _storeDecoration(decoration: IDecoration, match: ISearchResult): void {\n this._highlightedLines.add(decoration.marker.line);\n this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });\n }\n\n /**\n * Applies styles to the decoration when it is rendered.\n * @param element The decoration's element.\n * @param borderColor The border color to apply.\n * @param isActiveResult Whether the element is part of the active search result.\n */\n private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {\n if (!element.classList.contains('xterm-find-result-decoration')) {\n element.classList.add('xterm-find-result-decoration');\n if (borderColor) {\n element.style.outline = `1px solid ${borderColor}`;\n }\n }\n if (isActiveResult) {\n element.classList.add('xterm-find-active-result-decoration');\n }\n }\n\n /**\n * Creates a decoration for the result and applies styles\n * @param result the search result for which to create the decoration\n * @param options the options for the decoration\n * @param isActiveResult whether this is the currently active result\n * @returns the decorations or undefined if the marker has already been disposed of\n */\n private _createResultDecorations(result: ISearchResult, options: ISearchDecorationOptions, isActiveResult: boolean): IDecoration[] | undefined {\n // Gather decoration ranges for this match as it could wrap\n const decorationRanges: [number, number, number][] = [];\n let currentCol = result.col;\n let remainingSize = result.size;\n let markerOffset = -this._terminal.buffer.active.baseY - this._terminal.buffer.active.cursorY + result.row;\n while (remainingSize > 0) {\n const amountThisRow = Math.min(this._terminal.cols - currentCol, remainingSize);\n decorationRanges.push([markerOffset, currentCol, amountThisRow]);\n currentCol = 0;\n remainingSize -= amountThisRow;\n markerOffset++;\n }\n\n // Create the decorations\n const decorations: IDecoration[] = [];\n for (const range of decorationRanges) {\n const marker = this._terminal.registerMarker(range[0]);\n const decoration = this._terminal.registerDecoration({\n marker,\n x: range[1],\n width: range[2],\n layer: isActiveResult ? 'top' : 'bottom',\n backgroundColor: isActiveResult ? options.activeMatchBackground : options.matchBackground,\n overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {\n color: isActiveResult ? options.activeMatchColorOverviewRuler : options.matchOverviewRuler,\n position: 'center'\n }\n });\n if (decoration) {\n const disposables: IDisposable[] = [];\n disposables.push(marker);\n disposables.push(decoration.onRender((e) => this._applyStyles(e, isActiveResult ? options.activeMatchBorder : options.matchBorder, false)));\n disposables.push(decoration.onDispose(() => dispose(disposables)));\n decorations.push(decoration);\n }\n }\n\n return decorations.length === 0 ? undefined : decorations;\n }\n}\n\n\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { ISearchResultChangeEvent } from '@xterm/addon-search';\nimport type { IDisposable } from '@xterm/xterm';\nimport { Emitter, type IEvent } from 'common/Event';\nimport { Disposable } from 'common/Lifecycle';\nimport type { ISearchResult } from './SearchEngine';\n\n/**\n * Interface for managing a currently selected decoration.\n */\ninterface ISelectedDecoration extends IDisposable {\n match: ISearchResult;\n}\n\n/**\n * Tracks search results, manages result indexing, and fires events when results change.\n * This class provides centralized management of search result state and notifications.\n */\nexport class SearchResultTracker extends Disposable {\n private _searchResults: ISearchResult[] = [];\n private _selectedDecoration: ISelectedDecoration | undefined;\n\n private readonly _onDidChangeResults = this._register(new Emitter());\n public get onDidChangeResults(): IEvent { return this._onDidChangeResults.event; }\n\n /**\n * Gets the current search results.\n */\n public get searchResults(): ReadonlyArray {\n return this._searchResults;\n }\n\n /**\n * Gets the currently selected decoration.\n */\n public get selectedDecoration(): ISelectedDecoration | undefined {\n return this._selectedDecoration;\n }\n\n /**\n * Sets the currently selected decoration.\n */\n public set selectedDecoration(decoration: ISelectedDecoration | undefined) {\n this._selectedDecoration = decoration;\n }\n\n /**\n * Updates the search results with a new set of results.\n * @param results The new search results.\n * @param maxResults The maximum number of results to track.\n */\n public updateResults(results: ISearchResult[], maxResults: number): void {\n this._searchResults = results.slice(0, maxResults);\n }\n\n /**\n * Clears all search results.\n */\n public clearResults(): void {\n this._searchResults = [];\n }\n\n /**\n * Clears the selected decoration.\n */\n public clearSelectedDecoration(): void {\n if (this._selectedDecoration) {\n this._selectedDecoration.dispose();\n this._selectedDecoration = undefined;\n }\n }\n\n /**\n * Finds the index of a result in the current results array.\n * @param result The result to find.\n * @returns The index of the result, or -1 if not found.\n */\n public findResultIndex(result: ISearchResult): number {\n for (let i = 0; i < this._searchResults.length; i++) {\n const match = this._searchResults[i];\n if (match.row === result.row && match.col === result.col && match.size === result.size) {\n return i;\n }\n }\n return -1;\n }\n\n /**\n * Fires a result change event with the current state.\n * @param hasDecorations Whether decorations are enabled.\n */\n public fireResultsChanged(hasDecorations: boolean): void {\n if (!hasDecorations) {\n return;\n }\n\n let resultIndex = -1;\n if (this._selectedDecoration) {\n resultIndex = this.findResultIndex(this._selectedDecoration.match);\n }\n\n this._onDidChangeResults.fire({\n resultIndex,\n resultCount: this._searchResults.length\n });\n }\n\n /**\n * Resets all state.\n */\n public reset(): void {\n this.clearSelectedDecoration();\n this.clearResults();\n }\n}\n", "/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';\nimport type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';\nimport { Emitter, type IEvent } from 'common/Event';\nimport { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';\nimport { disposableTimeout } from 'common/Async';\nimport { SearchLineCache } from './SearchLineCache';\nimport { SearchState } from './SearchState';\nimport { SearchEngine, type ISearchResult } from './SearchEngine';\nimport { DecorationManager } from './DecorationManager';\nimport { SearchResultTracker } from './SearchResultTracker';\n\ninterface IInternalSearchOptions {\n noScroll: boolean;\n}\n\n/**\n * Configuration constants for the search addon functionality.\n */\nconst enum Constants {\n /**\n * Default maximum number of search results to highlight simultaneously. This limit prevents\n * performance degradation when searching for very common terms that would result in excessive\n * highlighting decorations.\n */\n DEFAULT_HIGHLIGHT_LIMIT = 1000\n}\n\nexport class SearchAddon extends Disposable implements ITerminalAddon, ISearchApi {\n private _terminal: Terminal | undefined;\n private _highlightLimit: number;\n private _highlightTimeout = this._register(new MutableDisposable());\n private _lineCache = this._register(new MutableDisposable());\n\n // Component instances\n private _state = new SearchState();\n private _engine: SearchEngine | undefined;\n private _decorationManager: DecorationManager | undefined;\n private _resultTracker = this._register(new SearchResultTracker());\n\n private readonly _onAfterSearch = this._register(new Emitter());\n public readonly onAfterSearch = this._onAfterSearch.event;\n private readonly _onBeforeSearch = this._register(new Emitter());\n public readonly onBeforeSearch = this._onBeforeSearch.event;\n\n public get onDidChangeResults(): IEvent {\n return this._resultTracker.onDidChangeResults;\n }\n\n constructor(options?: Partial) {\n super();\n\n this._highlightLimit = options?.highlightLimit ?? Constants.DEFAULT_HIGHLIGHT_LIMIT;\n }\n\n public activate(terminal: Terminal): void {\n this._terminal = terminal;\n this._lineCache.value = new SearchLineCache(terminal);\n this._engine = new SearchEngine(terminal, this._lineCache.value);\n this._decorationManager = new DecorationManager(terminal);\n this._register(this._terminal.onWriteParsed(() => this._updateMatches()));\n this._register(this._terminal.onResize(() => this._updateMatches()));\n this._register(toDisposable(() => this.clearDecorations()));\n }\n\n private _updateMatches(): void {\n this._highlightTimeout.clear();\n if (this._state.cachedSearchTerm && this._state.lastSearchOptions?.decorations) {\n this._highlightTimeout.value = disposableTimeout(() => {\n const term = this._state.cachedSearchTerm;\n this._state.clearCachedTerm();\n this.findPrevious(term!, { ...this._state.lastSearchOptions, incremental: true }, { noScroll: true });\n }, 200);\n }\n }\n\n public clearDecorations(retainCachedSearchTerm?: boolean): void {\n this._resultTracker.clearSelectedDecoration();\n this._decorationManager?.clearHighlightDecorations();\n this._resultTracker.clearResults();\n if (!retainCachedSearchTerm) {\n this._state.clearCachedTerm();\n }\n }\n\n public clearActiveDecoration(): void {\n this._resultTracker.clearSelectedDecoration();\n }\n\n /**\n * Find the next instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options.\n * @returns Whether a result was found.\n */\n public findNext(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n\n this._onBeforeSearch.fire();\n\n this._state.lastSearchOptions = searchOptions;\n\n if (this._state.shouldUpdateHighlighting(term, searchOptions)) {\n this._highlightAllMatches(term, searchOptions!);\n }\n\n const found = this._findNextAndSelect(term, searchOptions, internalSearchOptions);\n this._fireResults(searchOptions);\n this._state.cachedSearchTerm = term;\n\n this._onAfterSearch.fire();\n\n return found;\n }\n\n private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {\n if (!this._terminal || !this._engine || !this._decorationManager) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n if (!this._state.isValidSearchTerm(term)) {\n this.clearDecorations();\n return;\n }\n\n // new search, clear out the old decorations\n this.clearDecorations(true);\n\n const results: ISearchResult[] = [];\n let prevResult: ISearchResult | undefined = undefined;\n let result = this._engine.find(term, 0, 0, searchOptions);\n\n while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {\n if (results.length >= this._highlightLimit) {\n break;\n }\n prevResult = result;\n results.push(prevResult);\n const cols = this._terminal.cols;\n let nextCol = prevResult.col + prevResult.size;\n let nextRow = prevResult.row;\n if (nextCol >= cols) {\n nextRow += Math.floor(nextCol / cols);\n nextCol = nextCol % cols;\n }\n result = this._engine.find(term, nextRow, nextCol, searchOptions);\n }\n\n this._resultTracker.updateResults(results, this._highlightLimit);\n if (searchOptions.decorations) {\n this._decorationManager.createHighlightDecorations(results, searchOptions.decorations);\n }\n }\n\n private _findNextAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n return false;\n }\n if (!this._state.isValidSearchTerm(term)) {\n this._terminal.clearSelection();\n this.clearDecorations();\n return false;\n }\n\n const result = this._engine.findNextWithSelection(term, searchOptions, this._state.cachedSearchTerm);\n return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);\n }\n\n /**\n * Find the previous instance of the term, then scroll to and select it. If it\n * doesn't exist, do nothing.\n * @param term The search term.\n * @param searchOptions Search options.\n * @returns Whether a result was found.\n */\n public findPrevious(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n throw new Error('Cannot use addon until it has been loaded');\n }\n\n this._onBeforeSearch.fire();\n\n this._state.lastSearchOptions = searchOptions;\n\n if (this._state.shouldUpdateHighlighting(term, searchOptions)) {\n this._highlightAllMatches(term, searchOptions!);\n }\n\n const found = this._findPreviousAndSelect(term, searchOptions, internalSearchOptions);\n this._fireResults(searchOptions);\n this._state.cachedSearchTerm = term;\n\n this._onAfterSearch.fire();\n\n return found;\n }\n\n private _fireResults(searchOptions?: ISearchOptions): void {\n this._resultTracker.fireResultsChanged(!!searchOptions?.decorations);\n }\n\n private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions, internalSearchOptions?: IInternalSearchOptions): boolean {\n if (!this._terminal || !this._engine) {\n return false;\n }\n if (!this._state.isValidSearchTerm(term)) {\n this._terminal.clearSelection();\n this.clearDecorations();\n return false;\n }\n\n const result = this._engine.findPreviousWithSelection(term, searchOptions, this._state.cachedSearchTerm);\n return this._selectResult(result, searchOptions?.decorations, internalSearchOptions?.noScroll);\n }\n\n /**\n * Selects and scrolls to a result.\n * @param result The result to select.\n * @returns Whether a result was selected.\n */\n private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {\n if (!this._terminal || !this._decorationManager) {\n return false;\n }\n\n this._resultTracker.clearSelectedDecoration();\n if (!result) {\n this._terminal.clearSelection();\n return false;\n }\n\n this._terminal.select(result.col, result.row, result.size);\n if (options) {\n const activeDecoration = this._decorationManager.createActiveDecoration(result, options);\n if (activeDecoration) {\n this._resultTracker.selectedDecoration = activeDecoration;\n }\n }\n\n if (!noScroll) {\n // If it is not in the viewport then we scroll else it just gets selected\n if (result.row >= (this._terminal.buffer.active.viewportY + this._terminal.rows) || result.row < this._terminal.buffer.active.viewportY) {\n let scroll = result.row - this._terminal.buffer.active.viewportY;\n scroll -= Math.floor(this._terminal.rows / 2);\n this._terminal.scrollLines(scroll);\n }\n }\n return true;\n }\n}\n"],
++ "mappings": ";;;;;;;;;;;;;;;;AAYO,SAASA,EAAaC,EAA6B,CACxD,MAAO,CAAE,QAASA,CAAG,CACvB,CAKO,SAASC,EAA+BC,EAA+C,CAC5F,GAAI,CAACA,EACH,OAAOA,EAET,GAAI,MAAM,QAAQA,CAAG,EAAG,CACtB,QAAWC,KAAKD,EACdC,EAAE,QAAQ,EAEZ,MAAO,CAAC,CACV,CACA,OAAAD,EAAI,QAAQ,EACLA,CACT,CAEO,SAASE,KAAsBC,EAAyC,CAC7E,OAAON,EAAa,IAAME,EAAQI,CAAW,CAAC,CAChD,CAEO,IAAMC,EAAN,KAA6C,CAA7C,cACL,KAAiB,aAAe,IAAI,IACpC,KAAQ,YAAc,GAEtB,IAAW,YAAsB,CAC/B,OAAO,KAAK,WACd,CAEO,IAA2BC,EAAS,CACzC,OAAI,KAAK,YACPA,EAAE,QAAQ,EAEV,KAAK,aAAa,IAAIA,CAAC,EAElBA,CACT,CAEO,SAAgB,CACrB,GAAI,MAAK,YAGT,MAAK,YAAc,GACnB,QAAWJ,KAAK,KAAK,aACnBA,EAAE,QAAQ,EAEZ,KAAK,aAAa,MAAM,EAC1B,CAEO,OAAc,CACnB,QAAWA,KAAK,KAAK,aACnBA,EAAE,QAAQ,EAEZ,KAAK,aAAa,MAAM,CAC1B,CACF,EAEsBK,EAAf,KAAiD,CAAjD,cAGL,KAAmB,OAAS,IAAIF,EAEzB,SAAgB,CACrB,KAAK,OAAO,QAAQ,CACtB,CAEU,UAAiCC,EAAS,CAClD,OAAO,KAAK,OAAO,IAAIA,CAAC,CAC1B,CACF,EAZsBC,EACG,KAAoB,OAAO,OAAO,CAAE,SAAU,CAAE,CAAE,CAAC,EAarE,IAAMC,EAAN,KAAsE,CAAtE,cAEL,KAAQ,YAAc,GAEtB,IAAW,OAAuB,CAChC,OAAO,KAAK,YAAc,OAAY,KAAK,MAC7C,CAEA,IAAW,MAAMC,EAAsB,CACjC,KAAK,aAAeA,IAAU,KAAK,SAGvC,KAAK,QAAQ,QAAQ,EACrB,KAAK,OAASA,EAChB,CAEO,OAAc,CACnB,KAAK,MAAQ,MACf,CAEO,SAAgB,CACrB,KAAK,YAAc,GACnB,KAAK,QAAQ,QAAQ,EACrB,KAAK,OAAS,MAChB,CACF,EClGO,IAAMC,EAAN,KAAiB,CAAjB,cACL,KAAQ,WAAqD,CAAC,EAC9D,KAAQ,UAAY,GAGpB,IAAW,OAAmB,CAC5B,OAAI,KAAK,OACA,KAAK,QAEd,KAAK,OAAS,CAACC,EAAyBC,EAAgBC,IAAkD,CACxG,GAAI,KAAK,UACP,OAAOC,EAAa,IAAM,CAAC,CAAC,EAG9B,IAAMC,EAAQ,CAAE,GAAIJ,EAAU,SAAAC,CAAS,EACvC,KAAK,WAAa,KAAK,WAAW,MAAM,EACxC,KAAK,WAAW,KAAKG,CAAK,EAE1B,IAAMC,EAASF,EAAa,IAAM,CAChC,IAAMG,EAAM,KAAK,WAAW,QAAQF,CAAK,EACrCE,IAAQ,KACV,KAAK,WAAa,KAAK,WAAW,MAAM,EACxC,KAAK,WAAW,OAAOA,EAAK,CAAC,EAEjC,CAAC,EAED,OAAIJ,IACE,MAAM,QAAQA,CAAW,EAC3BA,EAAY,KAAKG,CAAM,EAEvBH,EAAY,IAAIG,CAAM,GAInBA,CACT,EACO,KAAK,OACd,CAEO,KAAKE,EAAgB,CAC1B,GAAI,KAAK,WAAa,CAAC,KAAK,WAAW,OACrC,OAEF,GAAI,KAAK,WAAW,SAAW,EAAG,CAChC,KAAK,WAAW,CAAC,EAAE,GAAG,KAAK,KAAK,WAAW,CAAC,EAAE,SAAUA,CAAK,EAC7D,MACF,CACA,IAAMC,EAAY,KAAK,WACvB,QAASC,EAAI,EAAGC,EAAMF,EAAU,OAAQC,EAAIC,EAAK,EAAED,EACjDD,EAAUC,CAAC,EAAE,GAAG,KAAKD,EAAUC,CAAC,EAAE,SAAUF,CAAK,CAErD,CAEO,SAAgB,CACjB,KAAK,YAGT,KAAK,UAAY,GACjB,KAAK,WAAW,OAAS,EAC3B,CACF,EAEiBI,MAAV,CACE,SAASC,EAAWC,EAAiBC,EAA6B,CACvE,OAAOD,EAAKE,GAAKD,EAAG,KAAKC,CAAC,CAAC,CAC7B,CAFOJ,EAAS,QAAAC,EAIT,SAASI,EAAUT,EAAkBS,EAA6B,CACvE,MAAO,CAAChB,EAAyBC,EAAgBC,IACxCK,EAAME,GAAKT,EAAS,KAAKC,EAAUe,EAAIP,CAAC,CAAC,EAAG,OAAWP,CAAW,CAE7E,CAJOS,EAAS,IAAAK,EAQT,SAASC,KAAUC,EAAgC,CACxD,MAAO,CAAClB,EAAyBC,EAAgBC,IAAkD,CACjG,IAAMiB,EAAQ,IAAIC,EAClB,QAAWb,KAASW,EAClBC,EAAM,IAAIZ,EAAMQ,GAAKf,EAAS,KAAKC,EAAUc,CAAC,CAAC,CAAC,EAElD,OAAIb,IACE,MAAM,QAAQA,CAAW,EAC3BA,EAAY,KAAKiB,CAAK,EAEtBjB,EAAY,IAAIiB,CAAK,GAGlBA,CACT,CACF,CAfOR,EAAS,IAAAM,EAmBT,SAASI,EAAmBd,EAAkBe,EAAqCC,EAA0B,CAClH,OAAAD,EAAQC,CAAO,EACRhB,EAAMQ,GAAKO,EAAQP,CAAC,CAAC,CAC9B,CAHOJ,EAAS,gBAAAU,IAhCDV,IAAA,ICxDV,SAASa,EAAkBC,EAAqBC,EAAU,EAAGC,EAAsC,CACxG,IAAMC,EAAQ,WAAW,IAAM,CAC7BH,EAAQ,EACJE,GACFE,EAAW,QAAQ,CAEvB,EAAGH,CAAO,EACJG,EAAaC,EAAa,IAAM,CACpC,aAAaF,CAAK,CACpB,CAAC,EACD,OAAAD,GAAO,IAAIE,CAAU,EACdA,CACT,CCDO,IAAME,EAAN,cAA8BC,CAAW,CAa9C,YAA6BC,EAAqB,CAChD,MAAM,EADqB,eAAAA,EAN7B,KAAQ,mBAAqB,KAAK,UAAU,IAAIC,CAAmB,EACnE,KAAQ,uBAAyB,KAAK,UAAU,IAAIA,CAAmB,EAGvE,KAAQ,qBAAuB,EAI7B,KAAK,UAAUC,EAAa,IAAM,KAAK,mBAAmB,CAAC,CAAC,CAC9D,CAKO,gBAAuB,CACvB,KAAK,cACR,KAAK,YAAc,IAAI,MAAM,KAAK,UAAU,OAAO,OAAO,MAAM,EAChE,KAAK,uBAAuB,MAAQC,EAClC,KAAK,UAAU,WAAW,IAAM,KAAK,mBAAmB,CAAC,EACzD,KAAK,UAAU,aAAa,IAAM,KAAK,mBAAmB,CAAC,EAC3D,KAAK,UAAU,SAAS,IAAM,KAAK,mBAAmB,CAAC,CACzD,GAGF,KAAK,qBAAuB,KAAK,IAAI,EAChC,KAAK,mBAAmB,OAC3B,KAAK,2BAA2B,IAAkC,CAEtE,CAEQ,oBAA2B,CACjC,KAAK,YAAc,OACnB,KAAK,qBAAuB,EAC5B,KAAK,uBAAuB,MAAM,EAClC,KAAK,mBAAmB,MAAM,CAChC,CAEQ,2BAA2BC,EAAqB,CACtD,KAAK,mBAAmB,MAAQC,EAAkB,IAAM,CACtD,GAAI,CAAC,KAAK,YACR,OAGF,IAAMC,EADM,KAAK,IAAI,EACC,KAAK,qBAC3B,GAAIA,GAAW,KAAoC,CACjD,KAAK,mBAAmB,EACxB,MACF,CACA,KAAK,2BAA2B,KAAqCA,CAAO,CAC9E,EAAGF,CAAK,CACV,CAEO,iBAAiBG,EAAyC,CAC/D,OAAO,KAAK,cAAcA,CAAG,CAC/B,CAEO,eAAeA,EAAaC,EAA6B,CAC1D,KAAK,cACP,KAAK,YAAYD,CAAG,EAAIC,EAE5B,CAUO,oCAAoCC,EAAmBC,EAAoC,CAChG,IAAMC,EAAU,CAAC,EACXC,EAAc,CAAC,CAAC,EAIhBC,EAAe,KAAK,UAAU,OAAO,OAAO,OAC9CC,EAAO,KAAK,UAAU,OAAO,OAAO,QAAQL,CAAS,EACzD,KAAOK,GAAM,CACX,IAAMC,EAAWN,EAAY,EAAII,EAAe,KAAK,UAAU,OAAO,OAAO,QAAQJ,EAAY,CAAC,EAAI,OAChGO,EAAkBD,EAAWA,EAAS,UAAY,GACpDE,EAASH,EAAK,kBAAkB,CAACE,GAAmBN,CAAS,EACjE,GAAIM,GAAmBD,EAAU,CAC/B,IAAMG,EAAWJ,EAAK,QAAQA,EAAK,OAAS,CAAC,EACtBI,GAAYA,EAAS,QAAQ,IAAM,GAAKA,EAAS,SAAS,IAAM,GAEjEH,EAAS,QAAQ,CAAC,GAAG,SAAS,IAAM,IACxDE,EAASA,EAAO,MAAM,EAAG,EAAE,EAE/B,CAEA,GADAN,EAAQ,KAAKM,CAAM,EACfD,EACFJ,EAAY,KAAKA,EAAYA,EAAY,OAAS,CAAC,EAAIK,EAAO,MAAM,MAEpE,OAEFR,IACAK,EAAOC,CACT,CACA,MAAO,CAACJ,EAAQ,KAAK,EAAE,EAAGC,CAAW,CACvC,CACF,EChIO,IAAMO,EAAN,KAAkB,CAOvB,IAAW,kBAAuC,CAChD,OAAO,KAAK,iBACd,CAKA,IAAW,iBAAiBC,EAA0B,CACpD,KAAK,kBAAoBA,CAC3B,CAKA,IAAW,mBAAgD,CACzD,OAAO,KAAK,kBACd,CAKA,IAAW,kBAAkBC,EAAqC,CAChE,KAAK,mBAAqBA,CAC5B,CAOO,kBAAkBD,EAAuB,CAC9C,MAAO,CAAC,EAAEA,GAAQA,EAAK,OAAS,EAClC,CAOO,iBAAiBE,EAAsC,CAC5D,OAAK,KAAK,mBAGLA,EAGD,KAAK,mBAAmB,gBAAkBA,EAAW,eAGrD,KAAK,mBAAmB,QAAUA,EAAW,OAG7C,KAAK,mBAAmB,YAAcA,EAAW,UAR5C,GAHA,EAeX,CAQO,yBAAyBF,EAAcC,EAAmC,CAC/E,OAAKA,GAAS,YAGP,KAAK,oBAAsB,QAC3BD,IAAS,KAAK,mBACd,KAAK,iBAAiBC,CAAO,EAJ3B,EAKX,CAKO,iBAAwB,CAC7B,KAAK,kBAAoB,MAC3B,CAKO,OAAc,CACnB,KAAK,kBAAoB,OACzB,KAAK,mBAAqB,MAC5B,CACF,EC9DO,IAAME,EAAN,KAAmB,CACxB,YACmBC,EACAC,EACjB,CAFiB,eAAAD,EACA,gBAAAC,CAChB,CAUI,KAAKC,EAAcC,EAAkBC,EAAkBC,EAA2D,CACvH,GAAI,CAACH,GAAQA,EAAK,SAAW,EAAG,CAC9B,KAAK,UAAU,eAAe,EAC9B,MACF,CACA,GAAIE,GAAY,KAAK,UAAU,KAC7B,MAAM,IAAI,MAAM,gBAAgBA,CAAQ,6BAA6B,KAAK,UAAU,IAAI,OAAO,EAGjG,KAAK,WAAW,eAAe,EAE/B,IAAME,EAAkC,CACtC,SAAAH,EACA,SAAAC,CACF,EAGIG,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,EAEjE,GAAI,CAACE,EACH,QAASC,EAAIL,EAAW,EAAGK,EAAI,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,MAC7E,QAAK,6BAA6BA,CAAC,IAGvCF,EAAe,SAAWE,EAC1BF,EAAe,SAAW,EAC1BC,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,EACzDE,IAPmFC,IACvF,CAWJ,OAAOD,CACT,CASO,sBAAsBL,EAAcG,EAAgCI,EAAsD,CAC/H,GAAI,CAACP,GAAQA,EAAK,SAAW,EAAG,CAC9B,KAAK,UAAU,eAAe,EAC9B,MACF,CAEA,IAAMQ,EAAkB,KAAK,UAAU,qBAAqB,EAC5D,KAAK,UAAU,eAAe,EAE9B,IAAIN,EAAW,EACXD,EAAW,EACXO,IACED,IAAqBP,GACvBE,EAAWM,EAAgB,IAAI,EAC/BP,EAAWO,EAAgB,IAAI,IAE/BN,EAAWM,EAAgB,MAAM,EACjCP,EAAWO,EAAgB,MAAM,IAIrC,KAAK,WAAW,eAAe,EAE/B,IAAMJ,EAAkC,CACtC,SAAAH,EACA,SAAAC,CACF,EAGIG,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,EAEjE,GAAI,CAACE,EACH,QAASC,EAAIL,EAAW,EAAGK,EAAI,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,MAC7E,QAAK,6BAA6BA,CAAC,IAGvCF,EAAe,SAAWE,EAC1BF,EAAe,SAAW,EAC1BC,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,EACzDE,IAPmFC,IACvF,CAYJ,GAAI,CAACD,GAAUJ,IAAa,EAC1B,QAASK,EAAI,EAAGA,EAAIL,GAGd,IAAAK,EAAI,GAAK,KAAK,6BAA6BA,CAAC,KAGhDF,EAAe,SAAWE,EAC1BF,EAAe,SAAW,EAC1BC,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,EACzDE,IATwBC,IAG5B,CAaJ,MAAI,CAACD,GAAUG,IACbJ,EAAe,SAAWI,EAAgB,MAAM,EAChDJ,EAAe,SAAW,EAC1BC,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,CAAa,GAGxDE,CACT,CASO,0BAA0BL,EAAcG,EAAgCI,EAAsD,CACnI,GAAI,CAACP,GAAQA,EAAK,SAAW,EAAG,CAC9B,KAAK,UAAU,eAAe,EAC9B,MACF,CAEA,IAAMQ,EAAkB,KAAK,UAAU,qBAAqB,EAC5D,KAAK,UAAU,eAAe,EAE9B,IAAIP,EAAW,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,KAAO,EACpEC,EAAW,KAAK,UAAU,KAC1BO,EAAkB,GAExB,KAAK,WAAW,eAAe,EAC/B,IAAML,EAAkC,CACtC,SAAAH,EACA,SAAAC,CACF,EAEIG,EAkBJ,GAjBIG,IACFJ,EAAe,SAAWH,EAAWO,EAAgB,MAAM,EAC3DJ,EAAe,SAAWI,EAAgB,MAAM,EAC5CD,IAAqBP,IAEvBK,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,EAAe,EAAK,EAC/DE,IAEHD,EAAe,SAAWH,EAAWO,EAAgB,IAAI,EACzDJ,EAAe,SAAWI,EAAgB,IAAI,KAKpDH,IAAW,KAAK,YAAYL,EAAMI,EAAgBD,EAAeM,CAAe,EAG5E,CAACJ,EAAQ,CACXD,EAAe,SAAW,KAAK,IAAIA,EAAe,SAAU,KAAK,UAAU,IAAI,EAC/E,QAASE,EAAIL,EAAW,EAAGK,GAAK,IAC9BF,EAAe,SAAWE,EAC1BD,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,EAAeM,CAAe,EAC1E,CAAAJ,GAH6BC,IAGjC,CAIJ,CAEA,GAAI,CAACD,GAAUJ,IAAc,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,KAAO,EACtF,QAASK,EAAK,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,KAAO,EAAIA,GAAKL,IAChFG,EAAe,SAAWE,EAC1BD,EAAS,KAAK,YAAYL,EAAMI,EAAgBD,EAAeM,CAAe,EAC1E,CAAAJ,GAHsFC,IAG1F,CAMJ,OAAOD,CACT,CASQ,aAAaK,EAAqBC,EAAcX,EAAuB,CAC7E,OAASU,IAAgB,GAAO,qCAA8B,SAASC,EAAKD,EAAc,CAAC,CAAC,KACvFA,EAAcV,EAAK,SAAYW,EAAK,QAAY,qCAA8B,SAASA,EAAKD,EAAcV,EAAK,MAAM,CAAC,EAC7H,CAGQ,oBAAoBU,EAAqBC,EAAcX,EAAcG,EAAwC,CACnH,MAAO,CAACA,EAAc,WAAa,KAAK,aAAaO,EAAaC,EAAMX,CAAI,CAC9E,CASQ,6BAA6BY,EAAsB,CACzD,OAAO,KAAK,UAAU,OAAO,OAAO,QAAQA,CAAG,GAAG,YAAc,EAClE,CAcQ,YAAYZ,EAAcI,EAAiCD,EAAgC,CAAC,EAAGM,EAA2B,GAAkC,CAElK,GAAIA,GAGF,GAAIL,EAAe,SAAW,GAAK,KAAK,UAAU,OAAO,OAAO,QAAQA,EAAe,QAAQ,GAAG,UAAW,CAC3GA,EAAe,UAAY,KAAK,UAAU,KAC1C,MACF,MAKA,MAAOA,EAAe,SAAW,GAAK,KAAK,UAAU,OAAO,OAAO,QAAQA,EAAe,QAAQ,GAAG,WACnGA,EAAe,WACfA,EAAe,UAAY,KAAK,UAAU,KAG9C,IAAMQ,EAAMR,EAAe,SACrBS,EAAMT,EAAe,SAEvBU,EAAQ,KAAK,WAAW,iBAAiBF,CAAG,EAC3CE,IACHA,EAAQ,KAAK,WAAW,oCAAoCF,EAAK,EAAI,EACrE,KAAK,WAAW,eAAeA,EAAKE,CAAK,GAE3C,GAAM,CAACC,EAAYC,CAAO,EAAIF,EAExBG,EAAS,KAAK,0BAA0BL,EAAKC,EAAKG,CAAO,EAC3DE,EAAalB,EACbmB,EAAmBJ,EAClBZ,EAAc,QACjBe,EAAaf,EAAc,cAAgBH,EAAOA,EAAK,YAAY,EACnEmB,EAAmBhB,EAAc,cAAgBY,EAAaA,EAAW,YAAY,GAGvF,IAAIK,EAAc,GAClB,GAAIjB,EAAc,MAAO,CACvB,IAAMkB,EAAc,OAAOH,EAAYf,EAAc,cAAgB,IAAM,IAAI,EAC3EmB,EACJ,GAAIb,EAEF,KAAOa,EAAYD,EAAY,KAAKF,EAAiB,MAAM,EAAGF,CAAM,CAAC,GAAG,CACtE,IAAMM,EAAaF,EAAY,UAAYC,EAAU,CAAC,EAAE,OACpDA,EAAU,CAAC,EAAE,OAAS,GAAK,KAAK,oBAAoBC,EAAYJ,EAAkBG,EAAU,CAAC,EAAGnB,CAAa,IAC/GiB,EAAcG,EACdvB,EAAOsB,EAAU,CAAC,GAEpBD,EAAY,UAAYE,EAAa,CACvC,KAOA,KADAF,EAAY,UAAYJ,EACjBK,EAAYD,EAAY,KAAKF,CAAgB,GAAG,CACrD,IAAMI,EAAaF,EAAY,UAAYC,EAAU,CAAC,EAAE,OACxD,GAAIA,EAAU,CAAC,EAAE,OAAS,GAAK,KAAK,oBAAoBC,EAAYJ,EAAkBG,EAAU,CAAC,EAAGnB,CAAa,EAAG,CAClHiB,EAAcG,EACdvB,EAAOsB,EAAU,CAAC,EAClB,KACF,CAEAD,EAAY,UAAYE,EAAa,CACvC,CAEJ,SAAWd,EAAiB,CAC1B,IAAIc,EAAaN,EAASC,EAAW,QAAU,EAAIC,EAAiB,YAAYD,EAAYD,EAASC,EAAW,MAAM,EAAI,GAE1H,KAAOK,GAAc,GAAK,CAAC,KAAK,oBAAoBA,EAAYJ,EAAkBD,EAAYf,CAAa,GACzGoB,EAAaA,EAAa,EAAIJ,EAAiB,YAAYD,EAAYK,EAAa,CAAC,EAAI,GAE3FH,EAAcG,CAChB,KAAO,CACL,IAAIA,EAAaJ,EAAiB,QAAQD,EAAYD,CAAM,EAC5D,KAAOM,GAAc,GAAK,CAAC,KAAK,oBAAoBA,EAAYJ,EAAkBD,EAAYf,CAAa,GACzGoB,EAAaJ,EAAiB,QAAQD,EAAYK,EAAa,CAAC,EAElEH,EAAcG,CAChB,CAEA,GAAIH,GAAe,EAAG,CAGpB,IAAII,EAAiB,EACrB,KAAOA,EAAiBR,EAAQ,OAAS,GAAKI,GAAeJ,EAAQQ,EAAiB,CAAC,GACrFA,IAEF,IAAIC,EAAeD,EACnB,KAAOC,EAAeT,EAAQ,OAAS,GAAKI,EAAcpB,EAAK,QAAUgB,EAAQS,EAAe,CAAC,GAC/FA,IAEF,IAAMC,EAAiBN,EAAcJ,EAAQQ,CAAc,EACrDG,EAAeP,EAAcpB,EAAK,OAASgB,EAAQS,CAAY,EAC/DG,EAAgB,KAAK,0BAA0BhB,EAAMY,EAAgBE,CAAc,EAEnFG,EADc,KAAK,0BAA0BjB,EAAMa,EAAcE,CAAY,EACxDC,EAAgB,KAAK,UAAU,MAAQH,EAAeD,GAEjF,MAAO,CACL,KAAAxB,EACA,IAAK4B,EACL,IAAKhB,EAAMY,EACX,KAAAK,CACF,CACF,CACF,CAEQ,0BAA0BjB,EAAaK,EAAwB,CACrE,IAAMN,EAAO,KAAK,UAAU,OAAO,OAAO,QAAQC,CAAG,EACrD,GAAI,CAACD,EACH,MAAO,GAET,QAASmB,EAAI,EAAGA,EAAIb,EAAQa,IAAK,CAC/B,IAAMC,EAAOpB,EAAK,QAAQmB,CAAC,EAC3B,GAAI,CAACC,EACH,MAGF,IAAMC,EAAOD,EAAK,SAAS,EACvBC,EAAK,OAAS,IAChBf,GAAUe,EAAK,OAAS,GAI1B,IAAMC,EAAWtB,EAAK,QAAQmB,EAAI,CAAC,EAC/BG,GAAYA,EAAS,SAAS,IAAM,GACtChB,GAEJ,CACA,OAAOA,CACT,CAUQ,0BAA0BhB,EAAkBiC,EAAcC,EAA+B,CAC/F,IAAMC,EAAW,KAAK,IAAI,KAAK,MAAMF,EAAO,KAAK,UAAU,IAAI,EAAGC,EAAY,OAAS,CAAC,EACpFlB,EAASkB,EAAYC,CAAQ,EAC3BzB,EAAO,KAAK,UAAU,OAAO,OAAO,QAAQV,EAAWmC,CAAQ,EACrE,GAAIzB,EAAM,CACR,IAAM0B,EAAY,KAAK,IAAIH,EAAOE,EAAW,KAAK,UAAU,KAAM,KAAK,UAAU,IAAI,EACrF,QAASN,EAAI,EAAGA,EAAIO,EAAWP,IAAK,CAClC,IAAMC,EAAOpB,EAAK,QAAQmB,CAAC,EAC3B,GAAI,CAACC,EACH,MAEEA,EAAK,SAAS,IAEhBd,GAAUc,EAAK,QAAQ,IAAM,EAAI,EAAIA,EAAK,SAAS,EAAE,OAEzD,CACF,CACA,OAAOd,CACT,CACF,ECxZO,IAAMqB,EAAN,cAAgCC,CAAW,CAIhD,YAA6BC,EAAqB,CAChD,MAAM,EADqB,eAAAA,EAH7B,KAAQ,sBAAsC,CAAC,EAC/C,KAAQ,kBAAiC,IAAI,IAI3C,KAAK,UAAUC,EAAa,IAAM,KAAK,0BAA0B,CAAC,CAAC,CACrE,CAOO,2BAA2BC,EAA0BC,EAAyC,CACnG,KAAK,0BAA0B,EAE/B,QAAWC,KAASF,EAAS,CAC3B,IAAMG,EAAc,KAAK,yBAAyBD,EAAOD,EAAS,EAAK,EACvE,GAAIE,EACF,QAAWC,KAAcD,EACvB,KAAK,iBAAiBC,EAAYF,CAAK,CAG7C,CACF,CAQO,uBAAuBG,EAAuBJ,EAAgE,CACnH,IAAME,EAAc,KAAK,yBAAyBE,EAAQJ,EAAS,EAAI,EACvE,GAAIE,EACF,MAAO,CAAE,YAAAA,EAAa,MAAOE,EAAQ,SAAU,CAAEC,EAAQH,CAAW,CAAG,CAAE,CAG7E,CAKO,2BAAkC,CACvCG,EAAQ,KAAK,qBAAqB,EAClC,KAAK,sBAAwB,CAAC,EAC9B,KAAK,kBAAkB,MAAM,CAC/B,CAOQ,iBAAiBF,EAAyBF,EAA4B,CAC5E,KAAK,kBAAkB,IAAIE,EAAW,OAAO,IAAI,EACjD,KAAK,sBAAsB,KAAK,CAAE,WAAAA,EAAY,MAAAF,EAAO,SAAU,CAAEE,EAAW,QAAQ,CAAG,CAAE,CAAC,CAC5F,CAQQ,aAAaG,EAAsBC,EAAiCC,EAA+B,CACpGF,EAAQ,UAAU,SAAS,8BAA8B,IAC5DA,EAAQ,UAAU,IAAI,8BAA8B,EAChDC,IACFD,EAAQ,MAAM,QAAU,aAAaC,CAAW,KAGhDC,GACFF,EAAQ,UAAU,IAAI,qCAAqC,CAE/D,CASQ,yBAAyBF,EAAuBJ,EAAmCQ,EAAoD,CAE7I,IAAMC,EAA+C,CAAC,EAClDC,EAAaN,EAAO,IACpBO,EAAgBP,EAAO,KACvBQ,EAAe,CAAC,KAAK,UAAU,OAAO,OAAO,MAAQ,KAAK,UAAU,OAAO,OAAO,QAAUR,EAAO,IACvG,KAAOO,EAAgB,GAAG,CACxB,IAAME,EAAgB,KAAK,IAAI,KAAK,UAAU,KAAOH,EAAYC,CAAa,EAC9EF,EAAiB,KAAK,CAACG,EAAcF,EAAYG,CAAa,CAAC,EAC/DH,EAAa,EACbC,GAAiBE,EACjBD,GACF,CAGA,IAAMV,EAA6B,CAAC,EACpC,QAAWY,KAASL,EAAkB,CACpC,IAAMM,EAAS,KAAK,UAAU,eAAeD,EAAM,CAAC,CAAC,EAC/CX,EAAa,KAAK,UAAU,mBAAmB,CACnD,OAAAY,EACA,EAAGD,EAAM,CAAC,EACV,MAAOA,EAAM,CAAC,EACd,MAAON,EAAiB,MAAQ,SAChC,gBAAiBA,EAAiBR,EAAQ,sBAAwBA,EAAQ,gBAC1E,qBAAsB,KAAK,kBAAkB,IAAIe,EAAO,IAAI,EAAI,OAAY,CAC1E,MAAOP,EAAiBR,EAAQ,8BAAgCA,EAAQ,mBACxE,SAAU,QACZ,CACF,CAAC,EACD,GAAIG,EAAY,CACd,IAAMa,EAA6B,CAAC,EACpCA,EAAY,KAAKD,CAAM,EACvBC,EAAY,KAAKb,EAAW,SAAUc,GAAM,KAAK,aAAaA,EAAGT,EAAiBR,EAAQ,kBAAoBA,EAAQ,YAAa,EAAK,CAAC,CAAC,EAC1IgB,EAAY,KAAKb,EAAW,UAAU,IAAME,EAAQW,CAAW,CAAC,CAAC,EACjEd,EAAY,KAAKC,CAAU,CAC7B,CACF,CAEA,OAAOD,EAAY,SAAW,EAAI,OAAYA,CAChD,CACF,ECrIO,IAAMgB,EAAN,cAAkCC,CAAW,CAA7C,kCACL,KAAQ,eAAkC,CAAC,EAG3C,KAAiB,oBAAsB,KAAK,UAAU,IAAIC,CAAmC,EAC7F,IAAW,oBAAuD,CAAE,OAAO,KAAK,oBAAoB,KAAO,CAK3G,IAAW,eAA8C,CACvD,OAAO,KAAK,cACd,CAKA,IAAW,oBAAsD,CAC/D,OAAO,KAAK,mBACd,CAKA,IAAW,mBAAmBC,EAA6C,CACzE,KAAK,oBAAsBA,CAC7B,CAOO,cAAcC,EAA0BC,EAA0B,CACvE,KAAK,eAAiBD,EAAQ,MAAM,EAAGC,CAAU,CACnD,CAKO,cAAqB,CAC1B,KAAK,eAAiB,CAAC,CACzB,CAKO,yBAAgC,CACjC,KAAK,sBACP,KAAK,oBAAoB,QAAQ,EACjC,KAAK,oBAAsB,OAE/B,CAOO,gBAAgBC,EAA+B,CACpD,QAASC,EAAI,EAAGA,EAAI,KAAK,eAAe,OAAQA,IAAK,CACnD,IAAMC,EAAQ,KAAK,eAAeD,CAAC,EACnC,GAAIC,EAAM,MAAQF,EAAO,KAAOE,EAAM,MAAQF,EAAO,KAAOE,EAAM,OAASF,EAAO,KAChF,OAAOC,CAEX,CACA,MAAO,EACT,CAMO,mBAAmBE,EAA+B,CACvD,GAAI,CAACA,EACH,OAGF,IAAIC,EAAc,GACd,KAAK,sBACPA,EAAc,KAAK,gBAAgB,KAAK,oBAAoB,KAAK,GAGnE,KAAK,oBAAoB,KAAK,CAC5B,YAAAA,EACA,YAAa,KAAK,eAAe,MACnC,CAAC,CACH,CAKO,OAAc,CACnB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,CACpB,CACF,ECtFO,IAAMC,EAAN,cAA0BC,CAAiD,CAqBhF,YAAYC,EAAwC,CAClD,MAAM,EAnBR,KAAQ,kBAAoB,KAAK,UAAU,IAAIC,CAAgC,EAC/E,KAAQ,WAAa,KAAK,UAAU,IAAIA,CAAoC,EAG5E,KAAQ,OAAS,IAAIC,EAGrB,KAAQ,eAAiB,KAAK,UAAU,IAAIC,CAAqB,EAEjE,KAAiB,eAAiB,KAAK,UAAU,IAAIC,CAAe,EACpE,KAAgB,cAAgB,KAAK,eAAe,MACpD,KAAiB,gBAAkB,KAAK,UAAU,IAAIA,CAAe,EACrE,KAAgB,eAAiB,KAAK,gBAAgB,MASpD,KAAK,gBAAkBJ,GAAS,gBAAkB,GACpD,CARA,IAAW,oBAAuD,CAChE,OAAO,KAAK,eAAe,kBAC7B,CAQO,SAASK,EAA0B,CACxC,KAAK,UAAYA,EACjB,KAAK,WAAW,MAAQ,IAAIC,EAAgBD,CAAQ,EACpD,KAAK,QAAU,IAAIE,EAAaF,EAAU,KAAK,WAAW,KAAK,EAC/D,KAAK,mBAAqB,IAAIG,EAAkBH,CAAQ,EACxD,KAAK,UAAU,KAAK,UAAU,cAAc,IAAM,KAAK,eAAe,CAAC,CAAC,EACxE,KAAK,UAAU,KAAK,UAAU,SAAS,IAAM,KAAK,eAAe,CAAC,CAAC,EACnE,KAAK,UAAUI,EAAa,IAAM,KAAK,iBAAiB,CAAC,CAAC,CAC5D,CAEQ,gBAAuB,CAC7B,KAAK,kBAAkB,MAAM,EACzB,KAAK,OAAO,kBAAoB,KAAK,OAAO,mBAAmB,cACjE,KAAK,kBAAkB,MAAQC,EAAkB,IAAM,CACrD,IAAMC,EAAO,KAAK,OAAO,iBACzB,KAAK,OAAO,gBAAgB,EAC5B,KAAK,aAAaA,EAAO,CAAE,GAAG,KAAK,OAAO,kBAAmB,YAAa,EAAK,EAAG,CAAE,SAAU,EAAK,CAAC,CACtG,EAAG,GAAG,EAEV,CAEO,iBAAiBC,EAAwC,CAC9D,KAAK,eAAe,wBAAwB,EAC5C,KAAK,oBAAoB,0BAA0B,EACnD,KAAK,eAAe,aAAa,EAC5BA,GACH,KAAK,OAAO,gBAAgB,CAEhC,CAEO,uBAA8B,CACnC,KAAK,eAAe,wBAAwB,CAC9C,CASO,SAASD,EAAcE,EAAgCC,EAAyD,CACrH,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,QAC3B,MAAM,IAAI,MAAM,2CAA2C,EAG7D,KAAK,gBAAgB,KAAK,EAE1B,KAAK,OAAO,kBAAoBD,EAE5B,KAAK,OAAO,yBAAyBF,EAAME,CAAa,GAC1D,KAAK,qBAAqBF,EAAME,CAAc,EAGhD,IAAME,EAAQ,KAAK,mBAAmBJ,EAAME,EAAeC,CAAqB,EAChF,YAAK,aAAaD,CAAa,EAC/B,KAAK,OAAO,iBAAmBF,EAE/B,KAAK,eAAe,KAAK,EAElBI,CACT,CAEQ,qBAAqBJ,EAAcE,EAAqC,CAC9E,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,SAAW,CAAC,KAAK,mBAC5C,MAAM,IAAI,MAAM,2CAA2C,EAE7D,GAAI,CAAC,KAAK,OAAO,kBAAkBF,CAAI,EAAG,CACxC,KAAK,iBAAiB,EACtB,MACF,CAGA,KAAK,iBAAiB,EAAI,EAE1B,IAAMK,EAA2B,CAAC,EAC9BC,EACAC,EAAS,KAAK,QAAQ,KAAKP,EAAM,EAAG,EAAGE,CAAa,EAExD,KAAOK,IAAWD,GAAY,MAAQC,EAAO,KAAOD,GAAY,MAAQC,EAAO,MACzE,EAAAF,EAAQ,QAAU,KAAK,kBADwD,CAInFC,EAAaC,EACbF,EAAQ,KAAKC,CAAU,EACvB,IAAME,EAAO,KAAK,UAAU,KACxBC,EAAUH,EAAW,IAAMA,EAAW,KACtCI,EAAUJ,EAAW,IACrBG,GAAWD,IACbE,GAAW,KAAK,MAAMD,EAAUD,CAAI,EACpCC,EAAUA,EAAUD,GAEtBD,EAAS,KAAK,QAAQ,KAAKP,EAAMU,EAASD,EAASP,CAAa,CAClE,CAEA,KAAK,eAAe,cAAcG,EAAS,KAAK,eAAe,EAC3DH,EAAc,aAChB,KAAK,mBAAmB,2BAA2BG,EAASH,EAAc,WAAW,CAEzF,CAEQ,mBAAmBF,EAAcE,EAAgCC,EAAyD,CAChI,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,QAC3B,MAAO,GAET,GAAI,CAAC,KAAK,OAAO,kBAAkBH,CAAI,EACrC,YAAK,UAAU,eAAe,EAC9B,KAAK,iBAAiB,EACf,GAGT,IAAMO,EAAS,KAAK,QAAQ,sBAAsBP,EAAME,EAAe,KAAK,OAAO,gBAAgB,EACnG,OAAO,KAAK,cAAcK,EAAQL,GAAe,YAAaC,GAAuB,QAAQ,CAC/F,CASO,aAAaH,EAAcE,EAAgCC,EAAyD,CACzH,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,QAC3B,MAAM,IAAI,MAAM,2CAA2C,EAG7D,KAAK,gBAAgB,KAAK,EAE1B,KAAK,OAAO,kBAAoBD,EAE5B,KAAK,OAAO,yBAAyBF,EAAME,CAAa,GAC1D,KAAK,qBAAqBF,EAAME,CAAc,EAGhD,IAAME,EAAQ,KAAK,uBAAuBJ,EAAME,EAAeC,CAAqB,EACpF,YAAK,aAAaD,CAAa,EAC/B,KAAK,OAAO,iBAAmBF,EAE/B,KAAK,eAAe,KAAK,EAElBI,CACT,CAEQ,aAAaF,EAAsC,CACzD,KAAK,eAAe,mBAAmB,CAAC,CAACA,GAAe,WAAW,CACrE,CAEQ,uBAAuBF,EAAcE,EAAgCC,EAAyD,CACpI,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,QAC3B,MAAO,GAET,GAAI,CAAC,KAAK,OAAO,kBAAkBH,CAAI,EACrC,YAAK,UAAU,eAAe,EAC9B,KAAK,iBAAiB,EACf,GAGT,IAAMO,EAAS,KAAK,QAAQ,0BAA0BP,EAAME,EAAe,KAAK,OAAO,gBAAgB,EACvG,OAAO,KAAK,cAAcK,EAAQL,GAAe,YAAaC,GAAuB,QAAQ,CAC/F,CAOQ,cAAcI,EAAmClB,EAAoCsB,EAA6B,CACxH,GAAI,CAAC,KAAK,WAAa,CAAC,KAAK,mBAC3B,MAAO,GAIT,GADA,KAAK,eAAe,wBAAwB,EACxC,CAACJ,EACH,YAAK,UAAU,eAAe,EACvB,GAIT,GADA,KAAK,UAAU,OAAOA,EAAO,IAAKA,EAAO,IAAKA,EAAO,IAAI,EACrDlB,EAAS,CACX,IAAMuB,EAAmB,KAAK,mBAAmB,uBAAuBL,EAAQlB,CAAO,EACnFuB,IACF,KAAK,eAAe,mBAAqBA,EAE7C,CAEA,GAAI,CAACD,IAECJ,EAAO,KAAQ,KAAK,UAAU,OAAO,OAAO,UAAY,KAAK,UAAU,MAASA,EAAO,IAAM,KAAK,UAAU,OAAO,OAAO,WAAW,CACvI,IAAIM,EAASN,EAAO,IAAM,KAAK,UAAU,OAAO,OAAO,UACvDM,GAAU,KAAK,MAAM,KAAK,UAAU,KAAO,CAAC,EAC5C,KAAK,UAAU,YAAYA,CAAM,CACnC,CAEF,MAAO,EACT,CACF",
++ "names": ["toDisposable", "fn", "dispose", "arg", "d", "combinedDisposable", "disposables", "DisposableStore", "o", "Disposable", "MutableDisposable", "value", "Emitter", "listener", "thisArgs", "disposables", "toDisposable", "entry", "result", "idx", "event", "listeners", "i", "len", "EventUtils", "forward", "from", "to", "e", "map", "any", "events", "store", "DisposableStore", "runAndSubscribe", "handler", "initial", "disposableTimeout", "handler", "timeout", "store", "timer", "disposable", "toDisposable", "SearchLineCache", "Disposable", "_terminal", "MutableDisposable", "toDisposable", "combinedDisposable", "delay", "disposableTimeout", "elapsed", "row", "entry", "lineIndex", "trimRight", "strings", "lineOffsets", "bufferLength", "line", "nextLine", "lineWrapsToNext", "string", "lastCell", "SearchState", "term", "options", "newOptions", "SearchEngine", "_terminal", "_lineCache", "term", "startRow", "startCol", "searchOptions", "searchPosition", "result", "y", "cachedSearchTerm", "prevSelectedPos", "isReverseSearch", "searchIndex", "line", "row", "col", "cache", "stringLine", "offsets", "offset", "searchTerm", "searchStringLine", "resultIndex", "searchRegex", "foundTerm", "matchIndex", "startRowOffset", "endRowOffset", "startColOffset", "endColOffset", "startColIndex", "size", "i", "cell", "char", "nextCell", "cols", "lineOffsets", "rowsBack", "colsInRow", "DecorationManager", "Disposable", "_terminal", "toDisposable", "results", "options", "match", "decorations", "decoration", "result", "dispose", "element", "borderColor", "isActiveResult", "decorationRanges", "currentCol", "remainingSize", "markerOffset", "amountThisRow", "range", "marker", "disposables", "e", "SearchResultTracker", "Disposable", "Emitter", "decoration", "results", "maxResults", "result", "i", "match", "hasDecorations", "resultIndex", "SearchAddon", "Disposable", "options", "MutableDisposable", "SearchState", "SearchResultTracker", "Emitter", "terminal", "SearchLineCache", "SearchEngine", "DecorationManager", "toDisposable", "disposableTimeout", "term", "retainCachedSearchTerm", "searchOptions", "internalSearchOptions", "found", "results", "prevResult", "result", "cols", "nextCol", "nextRow", "noScroll", "activeDecoration", "scroll"]
+ }
+diff --git a/src/SearchEngine.ts b/src/SearchEngine.ts
+index 1760bc2bd1fd274d23e2032fde631b39c739f0d9..5b3c5cc5e861356b87e8a15c55797f45bac20a5c 100644
+--- a/src/SearchEngine.ts
++++ b/src/SearchEngine.ts
+@@ -76,6 +76,9 @@ export class SearchEngine {
+ // Search from startRow + 1 to end
+ if (!result) {
+ for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
++ if (this._isRowCoveredByEarlierSearch(y)) {
++ continue;
++ }
+ searchPosition.startRow = y;
+ searchPosition.startCol = 0;
+ result = this._findInLine(term, searchPosition, searchOptions);
+@@ -127,6 +130,9 @@ export class SearchEngine {
+ // Search from startRow + 1 to end
+ if (!result) {
+ for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
++ if (this._isRowCoveredByEarlierSearch(y)) {
++ continue;
++ }
+ searchPosition.startRow = y;
+ searchPosition.startCol = 0;
+ result = this._findInLine(term, searchPosition, searchOptions);
+@@ -138,6 +144,11 @@ export class SearchEngine {
+ // If we hit the bottom and didn't search from the very top wrap back up
+ if (!result && startRow !== 0) {
+ for (let y = 0; y < startRow; y++) {
++ // Row 0 is never skipped: it can be a continuation whose line start was trimmed from the
++ // scrollback, and nothing earlier in this loop has searched it.
++ if (y > 0 && this._isRowCoveredByEarlierSearch(y)) {
++ continue;
++ }
+ searchPosition.startRow = y;
+ searchPosition.startCol = 0;
+ result = this._findInLine(term, searchPosition, searchOptions);
+@@ -237,6 +248,22 @@ export class SearchEngine {
+ (((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
+ }
+
++ /** `_isWholeWord` gated on the option, so a rejected hit can be stepped past instead of ending the scan. */
++ private _satisfiesWholeWord(searchIndex: number, line: string, term: string, searchOptions: ISearchOptions): boolean {
++ return !searchOptions.wholeWord || this._isWholeWord(searchIndex, line, term);
++ }
++
++ /**
++ * Whether an earlier `_findInLine` in this same call already scanned this row's line from an
++ * equal or lower offset, which makes rescanning it pure O(rows^2) work on one long line. Sound
++ * for every option because `_findInLine` returns the first accepted match at or after its
++ * offset, which is monotone in that offset. Only valid once such a search has happened — the
++ * wrap-around loop starts at row 0, whose line start may have been trimmed from the scrollback.
++ */
++ private _isRowCoveredByEarlierSearch(row: number): boolean {
++ return this._terminal.buffer.active.getLine(row)?.isWrapped === true;
++ }
++
+ /**
+ * Searches a line for a search term. Takes the provided terminal line and searches the text line,
+ * which may contain subsequent terminal lines if the text is wrapped. If the provided line number
+@@ -250,23 +277,26 @@ export class SearchEngine {
+ * @returns The search result if it was found.
+ */
+ private _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
+- const row = searchPosition.startRow;
+- const col = searchPosition.startCol;
+-
+ // Ignore wrapped lines, only consider on unwrapped line (first row of command string).
+- const firstLine = this._terminal.buffer.active.getLine(row);
+- if (firstLine?.isWrapped) {
+- if (isReverseSearch) {
++ if (isReverseSearch) {
++ // Reverse search never rewinds: its caller carries startCol down the rows of the line. Row 0
++ // is searched even when wrapped, since its line start may have been trimmed from the scrollback.
++ if (searchPosition.startRow > 0 && this._terminal.buffer.active.getLine(searchPosition.startRow)?.isWrapped) {
+ searchPosition.startCol += this._terminal.cols;
+ return;
+ }
+-
+- // This will iterate until we find the line start.
+- // When we find it, we will search using the calculated start column.
+- searchPosition.startRow--;
+- searchPosition.startCol += this._terminal.cols;
+- return this._findInLine(term, searchPosition, searchOptions);
++ } else {
++ // A loop rather than recursion: one frame per wrapped row overflows the stack on a line long
++ // enough to fill the scrollback. Bounded at row 0 because after a reflow the buffer's ring
++ // holds stale entries at negative indices, so `getLine(-1)` answers with a wrapped line.
++ while (searchPosition.startRow > 0 && this._terminal.buffer.active.getLine(searchPosition.startRow)?.isWrapped) {
++ searchPosition.startRow--;
++ searchPosition.startCol += this._terminal.cols;
++ }
+ }
++ const row = searchPosition.startRow;
++ const col = searchPosition.startCol;
++
+ let cache = this._lineCache.getLineFromCache(row);
+ if (!cache) {
+ cache = this._lineCache.translateBufferLineToStringWithWrap(row, true);
+@@ -274,7 +304,7 @@ export class SearchEngine {
+ }
+ const [stringLine, offsets] = cache;
+
+- const offset = this._bufferColsToStringOffset(row, col);
++ const offset = this._bufferColsToStringOffset(row, col, offsets);
+ let searchTerm = term;
+ let searchStringLine = stringLine;
+ if (!searchOptions.regex) {
+@@ -289,32 +319,46 @@ export class SearchEngine {
+ if (isReverseSearch) {
+ // This loop will get the resultIndex of the _last_ regex match in the range 0..offset
+ while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {
+- resultIndex = searchRegex.lastIndex - foundTerm[0].length;
+- term = foundTerm[0];
+- searchRegex.lastIndex -= (term.length - 1);
++ const matchIndex = searchRegex.lastIndex - foundTerm[0].length;
++ if (foundTerm[0].length > 0 && this._satisfiesWholeWord(matchIndex, searchStringLine, foundTerm[0], searchOptions)) {
++ resultIndex = matchIndex;
++ term = foundTerm[0];
++ }
++ searchRegex.lastIndex = matchIndex + 1;
+ }
+ } else {
+- foundTerm = searchRegex.exec(searchStringLine.slice(offset));
+- if (foundTerm && foundTerm[0].length > 0) {
+- resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);
+- term = foundTerm[0];
++ // Driven over the whole line from `offset` rather than over `slice(offset)`: a slice
++ // re-anchors ^ and \b at whatever column the row happened to wrap at, and only
++ // first-accepted-match-at-or-after-offset is monotone in `offset`, which is what lets
++ // `_isRowCoveredByEarlierSearch` skip a wrapped row an earlier scan already covered.
++ searchRegex.lastIndex = offset;
++ while (foundTerm = searchRegex.exec(searchStringLine)) {
++ const matchIndex = searchRegex.lastIndex - foundTerm[0].length;
++ if (foundTerm[0].length > 0 && this._satisfiesWholeWord(matchIndex, searchStringLine, foundTerm[0], searchOptions)) {
++ resultIndex = matchIndex;
++ term = foundTerm[0];
++ break;
++ }
++ // A zero-length or rejected match would otherwise repeat forever.
++ searchRegex.lastIndex = matchIndex + 1;
+ }
+ }
++ } else if (isReverseSearch) {
++ let matchIndex = offset - searchTerm.length >= 0 ? searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length) : -1;
++ // `lastIndexOf` clamps a negative fromIndex to 0, so index 0 has to end the walk.
++ while (matchIndex >= 0 && !this._satisfiesWholeWord(matchIndex, searchStringLine, searchTerm, searchOptions)) {
++ matchIndex = matchIndex > 0 ? searchStringLine.lastIndexOf(searchTerm, matchIndex - 1) : -1;
++ }
++ resultIndex = matchIndex;
+ } else {
+- if (isReverseSearch) {
+- if (offset - searchTerm.length >= 0) {
+- resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length);
+- }
+- } else {
+- resultIndex = searchStringLine.indexOf(searchTerm, offset);
++ let matchIndex = searchStringLine.indexOf(searchTerm, offset);
++ while (matchIndex >= 0 && !this._satisfiesWholeWord(matchIndex, searchStringLine, searchTerm, searchOptions)) {
++ matchIndex = searchStringLine.indexOf(searchTerm, matchIndex + 1);
+ }
++ resultIndex = matchIndex;
+ }
+
+ if (resultIndex >= 0) {
+- if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
+- return;
+- }
+-
+ // Adjust the row number and search index if needed since a "line" of text can span multiple
+ // rows
+ let startRowOffset = 0;
+@@ -365,12 +409,21 @@ export class SearchEngine {
+ return offset;
+ }
+
+- private _bufferColsToStringOffset(startRow: number, cols: number): number {
+- let lineIndex = startRow;
+- let offset = 0;
+- let line = this._terminal.buffer.active.getLine(lineIndex);
+- while (cols > 0 && line) {
+- for (let i = 0; i < cols && i < this._terminal.cols; i++) {
++ /**
++ * `cols` counts from the start of the logical line, so summing the cells of every row before the
++ * resume point costs O(line) per call and the highlight-all pass makes one call per match.
++ * `lineOffsets` already holds the string offset each wrapped row starts at — the same map used
++ * above to turn a match index back into a row — so only the last, partial row needs cells. It is
++ * also the map the row a match lands on is read from, which the cell sum disagreed with by one
++ * for a row whose trailing cell is the null placeholder of a wide character that wrapped.
++ */
++ private _bufferColsToStringOffset(startRow: number, cols: number, lineOffsets: number[]): number {
++ const rowsBack = Math.min(Math.floor(cols / this._terminal.cols), lineOffsets.length - 1);
++ let offset = lineOffsets[rowsBack];
++ const line = this._terminal.buffer.active.getLine(startRow + rowsBack);
++ if (line) {
++ const colsInRow = Math.min(cols - rowsBack * this._terminal.cols, this._terminal.cols);
++ for (let i = 0; i < colsInRow; i++) {
+ const cell = line.getCell(i);
+ if (!cell) {
+ break;
+@@ -380,12 +433,6 @@ export class SearchEngine {
+ offset += cell.getCode() === 0 ? 1 : cell.getChars().length;
+ }
+ }
+- lineIndex++;
+- line = this._terminal.buffer.active.getLine(lineIndex);
+- if (line && !line.isWrapped) {
+- break;
+- }
+- cols -= this._terminal.cols;
+ }
+ return offset;
+ }
+diff --git a/src/SearchLineCache.ts b/src/SearchLineCache.ts
+index 526f4bfcc74a881bb39b400ec79a25d33d602303..19b22f2f70e50a6b01d07966e15727cc5271c776 100644
+--- a/src/SearchLineCache.ts
++++ b/src/SearchLineCache.ts
+@@ -109,9 +109,13 @@ export class SearchLineCache extends Disposable {
+ public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
+ const strings = [];
+ const lineOffsets = [0];
++ // A single line longer than the whole scrollback leaves every buffer row wrapped, and the
++ // buffer's ring answers an out-of-range row by cycling back to the start, so an unbounded walk
++ // never reaches an unwrapped line.
++ const bufferLength = this._terminal.buffer.active.length;
+ let line = this._terminal.buffer.active.getLine(lineIndex);
+ while (line) {
+- const nextLine = this._terminal.buffer.active.getLine(lineIndex + 1);
++ const nextLine = lineIndex + 1 < bufferLength ? this._terminal.buffer.active.getLine(lineIndex + 1) : undefined;
+ const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
+ let string = line.translateToString(!lineWrapsToNext && trimRight);
+ if (lineWrapsToNext && nextLine) {
diff --git a/config/patches/xterm-src/@xterm__addon-search@0.17.0-beta.300.src.patch b/config/patches/xterm-src/@xterm__addon-search@0.17.0-beta.300.src.patch
new file mode 100644
index 00000000000..c38f1d1278e
--- /dev/null
+++ b/config/patches/xterm-src/@xterm__addon-search@0.17.0-beta.300.src.patch
@@ -0,0 +1,231 @@
+diff --git a/src/SearchEngine.ts b/src/SearchEngine.ts
+index 1760bc2bd1fd274d23e2032fde631b39c739f0d9..5b3c5cc5e861356b87e8a15c55797f45bac20a5c 100644
+--- a/src/SearchEngine.ts
++++ b/src/SearchEngine.ts
+@@ -76,6 +76,9 @@ export class SearchEngine {
+ // Search from startRow + 1 to end
+ if (!result) {
+ for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
++ if (this._isRowCoveredByEarlierSearch(y)) {
++ continue;
++ }
+ searchPosition.startRow = y;
+ searchPosition.startCol = 0;
+ result = this._findInLine(term, searchPosition, searchOptions);
+@@ -127,6 +130,9 @@ export class SearchEngine {
+ // Search from startRow + 1 to end
+ if (!result) {
+ for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
++ if (this._isRowCoveredByEarlierSearch(y)) {
++ continue;
++ }
+ searchPosition.startRow = y;
+ searchPosition.startCol = 0;
+ result = this._findInLine(term, searchPosition, searchOptions);
+@@ -138,6 +144,11 @@ export class SearchEngine {
+ // If we hit the bottom and didn't search from the very top wrap back up
+ if (!result && startRow !== 0) {
+ for (let y = 0; y < startRow; y++) {
++ // Row 0 is never skipped: it can be a continuation whose line start was trimmed from the
++ // scrollback, and nothing earlier in this loop has searched it.
++ if (y > 0 && this._isRowCoveredByEarlierSearch(y)) {
++ continue;
++ }
+ searchPosition.startRow = y;
+ searchPosition.startCol = 0;
+ result = this._findInLine(term, searchPosition, searchOptions);
+@@ -237,6 +248,22 @@ export class SearchEngine {
+ (((searchIndex + term.length) === line.length) || (Constants.NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
+ }
+
++ /** `_isWholeWord` gated on the option, so a rejected hit can be stepped past instead of ending the scan. */
++ private _satisfiesWholeWord(searchIndex: number, line: string, term: string, searchOptions: ISearchOptions): boolean {
++ return !searchOptions.wholeWord || this._isWholeWord(searchIndex, line, term);
++ }
++
++ /**
++ * Whether an earlier `_findInLine` in this same call already scanned this row's line from an
++ * equal or lower offset, which makes rescanning it pure O(rows^2) work on one long line. Sound
++ * for every option because `_findInLine` returns the first accepted match at or after its
++ * offset, which is monotone in that offset. Only valid once such a search has happened — the
++ * wrap-around loop starts at row 0, whose line start may have been trimmed from the scrollback.
++ */
++ private _isRowCoveredByEarlierSearch(row: number): boolean {
++ return this._terminal.buffer.active.getLine(row)?.isWrapped === true;
++ }
++
+ /**
+ * Searches a line for a search term. Takes the provided terminal line and searches the text line,
+ * which may contain subsequent terminal lines if the text is wrapped. If the provided line number
+@@ -250,23 +277,26 @@ export class SearchEngine {
+ * @returns The search result if it was found.
+ */
+ private _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
+- const row = searchPosition.startRow;
+- const col = searchPosition.startCol;
+-
+ // Ignore wrapped lines, only consider on unwrapped line (first row of command string).
+- const firstLine = this._terminal.buffer.active.getLine(row);
+- if (firstLine?.isWrapped) {
+- if (isReverseSearch) {
++ if (isReverseSearch) {
++ // Reverse search never rewinds: its caller carries startCol down the rows of the line. Row 0
++ // is searched even when wrapped, since its line start may have been trimmed from the scrollback.
++ if (searchPosition.startRow > 0 && this._terminal.buffer.active.getLine(searchPosition.startRow)?.isWrapped) {
+ searchPosition.startCol += this._terminal.cols;
+ return;
+ }
+-
+- // This will iterate until we find the line start.
+- // When we find it, we will search using the calculated start column.
+- searchPosition.startRow--;
+- searchPosition.startCol += this._terminal.cols;
+- return this._findInLine(term, searchPosition, searchOptions);
++ } else {
++ // A loop rather than recursion: one frame per wrapped row overflows the stack on a line long
++ // enough to fill the scrollback. Bounded at row 0 because after a reflow the buffer's ring
++ // holds stale entries at negative indices, so `getLine(-1)` answers with a wrapped line.
++ while (searchPosition.startRow > 0 && this._terminal.buffer.active.getLine(searchPosition.startRow)?.isWrapped) {
++ searchPosition.startRow--;
++ searchPosition.startCol += this._terminal.cols;
++ }
+ }
++ const row = searchPosition.startRow;
++ const col = searchPosition.startCol;
++
+ let cache = this._lineCache.getLineFromCache(row);
+ if (!cache) {
+ cache = this._lineCache.translateBufferLineToStringWithWrap(row, true);
+@@ -274,7 +304,7 @@ export class SearchEngine {
+ }
+ const [stringLine, offsets] = cache;
+
+- const offset = this._bufferColsToStringOffset(row, col);
++ const offset = this._bufferColsToStringOffset(row, col, offsets);
+ let searchTerm = term;
+ let searchStringLine = stringLine;
+ if (!searchOptions.regex) {
+@@ -289,32 +319,46 @@ export class SearchEngine {
+ if (isReverseSearch) {
+ // This loop will get the resultIndex of the _last_ regex match in the range 0..offset
+ while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {
+- resultIndex = searchRegex.lastIndex - foundTerm[0].length;
+- term = foundTerm[0];
+- searchRegex.lastIndex -= (term.length - 1);
++ const matchIndex = searchRegex.lastIndex - foundTerm[0].length;
++ if (foundTerm[0].length > 0 && this._satisfiesWholeWord(matchIndex, searchStringLine, foundTerm[0], searchOptions)) {
++ resultIndex = matchIndex;
++ term = foundTerm[0];
++ }
++ searchRegex.lastIndex = matchIndex + 1;
+ }
+ } else {
+- foundTerm = searchRegex.exec(searchStringLine.slice(offset));
+- if (foundTerm && foundTerm[0].length > 0) {
+- resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);
+- term = foundTerm[0];
++ // Driven over the whole line from `offset` rather than over `slice(offset)`: a slice
++ // re-anchors ^ and \b at whatever column the row happened to wrap at, and only
++ // first-accepted-match-at-or-after-offset is monotone in `offset`, which is what lets
++ // `_isRowCoveredByEarlierSearch` skip a wrapped row an earlier scan already covered.
++ searchRegex.lastIndex = offset;
++ while (foundTerm = searchRegex.exec(searchStringLine)) {
++ const matchIndex = searchRegex.lastIndex - foundTerm[0].length;
++ if (foundTerm[0].length > 0 && this._satisfiesWholeWord(matchIndex, searchStringLine, foundTerm[0], searchOptions)) {
++ resultIndex = matchIndex;
++ term = foundTerm[0];
++ break;
++ }
++ // A zero-length or rejected match would otherwise repeat forever.
++ searchRegex.lastIndex = matchIndex + 1;
+ }
+ }
++ } else if (isReverseSearch) {
++ let matchIndex = offset - searchTerm.length >= 0 ? searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length) : -1;
++ // `lastIndexOf` clamps a negative fromIndex to 0, so index 0 has to end the walk.
++ while (matchIndex >= 0 && !this._satisfiesWholeWord(matchIndex, searchStringLine, searchTerm, searchOptions)) {
++ matchIndex = matchIndex > 0 ? searchStringLine.lastIndexOf(searchTerm, matchIndex - 1) : -1;
++ }
++ resultIndex = matchIndex;
+ } else {
+- if (isReverseSearch) {
+- if (offset - searchTerm.length >= 0) {
+- resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length);
+- }
+- } else {
+- resultIndex = searchStringLine.indexOf(searchTerm, offset);
++ let matchIndex = searchStringLine.indexOf(searchTerm, offset);
++ while (matchIndex >= 0 && !this._satisfiesWholeWord(matchIndex, searchStringLine, searchTerm, searchOptions)) {
++ matchIndex = searchStringLine.indexOf(searchTerm, matchIndex + 1);
+ }
++ resultIndex = matchIndex;
+ }
+
+ if (resultIndex >= 0) {
+- if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
+- return;
+- }
+-
+ // Adjust the row number and search index if needed since a "line" of text can span multiple
+ // rows
+ let startRowOffset = 0;
+@@ -365,12 +409,21 @@ export class SearchEngine {
+ return offset;
+ }
+
+- private _bufferColsToStringOffset(startRow: number, cols: number): number {
+- let lineIndex = startRow;
+- let offset = 0;
+- let line = this._terminal.buffer.active.getLine(lineIndex);
+- while (cols > 0 && line) {
+- for (let i = 0; i < cols && i < this._terminal.cols; i++) {
++ /**
++ * `cols` counts from the start of the logical line, so summing the cells of every row before the
++ * resume point costs O(line) per call and the highlight-all pass makes one call per match.
++ * `lineOffsets` already holds the string offset each wrapped row starts at — the same map used
++ * above to turn a match index back into a row — so only the last, partial row needs cells. It is
++ * also the map the row a match lands on is read from, which the cell sum disagreed with by one
++ * for a row whose trailing cell is the null placeholder of a wide character that wrapped.
++ */
++ private _bufferColsToStringOffset(startRow: number, cols: number, lineOffsets: number[]): number {
++ const rowsBack = Math.min(Math.floor(cols / this._terminal.cols), lineOffsets.length - 1);
++ let offset = lineOffsets[rowsBack];
++ const line = this._terminal.buffer.active.getLine(startRow + rowsBack);
++ if (line) {
++ const colsInRow = Math.min(cols - rowsBack * this._terminal.cols, this._terminal.cols);
++ for (let i = 0; i < colsInRow; i++) {
+ const cell = line.getCell(i);
+ if (!cell) {
+ break;
+@@ -380,12 +433,6 @@ export class SearchEngine {
+ offset += cell.getCode() === 0 ? 1 : cell.getChars().length;
+ }
+ }
+- lineIndex++;
+- line = this._terminal.buffer.active.getLine(lineIndex);
+- if (line && !line.isWrapped) {
+- break;
+- }
+- cols -= this._terminal.cols;
+ }
+ return offset;
+ }
+diff --git a/src/SearchLineCache.ts b/src/SearchLineCache.ts
+index 526f4bfcc74a881bb39b400ec79a25d33d602303..19b22f2f70e50a6b01d07966e15727cc5271c776 100644
+--- a/src/SearchLineCache.ts
++++ b/src/SearchLineCache.ts
+@@ -109,9 +109,13 @@ export class SearchLineCache extends Disposable {
+ public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
+ const strings = [];
+ const lineOffsets = [0];
++ // A single line longer than the whole scrollback leaves every buffer row wrapped, and the
++ // buffer's ring answers an out-of-range row by cycling back to the start, so an unbounded walk
++ // never reaches an unwrapped line.
++ const bufferLength = this._terminal.buffer.active.length;
+ let line = this._terminal.buffer.active.getLine(lineIndex);
+ while (line) {
+- const nextLine = this._terminal.buffer.active.getLine(lineIndex + 1);
++ const nextLine = lineIndex + 1 < bufferLength ? this._terminal.buffer.active.getLine(lineIndex + 1) : undefined;
+ const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
+ let string = line.translateToString(!lineWrapsToNext && trimRight);
+ if (lineWrapsToNext && nextLine) {
diff --git a/config/patches/xterm-upstream.json b/config/patches/xterm-upstream.json
index ec36f65c71d..89afe4fb1fb 100644
--- a/config/patches/xterm-upstream.json
+++ b/config/patches/xterm-upstream.json
@@ -59,6 +59,33 @@
}
]
},
+ {
+ "name": "@xterm/addon-search",
+ "version": "0.17.0-beta.300",
+ "packageDir": "addons/addon-search",
+ "$note": "No versionStampFile: publish.js stamps the addon's package.json, which overlayBuildOutput never patches. The root `build` is required because the addon's own tsgo -p . has empty files/include and only project references, so it emits nothing on its own; `package` is the addon's webpack (CJS half) and the root `esbuild-package` emits the ESM half.",
+ "$upstream": "Submitted as https://github.com/xtermjs/xterm.js/pull/6149 (issue #6148). Once a release ships it, bump the addon and drop this entry.",
+ "sourcePatch": "config/patches/xterm-src/@xterm__addon-search@0.17.0-beta.300.src.patch",
+ "patch": "config/patches/@xterm__addon-search@0.17.0-beta.300.patch",
+ "generatedPaths": ["lib/"],
+ "build": [
+ {
+ "cwd": "../..",
+ "command": "npm",
+ "args": ["run", "build"]
+ },
+ {
+ "cwd": ".",
+ "command": "npm",
+ "args": ["run", "package"]
+ },
+ {
+ "cwd": "../..",
+ "command": "npm",
+ "args": ["run", "esbuild-package"]
+ }
+ ]
+ },
{
"name": "@xterm/addon-serialize",
"version": "0.15.0-beta.300",
diff --git a/docs/reference/xterm-patch-regeneration.md b/docs/reference/xterm-patch-regeneration.md
index bd49dd8ca3d..ecb30913d28 100644
--- a/docs/reference/xterm-patch-regeneration.md
+++ b/docs/reference/xterm-patch-regeneration.md
@@ -24,11 +24,11 @@ truth. Everything else is derived from it by
`config/scripts/regenerate-xterm-patches.mjs`, which is pinned to the exact
upstream commit the published tarball was built from.
-`@xterm/addon-webgl` and `@xterm/addon-serialize` are generated the same way,
-from their own source patches under `config/patches/xterm-src/`. Their entries
-differ only in `packageDir` and build steps; everything below applies to all
-three. `@xterm/addon-ligatures` is the one patch still written by hand — see
-[Known Gaps](#known-gaps).
+`@xterm/addon-webgl`, `@xterm/addon-search` and `@xterm/addon-serialize` are
+generated the same way, from their own source patches under
+`config/patches/xterm-src/`. Their entries differ only in `packageDir` and build
+steps; everything below applies to all four. `@xterm/addon-ligatures` is the one
+patch still written by hand — see [Known Gaps](#known-gaps).
## Rules
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eac56401344..a045833577f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -111,6 +111,7 @@ overrides:
patchedDependencies:
'@vscode/windows-process-tree@0.8.0': 9217ef36c01ed74127fef5512b0c92089cdbf820fd6c109dd671137eebdc7585
'@xterm/addon-ligatures@0.11.0-beta.300': 47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920
+ '@xterm/addon-search@0.17.0-beta.300': eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0
'@xterm/addon-serialize@0.15.0-beta.300': 851eac3d75e6d8c013b9f4c053e61d824b23965cb19ecc28e335e05059f3a294
'@xterm/addon-webgl@0.20.0-beta.299': 94687e89a0115e6e6aa102837f986debdc029c091527ee5eb4a4e17ceaf9473e
'@xterm/xterm@6.1.0-beta.303': 98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d
@@ -319,7 +320,7 @@ importers:
version: 0.11.0-beta.300(patch_hash=47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920)(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))
'@xterm/addon-search':
specifier: 0.17.0-beta.300
- version: 0.17.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))
+ version: 0.17.0-beta.300(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))
'@xterm/addon-unicode11':
specifier: 0.10.0-beta.300
version: 0.10.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))
@@ -9754,7 +9755,7 @@ snapshots:
lru-cache: 11.5.1
opentype.js: 2.0.0
- '@xterm/addon-search@0.17.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))':
+ '@xterm/addon-search@0.17.0-beta.300(patch_hash=eee5338dd2621ece46e79c61ec06766cd7fadaf79ffdb24e2a8ab68e97ef31f0)(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))':
dependencies:
'@xterm/xterm': 6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 8338c47e126..d241459f884 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -44,6 +44,7 @@ patchedDependencies:
node-pty@1.1.0: config/patches/node-pty@1.1.0.patch
'@xterm/addon-ligatures@0.11.0-beta.300': config/patches/@xterm__addon-ligatures@0.11.0-beta.300.patch
'@xterm/addon-webgl@0.20.0-beta.299': config/patches/@xterm__addon-webgl@0.20.0-beta.299.patch
+ '@xterm/addon-search@0.17.0-beta.300': config/patches/@xterm__addon-search@0.17.0-beta.300.patch
'@xterm/addon-serialize@0.15.0-beta.300': config/patches/@xterm__addon-serialize@0.15.0-beta.300.patch
'@xterm/xterm@6.1.0-beta.303': config/patches/@xterm__xterm@6.1.0-beta.303.patch
lint-staged@16.4.0: config/patches/lint-staged@16.4.0.patch
diff --git a/src/renderer/src/components/terminal-search-long-wrapped-line.test.ts b/src/renderer/src/components/terminal-search-long-wrapped-line.test.ts
new file mode 100644
index 00000000000..28be8b5fc7a
--- /dev/null
+++ b/src/renderer/src/components/terminal-search-long-wrapped-line.test.ts
@@ -0,0 +1,362 @@
+// @vitest-environment happy-dom
+
+import type { ISearchOptions } from '@xterm/addon-search'
+import { SearchAddon } from '@xterm/addon-search'
+import { Terminal } from '@xterm/xterm'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+import {
+ DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT,
+ DESKTOP_TERMINAL_SCROLLBACK_ROWS_MAX
+} from '../../../shared/terminal-scrollback-policy'
+import { safeFind } from './terminal-search-safe-find'
+
+/**
+ * Regression for crash report 012eb5be (Orca 1.4.194, win32): searching a pane
+ * that held one un-newlined line — base64, a minified bundle, a single huge log
+ * record — threw `RangeError: Maximum call stack size exceeded` out of
+ * TerminalSearch's effect and tripped the `terminal.workbench` error boundary.
+ *
+ * Mechanism, in @xterm/addon-search's SearchEngine (patched in
+ * config/patches/@xterm__addon-search@*.patch, generated from the source patch
+ * under config/patches/xterm-src/; submitted upstream as
+ * https://github.com/xtermjs/xterm.js/pull/6149): `_findInLine` rewound to the first row of a
+ * wrapped line by calling itself once per wrapped row, so recursion depth equals
+ * the number of screen rows the logical line occupies. Scrollback reaches
+ * DESKTOP_TERMINAL_SCROLLBACK_ROWS_MAX rows, which is far past V8's stack.
+ *
+ * The rewind is reached on every re-entry into the middle of a wrapped line —
+ * `_highlightAllMatches` restarting at the row after a match, and `findNext`
+ * resuming from the current selection — so this drives the real Terminal +
+ * SearchAddon through Orca's own `safeFind`, which deliberately rethrows
+ * anything that is not the decoration error.
+ */
+
+/** Reaches the ring behind the public buffer API to pin the negative-index state reflow leaves. */
+type RingBufferProbe = {
+ _core: { buffer: { lines: { _array: unknown[] } } }
+}
+
+const COLS = 80
+const ROWS = 24
+/** Long enough that the wrap chain outruns V8's stack on any host. */
+const WRAPPED_ROWS = 12_000
+/** A line longer than this scrollback loses its first rows: the bug is eviction, not size. */
+const TRIMMED_HEAD_SCROLLBACK = 100
+const TRIMMED_HEAD_LINE_ROWS = 200
+const NEEDLE = 'needle'
+/**
+ * Rows of one wrapped line per match, and how many matches that line holds. Enough matches to make
+ * the highlight-all pass — which re-enters the line once per match — the dominant cost, and enough
+ * rows that a per-match walk of the line is a freeze rather than a slow search. Kept under the
+ * addon's 1 000-decoration limit so the match count is exact.
+ */
+const MATCH_ROW_STRIDE = 40
+const MATCHES_IN_LINE = 750
+/**
+ * A full-buffer scan runs on the renderer's main thread on every keystroke in
+ * the find bar, so anything near this is a visible freeze rather than a slow
+ * search. Unfixed it is ~18s for a default-scrollback buffer; fixed, ~6ms.
+ */
+const FULL_SCAN_BUDGET_MS = 5_000
+
+// Matches the decoration options TerminalSearch passes, so the highlight-all
+// pass (the crash's entry point) actually runs.
+const SEARCH_DECORATIONS = {
+ matchBackground: '#5c4a00',
+ matchBorder: '#5c4a00',
+ matchOverviewRuler: '#ffcc00',
+ activeMatchBackground: '#c4580e',
+ activeMatchBorder: '#ffcf6b',
+ activeMatchColorOverviewRuler: '#ff9900'
+} as const
+
+/**
+ * Every mode the find bar can put the engine in. Regex and whole word used to be
+ * excluded from the wrapped-row skip, which left them on the O(rows^2) walk after
+ * the recursion that used to abort it was gone: a 12 000-row line took 5.4 minutes
+ * of blocked main thread instead of throwing after 48 seconds.
+ */
+const SEARCH_MODES = [
+ ['plain', {}],
+ ['regex', { regex: true }],
+ ['whole word', { wholeWord: true }]
+] as const satisfies readonly (readonly [string, ISearchOptions])[]
+
+function write(terminal: Terminal, data: string): Promise {
+ return new Promise((resolve) => terminal.write(data, resolve))
+}
+
+function openTerminalWithSearch(scrollback: number = DESKTOP_TERMINAL_SCROLLBACK_ROWS_MAX): {
+ terminal: Terminal
+ search: SearchAddon
+} {
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ const terminal = new Terminal({ cols: COLS, rows: ROWS, scrollback })
+ terminal.open(container)
+ const search = new SearchAddon()
+ terminal.loadAddon(search)
+ return { terminal, search }
+}
+
+describe('terminal search inside one very long wrapped line', () => {
+ beforeEach(() => {
+ // happy-dom has no canvas text metrics; xterm measures glyphs on open().
+ vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
+ measureText: () => ({ width: 10 })
+ } as unknown as CanvasRenderingContext2D)
+ })
+
+ afterEach(() => {
+ vi.restoreAllMocks()
+ document.body.replaceChildren()
+ })
+
+ it.each(SEARCH_MODES)(
+ 'rewinds to the start of the line without overflowing the stack (%s)',
+ async (_mode, options) => {
+ const { terminal, search } = openTerminalWithSearch()
+ // One line of WRAPPED_ROWS screen rows whose only match ends on the
+ // second-to-last row, so the highlight pass resumes one row further on and
+ // has to rewind the whole chain to reach the line start. Space-delimited so
+ // the whole-word mode has something to find.
+ await write(
+ terminal,
+ `${'x'.repeat(COLS * (WRAPPED_ROWS - 1) - NEEDLE.length - 1)} ${NEEDLE} ${'x'.repeat(COLS - 1)}`
+ )
+
+ const find = (): boolean =>
+ safeFind((term, searchOptions) => search.findNext(term, searchOptions), NEEDLE, {
+ ...options,
+ decorations: SEARCH_DECORATIONS
+ })
+
+ let found: boolean | undefined
+ expect(() => {
+ found = find()
+ }).not.toThrow()
+ expect(found).toBe(true)
+
+ // Second find resumes from the selection, deep inside the wrapped line.
+ expect(() => {
+ found = find()
+ }).not.toThrow()
+ expect(found).toBe(true)
+ }
+ )
+
+ it.each(SEARCH_MODES)(
+ 'scans a long wrapped line once, not once per wrapped row (%s)',
+ async (_mode, options) => {
+ const { terminal, search } = openTerminalWithSearch()
+ await write(terminal, 'x'.repeat(COLS * DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT))
+
+ // No match, so the scan visits every row: the shape that froze the pane.
+ const startedAt = performance.now()
+ safeFind((term, searchOptions) => search.findNext(term, searchOptions), NEEDLE, {
+ ...options,
+ decorations: SEARCH_DECORATIONS
+ })
+
+ expect(performance.now() - startedAt).toBeLessThan(FULL_SCAN_BUDGET_MS)
+ }
+ )
+
+ it.each(SEARCH_MODES)(
+ 'highlights many matches in one wrapped line without re-walking it per match (%s)',
+ async (_mode, options) => {
+ const { terminal, search } = openTerminalWithSearch()
+ // `_highlightAllMatches` calls `SearchEngine.find` once per match, and each call resumes
+ // deep inside the line. Converting the resume column to a string offset walked every cell
+ // before it, O(line) per match, so this shape stayed quadratic after the no-match scan was
+ // bounded: ~11s for a line this long, a renderer freeze rather than a RangeError the
+ // boundary recovered from. The per-match rewind and case fold are O(rows) and O(chars)
+ // but measured at well under 1s combined, so they stay simple.
+ const block = ` ${NEEDLE} ${'x'.repeat(COLS * MATCH_ROW_STRIDE - NEEDLE.length - 2)}`
+ await write(terminal, block.repeat(MATCHES_IN_LINE))
+
+ let resultCount = -1
+ search.onDidChangeResults((event) => {
+ resultCount = event.resultCount
+ })
+ const startedAt = performance.now()
+ const found = safeFind(
+ (term, searchOptions) => search.findNext(term, searchOptions),
+ NEEDLE,
+ {
+ ...options,
+ decorations: SEARCH_DECORATIONS
+ }
+ )
+
+ expect(performance.now() - startedAt).toBeLessThan(FULL_SCAN_BUDGET_MS)
+ expect(found).toBe(true)
+ expect(resultCount).toBe(MATCHES_IN_LINE)
+ }
+ )
+
+ it('reports every match inside a wrapped line', async () => {
+ const { terminal, search } = openTerminalWithSearch()
+ let resultCount = -1
+ search.onDidChangeResults((event) => {
+ resultCount = event.resultCount
+ })
+ // One logical line wrapping over three rows with a match in each, then a
+ // separate unwrapped line.
+ const paddedNeedle = NEEDLE + 'x'.repeat(COLS - NEEDLE.length)
+ await write(terminal, `${paddedNeedle.repeat(3)}\r\nplain ${NEEDLE}\r\n`)
+
+ safeFind((term, options) => search.findNext(term, options), NEEDLE, {
+ decorations: SEARCH_DECORATIONS
+ })
+
+ expect(resultCount).toBe(4)
+ })
+
+ it('keeps reaching matches in a wrapped line whose first row was trimmed away', async () => {
+ const { terminal, search } = openTerminalWithSearch(TRIMMED_HEAD_SCROLLBACK)
+ // One long line whose head is evicted, so the surviving chain begins on a
+ // row marked isWrapped and no line start is left in the buffer to cover it.
+ const paddedNeedle = NEEDLE + 'x'.repeat(COLS - NEEDLE.length)
+ const lead = 'x'.repeat(COLS * (TRIMMED_HEAD_LINE_ROWS - 3))
+ await write(terminal, `${lead}${paddedNeedle.repeat(2)}${'x'.repeat(COLS)}\r\n`)
+ for (let i = 0; i < 60; i++) {
+ await write(terminal, `line ${i}\r\n`)
+ }
+ const buffer = terminal.buffer.active
+ expect(buffer.getLine(0)?.isWrapped).toBe(true)
+
+ const matchRows: number[] = []
+ for (let y = 0; y < buffer.length; y++) {
+ if (buffer.getLine(y)?.translateToString().includes(NEEDLE)) {
+ matchRows.push(y)
+ }
+ }
+ expect(matchRows.length).toBe(2)
+
+ // Cycle far enough to come back round in both directions: the surviving rows must stay
+ // reachable, not be visited once and then stranded. Reverse search used to return for any
+ // wrapped row including row 0, so a trimmed-head line was never searched backwards at all.
+ for (const direction of ['findNext', 'findPrevious'] as const) {
+ const visits = new Map()
+ for (let i = 0; i < 12; i++) {
+ safeFind((term, options) => search[direction](term, options), NEEDLE, {
+ decorations: SEARCH_DECORATIONS
+ })
+ const row = terminal.getSelectionPosition()?.start.y
+ if (row !== undefined) {
+ visits.set(row, (visits.get(row) ?? 0) + 1)
+ }
+ }
+
+ for (const row of matchRows) {
+ expect(visits.get(row) ?? 0, direction).toBeGreaterThan(1)
+ }
+ }
+ })
+
+ it('searches a line that is longer than the whole scrollback', async () => {
+ const { terminal, search } = openTerminalWithSearch(TRIMMED_HEAD_SCROLLBACK)
+ // Every row of the buffer is then a continuation, and the ring answers an out-of-range row by
+ // cycling back to row 0, so walking forward for the end of the line never terminates.
+ const paddedNeedle = NEEDLE + 'x'.repeat(COLS - NEEDLE.length)
+ await write(terminal, `${'x'.repeat(COLS * TRIMMED_HEAD_LINE_ROWS)}${paddedNeedle.repeat(3)}`)
+ const buffer = terminal.buffer.active
+ expect(buffer.getLine(0)?.isWrapped).toBe(true)
+ expect(buffer.getLine(buffer.length - 1)?.isWrapped).toBe(true)
+
+ const startedAt = performance.now()
+ const found = safeFind((term, options) => search.findNext(term, options), NEEDLE, {
+ decorations: SEARCH_DECORATIONS
+ })
+
+ expect(found).toBe(true)
+ expect(performance.now() - startedAt).toBeLessThan(FULL_SCAN_BUDGET_MS)
+ })
+
+ it('stops rewinding at row 0 when a reflow trims a wrapped line head', async () => {
+ const { terminal, search } = openTerminalWithSearch(TRIMMED_HEAD_SCROLLBACK)
+ const paddedNeedle = NEEDLE + 'x'.repeat(COLS - NEEDLE.length)
+ await write(terminal, `${'x'.repeat(COLS * TRIMMED_HEAD_LINE_ROWS)}${paddedNeedle.repeat(4)}`)
+ for (let i = 0; i < 20; i++) {
+ await write(terminal, `line ${i}\r\n`)
+ }
+ // Narrowing a pane reflows the buffer, which leaves the ring holding entries at negative
+ // indices, so `getLine(-1)` answers with a stale wrapped line instead of undefined. The rewind
+ // has to stop at row 0 or it walks backwards forever and hangs the renderer.
+ terminal.resize(15, 5)
+ await write(terminal, '')
+ expect(terminal.buffer.active.getLine(0)?.isWrapped).toBe(true)
+ expect(
+ Object.keys((terminal as unknown as RingBufferProbe)._core.buffer.lines._array)
+ ).toContain('-1')
+
+ const startedAt = performance.now()
+ const found = safeFind((term, options) => search.findNext(term, options), NEEDLE, {
+ decorations: SEARCH_DECORATIONS
+ })
+
+ expect(found).toBe(true)
+ expect(performance.now() - startedAt).toBeLessThan(FULL_SCAN_BUDGET_MS)
+ })
+
+ it('keeps scanning a line past a hit whole word rejects', async () => {
+ const { terminal, search } = openTerminalWithSearch()
+ // Upstream stopped at the first `indexOf` hit, so `aneedlea` hid the real word
+ // nine columns later and the find bar reported no match at all. Scanning on is
+ // also what makes the wrapped-row skip sound for wholeWord.
+ await write(terminal, `a${NEEDLE}a ${NEEDLE} done\r\n`)
+
+ const found = safeFind((term, options) => search.findNext(term, options), NEEDLE, {
+ wholeWord: true,
+ decorations: SEARCH_DECORATIONS
+ })
+
+ expect(found).toBe(true)
+ expect(terminal.getSelectionPosition()?.start).toEqual({ x: 9, y: 0 })
+ })
+
+ it('finds a whole-word match that only matches from a later wrapped row', async () => {
+ const { terminal, search } = openTerminalWithSearch()
+ // The first hit on the line is `aneedlea`; the real word is on the second
+ // wrapped row, which the skip removes from the walk.
+ const filler = 'x'.repeat(COLS - NEEDLE.length - 2)
+ await write(terminal, `a${NEEDLE}a${filler} ${NEEDLE} ${filler}`)
+
+ const found = safeFind((term, options) => search.findNext(term, options), NEEDLE, {
+ wholeWord: true,
+ decorations: SEARCH_DECORATIONS
+ })
+
+ expect(found).toBe(true)
+ })
+
+ it('steps past a zero-length regex match instead of abandoning the line', async () => {
+ const { terminal, search } = openTerminalWithSearch()
+ await write(terminal, `abc ${NEEDLE} def\r\n`)
+
+ // `^` matches empty at offset 0, which upstream took as the line's only answer.
+ const found = safeFind((term, options) => search.findNext(term, options), `^|${NEEDLE}`, {
+ regex: true,
+ decorations: SEARCH_DECORATIONS
+ })
+
+ expect(found).toBe(true)
+ expect(terminal.getSelectionPosition()?.start).toEqual({ x: 4, y: 0 })
+ })
+
+ it('anchors a regex to the logical line, not to every wrapped row', async () => {
+ const { terminal, search } = openTerminalWithSearch()
+ // The needle starts the second row of one wrapped line. A wrap column is a
+ // rendering artifact, so `^` must not match there.
+ await write(terminal, 'x'.repeat(COLS) + NEEDLE + 'x'.repeat(COLS - NEEDLE.length))
+
+ const found = safeFind((term, options) => search.findNext(term, options), `^${NEEDLE}`, {
+ regex: true,
+ decorations: SEARCH_DECORATIONS
+ })
+
+ expect(found).toBe(false)
+ })
+})
From 90780acb8561298d1cbb793373e8684c68b87445 Mon Sep 17 00:00:00 2001
From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Date: Thu, 3 Sep 2026 18:07:56 -0700
Subject: [PATCH 13/43] refactor(agents): one pane-identity resolver behind six
thin adapters (tranche 0) (#18243)
* feat(agents): pane-identity canonical adapter, comparison telemetry, inventory ratchet phase 1
* fix(agents): preserve canonical coverage provenance
* refactor(agents): unify pane identity adapters for tranche 0
* fix(agents): keep title resolver cache-free after rebase
* Fix ladder tranche zero review findings
* fix(agents): restore title classifier memoization
* fix(agents): fence unknown canonical evidence sources
* docs: drop the ladder plan and decision table from the PR
Design docs stay out of the shipped tree; the code carries its own comments
and the decision table lives in the test fixture.
---------
Co-authored-by: Merge Sim
---
.../tab-agent-identity-decision-table.test.ts | 198 ++++++++++
src/shared/agent-status-identity.ts | 9 +-
src/shared/pane-agent-evidence-sources.ts | 19 +
.../pane-agent-identity-adapter.test.ts | 278 ++++++++++++++
src/shared/pane-agent-identity-adapter.ts | 361 ++++++++++++++++++
.../pane-agent-identity-inventory.test.ts | 138 ++++++-
.../pane-agent-identity-resolver.test.ts | 20 +
src/shared/pane-agent-identity-resolver.ts | 75 +---
...e-agent-identity-surface-inventory.test.ts | 284 ++++++++++++++
.../pane-agent-identity-title-corpus.test.ts | 156 ++++++++
src/shared/pane-agent-owner.test.ts | 36 ++
src/shared/pane-agent-owner.ts | 7 +-
src/shared/terminal-title-agent-type.ts | 23 +-
13 files changed, 1520 insertions(+), 84 deletions(-)
create mode 100644 src/renderer/src/lib/tab-agent-identity-decision-table.test.ts
create mode 100644 src/shared/pane-agent-evidence-sources.ts
create mode 100644 src/shared/pane-agent-identity-adapter.test.ts
create mode 100644 src/shared/pane-agent-identity-adapter.ts
create mode 100644 src/shared/pane-agent-identity-surface-inventory.test.ts
create mode 100644 src/shared/pane-agent-identity-title-corpus.test.ts
diff --git a/src/renderer/src/lib/tab-agent-identity-decision-table.test.ts b/src/renderer/src/lib/tab-agent-identity-decision-table.test.ts
new file mode 100644
index 00000000000..6c543db3594
--- /dev/null
+++ b/src/renderer/src/lib/tab-agent-identity-decision-table.test.ts
@@ -0,0 +1,198 @@
+import { writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { describe, expect, it } from 'vitest'
+import {
+ resolveCanonicalPaneAgentIdentity,
+ type CanonicalPaneAgentIdentity
+} from '../../../shared/pane-agent-identity-adapter'
+import { resolveTabAgentFromSignals } from './tab-agent-from-signals'
+import type { TuiAgent } from '../../../shared/tui-agent'
+
+const AGENTS: readonly TuiAgent[] = ['claude', 'codex']
+const SLOT_COUNT = 7
+const SHAPE_COUNT = 3 ** SLOT_COUNT * 4 * 2
+const TITLES: readonly string[] = ['', 'zsh', 'Task - claude', 'Task - codex']
+
+type Breakdown = Record<
+ 'launch' | 'completed-hook' | 'sleeping-session' | 'process' | 'sibling' | 'title',
+ number
+>
+
+function slotValues(mask: number): (TuiAgent | null)[] {
+ let remaining = mask
+ return Array.from({ length: SLOT_COUNT }, () => {
+ const value = remaining % 3
+ remaining = Math.floor(remaining / 3)
+ return value === 0 ? null : AGENTS[value - 1]
+ })
+}
+
+function canonicalResult(
+ values: readonly (TuiAgent | null)[],
+ title: string,
+ withProof: boolean
+): CanonicalPaneAgentIdentity {
+ const [hook, siblingHook, completed, siblingCompleted, process, sleeping, launch] = values
+ return resolveCanonicalPaneAgentIdentity({
+ hookAgent: hook,
+ hookIsLive: hook !== null,
+ completedHookAgent: completed,
+ launchAgent: launch,
+ foregroundAgent: process,
+ processProof:
+ withProof && process
+ ? {
+ agent: process,
+ processIncarnation: 'fixture-process',
+ authorityId: 'fixture-authority',
+ capturedAgeMs: 10,
+ validForMs: 1_000
+ }
+ : undefined,
+ sleepingSessionAgent: sleeping,
+ siblingAgents: [siblingHook, siblingCompleted].filter(
+ (agent): agent is TuiAgent => agent !== null
+ ),
+ allowSibling: true,
+ title
+ })
+}
+
+function realResult(values: readonly (TuiAgent | null)[], title: string, remote: boolean) {
+ const [hook, siblingHook, completed, siblingCompleted, process, sleeping, launch] = values
+ // The seven slots model steady-state observations; this runtime memory bit is intentionally
+ // held true instead of adding an eighth dimension to the approved 17,496-shape table.
+ return resolveTabAgentFromSignals({
+ hasObservedAgentSignal: true,
+ isRemote: remote,
+ title,
+ hookAgent: hook,
+ siblingHookAgent: siblingHook,
+ focusedCompletedHookAgent: completed,
+ siblingCompletedHookAgent: siblingCompleted,
+ processAgent: process,
+ processShellForeground: false,
+ sleepingSessionAgent: sleeping,
+ launchAgent: launch ?? undefined
+ })
+}
+
+function runDecisionTable(withProof: boolean) {
+ let disagreements = 0
+ let flipped = 0
+ const breakdown: Breakdown = {
+ launch: 0,
+ 'completed-hook': 0,
+ 'sleeping-session': 0,
+ process: 0,
+ sibling: 0,
+ title: 0
+ }
+ for (let mask = 0; mask < 3 ** SLOT_COUNT; mask += 1) {
+ const values = slotValues(mask)
+ for (const title of TITLES) {
+ for (const remote of [false, true]) {
+ const real = realResult(values, title, remote)
+ const canonical = canonicalResult(values, title, withProof)
+ if (real !== canonical.agent) {
+ disagreements += 1
+ if (canonical.source !== null) {
+ breakdown[canonical.source] += 1
+ }
+ }
+ if (!withProof) {
+ const proven = canonicalResult(values, title, true)
+ if (
+ canonical.agent !== proven.agent &&
+ proven.source === 'process' &&
+ (canonical.source === 'launch' ||
+ canonical.source === 'completed-hook' ||
+ canonical.source === 'sleeping-session')
+ ) {
+ flipped += 1
+ }
+ }
+ }
+ }
+ }
+ return { disagreements, flipped, breakdown }
+}
+
+describe('renderer ladder decision table', () => {
+ it('replays the real shipping ladder and records all rung disagreements', () => {
+ const proofFree = runDecisionTable(false)
+ const freshProof = runDecisionTable(true)
+ const result = {
+ shapes: SHAPE_COUNT,
+ proofOmitted: proofFree,
+ freshProof,
+ flippedByAddingProof: proofFree.flipped
+ }
+ writeFileSync(
+ join(tmpdir(), 'orca-pane-agent-identity-decision-table-real.json'),
+ `${JSON.stringify(result, null, 2)}\n`
+ )
+ // Re-derived against resolveTabAgentFromSignals (not a hand-written model). These differ from
+ // the approved 2,520/648 totals and 396/144/72/36 breakdown; see the PR comment.
+ expect(proofFree).toEqual({
+ disagreements: 2_622,
+ flipped: 1_872,
+ breakdown: {
+ launch: 1_884,
+ 'completed-hook': 478,
+ 'sleeping-session': 144,
+ process: 0,
+ sibling: 54,
+ title: 6
+ }
+ })
+ expect(freshProof).toEqual({
+ disagreements: 658,
+ flipped: 0,
+ breakdown: {
+ launch: 588,
+ 'completed-hook': 46,
+ 'sleeping-session': 0,
+ process: 0,
+ sibling: 6,
+ title: 2
+ }
+ })
+ expect(proofFree.flipped).toBe(1_872)
+ })
+
+ it('requires both freshness fields before process evidence can change the no-proof result', () => {
+ const values = [null, null, null, null, 'codex', null, 'claude'] as const
+ expect(canonicalResult(values, '', false)).toMatchObject({
+ agent: 'claude',
+ source: 'launch'
+ })
+ expect(
+ resolveCanonicalPaneAgentIdentity({
+ foregroundAgent: 'codex',
+ processProof: {
+ agent: 'codex',
+ processIncarnation: 'fixture-process',
+ authorityId: 'fixture-authority',
+ capturedAgeMs: undefined as unknown as number,
+ validForMs: 1_000
+ },
+ launchAgent: 'claude'
+ })
+ ).toMatchObject({ agent: 'claude', source: 'launch' })
+ expect(
+ resolveCanonicalPaneAgentIdentity({
+ foregroundAgent: 'codex',
+ processProof: {
+ agent: 'codex',
+ processIncarnation: 'fixture-process',
+ authorityId: 'fixture-authority',
+ capturedAgeMs: 10,
+ validForMs: undefined as unknown as number
+ },
+ launchAgent: 'claude'
+ })
+ ).toMatchObject({ agent: 'claude', source: 'launch' })
+ })
+})
diff --git a/src/shared/agent-status-identity.ts b/src/shared/agent-status-identity.ts
index f3d4b65a86b..9cb81625310 100644
--- a/src/shared/agent-status-identity.ts
+++ b/src/shared/agent-status-identity.ts
@@ -4,6 +4,8 @@ import {
type AgentStatusState,
type AgentType
} from './agent-status-types'
+import { resolveCanonicalPaneAgentIdentity } from './pane-agent-identity-adapter'
+import type { TuiAgent } from './tui-agent'
type ExistingAgentIdentity = {
agentType?: AgentType
@@ -63,6 +65,11 @@ export function resolveAgentStatusIdentity(args: {
inheritedFromActivePane: false
}
}
+ const canonical = resolveCanonicalPaneAgentIdentity({
+ hookAgent: incomingAgentType as TuiAgent,
+ hookIsLive: true,
+ completedHookAgent: args.existing.state === 'done' ? (existingAgentType as TuiAgent) : undefined
+ })
if (isActiveExistingIdentity(args.existing, args.now, staleAfterMs)) {
return {
// Why: child agent CLIs inherit ORCA_PANE_KEY from their parent terminal.
@@ -74,7 +81,7 @@ export function resolveAgentStatusIdentity(args: {
}
return {
- agentType: incomingAgentType,
+ agentType: canonical.agent ?? incomingAgentType,
inheritedFromActivePane: false
}
}
diff --git a/src/shared/pane-agent-evidence-sources.ts b/src/shared/pane-agent-evidence-sources.ts
new file mode 100644
index 00000000000..32b3648494a
--- /dev/null
+++ b/src/shared/pane-agent-evidence-sources.ts
@@ -0,0 +1,19 @@
+/** Evidence classes in canonical strength order, strongest first. */
+export const PANE_AGENT_EVIDENCE_SOURCES = [
+ /** A live provider hook for a turn in progress. The agent is running and said so. */
+ 'live-hook',
+ /** The pane's foreground process, as read on the execution host. */
+ 'process',
+ /** Orca launched, resumed, or accepted a command for this agent. A fact Orca owns. */
+ 'launch',
+ /** A provider hook from a turn that finished. Still authoritative about identity. */
+ 'completed-hook',
+ /** A sleeping session record restored for this pane. */
+ 'sleeping-session',
+ /** Another pane in the same tab. Tab-level surfaces only; never pane-scoped routing. */
+ 'sibling',
+ /** Parsed from the terminal title. A decoration channel; anyone can type an agent's name. */
+ 'title'
+] as const
+
+export type PaneAgentEvidenceSource = (typeof PANE_AGENT_EVIDENCE_SOURCES)[number]
diff --git a/src/shared/pane-agent-identity-adapter.test.ts b/src/shared/pane-agent-identity-adapter.test.ts
new file mode 100644
index 00000000000..d898cb9adcf
--- /dev/null
+++ b/src/shared/pane-agent-identity-adapter.test.ts
@@ -0,0 +1,278 @@
+import { describe, expect, it } from 'vitest'
+import {
+ buildPaneAgentIdentityEvidenceWire,
+ isForegroundProcessProofFresh,
+ resolveCanonicalPaneAgentIdentity,
+ type ForegroundProcessProof
+} from './pane-agent-identity-adapter'
+
+const freshProof: ForegroundProcessProof = {
+ agent: 'codex',
+ processIncarnation: 'opaque-pid-token',
+ authorityId: 'main:test',
+ capturedAgeMs: 50,
+ validForMs: 5_000
+}
+
+describe('per-pane coverage gate', () => {
+ it('covers a pane from hook, launch, or sleeping-session evidence alone', () => {
+ expect(
+ resolveCanonicalPaneAgentIdentity({ hookAgent: 'claude', hookIsLive: true }).coverage
+ ).toBe('covered')
+ expect(resolveCanonicalPaneAgentIdentity({ completedHookAgent: 'claude' }).coverage).toBe(
+ 'covered'
+ )
+ expect(resolveCanonicalPaneAgentIdentity({ launchAgent: 'codex' }).coverage).toBe('covered')
+ expect(resolveCanonicalPaneAgentIdentity({ sleepingSessionAgent: 'gemini' }).coverage).toBe(
+ 'covered'
+ )
+ })
+
+ it('never covers a pane from a title, a sibling, or a bare foreground name', () => {
+ expect(resolveCanonicalPaneAgentIdentity({ title: 'claude' }).coverage).toBe('uncovered')
+ expect(
+ resolveCanonicalPaneAgentIdentity({ siblingAgent: 'claude', allowSibling: true }).coverage
+ ).toBe('uncovered')
+ expect(resolveCanonicalPaneAgentIdentity({ foregroundAgent: 'codex' }).coverage).toBe(
+ 'uncovered'
+ )
+ })
+
+ it('is computed from evidence, never from a platform or remote flag', () => {
+ // The input deliberately has no platform/isRemote field to branch on; this pins that a
+ // hook-covered pane resolves identically regardless of any caller-side host knowledge.
+ const identity = resolveCanonicalPaneAgentIdentity({ hookAgent: 'claude', hookIsLive: true })
+ expect(identity).toMatchObject({ agent: 'claude', source: 'live-hook', coverage: 'covered' })
+ })
+})
+
+describe('process rung requires a host-stamped proof', () => {
+ it('rejects a stale or malformed proof and accepts a fresh one', () => {
+ expect(isForegroundProcessProofFresh(freshProof)).toBe(true)
+ expect(isForegroundProcessProofFresh({ ...freshProof, capturedAgeMs: 6_000 })).toBe(false)
+ expect(isForegroundProcessProofFresh({ ...freshProof, capturedAgeMs: -1 })).toBe(false)
+ expect(isForegroundProcessProofFresh({ ...freshProof, validForMs: 0 })).toBe(false)
+ expect(isForegroundProcessProofFresh({ ...freshProof, capturedAgeMs: Number.NaN })).toBe(false)
+ })
+
+ it('a bare foreground name cannot outrank launch; a proven process can', () => {
+ const unproven = resolveCanonicalPaneAgentIdentity({
+ launchAgent: 'claude',
+ foregroundAgent: 'codex'
+ })
+ expect(unproven).toMatchObject({ agent: 'claude', source: 'launch' })
+
+ const proven = resolveCanonicalPaneAgentIdentity({
+ launchAgent: 'claude',
+ foregroundAgent: 'codex',
+ processProof: freshProof
+ })
+ expect(proven).toMatchObject({ agent: 'codex', source: 'process', coverage: 'covered' })
+ })
+
+ it('an expired proof and a name-mismatched proof both drop the process rung', () => {
+ const expired = resolveCanonicalPaneAgentIdentity({
+ launchAgent: 'claude',
+ foregroundAgent: 'codex',
+ processProof: { ...freshProof, capturedAgeMs: 10_000 }
+ })
+ expect(expired).toMatchObject({ agent: 'claude', source: 'launch' })
+
+ const mismatched = resolveCanonicalPaneAgentIdentity({
+ launchAgent: 'claude',
+ foregroundAgent: 'gemini',
+ processProof: freshProof
+ })
+ expect(mismatched).toMatchObject({ agent: 'claude', source: 'launch' })
+ })
+})
+
+describe('uncovered compatibility lane', () => {
+ it('preserves the caller-provided legacy result verbatim', () => {
+ const identity = resolveCanonicalPaneAgentIdentity({
+ title: 'Fix the parser - grok',
+ uncoveredFallback: { agent: 'grok', titleOnly: true }
+ })
+ expect(identity).toMatchObject({
+ agent: 'grok',
+ source: 'title',
+ coverage: 'uncovered',
+ titleOnly: true
+ })
+ })
+
+ it('answers from title evidence marked title-only when no fallback is supplied', () => {
+ const identity = resolveCanonicalPaneAgentIdentity({
+ title: 'STA-4011 Linux Antigravity Commit Messages - grok'
+ })
+ expect(identity).toMatchObject({
+ agent: 'grok',
+ source: 'title',
+ coverage: 'uncovered',
+ titleOnly: true
+ })
+ })
+
+ it('a legacy null stays null rather than re-deriving from the title', () => {
+ const identity = resolveCanonicalPaneAgentIdentity({
+ title: 'anything - grok',
+ uncoveredFallback: { agent: null }
+ })
+ expect(identity).toMatchObject({ agent: null, source: null, coverage: 'uncovered' })
+ })
+
+ it('does not let a legacy title fallback bypass the ambiguity fence', () => {
+ expect(
+ resolveCanonicalPaneAgentIdentity({
+ title: 'OC | something - grok',
+ uncoveredFallback: { agent: 'opencode', titleOnly: true }
+ })
+ ).toMatchObject({ agent: null, source: null, ambiguousAt: 'title' })
+ expect(
+ resolveCanonicalPaneAgentIdentity({
+ title: 'compare codex with grok',
+ uncoveredFallback: { agent: 'codex', titleOnly: true }
+ })
+ ).toMatchObject({ agent: null, source: null, coverage: 'uncovered' })
+ })
+
+ it('does not label a foreground-only compatibility answer as title-only', () => {
+ const identity = resolveCanonicalPaneAgentIdentity({
+ foregroundAgent: 'codex',
+ uncoveredFallback: { agent: 'codex' }
+ })
+ expect(identity).toMatchObject({
+ agent: 'codex',
+ source: null,
+ coverage: 'uncovered',
+ titleOnly: false
+ })
+ })
+})
+
+describe('canonical ladder inside the covered lane', () => {
+ it('keeps title last: a covered launch beats a parsed title', () => {
+ const identity = resolveCanonicalPaneAgentIdentity({
+ launchAgent: 'claude',
+ title: 'STA-4011 Linux Antigravity Commit Messages - grok'
+ })
+ expect(identity).toMatchObject({ agent: 'claude', source: 'launch', titleOnly: false })
+ })
+
+ it('sibling evidence needs the explicit tab-scope opt-in', () => {
+ const withoutOptIn = resolveCanonicalPaneAgentIdentity({
+ launchAgent: 'claude',
+ siblingAgent: 'codex'
+ })
+ expect(withoutOptIn.agent).toBe('claude')
+ const optedIn = resolveCanonicalPaneAgentIdentity({
+ hookAgent: 'claude',
+ hookIsLive: true,
+ siblingAgent: 'codex',
+ allowSibling: true
+ })
+ expect(optedIn).toMatchObject({ agent: 'claude', source: 'live-hook' })
+ })
+
+ it('surfaces ambiguity instead of picking by array order', () => {
+ const identity = resolveCanonicalPaneAgentIdentity({
+ hookAgent: 'claude',
+ hookIsLive: false,
+ completedHookAgent: 'codex'
+ })
+ expect(identity).toMatchObject({ agent: null, ambiguousAt: 'completed-hook' })
+ })
+})
+
+describe('reclaim-versus-stale-hook discriminator (run keys, not title text)', () => {
+ const run1 = { authorityId: 'main:a', incarnation: 1 }
+ const run2 = { authorityId: 'main:a', incarnation: 2 }
+ const otherAuthority = { authorityId: 'renderer:b', incarnation: 9 }
+
+ it('bug shape: hook and pane share the current run, so the completed hook wins over the title', () => {
+ const identity = resolveCanonicalPaneAgentIdentity({
+ completedHookAgent: 'claude',
+ completedHookRun: run1,
+ currentRun: run1,
+ title: 'STA-4011 Linux Antigravity Commit Messages - grok'
+ })
+ expect(identity).toMatchObject({ agent: 'claude', source: 'completed-hook' })
+ })
+
+ it('reclaim shape: a superseded hook is ineligible and the current title evidence answers', () => {
+ const identity = resolveCanonicalPaneAgentIdentity({
+ completedHookAgent: 'claude',
+ completedHookRun: run1,
+ currentRun: run2,
+ title: 'STA-4011 Linux Antigravity Commit Messages - grok'
+ })
+ expect(identity).toMatchObject({
+ agent: 'grok',
+ source: 'title',
+ coverage: 'uncovered',
+ titleOnly: true
+ })
+ expect(identity.supersededSources).toEqual(['completed-hook'])
+ })
+
+ it('cross-authority runs are incomparable, so the hook stays eligible', () => {
+ const identity = resolveCanonicalPaneAgentIdentity({
+ completedHookAgent: 'claude',
+ completedHookRun: otherAuthority,
+ currentRun: run2,
+ title: 'STA-4011 Linux Antigravity Commit Messages - grok'
+ })
+ expect(identity).toMatchObject({ agent: 'claude', source: 'completed-hook' })
+ })
+
+ it('an absent run key keeps evidence eligible (old peer), never guessed stale', () => {
+ const identity = resolveCanonicalPaneAgentIdentity({
+ completedHookAgent: 'claude',
+ currentRun: run2,
+ title: 'STA-4011 Linux Antigravity Commit Messages - grok'
+ })
+ expect(identity).toMatchObject({ agent: 'claude', source: 'completed-hook' })
+ })
+})
+
+describe('action floor', () => {
+ it("minimumSource: 'launch' refuses title and completed-hook answers outright", () => {
+ const identity = resolveCanonicalPaneAgentIdentity({
+ completedHookAgent: 'claude',
+ title: 'claude',
+ minimumSource: 'launch'
+ })
+ expect(identity).toMatchObject({ agent: null, source: null, coverage: 'covered' })
+ })
+})
+
+describe('wire evidence projection', () => {
+ it('publishes nothing for an absent identity — absence stays absence', () => {
+ expect(
+ buildPaneAgentIdentityEvidenceWire(resolveCanonicalPaneAgentIdentity({}))
+ ).toBeUndefined()
+ })
+
+ it('marks the uncovered title-only route explicitly and carries the run key when known', () => {
+ const wire = buildPaneAgentIdentityEvidenceWire(
+ resolveCanonicalPaneAgentIdentity({ title: 'claude - claude' }),
+ { authorityId: 'main:a', incarnation: 3 },
+ { capturedAgeMs: 10, validForMs: 1_000 }
+ )
+ expect(wire).toMatchObject({
+ coverage: 'uncovered',
+ titleOnlyActionFallback: true,
+ authorityId: 'main:a',
+ incarnation: 3,
+ freshness: { capturedAgeMs: 10, validForMs: 1_000 }
+ })
+ })
+
+ it('a covered identity never carries the title-only action marker', () => {
+ const wire = buildPaneAgentIdentityEvidenceWire(
+ resolveCanonicalPaneAgentIdentity({ launchAgent: 'claude' })
+ )
+ expect(wire).toMatchObject({ source: 'launch', coverage: 'covered' })
+ expect(wire?.titleOnlyActionFallback).toBeUndefined()
+ })
+})
diff --git a/src/shared/pane-agent-identity-adapter.ts b/src/shared/pane-agent-identity-adapter.ts
new file mode 100644
index 00000000000..c08e1b77dea
--- /dev/null
+++ b/src/shared/pane-agent-identity-adapter.ts
@@ -0,0 +1,361 @@
+import { collectAgentTitleEvidence } from './agent-title-evidence'
+import { PANE_AGENT_EVIDENCE_SOURCES } from './pane-agent-evidence-sources'
+import type {
+ PaneAgentEvidence,
+ PaneAgentIdentity,
+ PaneAgentIdentityInput,
+ PaneAgentRunKey
+} from './pane-agent-identity-resolver'
+import type { PaneAgentEvidenceSource } from './pane-agent-evidence-sources'
+import type { TuiAgent } from './tui-agent'
+
+/**
+ * Canonical pane identity ranking. All adapters, including the compatibility resolver, delegate to
+ * this implementation so source precedence, ambiguity, and run eligibility cannot drift.
+ */
+
+/**
+ * Whether the execution authority proved at least one identity-bearing source for this pane.
+ * Computed from evidence presence, never from `platform`, `isRemote`, or OS: a remote pane with a
+ * host-stamped hook is covered; a local pane with only a title is uncovered.
+ */
+export type PaneAgentCoverage = 'covered' | 'uncovered'
+
+/**
+ * Host-stamped proof that a recognized agent process is the pane's foreground process.
+ *
+ * A process NAME is not a PID-reuse-safe identity, so a bare foreground read never enters the
+ * covered process rung. `processIncarnation` is an opaque token the execution host derives from
+ * the selected PID plus start/creation time (or an equivalent platform-native identity); the raw
+ * tuple never crosses the renderer/remote wire. The host emits no proof when the start identity
+ * is unavailable or ambiguous, so that pane reads `uncovered` rather than guessed.
+ */
+export type ForegroundProcessProof = {
+ agent: TuiAgent
+ /** Opaque host-derived PID+start-time token. Compared for equality only, never decoded. */
+ processIncarnation: string
+ ptyIncarnationId?: string
+ /** The execution authority that stamped the proof (see agent-status-observation.ts). */
+ authorityId: string
+ /** Age on the AUTHORITY's clock at capture. Replicas decay from this plus `validForMs`,
+ * never by subtracting a host wall clock from a local `Date.now()`. */
+ capturedAgeMs: number
+ validForMs: number
+}
+
+/**
+ * Positive evidence that a pane/process was REPLACED, required before any consumer may advance a
+ * pane incarnation. A retired-pane `restart` disposition, an accepted send, an ordinary provider
+ * turn boundary, a title change, a transport loss, or a renderer-only foreground change is never
+ * one of these. Defined here so the rebind-gate wave has a contract to be correct against; no
+ * sequencer call site consumes it yet.
+ */
+export type PaneReplacementProof =
+ | { kind: 'accepted-launch'; launchToken: string; ptyIncarnationId: string }
+ | { kind: 'process-replacement'; processIncarnation: string; authorityId: string }
+ | { kind: 'provider-session-attach'; providerSessionId: string }
+
+/**
+ * The optional wire object a host will publish alongside `agentIdentity` after capability
+ * negotiation (host-publisher wave, not now). All fields bounded and JSON-safe; old peers ignore
+ * it. Never inferred from the bare `agentIdentity` string.
+ */
+export type PaneAgentIdentityEvidenceWire = {
+ source: PaneAgentEvidenceSource
+ coverage: PaneAgentCoverage
+ authorityId?: string
+ incarnation?: number
+ freshness?: { capturedAgeMs: number; validForMs: number }
+ /** Marks the scoped host-published title-only best-effort route (hand-started WSL panes).
+ * Counted separately, `unverifiable` for liveness, and never relabeled as covered proof. */
+ titleOnlyActionFallback?: true
+}
+
+export type CanonicalPaneAgentIdentityInput = {
+ hookAgent?: TuiAgent | null
+ hookIsLive?: boolean
+ hookRun?: PaneAgentRunKey
+ /** A distinct completed-hook signal for callers that hold live and completed rows separately
+ * (the tab ladder does); `hookAgent` + `hookIsLive: false` remains the single-slot spelling. */
+ completedHookAgent?: TuiAgent | null
+ completedHookRun?: PaneAgentRunKey
+ launchAgent?: TuiAgent | null
+ launchRun?: PaneAgentRunKey
+ /**
+ * Foreground process NAME as currently read. Without a fresh `processProof` this is a weak
+ * hint: it neither enters the covered process rung nor makes the pane covered.
+ */
+ foregroundAgent?: TuiAgent | null
+ processProof?: ForegroundProcessProof | null
+ sleepingSessionAgent?: TuiAgent | null
+ sleepingRun?: PaneAgentRunKey
+ /** Tab-level display fallback only; ignored unless `allowSibling` opts in. */
+ siblingAgent?: TuiAgent | null
+ /** Additional tab-level sibling observations retained for ambiguity checking. */
+ siblingAgents?: readonly TuiAgent[]
+ allowSibling?: boolean
+ title?: string | null
+ currentRun?: PaneAgentRunKey
+ minimumSource?: PaneAgentEvidenceSource
+ /**
+ * The caller's CURRENT ladder result, preserved verbatim while the pane is uncovered. The
+ * uncovered lane is a temporary compatibility lane, not a new host-specific ranking; absent a
+ * fallback, an uncovered pane answers from title evidence alone, marked title-only.
+ */
+ uncoveredFallback?: { agent: TuiAgent | null; titleOnly?: boolean }
+}
+
+export type CanonicalPaneAgentIdentity = {
+ agent: TuiAgent | null
+ source: PaneAgentEvidenceSource | null
+ coverage: PaneAgentCoverage
+ /** True when the answer was derived from a parsed title (the uncovered/title-only marking). */
+ titleOnly: boolean
+ ambiguousAt?: PaneAgentEvidenceSource
+ supersededSources: readonly PaneAgentEvidenceSource[]
+}
+
+/** Authority order, strongest first. This is the only place precedence is expressed. */
+const SOURCE_RANK: readonly PaneAgentEvidenceSource[] = PANE_AGENT_EVIDENCE_SOURCES
+
+/** Exported for the source/rank drift ratchet; the rank is the canonical source list itself. */
+export const PANE_AGENT_SOURCE_RANK = SOURCE_RANK
+
+/** Reject an unrecognised source instead of silently dropping it from the ranking loop. */
+function sourceRankIndex(source: PaneAgentEvidenceSource): number {
+ const index = SOURCE_RANK.indexOf(source)
+ if (index === -1) {
+ throw new Error(`Unknown pane-agent evidence source: ${String(source)}`)
+ }
+ return index
+}
+
+/** Run keys only supersede evidence from the same authority; unknown authorities stay eligible. */
+function isPaneAgentRunEligible(
+ run: PaneAgentRunKey | undefined,
+ currentRun: PaneAgentRunKey | undefined
+): boolean {
+ return (
+ run === undefined ||
+ currentRun === undefined ||
+ run.authorityId !== currentRun.authorityId ||
+ run.incarnation === currentRun.incarnation
+ )
+}
+
+/** Shared evidence ranking primitive used by every pane-identity adapter. */
+export function resolveCanonicalPaneAgentEvidence(
+ input: PaneAgentIdentityInput
+): PaneAgentIdentity {
+ const superseded: PaneAgentEvidenceSource[] = []
+ const floor = input.minimumSource ? sourceRankIndex(input.minimumSource) : Number.MAX_SAFE_INTEGER
+ const eligible = input.evidence.filter((item) => {
+ if (item.source === 'sibling' && input.allowSibling !== true) {
+ return false
+ }
+ if (sourceRankIndex(item.source) > floor) {
+ return false
+ }
+ if (isPaneAgentRunEligible(item.run, input.currentRun)) {
+ return true
+ }
+ superseded.push(item.source)
+ return false
+ })
+
+ for (const source of SOURCE_RANK) {
+ const matches = eligible.filter((item) => item.source === source)
+ if (matches.length === 0) {
+ continue
+ }
+ const agents = new Set(matches.map((item) => item.agent))
+ if (agents.size > 1) {
+ return { agent: null, source: null, ambiguousAt: source, supersededSources: superseded }
+ }
+ return { agent: matches[0].agent, source, supersededSources: superseded }
+ }
+ return { agent: null, source: null, supersededSources: superseded }
+}
+
+/** Freshness is judged on the authority's own clock: age at capture against its TTL. */
+export function isForegroundProcessProofFresh(proof: ForegroundProcessProof): boolean {
+ return (
+ Number.isFinite(proof.capturedAgeMs) &&
+ Number.isFinite(proof.validForMs) &&
+ proof.capturedAgeMs >= 0 &&
+ proof.validForMs > 0 &&
+ proof.capturedAgeMs <= proof.validForMs
+ )
+}
+
+/** A proof only carries identity for the agent it names; a name mismatch is no proof at all. */
+function processEvidenceFromProof(
+ input: CanonicalPaneAgentIdentityInput
+): PaneAgentEvidence | null {
+ const proof = input.processProof
+ if (!proof || !isForegroundProcessProofFresh(proof)) {
+ return null
+ }
+ if (input.foregroundAgent && input.foregroundAgent !== proof.agent) {
+ return null
+ }
+ return { source: 'process', agent: proof.agent }
+}
+
+export function resolveCanonicalPaneAgentIdentity(
+ input: CanonicalPaneAgentIdentityInput
+): CanonicalPaneAgentIdentity {
+ const processEvidence = processEvidenceFromProof(input)
+ // Coverage comes from authority-bearing sources that are still eligible for this run. A stale
+ // hook/launch row can remain in the input after a pane is replaced; it must not make a title-only
+ // answer look covered to a future action consumer.
+ const covered = Boolean(
+ (input.hookAgent && isPaneAgentRunEligible(input.hookRun, input.currentRun)) ||
+ (input.completedHookAgent &&
+ isPaneAgentRunEligible(input.completedHookRun, input.currentRun)) ||
+ processEvidence ||
+ (input.launchAgent && isPaneAgentRunEligible(input.launchRun, input.currentRun)) ||
+ (input.sleepingSessionAgent && isPaneAgentRunEligible(input.sleepingRun, input.currentRun))
+ )
+ // Keep stale evidence in the resolver so diagnostics still report which source was superseded,
+ // even when it no longer qualifies the pane as covered.
+ const hasAuthorityEvidence = Boolean(
+ input.hookAgent ||
+ input.completedHookAgent ||
+ processEvidence ||
+ input.launchAgent ||
+ input.sleepingSessionAgent
+ )
+ const titleEvidence = input.title ? collectAgentTitleEvidence(input.title) : null
+ const titleAgent = titleEvidence?.agent ?? null
+
+ if (!hasAuthorityEvidence) {
+ if (input.uncoveredFallback) {
+ const agent = input.uncoveredFallback.agent
+ // A legacy title parser may have picked the first token from an ambiguous or
+ // free-text-only title. Do not let that compatibility value bypass the canonical
+ // ambiguity fence when the caller marks it as title-only evidence.
+ const rejectTitleFallback =
+ input.uncoveredFallback.titleOnly === true &&
+ ((titleEvidence?.reason === 'free-text-only' &&
+ (titleEvidence.freeTextNames?.length ?? 0) > 1) ||
+ titleEvidence?.reason === 'conflicting-anchored-names' ||
+ titleEvidence?.reason === 'conflicting-vendor-markers')
+ if (rejectTitleFallback) {
+ return {
+ agent: null,
+ source: null,
+ coverage: 'uncovered',
+ titleOnly: false,
+ ...(titleEvidence?.reason === 'free-text-only' ? {} : { ambiguousAt: 'title' as const }),
+ supersededSources: []
+ }
+ }
+ const titleOnly =
+ input.uncoveredFallback.titleOnly ?? (agent !== null && agent === titleAgent)
+ return {
+ agent,
+ source: agent === null ? null : titleOnly ? 'title' : null,
+ coverage: 'uncovered',
+ titleOnly,
+ supersededSources: []
+ }
+ }
+ const siblingEvidence = [
+ ...(input.siblingAgent ? [{ source: 'sibling' as const, agent: input.siblingAgent }] : []),
+ ...(input.siblingAgents?.map((agent) => ({ source: 'sibling' as const, agent })) ?? []),
+ ...(titleAgent ? [{ source: 'title' as const, agent: titleAgent }] : [])
+ ]
+ const siblingResolved = resolveCanonicalPaneAgentEvidence({
+ evidence: siblingEvidence,
+ allowSibling: input.allowSibling,
+ minimumSource: input.minimumSource
+ })
+ return {
+ agent: siblingResolved.agent,
+ source: siblingResolved.source,
+ coverage: 'uncovered',
+ titleOnly: siblingResolved.source === 'title',
+ ...(siblingResolved.ambiguousAt ? { ambiguousAt: siblingResolved.ambiguousAt } : {}),
+ supersededSources: siblingResolved.supersededSources
+ }
+ }
+
+ const resolved = resolveCanonicalPaneAgentEvidence({
+ evidence: [
+ ...(input.hookAgent
+ ? [
+ {
+ source: input.hookIsLive ? ('live-hook' as const) : ('completed-hook' as const),
+ agent: input.hookAgent,
+ ...(input.hookRun ? { run: input.hookRun } : {})
+ }
+ ]
+ : []),
+ ...(input.completedHookAgent
+ ? [
+ {
+ source: 'completed-hook' as const,
+ agent: input.completedHookAgent,
+ ...(input.completedHookRun ? { run: input.completedHookRun } : {})
+ }
+ ]
+ : []),
+ ...(processEvidence ? [processEvidence] : []),
+ ...(input.launchAgent
+ ? [
+ {
+ source: 'launch' as const,
+ agent: input.launchAgent,
+ ...(input.launchRun ? { run: input.launchRun } : {})
+ }
+ ]
+ : []),
+ ...(input.sleepingSessionAgent
+ ? [
+ {
+ source: 'sleeping-session' as const,
+ agent: input.sleepingSessionAgent,
+ ...(input.sleepingRun ? { run: input.sleepingRun } : {})
+ }
+ ]
+ : []),
+ ...(input.siblingAgent ? [{ source: 'sibling' as const, agent: input.siblingAgent }] : []),
+ ...(input.siblingAgents?.map((agent) => ({ source: 'sibling' as const, agent })) ?? []),
+ ...(titleAgent ? [{ source: 'title' as const, agent: titleAgent }] : [])
+ ],
+ currentRun: input.currentRun,
+ minimumSource: input.minimumSource,
+ allowSibling: input.allowSibling
+ })
+ return {
+ agent: resolved.agent,
+ source: resolved.source,
+ coverage: covered ? 'covered' : 'uncovered',
+ titleOnly: resolved.source === 'title',
+ ...(resolved.ambiguousAt ? { ambiguousAt: resolved.ambiguousAt } : {}),
+ supersededSources: resolved.supersededSources
+ }
+}
+
+/** Projects the host-local sidecar onto the optional wire shape. Returns undefined when there is
+ * nothing to publish — absence stays absence, and a bare `agentIdentity` with no sidecar is
+ * never treated as covered proof by any consumer. */
+export function buildPaneAgentIdentityEvidenceWire(
+ identity: CanonicalPaneAgentIdentity,
+ run?: PaneAgentRunKey,
+ freshness?: { capturedAgeMs: number; validForMs: number }
+): PaneAgentIdentityEvidenceWire | undefined {
+ if (identity.agent === null || identity.source === null) {
+ return undefined
+ }
+ return {
+ source: identity.source,
+ coverage: identity.coverage,
+ ...(run ? { authorityId: run.authorityId, incarnation: run.incarnation } : {}),
+ ...(freshness ? { freshness } : {}),
+ ...(identity.coverage === 'uncovered' && identity.titleOnly
+ ? { titleOnlyActionFallback: true as const }
+ : {})
+ }
+}
diff --git a/src/shared/pane-agent-identity-inventory.test.ts b/src/shared/pane-agent-identity-inventory.test.ts
index 6871a38eb6d..ee868bfcc16 100644
--- a/src/shared/pane-agent-identity-inventory.test.ts
+++ b/src/shared/pane-agent-identity-inventory.test.ts
@@ -18,7 +18,13 @@ const HELPERS = [
'resolveExplicitTerminalTitleAgentType',
'resolveCommittedTitleAgentType',
'resolvePaneAgentOwner',
- 'resolveCompatibleAgentTypeForOwner'
+ 'resolveCompatibleAgentTypeForOwner',
+ 'classifyTitleActivity',
+ 'detectAgentStatusFromTitle',
+ 'resolveAgentTypeFromTerminalTitle',
+ 'resolvePaneAgentIdentity',
+ 'resolveCanonicalPaneAgentIdentity',
+ 'resolvePublishedPaneAgentIdentity'
] as const
const TEST_SUPPORT_PATHS = new Set([
@@ -242,6 +248,136 @@ const INVENTORY: readonly InventoryGroup[] = [
['src/renderer/src/components/terminal-pane/pty-connection/direct-ssh-retry-status.ts', 2],
['src/renderer/src/components/terminal-pane/pty-connection/title-spawn-bell.ts', 2]
]
+ },
+ {
+ helper: 'classifyTitleActivity',
+ classification: 'identity-consumer',
+ paths: [
+ ['src/renderer/src/components/sidebar/smart-attention.ts', 3],
+ ['src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts', 2],
+ ['src/renderer/src/components/status-bar/workspace-space-presentation.ts', 3],
+ ['src/renderer/src/lib/active-agent-note-target.ts', 2],
+ ['src/renderer/src/lib/worktree-status.ts', 3],
+ ['src/renderer/src/store/slices/terminal-helpers.ts', 2]
+ ]
+ },
+ {
+ helper: 'classifyTitleActivity',
+ classification: 'action-consumer',
+ paths: [
+ ['src/renderer/src/components/terminal-pane/cache-timer-seeding.ts', 2],
+ ['src/renderer/src/lib/agent-ready-wait.ts', 2],
+ ['src/renderer/src/store/terminals/terminal-ephemeral-state.ts', 2]
+ ]
+ },
+ {
+ helper: 'classifyTitleActivity',
+ classification: 'activity-only',
+ paths: [
+ ['src/renderer/src/store/slices/workspace-cleanup-local-evidence.ts', 3],
+ ['src/renderer/src/store/terminals/terminal-tab-presentation.ts', 4]
+ ]
+ },
+ {
+ helper: 'classifyTitleActivity',
+ classification: 'evidence-producer',
+ paths: [
+ ['src/renderer/src/lib/agent-send-title-status.ts', 2],
+ ['src/renderer/src/lib/agent-status-terminal-title.ts', 2]
+ ]
+ },
+ {
+ helper: 'classifyTitleActivity',
+ classification: 'parser-implementation',
+ paths: [
+ ['src/renderer/src/lib/agent-status.ts', 5],
+ 'src/renderer/src/lib/pane-agent-evidence.ts'
+ ]
+ },
+ {
+ helper: 'detectAgentStatusFromTitle',
+ classification: 'evidence-producer',
+ paths: [
+ ['src/main/runtime/orca-runtime-apply-tracked-pty-title.ts', 2],
+ ['src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts', 2],
+ ['src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts', 2],
+ ['src/main/runtime/orca-runtime-maybe-hydrate-headless-from-renderer.ts', 2],
+ ['src/main/runtime/orca-runtime-record-agent-prompt-lifecycle-state.ts', 2],
+ ['src/main/runtime/runtime-terminal-agent-status-query.ts', 3],
+ ['src/main/runtime/runtime-worktree-status-projection.ts', 4],
+ ['src/main/runtime/terminal-wait-detection.ts', 2],
+ ['src/renderer/src/components/terminal-pane/agent-completion-title-observer.ts', 2],
+ ['src/renderer/src/components/terminal-pane/pty-connection/shell-command-inference.ts', 4],
+ ['src/renderer/src/components/terminal-pane/pty-output-title-observer.ts', 2],
+ ['src/shared/terminal-output-side-effects.ts', 3]
+ ]
+ },
+ {
+ helper: 'detectAgentStatusFromTitle',
+ classification: 'action-consumer',
+ paths: [
+ ['src/renderer/src/components/terminal-pane/pty-connection/agent-task-complete-notify.ts', 2],
+ [
+ 'src/renderer/src/components/terminal-pane/pty-connection/command-inferred-pane-agent.ts',
+ 3
+ ],
+ ['src/renderer/src/components/terminal-pane/pty-connection/interrupt-input-intent.ts', 3]
+ ]
+ },
+ {
+ helper: 'detectAgentStatusFromTitle',
+ classification: 'parser-implementation',
+ paths: [
+ ['src/renderer/src/components/terminal-pane/title-agent-identity.ts', 2],
+ 'src/renderer/src/lib/agent-status.ts',
+ ['src/renderer/src/lib/pane-agent-evidence.ts', 3],
+ ['src/shared/agent-decorative-title-signature.ts', 2],
+ 'src/shared/agent-detection.ts',
+ ['src/shared/agent-title-owner.ts', 2],
+ ['src/shared/agent-title-status.ts', 6]
+ ]
+ },
+ {
+ helper: 'resolveAgentTypeFromTerminalTitle',
+ classification: 'identity-consumer',
+ paths: [
+ ['src/renderer/src/components/sidebar/worktree-agent-row-type.ts', 2],
+ 'src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts',
+ ['src/renderer/src/lib/worktree-status.ts', 2]
+ ]
+ },
+ {
+ helper: 'resolvePaneAgentIdentity',
+ classification: 'parser-implementation',
+ paths: ['src/shared/pane-agent-identity-resolver.ts']
+ },
+ {
+ helper: 'resolvePaneAgentIdentity',
+ classification: 'identity-consumer',
+ paths: [['src/shared/published-pane-agent-identity.ts', 2]]
+ },
+ {
+ helper: 'resolveCanonicalPaneAgentIdentity',
+ classification: 'parser-implementation',
+ paths: ['src/shared/pane-agent-identity-adapter.ts']
+ },
+ {
+ helper: 'resolveCanonicalPaneAgentIdentity',
+ classification: 'identity-consumer',
+ paths: [['src/shared/agent-status-identity.ts', 2]]
+ },
+ {
+ helper: 'resolveCanonicalPaneAgentIdentity',
+ classification: 'identity-consumer',
+ paths: [['src/shared/terminal-title-agent-type.ts', 2]]
+ },
+ {
+ helper: 'resolvePublishedPaneAgentIdentity',
+ classification: 'parser-implementation',
+ paths: [
+ 'src/shared/published-pane-agent-identity.ts',
+ ['src/main/runtime/orca-runtime-write-orchestration-pointer-pty.ts', 2]
+ ]
}
]
diff --git a/src/shared/pane-agent-identity-resolver.test.ts b/src/shared/pane-agent-identity-resolver.test.ts
index e28fb0cbd22..2f14a54976a 100644
--- a/src/shared/pane-agent-identity-resolver.test.ts
+++ b/src/shared/pane-agent-identity-resolver.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest'
+import { PANE_AGENT_SOURCE_RANK } from './pane-agent-identity-adapter'
import {
PANE_AGENT_EVIDENCE_SOURCES,
type PaneAgentEvidence,
@@ -11,6 +12,14 @@ const resolve = (evidence: PaneAgentEvidence[], extra = {}) =>
const H = 'authority-a'
describe('resolvePaneAgentIdentity', () => {
+ it('keeps every evidence source ranked exactly once', () => {
+ expect(PANE_AGENT_SOURCE_RANK).toBe(PANE_AGENT_EVIDENCE_SOURCES)
+ expect(new Set(PANE_AGENT_SOURCE_RANK).size).toBe(PANE_AGENT_SOURCE_RANK.length)
+ for (const source of PANE_AGENT_EVIDENCE_SOURCES) {
+ expect(PANE_AGENT_SOURCE_RANK.indexOf(source)).toBeGreaterThanOrEqual(0)
+ }
+ })
+
describe('a display title is the last thing consulted', () => {
it.each(PANE_AGENT_EVIDENCE_SOURCES.filter((s) => s !== 'title' && s !== 'sibling'))(
'lets %s outrank a conflicting title',
@@ -153,6 +162,17 @@ describe('resolvePaneAgentIdentity', () => {
})
})
+ it('fails loudly when an evidence source is missing from the rank', () => {
+ expect(() =>
+ resolve([
+ {
+ source: 'future-source' as PaneAgentEvidence['source'],
+ agent: 'codex'
+ }
+ ])
+ ).toThrow('Unknown pane-agent evidence source')
+ })
+
describe('input order does not decide the answer', () => {
it('resolves the same regardless of how evidence is listed', () => {
const evidence: PaneAgentEvidence[] = [
diff --git a/src/shared/pane-agent-identity-resolver.ts b/src/shared/pane-agent-identity-resolver.ts
index 98342963415..4b03c5de16d 100644
--- a/src/shared/pane-agent-identity-resolver.ts
+++ b/src/shared/pane-agent-identity-resolver.ts
@@ -1,5 +1,10 @@
+import { resolveCanonicalPaneAgentEvidence } from './pane-agent-identity-adapter'
+import type { PaneAgentEvidenceSource } from './pane-agent-evidence-sources'
import type { TuiAgent } from './tui-agent'
+export { PANE_AGENT_EVIDENCE_SOURCES } from './pane-agent-evidence-sources'
+export type { PaneAgentEvidenceSource } from './pane-agent-evidence-sources'
+
/**
* One place that answers "which agent is in this pane".
*
@@ -23,27 +28,6 @@ import type { TuiAgent } from './tui-agent'
* launch, a recognized command at a shell prompt, a host-confirmed foreground change, a new
* provider session. Never by a title changing, and never by transport loss.
*/
-export const PANE_AGENT_EVIDENCE_SOURCES = [
- /** A live provider hook for a turn in progress. The agent is running and said so. */
- 'live-hook',
- /** The pane's foreground process, as read on the execution host. */
- 'process',
- /** Orca launched, resumed, or accepted a command for this agent. A fact Orca owns. */
- 'launch',
- /** A provider hook from a turn that finished. Still authoritative about identity. */
- 'completed-hook',
- /** A sleeping session record restored for this pane. */
- 'sleeping-session',
- /** Another pane in the same tab. Tab-level surfaces only; never pane-scoped routing. */
- 'sibling',
- /** Parsed from the terminal title. A decoration channel; anyone can type an agent's name. */
- 'title'
-] as const
-export type PaneAgentEvidenceSource = (typeof PANE_AGENT_EVIDENCE_SOURCES)[number]
-
-/** Authority order, strongest first. Position here is the ONLY place precedence is expressed. */
-const SOURCE_RANK: readonly PaneAgentEvidenceSource[] = PANE_AGENT_EVIDENCE_SOURCES
-
/**
* Which agent run a piece of evidence belongs to.
*
@@ -112,52 +96,5 @@ export type PaneAgentIdentity = {
export function resolvePaneAgentIdentity (
input: PaneAgentIdentityInput
): PaneAgentIdentity {
- const superseded: PaneAgentEvidenceSource[] = []
- const floor = input.minimumSource
- ? SOURCE_RANK.indexOf(input.minimumSource)
- : Number.MAX_SAFE_INTEGER
-
- const eligible = input.evidence.filter((item) => {
- if (item.source === 'sibling' && input.allowSibling !== true) {
- return false
- }
- // Why the floor: an action consumer must not be able to act on a title, at any rank. Dropping
- // the evidence entirely rather than ranking it lower makes misuse impossible rather than
- // unlikely — a caller cannot accidentally consult it by reordering.
- if (SOURCE_RANK.indexOf(item.source) > floor) {
- return false
- }
- if (input.currentRun === undefined || item.run === undefined) {
- // Why eligible: absence means "this peer does not publish run keys", not "this is stale".
- // Treating unknown as superseded would blank every row from an older host.
- return true
- }
- if (item.run.authorityId !== input.currentRun.authorityId) {
- // Why eligible and NOT superseded: runs from different authorities are incomparable, not
- // older. A restarted main counts from its own floor, so `incarnation` alone would falsely
- // equate unrelated runs. Incomparable evidence is treated as unknown, like an absent key.
- return true
- }
- if (item.run.incarnation === input.currentRun.incarnation) {
- return true
- }
- superseded.push(item.source)
- return false
- })
-
- for (const source of SOURCE_RANK) {
- const matches = eligible.filter((item) => item.source === source)
- if (matches.length === 0) {
- continue
- }
- const agents = new Set(matches.map((item) => item.agent))
- if (agents.size > 1) {
- // Why null and not the first: two observations of the same class naming different agents is
- // a genuine conflict, and picking one would make the answer depend on array order — the very
- // property this resolver exists to remove. Fall through to nothing rather than guess.
- return { agent: null, source: null, ambiguousAt: source, supersededSources: superseded }
- }
- return { agent: matches[0].agent, source, supersededSources: superseded }
- }
- return { agent: null, source: null, supersededSources: superseded }
+ return resolveCanonicalPaneAgentEvidence(input)
}
diff --git a/src/shared/pane-agent-identity-surface-inventory.test.ts b/src/shared/pane-agent-identity-surface-inventory.test.ts
new file mode 100644
index 00000000000..77d9edb3b84
--- /dev/null
+++ b/src/shared/pane-agent-identity-surface-inventory.test.ts
@@ -0,0 +1,284 @@
+import { readFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { describe, expect, it } from 'vitest'
+import { glob } from 'tinyglobby'
+import { isTestFile, stripComments } from './source-scan/source-tree-scan'
+
+/**
+ * Surface half of the identity inventory ratchet: every consumer decision point from the closed
+ * 65-row inventory (rows 32–65 — the direct title/native-chat selectors, tab projections, mobile
+ * sync graph, lifecycle selectors, status/OSC ingress, worktree status, attention, and
+ * title-reset paths) is pinned to a marker symbol in its file. The helper-name census
+ * (`pane-agent-identity-inventory.test.ts`) is necessary but not sufficient — these files reach
+ * identity through direct reads a name census cannot see. Moving or renaming a marker means the
+ * inventory row must be re-classified, deliberately, before review.
+ */
+
+type SurfaceRow = {
+ /** Row number in the closed consumer inventory. */
+ row: number
+ path: string
+ marker: string
+}
+
+const SURFACE_ROWS: readonly SurfaceRow[] = [
+ {
+ row: 32,
+ path: 'src/renderer/src/components/terminal-pane/native-chat-leaf-title-agent.ts',
+ marker: 'resolveNativeChatLeafTitleAgent'
+ },
+ {
+ row: 32,
+ path: 'src/renderer/src/components/terminal-pane/use-terminal-pane-chat-state.ts',
+ marker: 'resolveNativeChatLeafTitleAgent'
+ },
+ {
+ row: 33,
+ path: 'src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts',
+ marker: 'installPaneAgentIdentity'
+ },
+ { row: 34, path: 'src/main/runtime/orchestration/groups.ts', marker: 'terminalIsAgent' },
+ {
+ row: 35,
+ path: 'src/renderer/src/lib/active-agent-note-target.ts',
+ marker: 'getActiveTerminalNoteTarget'
+ },
+ {
+ row: 36,
+ path: 'src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.ts',
+ marker: 'resolveProtectedMultilinePasteOptionsForPane'
+ },
+ {
+ row: 37,
+ path: 'src/renderer/src/components/terminal-pane/command-code-output-ownership.ts',
+ marker: 'canCommandCodeOutputOwnPane'
+ },
+ { row: 38, path: 'src/renderer/src/lib/agent-ready-wait.ts', marker: 'waitForAgentReady' },
+ {
+ row: 39,
+ path: 'src/renderer/src/lib/agent-paste-draft.ts',
+ marker: 'getSettingsForAgentTabRuntimeOwner'
+ },
+ {
+ row: 40,
+ path: 'src/renderer/src/lib/agent-followup-delivery.ts',
+ marker: 'sendFollowupPromptWhenAgentReady'
+ },
+ {
+ row: 41,
+ path: 'src/renderer/src/lib/codex-session-restart.ts',
+ marker: 'markLiveCodexSessionsForRestart'
+ },
+ {
+ row: 41,
+ path: 'src/renderer/src/lib/codex-pane-restart-eligibility.ts',
+ marker: 'isCodexForegroundProcess'
+ },
+ {
+ row: 42,
+ path: 'src/renderer/src/components/native-chat/native-chat-availability.ts',
+ marker: 'canToggleNativeChat'
+ },
+ {
+ row: 43,
+ path: 'src/renderer/src/components/native-chat/native-chat-pane-resolution.ts',
+ marker: 'resolveNativeChatSession'
+ },
+ {
+ row: 44,
+ path: 'src/renderer/src/components/terminal-pane/terminal-agent-session-continuation.ts',
+ marker: 'canContinueAgentSessionInNewSession'
+ },
+ {
+ row: 45,
+ path: 'src/renderer/src/components/terminal-pane/terminal-agent-session-fork.ts',
+ marker: 'prepareAgentSessionForkFromPane'
+ },
+ {
+ row: 46,
+ path: 'src/renderer/src/components/terminal-pane/agent-interrupt-inference.ts',
+ marker: 'isPlainEscapeKeyEvent'
+ },
+ {
+ row: 46,
+ path: 'src/renderer/src/components/terminal-pane/agent-question-answered-inference.ts',
+ marker: 'inferQuestionAnsweredFromCurrentStatus'
+ },
+ {
+ row: 47,
+ path: 'src/renderer/src/components/terminal-pane/terminal-keyboard-protocol-pane-agent.ts',
+ marker: 'resolvePaneKeyboardProtocolAgent'
+ },
+ {
+ row: 47,
+ path: 'src/renderer/src/components/terminal-pane/terminal-pane-manager-options.ts',
+ marker: 'resolvePaneKeyboardProtocolAgent'
+ },
+ {
+ row: 48,
+ path: 'src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts',
+ marker: 'selectTabAgentTypesByTabId'
+ },
+ {
+ row: 49,
+ path: 'src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index.ts',
+ marker: 'createTerminalTabAgentTypeSelector'
+ },
+ {
+ row: 50,
+ path: 'src/renderer/src/lib/tab-agent-status-index.ts',
+ marker: 'selectLiveTabAgentPanes'
+ },
+ {
+ row: 51,
+ path: 'src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts',
+ marker: 'resolveTerminalTabActivityStatus'
+ },
+ {
+ row: 52,
+ path: 'src/renderer/src/lib/workspace-tab-agent-metadata.ts',
+ marker: 'maxAgentActivityAt'
+ },
+ {
+ row: 52,
+ path: 'src/renderer/src/lib/workspace-tab-palette-entry-builder.ts',
+ marker: 'buildSearchableWorkspaceTabEntries'
+ },
+ {
+ row: 53,
+ path: 'src/renderer/src/lib/running-agent-targets.ts',
+ marker: 'deriveRunningAgentSendTargets'
+ },
+ {
+ row: 54,
+ path: 'src/renderer/src/runtime/sync-runtime-graph.ts',
+ marker: 'buildMobileSessionTabSnapshots'
+ },
+ {
+ row: 55,
+ path: 'src/renderer/src/lib/agent-hibernation-pane-eligibility.ts',
+ marker: 'toRuntimePtyId'
+ },
+ {
+ row: 56,
+ path: 'src/renderer/src/lib/resume-sleeping-agent-session.ts',
+ marker: 'resumeSleepingAgentSessionsForWorktree'
+ },
+ {
+ row: 57,
+ path: 'src/renderer/src/lib/automation-session-reuse.ts',
+ marker: 'findReusableAutomationSession'
+ },
+ {
+ row: 58,
+ path: 'src/renderer/src/components/terminal-pane/pty-connection/cold-restore-resume-startup.ts',
+ marker: 'bindBuildColdRestoreAgentResumeStartup'
+ },
+ {
+ row: 59,
+ path: 'src/main/agent-hooks/server/server-authority-evidence.ts',
+ marker: 'recordCurrentAuthorityObservation'
+ },
+ {
+ row: 59,
+ path: 'src/main/runtime/orca-runtime-write-orchestration-pointer-pty.ts',
+ marker: 'resolvePaneAgentIdentityField'
+ },
+ {
+ row: 59,
+ path: 'src/renderer/src/hooks/ipc-events/agent-status-event-applicator.ts',
+ marker: 'createAgentStatusEventApplicator'
+ },
+ {
+ row: 59,
+ path: 'src/renderer/src/store/slices/agent-status-authority-actions.ts',
+ marker: 'transferAgentPaneAuthority'
+ },
+ {
+ row: 59,
+ path: 'src/renderer/src/store/slices/pane-foreground-agent.ts',
+ marker: 'createPaneForegroundAgentSlice'
+ },
+ {
+ row: 59,
+ path: 'src/renderer/src/hooks/ipc-events/agent-status-routing.ts',
+ marker: 'isAgentStatusForRecentlyClosedTab'
+ },
+ {
+ row: 60,
+ path: 'src/renderer/src/components/terminal-pane/pty-connection/title-spawn-bell.ts',
+ marker: 'installTitleSpawnBell'
+ },
+ { row: 61, path: 'src/renderer/src/lib/worktree-status.ts', marker: 'getWorktreeStatus' },
+ {
+ row: 62,
+ path: 'src/main/runtime/runtime-worktree-status-projection.ts',
+ marker: 'getLeafWorktreeStatus'
+ },
+ {
+ row: 63,
+ path: 'src/renderer/src/components/sidebar/smart-attention.ts',
+ marker: 'buildAttentionByWorktree'
+ },
+ {
+ row: 64,
+ path: 'src/renderer/src/components/status-bar/workspace-space-presentation.ts',
+ marker: 'countWorkspaceSpaceActiveAgents'
+ },
+ { row: 65, path: 'src/renderer/src/store/slices/terminal-helpers.ts', marker: 'getResetTitle' },
+ {
+ row: 6,
+ path: 'src/renderer/src/runtime/web-session-tabs-sync.ts',
+ marker: 'applyWebSessionTabs'
+ }
+]
+
+describe('pane agent identity surface inventory (rows 6, 32–65)', () => {
+ it('every pinned surface still carries its marker symbol', () => {
+ for (const row of SURFACE_ROWS) {
+ const source = stripComments(readFileSync(join(process.cwd(), row.path), 'utf8'))
+ expect({ row: row.row, path: row.path, hasMarker: source.includes(row.marker) }).toEqual({
+ row: row.row,
+ path: row.path,
+ hasMarker: true
+ })
+ }
+ })
+})
+
+/**
+ * Identity-observation rebind audit. Advancing a pane incarnation without a positive replacement
+ * proof is how a legitimate reclaim and a stale-hook bug get conflated (see
+ * `PaneReplacementProof` in pane-agent-identity-adapter.ts). Every existing sequencer `rebind`
+ * call is pinned here by file and count: today they are the retired-pane `restart` disposition
+ * (three ingress paths) and the renderer pane-key transfer. Adding a rebind call, or changing
+ * these, requires updating this audit — and per the migration plan, a `replacementProof`.
+ */
+const IDENTITY_SEQUENCER_REBIND_RE = /\b(?:observations|rendererAgentStatusObservations)\.rebind\(/g
+
+const EXPECTED_REBIND_SITES: readonly (readonly [path: string, occurrences: number])[] = [
+ ['src/main/agent-hooks/server/server-ingest-normalization.ts', 1],
+ ['src/main/agent-hooks/server/server-ingest-remote.ts', 1],
+ ['src/main/agent-hooks/server/server-lifecycle.ts', 1],
+ ['src/renderer/src/store/slices/agent-status-authority-actions.ts', 1]
+]
+
+describe('identity observation rebind audit', () => {
+ it('pins every identity-sequencer rebind call site by file and count', async () => {
+ const files = await glob(['src/**/*.{ts,tsx}', 'mobile/src/**/*.{ts,tsx}'], {
+ ignore: ['**/*.test.*', '**/*.spec.*']
+ })
+ const actual: [string, number][] = []
+ for (const path of files.sort()) {
+ if (isTestFile(path)) {
+ continue
+ }
+ const source = stripComments(readFileSync(join(process.cwd(), path), 'utf8'))
+ const occurrences = source.match(IDENTITY_SEQUENCER_REBIND_RE)?.length ?? 0
+ if (occurrences > 0) {
+ actual.push([path, occurrences])
+ }
+ }
+ expect(actual).toEqual(EXPECTED_REBIND_SITES.map((site) => [...site]))
+ }, 30_000)
+})
diff --git a/src/shared/pane-agent-identity-title-corpus.test.ts b/src/shared/pane-agent-identity-title-corpus.test.ts
new file mode 100644
index 00000000000..f24cd431065
--- /dev/null
+++ b/src/shared/pane-agent-identity-title-corpus.test.ts
@@ -0,0 +1,156 @@
+import { existsSync, readdirSync, readFileSync } from 'node:fs'
+import { homedir } from 'node:os'
+import { join } from 'node:path'
+import { createHash, randomBytes } from 'node:crypto'
+import { describe, expect, it } from 'vitest'
+import { collectAgentTitleEvidence } from './agent-title-evidence'
+import { resolveCanonicalPaneAgentIdentity } from './pane-agent-identity-adapter'
+import type { TuiAgent } from './tui-agent'
+
+/**
+ * Title regression gates for the identity-ladder migration.
+ *
+ * Two layers: a controlled fixture table that always runs (CI-safe), and a local characterization
+ * gate over the machine's real recorded corpus. The corpus gate is not a CI prerequisite tied to
+ * one developer's home — when no history exists it reports `corpus unavailable — skipped`
+ * explicitly, never a silently green zero-title run. Raw titles never reach logs or failure
+ * output; changed titles are reported as salted hashes plus old/new agent summaries only.
+ */
+
+const RECORDED_HISTORY_DIR = 'terminal-history'
+const QUARANTINE_DIR = '.recovery-quarantine'
+
+function orcaAppSupportCandidates(): string[] {
+ if (process.platform === 'darwin') {
+ return [join(homedir(), 'Library', 'Application Support', 'Orca')]
+ }
+ if (process.platform === 'win32') {
+ return [join(process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'), 'Orca')]
+ }
+ return [
+ join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'Orca'),
+ join(process.env.XDG_DATA_HOME ?? join(homedir(), '.local', 'share'), 'Orca')
+ ]
+}
+
+/** Deliberately shallow: `terminal-history//checkpoint.json` only, with the hidden
+ * quarantine subtree excluded BY NAME so a future recursive rewrite cannot silently turn
+ * quarantined recovery data into product regressions. */
+function loadRecordedTitleCorpus(): { checkpointCount: number; titles: string[] } | null {
+ const root = orcaAppSupportCandidates()
+ .map((candidate) => join(candidate, RECORDED_HISTORY_DIR))
+ .find((candidate) => existsSync(candidate))
+ if (!root) {
+ return null
+ }
+ let checkpointCount = 0
+ const titles = new Set()
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
+ if (!entry.isDirectory() || entry.name === QUARANTINE_DIR || entry.name.startsWith('.')) {
+ continue
+ }
+ const checkpointPath = join(root, entry.name, 'checkpoint.json')
+ if (!existsSync(checkpointPath)) {
+ continue
+ }
+ const parsed: unknown = JSON.parse(readFileSync(checkpointPath, 'utf8'))
+ checkpointCount += 1
+ const lastTitle = (parsed as { lastTitle?: unknown }).lastTitle
+ if (typeof lastTitle === 'string' && lastTitle.length > 0) {
+ titles.add(lastTitle)
+ }
+ }
+ return { checkpointCount, titles: [...titles] }
+}
+
+/** What the canonical adapter answers when a title is all a pane has (the uncovered lane). */
+function canonicalTitleOnlyAgent(title: string): TuiAgent | null {
+ return resolveCanonicalPaneAgentIdentity({ title }).agent
+}
+
+describe('controlled title fixtures (always run)', () => {
+ const FIXTURES: readonly { name: string; title: string; expected: TuiAgent | null }[] = [
+ {
+ name: 'mandatory adversarial owner suffix beats the agent names in task text',
+ title: 'STA-4011 Linux Antigravity Commit Messages - grok',
+ expected: 'grok'
+ },
+ {
+ name: 'task text mentioning other agents is not identity',
+ title: 'Compare Antigravity with Gemini 3.7 Flash',
+ expected: null
+ },
+ {
+ name: 'owner suffix still answers over mentioned agents',
+ title: 'Compare Antigravity with Gemini 3.7 Flash… - grok',
+ expected: 'grok'
+ },
+ { name: 'Claude status sigil is a vendor marker', title: '✳', expected: 'claude' },
+ { name: 'Claude management screen is not identity', title: 'claude agents', expected: null },
+ { name: 'a shell title names no agent', title: 'zsh', expected: null },
+ { name: 'a default worktree-ish title names no agent', title: 'my-claude-fix', expected: null },
+ {
+ name: 'conflicting vendor markers resolve to nothing',
+ title: '✳ | ✦ two sigils',
+ expected: null
+ },
+ {
+ name: 'conflicting anchored names resolve to nothing',
+ title: 'OC | something… - grok',
+ expected: null
+ },
+ { name: 'a bare Pi title anchors as Pi', title: 'pi', expected: 'pi' },
+ { name: 'an OMP status title anchors as OMP', title: 'omp ready', expected: 'omp' },
+ {
+ // Wrapper-frame π/OMP separators are handled by the synthetic-title path, not this
+ // evidence parser; pinned so a parser change here is a deliberate decision.
+ name: 'a π wrapper frame is declined by the evidence parser',
+ title: 'π : ready',
+ expected: null
+ }
+ ]
+
+ for (const fixture of FIXTURES) {
+ it(fixture.name, () => {
+ expect(collectAgentTitleEvidence(fixture.title).agent).toBe(fixture.expected)
+ // The adapter's title-only lane must give the very same answer — phase 1 changes no
+ // parser semantics, only provenance.
+ expect(canonicalTitleOnlyAgent(fixture.title)).toBe(fixture.expected)
+ })
+ }
+})
+
+describe('recorded title corpus characterization (local gate)', () => {
+ it('the canonical title-only lane matches the shipped parser on every recorded title', (ctx) => {
+ const corpus = loadRecordedTitleCorpus()
+ if (corpus === null) {
+ console.info('corpus unavailable — skipped (no recorded terminal history on this machine)')
+ ctx.skip()
+ return
+ }
+ // A machine WITH history must never pass on an empty read — that would be a silently green
+ // zero-title run, not a characterization.
+ expect(corpus.checkpointCount).toBeGreaterThan(0)
+ expect(corpus.titles.length).toBeGreaterThan(0)
+ console.info(
+ `corpus: ${corpus.checkpointCount} checkpoints, ${corpus.titles.length} distinct titles`
+ )
+
+ const salt = randomBytes(16).toString('hex')
+ const changed: { titleHash: string; oldAgent: string | null; newAgent: string | null }[] = []
+ for (const title of corpus.titles) {
+ const oldAgent = collectAgentTitleEvidence(title).agent
+ const newAgent = canonicalTitleOnlyAgent(title)
+ if (oldAgent !== newAgent) {
+ changed.push({
+ titleHash: createHash('sha256').update(`${salt}:${title}`).digest('hex').slice(0, 16),
+ oldAgent,
+ newAgent
+ })
+ }
+ }
+ // Report hashes and agent summaries only; a reviewer who needs the raw value inspects the
+ // protected corpus on the machine that owns it.
+ expect(changed).toEqual([])
+ }, 60_000)
+})
diff --git a/src/shared/pane-agent-owner.test.ts b/src/shared/pane-agent-owner.test.ts
index 13cfd217c45..b0d61801392 100644
--- a/src/shared/pane-agent-owner.test.ts
+++ b/src/shared/pane-agent-owner.test.ts
@@ -37,6 +37,42 @@ describe('resolvePaneAgentOwner', () => {
).toBe('omp')
})
+ it('preserves the pre-tranche precedence for every conflicting owner tier', () => {
+ expect(
+ resolvePaneAgentOwnerRecord({
+ launchAgent: 'claude',
+ hookAgent: 'codex',
+ siblingHookAgent: 'gemini',
+ completedHookAgent: 'pi',
+ sleepingSessionAgent: 'omp'
+ })
+ ).toEqual({ agent: 'claude', ownerIsLaunch: true })
+ expect(
+ resolvePaneAgentOwnerRecord({
+ hookAgent: 'claude',
+ siblingHookAgent: 'codex',
+ completedHookAgent: 'gemini',
+ siblingCompletedHookAgent: 'pi',
+ sleepingSessionAgent: 'omp'
+ })
+ ).toEqual({ agent: 'claude', ownerIsLaunch: false })
+ expect(
+ resolvePaneAgentOwnerRecord({
+ siblingHookAgent: 'codex',
+ completedHookAgent: 'claude',
+ siblingCompletedHookAgent: 'gemini',
+ sleepingSessionAgent: 'omp'
+ })
+ ).toEqual({ agent: 'codex', ownerIsLaunch: false })
+ expect(
+ resolvePaneAgentOwnerRecord({
+ completedHookAgent: 'claude',
+ siblingCompletedHookAgent: 'codex',
+ sleepingSessionAgent: 'gemini'
+ })
+ ).toEqual({ agent: 'claude', ownerIsLaunch: false })
+ })
+
it('returns null when no owner evidence exists', () => {
expect(resolvePaneAgentOwner({})).toBeNull()
expect(resolvePaneAgentOwner({ launchAgent: null, hookAgent: undefined })).toBeNull()
diff --git a/src/shared/pane-agent-owner.ts b/src/shared/pane-agent-owner.ts
index 31b878d67ec..5b5563670e8 100644
--- a/src/shared/pane-agent-owner.ts
+++ b/src/shared/pane-agent-owner.ts
@@ -48,9 +48,10 @@ const PANE_OWNER_RANK: readonly {
]
/**
- * The single authoritative resolver for "which agent owns this pane", shared by
- * the tab-icon resolver, the terminal-pane display/renderer owner, and the
- * mirrored-tab title owner so they cannot drift apart.
+ * Compatibility owner lookup shared by the existing consumer surfaces.
+ *
+ * Tranche 0 intentionally preserves this pre-migration precedence byte-for-byte; switching
+ * these consumers to canonical evidence belongs to tranche 1.
*
* Why this precedence: launch intent is the authoritative bootstrap before any
* process signal exists, so it leads. Once launch metadata is gone — a mirrored
diff --git a/src/shared/terminal-title-agent-type.ts b/src/shared/terminal-title-agent-type.ts
index cdbce788806..4a000f8be9b 100644
--- a/src/shared/terminal-title-agent-type.ts
+++ b/src/shared/terminal-title-agent-type.ts
@@ -10,6 +10,7 @@ import {
getPiCompatibleSyntheticAgentLabel,
isLegacyPiCompatibleTitle
} from './pi-compatible-synthetic-title'
+import { resolveCanonicalPaneAgentIdentity } from './pane-agent-identity-adapter'
import { memoizeTitleClassification } from './terminal-title-classification-memo'
import type { TuiAgent } from './tui-agent'
@@ -217,10 +218,10 @@ function computeAgentLabel(title: string): string | null {
return null
}
-// Maps getAgentLabel()'s product labels to TuiAgent ids — the fallback for
-// agents whose foreground PROCESS name isn't self-identifying (Claude Code runs
-// as `node`, but its "✳ Claude Code" title resolves here). Agents whose process
-// name already matches (codex, etc.) never reach this path.
+/** Pure in `title` — memoized so repeated selector reads skip the regex ladder. */
+export const getAgentLabel: (title: string) => string | null =
+ memoizeTitleClassification(computeAgentLabel)
+
const TITLE_LABEL_TO_AGENT: Partial> = {
'Claude Code': 'claude',
OpenClaude: 'openclaude',
@@ -240,10 +241,6 @@ const TITLE_LABEL_TO_AGENT: Partial> = {
OMP: 'omp'
}
-/** Pure in `title` — memoized so repeated selector reads skip the regex ladder. */
-export const getAgentLabel: (title: string) => string | null =
- memoizeTitleClassification(computeAgentLabel)
-
function hasGenericClaudeStatusPrefix(title: string): boolean {
return (
containsAgentSpinnerGlyph(title) ||
@@ -266,7 +263,13 @@ function isGenericClaudeStatusClaim(title: string, titleAgent: TuiAgent | null):
export function resolveTerminalTitleAgentType(title: string): TuiAgent | null {
const label = getAgentLabel(title)
- return label ? (TITLE_LABEL_TO_AGENT[label] ?? null) : null
+ const parsed = label ? (TITLE_LABEL_TO_AGENT[label] ?? null) : null
+ return resolveCanonicalPaneAgentIdentity({
+ title,
+ // Preserve this public title-parser adapter's historical answer; pane identity
+ // consumers pass raw titles to the canonical resolver and enforce its fence.
+ uncoveredFallback: { agent: parsed, titleOnly: false }
+ }).agent
}
/**
@@ -283,6 +286,6 @@ function computeExplicitTerminalTitleAgentType(title: string): TuiAgent | null {
return titleAgent
}
-/** Pure in `title` — memoized so repeated selector reads skip the regex ladder. */
+/** Pure in `title` — memoized so repeated selector reads skip the canonical/title parse. */
export const resolveExplicitTerminalTitleAgentType: (title: string) => TuiAgent | null =
memoizeTitleClassification(computeExplicitTerminalTitleAgentType)
From dd9eaa958545de250e504bc5a0ff0e05afc90da1 Mon Sep 17 00:00:00 2001
From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
Date: Thu, 3 Sep 2026 21:20:27 -0400
Subject: [PATCH 14/43] fix(cloud): retry the committed-winner collision codes
in relay schema startup (#18553)
* fix(cloud): retry the committed-winner collision codes in relay schema startup
`CREATE TABLE IF NOT EXISTS` only checks the name before the catalog inserts, so
the loser of a concurrent CREATE fails in one of two ways depending on timing:
on the catalog unique index (23505, which the startup retry already handled) or,
when the winner has committed by the time the loser reaches TypeCreate /
heap_create_with_catalog, on the name check those routines repeat (42710
duplicate type, 42P07 duplicate relation). The predicate treated the latter as
fatal, so a director could fail startup on a table it was about to find present.
This is what turned `postgres-schema-concurrency-postgres.test.ts` red on main
and on every relay PR (CI's shared runner loses the race more often than a dev
box): a throwaway diagnostic run in CI reported 42710 from TypeCreate and 42P07
from heap_create_with_catalog as the only rejection reasons.
Treat 42710/42P07 as retryable for `CREATE TABLE IF NOT EXISTS` and 42P07 for
`CREATE [UNIQUE] INDEX IF NOT EXISTS`; every other statement shape still fails
fast. The concurrency test now runs ten rounds and reports the loser's SQLSTATE
instead of a bare boolean.
* chore(cloud): allowlist the RFC 6455 example Sec-WebSocket-Key for upgrade tests
Cloud Verify's Secret scan runs gitleaks over --all refs, so the raw-socket
upgrade test on fix/relay-upgrade-malformed-uri (#18547) trips every cloud PR's
scan until its allowlist reaches main. Land the allowlist here first.
---
cloud/.gitleaks.toml | 7 ++++
.../src/database-postgres-timeout.test.ts | 33 ++++++++++++++++++
...stgres-schema-concurrency-postgres.test.ts | 34 +++++++++++--------
.../apps/relay/src/postgres-schema-startup.ts | 34 +++++++++++++++----
4 files changed, 88 insertions(+), 20 deletions(-)
diff --git a/cloud/.gitleaks.toml b/cloud/.gitleaks.toml
index fc5c725c37d..0bb1f966fae 100644
--- a/cloud/.gitleaks.toml
+++ b/cloud/.gitleaks.toml
@@ -13,3 +13,10 @@ description = "Cloud SQL rollout lease holder keys in the action's unit tests"
regexTarget = "secret"
paths = ['''\.github/actions/cloud-sql-rollout-lease/[a-z-]+\.test\.mjs$''']
regexes = ['''^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/[0-9]+$''']
+
+# RFC 6455 §1.3 example handshake nonce ("the sample nonce" in base64), sent by the raw-socket
+# upgrade tests; the generic key rule reads any base64 header value as a secret.
+[[allowlists]]
+description = "RFC 6455 example Sec-WebSocket-Key in upgrade tests"
+regexTarget = "secret"
+regexes = ['''^dGhlIHNhbXBsZSBub25jZQ==$''']
diff --git a/cloud/apps/relay/src/database-postgres-timeout.test.ts b/cloud/apps/relay/src/database-postgres-timeout.test.ts
index a04a9eea042..7aba1234f7f 100644
--- a/cloud/apps/relay/src/database-postgres-timeout.test.ts
+++ b/cloud/apps/relay/src/database-postgres-timeout.test.ts
@@ -118,6 +118,39 @@ describe('PostgreSQL schema startup', () => {
expect(query).toHaveBeenCalledTimes(2)
})
+ it.each([
+ ['42710', 'CREATE TABLE IF NOT EXISTS test'],
+ ['42P07', 'CREATE TABLE IF NOT EXISTS test'],
+ ['42P07', 'CREATE INDEX IF NOT EXISTS test_index ON test(id)'],
+ ['42P07', 'CREATE UNIQUE INDEX IF NOT EXISTS test_index ON test(id)']
+ ])('retries the committed-winner %s collision for %s', async (code, statement) => {
+ vi.spyOn(console, 'warn').mockImplementation(() => undefined)
+ const collision = Object.assign(new Error('already exists'), { code })
+ const query = vi
+ .fn<(statement: string) => Promise>()
+ .mockRejectedValueOnce(collision)
+ .mockResolvedValue(undefined)
+
+ await applyPostgresSchema([statement], query, { wait: async () => undefined })
+
+ expect(query).toHaveBeenCalledTimes(2)
+ })
+
+ it.each([
+ ['42710', 'CREATE INDEX IF NOT EXISTS test_index ON test(id)'],
+ ['42710', 'CREATE TABLE test'],
+ ['42P07', 'CREATE TABLE test'],
+ ['42P07', 'CREATE INDEX test_index ON test(id)']
+ ])('does not retry %s for %s', async (code, statement) => {
+ const error = Object.assign(new Error('already exists'), { code })
+ const query = vi.fn<(statement: string) => Promise>().mockRejectedValue(error)
+ const pause = vi.fn(async () => undefined)
+
+ await expect(applyPostgresSchema([statement], query, { wait: pause })).rejects.toBe(error)
+
+ expect(pause).not.toHaveBeenCalled()
+ })
+
it.each([
['pg_type_typname_nsp_index', 'CREATE TABLE test'],
['pg_class_relname_nsp_index', 'CREATE INDEX test_index ON test(id)']
diff --git a/cloud/apps/relay/src/postgres-schema-concurrency-postgres.test.ts b/cloud/apps/relay/src/postgres-schema-concurrency-postgres.test.ts
index 5208f137ae8..9ed3cb2f324 100644
--- a/cloud/apps/relay/src/postgres-schema-concurrency-postgres.test.ts
+++ b/cloud/apps/relay/src/postgres-schema-concurrency-postgres.test.ts
@@ -34,22 +34,28 @@ describePostgres('PostgreSQL schema concurrency', () => {
})
it('opens five directors when one new table is absent', async () => {
- const initial = await openRelayDatabase({ databaseUrl: scopedUrl, dataDir: '' })
- await initial.query(`DROP TABLE relay_cell_legacy_fence_adoptions`)
- await initial.close()
+ // Which catalog step the race loser fails on depends on scheduling, so run several rounds and
+ // keep the loser's SQLSTATE in the failure instead of a bare boolean.
+ for (let round = 0; round < 10; round += 1) {
+ const initial = await openRelayDatabase({ databaseUrl: scopedUrl, dataDir: '' })
+ await initial.query(`DROP TABLE relay_cell_legacy_fence_adoptions`)
+ await initial.close()
- const results = await Promise.allSettled(
- Array.from({ length: 5 }, async (): Promise =>
- await openRelayDatabase({ databaseUrl: scopedUrl, dataDir: '' })
+ const results = await Promise.allSettled(
+ Array.from({ length: 5 }, async (): Promise =>
+ await openRelayDatabase({ databaseUrl: scopedUrl, dataDir: '' })
+ )
+ )
+ const databases = results.flatMap((result) =>
+ result.status === 'fulfilled' ? [result.value] : []
)
- )
- const databases = results.flatMap((result) =>
- result.status === 'fulfilled' ? [result.value] : []
- )
- try {
- expect(results.every((result) => result.status === 'fulfilled')).toBe(true)
- } finally {
await Promise.all(databases.map(async (database) => await database.close()))
+ const rejections = results.flatMap((result) =>
+ result.status === 'rejected'
+ ? [{ round, code: (result.reason as { code?: unknown }).code, message: String(result.reason) }]
+ : []
+ )
+ expect(rejections).toEqual([])
}
- })
+ }, 60_000)
})
diff --git a/cloud/apps/relay/src/postgres-schema-startup.ts b/cloud/apps/relay/src/postgres-schema-startup.ts
index 5e6260ad2fb..ba9efc6a792 100644
--- a/cloud/apps/relay/src/postgres-schema-startup.ts
+++ b/cloud/apps/relay/src/postgres-schema-startup.ts
@@ -22,15 +22,37 @@ function wait(delayMs: number): Promise {
return new Promise((resolve) => setTimeout(resolve, delayMs))
}
+const CREATE_TABLE_IF_NOT_EXISTS = /^\s*CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\b/i
+const CREATE_INDEX_IF_NOT_EXISTS = /^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+IF\s+NOT\s+EXISTS\b/i
+
+// `IF NOT EXISTS` only checks the name before the catalog inserts, so the loser of a concurrent
+// CREATE can fail on the catalog unique index (23505) or, when the winner has already committed by
+// the time the loser reaches TypeCreate/heap_create_with_catalog, on the name check those routines
+// repeat (42710 duplicate type, 42P07 duplicate relation). Each is a no-op on the next attempt.
+function concurrentCreateCollision(
+ value: { code?: unknown; constraint?: unknown },
+ statement: string
+): boolean {
+ if (CREATE_TABLE_IF_NOT_EXISTS.test(statement)) {
+ return (
+ (value.code === '23505' && value.constraint === 'pg_type_typname_nsp_index') ||
+ value.code === '42710' ||
+ value.code === '42P07'
+ )
+ }
+ if (CREATE_INDEX_IF_NOT_EXISTS.test(statement)) {
+ return (
+ (value.code === '23505' && value.constraint === 'pg_class_relname_nsp_index') ||
+ value.code === '42P07'
+ )
+ }
+ return false
+}
+
function retryableSchemaError(error: unknown, statement: string): boolean {
const value = error as { code?: unknown; constraint?: unknown }
return (
- RETRYABLE_SCHEMA_CODES.has(String(value.code)) ||
- (value.code === '23505' &&
- ((value.constraint === 'pg_type_typname_nsp_index' &&
- /^\s*CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\b/i.test(statement)) ||
- (value.constraint === 'pg_class_relname_nsp_index' &&
- /^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+IF\s+NOT\s+EXISTS\b/i.test(statement))))
+ RETRYABLE_SCHEMA_CODES.has(String(value.code)) || concurrentCreateCollision(value, statement)
)
}
From 8c1a28d39c283b358e7dd4217ddaf7fb8c9a2250 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Thu, 3 Sep 2026 18:24:43 -0700
Subject: [PATCH 15/43] fix(i18n): repair French locale drift breaking static
analysis (#18550)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The French UI locale landed with two catalog drifts that fail `static
analysis` on every PR in the repo:
- `fr.json` carried 15 keys absent from `en.json` (and from every other
locale), so `verify:localization-catalog` rejected it. They are stale
entries generated against an older `en.json` snapshot; none is
referenced anywhere in the source.
- `settings.appearance.language.french` had no call site supplying a
literal default, which promotes it to a boot-bundle-required entry
that `en-runtime-required.json` does not ship, so
`verify:localization-runtime-catalog` rejected it.
Registering the key in settings search alongside its siblings fixes the
runtime-catalog failure at its source and closes the real gap the drift
exposed: French was the only supported language not findable in settings
search.
`en-runtime-required.json` is deliberately untouched — the sync script
regenerates it wholesale and would drop 925 entries the check itself
documents as harmless.
---
config/localization-coverage-allowlist.json | 7 +++++++
.../settings/appearance-search.test.ts | 9 +++++++--
.../components/settings/appearance-search.ts | 2 ++
src/renderer/src/i18n/locales/fr.json | 17 -----------------
4 files changed, 16 insertions(+), 19 deletions(-)
diff --git a/config/localization-coverage-allowlist.json b/config/localization-coverage-allowlist.json
index 57694b2033f..a10139d218f 100644
--- a/config/localization-coverage-allowlist.json
+++ b/config/localization-coverage-allowlist.json
@@ -55,6 +55,13 @@
"dynamic": false,
"count": 1
},
+ {
+ "filePath": "src/renderer/src/components/settings/appearance-search.ts",
+ "kind": "object-property:keywords",
+ "text": "Langue",
+ "dynamic": false,
+ "count": 1
+ },
{
"filePath": "src/renderer/src/components/settings/terminal-advanced-platform-search.ts",
"kind": "object-property:keywords",
diff --git a/src/renderer/src/components/settings/appearance-search.test.ts b/src/renderer/src/components/settings/appearance-search.test.ts
index 4b8c29781c4..6169a3cccb1 100644
--- a/src/renderer/src/components/settings/appearance-search.test.ts
+++ b/src/renderer/src/components/settings/appearance-search.test.ts
@@ -7,14 +7,14 @@ import { matchesSettingsSearch } from './settings-search'
// Native word for "language" in each supported UI language. These must be
// findable no matter which locale the interface is currently rendered in, so a
// speaker can locate (and switch to) their language from any starting point.
-const NATIVE_LANGUAGE_WORDS = ['语言', '語言', '언어', '言語', 'Idioma']
+const NATIVE_LANGUAGE_WORDS = ['语言', '語言', '언어', '言語', 'Idioma', 'Langue']
describe('getLanguageEntries', () => {
afterEach(async () => {
await i18n.changeLanguage('en')
})
- it.each(['en', 'zh', 'ko', 'ja', 'es'])(
+ it.each(['en', 'zh', 'ko', 'ja', 'es', 'fr'])(
'indexes every native word for "language" under the %s UI locale',
async (locale) => {
await i18n.changeLanguage(locale)
@@ -29,4 +29,9 @@ describe('getLanguageEntries', () => {
await i18n.changeLanguage('en')
expect(matchesSettingsSearch('Español', getLanguageEntries()[0])).toBe(true)
})
+
+ it('matches the French native language name in English UI', async () => {
+ await i18n.changeLanguage('en')
+ expect(matchesSettingsSearch('Français', getLanguageEntries()[0])).toBe(true)
+ })
})
diff --git a/src/renderer/src/components/settings/appearance-search.ts b/src/renderer/src/components/settings/appearance-search.ts
index 1886409e9e8..726ecaa1b01 100644
--- a/src/renderer/src/components/settings/appearance-search.ts
+++ b/src/renderer/src/components/settings/appearance-search.ts
@@ -50,6 +50,7 @@ export const getLanguageEntries = createLocalizedCatalog((): SettingsSearchEntry
...translateSearchKeyword('settings.appearance.language.korean', '한국어'),
...translateSearchKeyword('settings.appearance.language.japanese', '日本語'),
...translateSearchKeyword('settings.appearance.language.spanish', 'Español'),
+ ...translateSearchKeyword('settings.appearance.language.french', 'Français'),
// Why: the native word for "language" only reaches search via the localized
// title in its own UI locale — index each here so speakers can find (and
// switch to) their language whatever the current interface locale is.
@@ -58,6 +59,7 @@ export const getLanguageEntries = createLocalizedCatalog((): SettingsSearchEntry
'언어', // Korean
'言語', // Japanese
'Idioma', // Spanish
+ 'Langue', // French
...translateSearchKeyword(
'auto.components.settings.appearance.search.language.locale',
'locale'
diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json
index ed9307b30db..8eff250a820 100644
--- a/src/renderer/src/i18n/locales/fr.json
+++ b/src/renderer/src/i18n/locales/fr.json
@@ -2934,7 +2934,6 @@
"a7e2fd2699": "signaler un problème",
"5c8ce20be6": "Si le problème persiste, veuillez",
"cc6d997c65": "Redémarrez le daemon de terminal depuis ici pour effacer un état de daemon obsolète.",
- "7ee11bc0db": "Orca n'a pas pu confirmer si la session précédente de ce terminal tourne encore ; il a donc laissé la session intacte. Rouvrez ce volet pour réessayer.",
"e16012e31e": "Le daemon de terminal propriétaire de cette session s'est arrêté ; la session et son historique de défilement n'ont pas pu être récupérés. Ouvrez un nouveau terminal pour continuer.",
"sessionUnavailable": "Orca n'a pas pu se rattacher à la session de terminal de ce volet sur l'hôte. Ouvrez un nouveau terminal pour continuer."
},
@@ -3228,7 +3227,6 @@
"25dc1cd653": "Ouvrir le fichier",
"7cdf8ee0c8": "Ouvrir une URL",
"b27864279e": "Lancer un agent",
- "0e5b7a3f16": "Rechercher parmi les onglets ouverts, fichiers, URL et agents…",
"8f0a1c4d92": "Basculer vers l'onglet",
"2c38630a01": "L'espace de travail n'existe plus",
"4f0d9a71c2": "L'onglet n'existe plus",
@@ -3274,7 +3272,6 @@
"classifier": {
"42e6262ae9": "Aucune action disponible.",
"097a982ee0": "Chargement des fichiers...",
- "c41f8d20b7": "Rechercher parmi les onglets ouverts, fichiers, URL et agents…",
"90eb94dc48": "Saisissez une URL http:// ou https://.",
"5553b283ce": "Saisissez une URL ou un chemin de fichier.",
"queryTooLarge": "Le texte recherché est trop long.",
@@ -6999,12 +6996,6 @@
"compareBaseRepositoryDefault": "Valeur par défaut du dépôt",
"compareBaseBranchUpstream": "Amont de la branche"
},
- "HiddenExperimentalGroup": {
- "d0f914a528": "Bascule fictive",
- "1014ddbfaf": "Sans effet aujourd'hui. Réservée comme premier emplacement pour les options expérimentales masquées.",
- "232cf83de8": "Bascules non répertoriées pour les tests internes. Rien ici n'est pris en charge.",
- "3e9e827ca5": "Expérimental masqué"
- },
"InputPane": {
"db15068196": "Activé par défaut sur Linux et macOS. Linux utilise le presse-papiers de sélection du système ; les autres plateformes utilisent un tampon privé.",
"ad31c3c5fb": "Collage de la sélection au clic milieu"
@@ -11832,10 +11823,6 @@
"b3c8f1a902": "Filtrer les fichiers par nom",
"d4f8c2a901": "Effacer et fermer le filtre",
"e8a1c4b203": "vs",
- "f9b2441bb6": "1 commit d'avance sur {{value0}}",
- "b715ef615b": "{{value0}} commits d'avance sur {{value1}}",
- "c1a8f3e204": "1 commit de retard sur {{value0}}",
- "d2b9g4f315": "{{value0}} commits de retard sur {{value1}}",
"4b4a7de138": "Ouvrir la page de revue dans le navigateur",
"createPrIntentCommitBlockedSummary": "Commit bloqué : {{value0}} Corrigez le problème, puis réessayez Créer une PR.",
"pushRecovery": {
@@ -11858,7 +11845,6 @@
"a4e93c21d7": "Branche actuelle : {{value0}}",
"c7d4e2f801": "Modifier la ref de base : {{value0}}",
"f3a1b8c204": "upstream",
- "createPrIntentEmptyGeneratedBody": "Les détails de revue générés n'incluent pas de description. Réessayez Créer une PR.",
"createPrIntentGenerateDetailsFailed": "Impossible de générer les détails de revue. Réessayez Créer une PR."
},
"SourceControlAgentActionDialog": {
@@ -14038,8 +14024,6 @@
"7e7ca60816": "fichiers modifiés",
"b6c3b84476": "Afficher l'arborescence des fichiers",
"39f8007549": "Examiner les conflits",
- "39e73e7181": "ont été exclus de cette vue de diff.",
- "689b99f8ad": "conflit non résolu",
"820ec01f24": "Les fichiers en conflit sont examinés séparément",
"fd8892b120": "Aucune modification à afficher",
"eb5f40e49c": "Cette vue de diff exclut les conflits non résolus, car le pipeline de diff bidirectionnel habituel n'est pas conçu pour gérer les conflits.",
@@ -15793,7 +15777,6 @@
},
"LinuxPackageInstallRecoveryCard": {
"e3de29c86a": "Afficher le paquet",
- "3da99454c6": "Réessayer l'installation automatique",
"55c86654b7": "Copier la commande d'installation",
"53e1559f99": "Échec de l'installation automatique",
"a7ac6ec78b": "Orca a téléchargé la mise à jour mais n'a pas pu installer le paquet système automatiquement.",
From 11aace8dec5437ff5d2ad0e5936a049eaa1db9eb Mon Sep 17 00:00:00 2001
From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
Date: Thu, 3 Sep 2026 21:31:53 -0400
Subject: [PATCH 16/43] fix(relay): reject malformed percent-escapes on upgrade
instead of throwing (#18547)
decodeURIComponent on the /v1/connect/ and /v1/host/data/ path segments threw
URIError out of the http 'upgrade' listener, which is uncaught and kills the
relay process. Any client that sends GET /v1/connect/% could take down a cell
(and every connection on it) or a director instance. Pre-existing since the
splice landed (orca-cloud #20); not introduced by the import.
A malformed escape now takes the existing 4xx reject branch. The blackbox test
sends three malformed connect targets and one host-data target to the real
server and asserts no uncaughtException fires and a well-formed upgrade still
gets 101 afterwards; reverting either site fails it.
---
cloud/apps/relay/src/relay-server.ts | 16 ++-
...lay-upgrade-malformed-uri.blackbox.test.ts | 122 ++++++++++++++++++
2 files changed, 135 insertions(+), 3 deletions(-)
create mode 100644 cloud/apps/relay/src/relay-upgrade-malformed-uri.blackbox.test.ts
diff --git a/cloud/apps/relay/src/relay-server.ts b/cloud/apps/relay/src/relay-server.ts
index a14240cfa6a..32a83962d81 100644
--- a/cloud/apps/relay/src/relay-server.ts
+++ b/cloud/apps/relay/src/relay-server.ts
@@ -31,6 +31,16 @@ import { createRelayTokenVerifier, readBearer } from './relay-token-verifier.js'
import { closeRelayWebSocket } from './relay-websocket-close.js'
import { ProcessQueuedByteBudget } from './splice-forwarder.js'
+// A malformed percent-escape in the request target must be a client error, never a URIError
+// thrown out of the `upgrade` listener (which is uncaught and kills the process).
+function decodePathSegment(value: string): string | null {
+ try {
+ return decodeURIComponent(value)
+ } catch {
+ return null
+ }
+}
+
function rejectUpgrade(socket: NodeJS.WritableStream, status: number, message: string): void {
socket.write(`HTTP/1.1 ${status} ${message}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`)
if ('destroy' in socket && typeof socket.destroy === 'function') socket.destroy()
@@ -278,8 +288,8 @@ export function createRelayServer(
return
}
if (url.pathname.startsWith('/v1/connect/')) {
- const hostId = decodeURIComponent(url.pathname.slice('/v1/connect/'.length))
- if (!/^[A-Za-z0-9_-]{16}$/.test(hostId)) {
+ const hostId = decodePathSegment(url.pathname.slice('/v1/connect/'.length))
+ if (hostId === null || !/^[A-Za-z0-9_-]{16}$/.test(hostId)) {
rejectUpgrade(socket, 429, 'Too Many Requests')
return
}
@@ -373,7 +383,7 @@ export function createRelayServer(
rejectUpgrade(socket, 404, 'Not Found')
return
}
- const connId = decodeURIComponent(url.pathname.slice('/v1/host/data/'.length))
+ const connId = decodePathSegment(url.pathname.slice('/v1/host/data/'.length))
if (!connId || connId.length > 128) {
rejectUpgrade(socket, 429, 'Too Many Requests')
return
diff --git a/cloud/apps/relay/src/relay-upgrade-malformed-uri.blackbox.test.ts b/cloud/apps/relay/src/relay-upgrade-malformed-uri.blackbox.test.ts
new file mode 100644
index 00000000000..24635a68e6f
--- /dev/null
+++ b/cloud/apps/relay/src/relay-upgrade-malformed-uri.blackbox.test.ts
@@ -0,0 +1,122 @@
+import { connect, createServer as createNetServer } from 'node:net'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import type { RelayConfig } from './config.js'
+import type { RelayDatabase } from './database.js'
+import { createRelayServer } from './relay-server.js'
+
+async function unusedPort(): Promise {
+ const server = createNetServer()
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
+ const address = server.address()
+ if (!address || typeof address === 'string') throw new Error('missing test port')
+ await new Promise((resolve) => server.close(() => resolve()))
+ return address.port
+}
+
+function rawUpgrade(port: number, target: string): Promise<{ status: string; closed: boolean }> {
+ return new Promise((resolve, reject) => {
+ const socket = connect(port, '127.0.0.1')
+ let data = ''
+ socket.once('connect', () => {
+ socket.write(
+ `GET ${target} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: Upgrade\r\n` +
+ 'Upgrade: websocket\r\nSec-WebSocket-Version: 13\r\n' +
+ // RFC 6455 §1.3 example nonce; allowlisted in cloud/.gitleaks.toml.
+ 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n'
+ )
+ })
+ socket.on('data', (chunk) => {
+ data += chunk.toString()
+ })
+ socket.once('close', () => resolve({ status: data.split('\r\n')[0] ?? '', closed: true }))
+ socket.once('error', reject)
+ setTimeout(() => {
+ socket.destroy()
+ resolve({ status: data.split('\r\n')[0] ?? '', closed: false })
+ }, 1_500).unref()
+ })
+}
+
+describe('relay upgrade with a malformed request target', () => {
+ const cleanup: Array<() => Promise | void> = []
+
+ afterEach(async () => {
+ for (const close of cleanup.splice(0).reverse()) await close()
+ vi.restoreAllMocks()
+ })
+
+ it('rejects an undecodable /v1/connect path without an uncaught exception', async () => {
+ const port = await unusedPort()
+ const relayUrl = `http://127.0.0.1:${port}`
+ const database: RelayDatabase = {
+ query: vi.fn(async () => []),
+ queryLocked: vi.fn(async () => []),
+ transaction: vi.fn(async (operation) => await operation(database)),
+ close: vi.fn(async () => undefined)
+ }
+ const config = {
+ port,
+ publicUrl: relayUrl,
+ cellUrl: relayUrl,
+ authIssuer: 'https://auth.example.com',
+ authAudience: 'orca-relay',
+ jwksUrl: 'https://auth.example.com/jwks',
+ assignmentSigningKey: new Uint8Array(32),
+ role: 'cell',
+ cellId: 'production-gce-c3',
+ cells: [{ id: 'production-gce-c3', url: relayUrl, capacityRequests: 4_000 }],
+ adminAudience: `${relayUrl}/admin`,
+ deployServiceAccount: 'deploy@example.com',
+ runtimeServiceAccount: 'runtime@example.com',
+ connectionHardCap: 600,
+ connectionUnobservedBound: 60,
+ adminJwksUrl: 'https://auth.example.com/admin-jwks',
+ databasePoolMax: 10,
+ publicAssignmentsEnabled: true,
+ publicAssignmentConcurrency: 2,
+ publicAssignmentQueueMax: 128,
+ publicAssignmentWaitMs: 4_000,
+ publicResolveConcurrency: 1,
+ publicResolveWaitMs: 5_000,
+ publicAssignmentRetryAfterSeconds: 5,
+ dataDir: './test-data'
+ } satisfies RelayConfig
+ const relay = createRelayServer(config, database, {
+ connectionLedgerLimits: { hardCap: 5, controlReserve: 1 }
+ })
+ relay.server.listen(port, '127.0.0.1')
+ await new Promise((resolve) => relay.server.once('listening', resolve))
+ cleanup.push(() => new Promise((resolve) => relay.server.close(() => resolve())))
+ vi.spyOn(console, 'log').mockImplementation(() => undefined)
+ vi.spyOn(console, 'warn').mockImplementation(() => undefined)
+
+ // Vitest installs its own uncaughtException listener; capture ours first so the test reports
+ // the exception as a verdict instead of dying with it.
+ const uncaught: unknown[] = []
+ const onUncaught = (error: unknown): void => {
+ uncaught.push(error)
+ }
+ process.prependListener('uncaughtException', onUncaught)
+ cleanup.push(() => {
+ process.off('uncaughtException', onUncaught)
+ })
+
+ const results = []
+ for (const target of [
+ '/v1/connect/%',
+ '/v1/connect/%E0%A4%A',
+ '/v1/connect/%C0%AF',
+ '/v1/host/data/%'
+ ]) {
+ results.push(await rawUpgrade(port, target))
+ }
+ // A malformed percent-escape must be a client error, never a process-level throw.
+ expect(uncaught).toEqual([])
+ for (const result of results) {
+ expect(result.status).toMatch(/^HTTP\/1\.1 4\d\d/)
+ }
+ // The server must still serve a well-formed upgrade afterwards.
+ const after = await rawUpgrade(port, '/v1/connect/abcdefghijklmnop')
+ expect(after.status).toMatch(/^HTTP\/1\.1 101/)
+ })
+})
From 7106101ed2c913858cc088930158a2d25b441c71 Mon Sep 17 00:00:00 2001
From: Shahar Mor
Date: Thu, 3 Sep 2026 18:32:53 -0700
Subject: [PATCH 17/43] fix(mobile): restore terminal input when reopening
worktrees (#16239)
* fix(mobile): restore terminal input when reopening worktrees
* test(mobile): update session parity facts
* refactor(mobile): split host client hooks
* chore: restore localization formatter scope
* fix(mobile): retain RpcClient type import
---------
Co-authored-by: Merge Sim
---
.../mobile-session-route-parity.test.ts | 14 +--
.../mobile-session-startup-source.test.ts | 21 ++++
.../session/use-mobile-session-foundation.ts | 3 +-
.../session/use-mobile-session-lifecycle.ts | 4 +-
.../use-mobile-session-terminal-runtime.ts | 9 +-
...se-mobile-session-terminal-subscription.ts | 9 +-
mobile/src/transport/client-context.test.ts | 10 +-
mobile/src/transport/client-context.tsx | 106 ++----------------
.../transport/host-client-context-state.ts | 1 +
mobile/src/transport/host-client-hooks.ts | 92 +++++++++++++++
mobile/src/transport/host-entry-opener.ts | 2 +
.../transport/rpc-client-context-contract.ts | 1 +
12 files changed, 157 insertions(+), 115 deletions(-)
create mode 100644 mobile/src/transport/host-client-hooks.ts
diff --git a/mobile/src/session/mobile-session-route-parity.test.ts b/mobile/src/session/mobile-session-route-parity.test.ts
index b6abab8001e..c6e5f434326 100644
--- a/mobile/src/session/mobile-session-route-parity.test.ts
+++ b/mobile/src/session/mobile-session-route-parity.test.ts
@@ -63,11 +63,11 @@ const HOST_COMPONENT_NAMES = new Set([
])
const HEAD_MAIN_HOOK_SHA256 = '10071240ef9edafc2b9c8bed73be83dceaf7828e3b29f17dab55da020a7697a6'
-const HEAD_HOOK_BINDING_SHA256 = 'ecd4c1dad066cf13698447b8ffb61f82e6cc3ebe7d484f71189626efed430272'
+const HEAD_HOOK_BINDING_SHA256 = '1dadb8c3dc0573ea20659ce7251629669e618dd0effaeac3a4536b29c2e865a1'
const HEAD_CALLBACK_IDENTITY_SHA256 =
- 'df073bc13d94a93e7fbd8b1fca2b57eaf43cbf7ca799a649e0ebb783e5b8eecc'
-const HEAD_CALLBACK_BODY_SHA256 = '690e3069e08ecf805af726b658e900c973565259160f25e3a643175e2ab1bc75'
-const HEAD_EFFECT_SHA256 = '346d384ea0bf2f8f926c5092c5bf57bc2a03494f49f9639e9d6b8a2c51c9f882'
+ '2a9e4825df007f6ef53b81aa5004991d6318eee7507b44d625c07e630be432eb'
+const HEAD_CALLBACK_BODY_SHA256 = '22103ba85a86e3a3fcb80a7509c7a455d79863010cde3af02db6565b55e3ebe9'
+const HEAD_EFFECT_SHA256 = 'd9ebfaabc1e79773cdada7ab370b20459ed972f1f8edce1652199f4d0391cd13'
const HEAD_CONTENT_HOOK_SHA256 = '9c3b612fef3f370d66873aefdbe1d701f20cb64ded31fef5cc45fde6f8189581'
const HEAD_NESTED_FUNCTION_SHA256 =
'6a13919ede2a8033436fb03e0ff7c426fbed97f470875a7b21b00aaada17fb73'
@@ -79,13 +79,13 @@ const HEAD_TIMER_CREATION_SHA256 =
'1a31b625e2174c3db77272249843196d2b6b06ab1e654a96d8f7858e3082e66b'
const HEAD_TIMER_CLEANUP_SHA256 = 'c73f1d1c2cc89642f3d727d6f3b6b81860a9d6f34234541a2065ec3d1a8cd116'
const HEAD_RUNTIME_STRING_SHA256 =
- '1cb95fe0095c1c57e1b0629472e1cce5328eb7f5bfeca38095f41f4612a37887'
+ 'ba52a3ede721bd29acbe8593161e90b216b7f361ff896e085927d4d73fa83b2f'
const HEAD_HOST_JSX_SHA256 = '390405926b1695fa3a33686f0bc192b432f5468d8576499d7cafbb4922defbb5'
const HEAD_LEAF_JSX_SHA256 = '21dba981875e173f692590bf910d60964660c5f4cbb79f3a377c7e54f6a1f016'
const HEAD_STYLE_REFERENCE_SHA256 =
'295a3501c2c6d7bea7c8bbf38b3f3534f01344cd7e1b91bb8e07c040821d596a'
const HEAD_IDENTITY_FIELD_SHA256 =
- '6b37a0351795a387a358df76a5ab919a7098ddb76bf25a936c8902c062c8951c'
+ '91146853930a34dd1f3d80e5c97fbacd7cf19fb93dd26fe8fc6f29169622f9d6'
const HEAD_NAVIGATION_SHA256 = '9d96f5dad7de555d6553eac39c0fab00efad507470fd562cb9beaa32db16f512'
const HEAD_CAPABILITY_SHA256 = 'ca219f7909a091717110b823d5b94a20770ad3ae51894e0fa765e8628309392d'
@@ -517,7 +517,7 @@ describe('mobile session route extraction parity', () => {
it('preserves runtime strings, styles, and the expanded JSX tree', () => {
const strings = readRuntimeStrings()
- expect(strings).toHaveLength(545)
+ expect(strings).toHaveLength(546)
expect(hash(strings)).toBe(HEAD_RUNTIME_STRING_SHA256)
const jsx = readJsxFacts(readDefinitions())
expect(jsx.host).toHaveLength(124)
diff --git a/mobile/src/session/mobile-session-startup-source.test.ts b/mobile/src/session/mobile-session-startup-source.test.ts
index acab1d5e7c6..83b2021b95e 100644
--- a/mobile/src/session/mobile-session-startup-source.test.ts
+++ b/mobile/src/session/mobile-session-startup-source.test.ts
@@ -29,6 +29,14 @@ const tabReconciliationOwnerSource = readMobileSessionRouteSource(
const autoCreateHookSource = readMobileSessionRouteSource(
'./use-initial-session-terminal-autocreate.ts'
)
+const foundationSource = readMobileSessionRouteSource('./use-mobile-session-foundation.ts')
+const terminalRuntimeSource = readMobileSessionRouteSource(
+ './use-mobile-session-terminal-runtime.ts'
+)
+const terminalSubscriptionSourceForIdentity = readMobileSessionRouteSource(
+ './use-mobile-session-terminal-subscription.ts'
+)
+const lifecycleSource = readMobileSessionRouteSource('./use-mobile-session-lifecycle.ts')
function sliceBetween(startPattern: string, endPattern: string, targetSource = source): string {
const start = targetSource.indexOf(startPattern)
@@ -106,6 +114,19 @@ describe('mobile session startup', () => {
expect(reconciliationHookSource).toContain('appStateSubscription.remove()')
})
+ it('binds terminal identity to the shared client before subscription effects run', () => {
+ expect(foundationSource).toContain('const { client, clientId, state: connState }')
+ expect(foundationSource).toContain(' clientId,')
+ expect(terminalRuntimeSource).toContain('useRef(clientId)')
+ expect(terminalRuntimeSource).toContain('deviceTokenRef.current = clientId')
+ expect(terminalRuntimeSource).toContain('inputGate.canSend && clientId !== null')
+ expect(terminalSubscriptionSourceForIdentity).toContain('if (clientId === null)')
+ expect(terminalSubscriptionSourceForIdentity).toContain(
+ "client: { id: clientId, type: 'mobile' as const }"
+ )
+ expect(lifecycleSource).not.toContain('deviceTokenRef.current = host.deviceToken')
+ })
+
it('confirms terminal stream teardown with a committed inventory-recovery bridge', () => {
expect(terminalSubscriptionSource).toContain(
"if (data.type === 'end' || data.type === 'error')"
diff --git a/mobile/src/session/use-mobile-session-foundation.ts b/mobile/src/session/use-mobile-session-foundation.ts
index 9a7889e10cb..fa2f9607bbc 100644
--- a/mobile/src/session/use-mobile-session-foundation.ts
+++ b/mobile/src/session/use-mobile-session-foundation.ts
@@ -35,7 +35,7 @@ export function useMobileSessionFoundation() {
const router = useRouter()
const insets = useSafeAreaInsets()
// Why: shared client per host owned by RpcClientProvider (docs/mobile-shared-client-per-host.md).
- const { client, state: connState } = useHostClient(hostId)
+ const { client, clientId, state: connState } = useHostClient(hostId)
const reconnectAttempts = useReconnectAttempt(hostId)
const lastConnectedAt = useLastConnectedAt(hostId)
const forceReconnectHost = useForceReconnect()
@@ -96,6 +96,7 @@ export function useMobileSessionFoundation() {
router,
insets,
client,
+ clientId,
connState,
reconnectAttempts,
lastConnectedAt,
diff --git a/mobile/src/session/use-mobile-session-lifecycle.ts b/mobile/src/session/use-mobile-session-lifecycle.ts
index 912ebdb4921..7c58e84a3e5 100644
--- a/mobile/src/session/use-mobile-session-lifecycle.ts
+++ b/mobile/src/session/use-mobile-session-lifecycle.ts
@@ -16,7 +16,6 @@ export function useMobileSessionLifecycle(scope: MobileSessionTabReconciliationM
connState,
setCustomKeys,
setVisibleBuiltInIds,
- deviceTokenRef,
setHostEndpoint,
connStateRef,
terminalRefs,
@@ -26,7 +25,7 @@ export function useMobileSessionLifecycle(scope: MobileSessionTabReconciliationM
unsubscribeTerminal,
subscribeToTerminal
} = scope
- // Why: read deviceToken from host record so code can pass client.id on subscribe/send for driver-state-machine identity.
+ // Why: the shared client owns authenticated identity; this host read only supplies connection-hint metadata.
useEffect(() => {
if (!hostId) {
return
@@ -38,7 +37,6 @@ export function useMobileSessionLifecycle(scope: MobileSessionTabReconciliationM
}
const host = hosts.find((h) => h.id === hostId)
if (host) {
- deviceTokenRef.current = host.deviceToken
setHostEndpoint(host.endpoint)
}
})
diff --git a/mobile/src/session/use-mobile-session-terminal-runtime.ts b/mobile/src/session/use-mobile-session-terminal-runtime.ts
index 8ef472fb845..5086efe1ba1 100644
--- a/mobile/src/session/use-mobile-session-terminal-runtime.ts
+++ b/mobile/src/session/use-mobile-session-terminal-runtime.ts
@@ -27,6 +27,7 @@ export function useMobileSessionTerminalRuntime(scope: MobileSessionScreenStateM
worktreeId,
connState,
client,
+ clientId,
sessionTabs,
setLiveInputCapture,
liveInputTerminalHandles,
@@ -42,7 +43,9 @@ export function useMobileSessionTerminalRuntime(scope: MobileSessionScreenStateM
const terminalGestureInputInFlightRef = useRef>(new Set())
const terminalCwdRef = useRef>(new Map())
const initialModesSeenRef = useRef>(new Set())
- const deviceTokenRef = useRef(null)
+ const deviceTokenRef = useRef(clientId)
+ // Keep the authenticated identity synchronous with the client exposed to downstream hooks.
+ deviceTokenRef.current = clientId
// Why: state (not a ref) so the connection verdict re-renders when the endpoint loads and the Tailscale hint can appear.
const [hostEndpoint, setHostEndpoint] = useState(null)
const clientRef = useRef(null)
@@ -123,11 +126,13 @@ export function useMobileSessionTerminalRuntime(scope: MobileSessionScreenStateM
sendLiveTerminalInputRef,
setLiveInputCapture
})
- const { canCompose, canSend } = resolveMobileTerminalInputGate({
+ const inputGate = resolveMobileTerminalInputGate({
connState,
activeHandle,
activeSessionTabType: activeSessionTab?.type
})
+ const canCompose = inputGate.canCompose
+ const canSend = inputGate.canSend && clientId !== null
const liveInputEnabled = activeHandle ? liveInputTerminalHandles.has(activeHandle) : false
const { focusLiveInput, handleTerminalTap, resetLiveInputFocus } = useTerminalLiveInputFocus({
activeHandleRef,
diff --git a/mobile/src/session/use-mobile-session-terminal-subscription.ts b/mobile/src/session/use-mobile-session-terminal-subscription.ts
index 8ad0bb7383a..333d472a30e 100644
--- a/mobile/src/session/use-mobile-session-terminal-subscription.ts
+++ b/mobile/src/session/use-mobile-session-terminal-subscription.ts
@@ -15,9 +15,9 @@ export function useMobileSessionTerminalSubscription(
) {
const {
client,
+ clientId,
setTerminalModes,
terminalCwdRef,
- deviceTokenRef,
viewportRef,
viewportMeasuredRef,
terminalUnsubsRef,
@@ -49,6 +49,10 @@ export function useMobileSessionTerminalSubscription(
logSkippedGate('no-client')
return
}
+ if (clientId === null) {
+ logSkippedGate('no-client-identity')
+ return
+ }
if (terminalUnsubsRef.current.has(handle)) {
logSkippedGate('already-subscribed')
return
@@ -89,7 +93,7 @@ export function useMobileSessionTerminalSubscription(
client,
{
terminal: handle,
- client: { id: deviceTokenRef.current!, type: 'mobile' as const },
+ client: { id: clientId, type: 'mobile' as const },
viewport: nativeChatTerminalStream.mobileNativeChatSubscribeViewport(
covered,
viewportRef.current
@@ -263,6 +267,7 @@ export function useMobileSessionTerminalSubscription(
},
[
client,
+ clientId,
getTerminalRef,
markNativeChatInputLeaseReady,
scheduleDelayedAction,
diff --git a/mobile/src/transport/client-context.test.ts b/mobile/src/transport/client-context.test.ts
index a311f362f85..4a7d5d7b0e8 100644
--- a/mobile/src/transport/client-context.test.ts
+++ b/mobile/src/transport/client-context.test.ts
@@ -146,8 +146,8 @@ beforeEach(() => {
})
describe('useHostClient', () => {
- it('rebinds when Expo reuses a screen between two connected cached hosts', async () => {
- const host2 = { ...HOST, id: 'host-2', name: 'Host 2' }
+ it('rebinds the client and its authenticated identity together across cached hosts', async () => {
+ const host2 = { ...HOST, id: 'host-2', name: 'Host 2', deviceToken: 'token-2' }
const client1 = makeFakeClient('connected')
const client2 = makeFakeClient('connected')
connectMock.mockReturnValueOnce(client1).mockReturnValueOnce(client2)
@@ -155,11 +155,13 @@ describe('useHostClient', () => {
let selectedHostId = HOST.id
let selectedClient: RpcClient | null = null
+ let selectedClientId: string | null = null
let selectedState: ConnectionState = 'disconnected'
let renderer: ReactTestRenderer | null = null
function Probe(): null {
const selected = useHostClient(selectedHostId)
selectedClient = selected.client
+ selectedClientId = selected.clientId
selectedState = selected.state
useHostClient(host2.id)
return null
@@ -171,16 +173,18 @@ describe('useHostClient', () => {
await Promise.resolve()
})
expect(selectedClient).toBe(client1)
+ expect(selectedClientId).toBe(HOST.deviceToken)
expect(selectedState).toBe('connected')
selectedHostId = host2.id
- client2.emitState('disconnected')
await act(async () => {
+ client2.emitState('disconnected')
renderer?.update(createElement(RpcClientProvider, null, createElement(Probe)))
await Promise.resolve()
})
expect(selectedClient).toBe(client2)
+ expect(selectedClientId).toBe(host2.deviceToken)
expect(selectedState).toBe('disconnected')
expect(connectMock).toHaveBeenCalledTimes(2)
} finally {
diff --git a/mobile/src/transport/client-context.tsx b/mobile/src/transport/client-context.tsx
index f83519940c1..76d26c1bab5 100644
--- a/mobile/src/transport/client-context.tsx
+++ b/mobile/src/transport/client-context.tsx
@@ -7,7 +7,6 @@ import {
useEffect,
useMemo,
useRef,
- useState,
type ReactNode
} from 'react'
import type { RpcClient } from './rpc-client'
@@ -35,6 +34,15 @@ import {
import type { ConnectionState, HostProfile } from './types'
import type { RpcClientContextValue } from './rpc-client-context-contract'
+export {
+ useDisconnectHostClient,
+ useForceReconnect,
+ useForgetHostClient,
+ useHostClient,
+ usePrimeHosts,
+ useRefreshHostClient
+} from './host-client-hooks'
+
type StoreEntry = HostClientStoreEntry
const Ctx = createContext(null)
@@ -364,99 +372,3 @@ export function useRpcClientContext(): RpcClientContextValue {
}
return ctx
}
-
-// Primary hook for screens: acquires the shared client on mount, releases on unmount, re-renders on state change.
-export function useHostClient(hostId: string | undefined): {
- client: RpcClient | null
- state: ConnectionState
-} {
- const ctx = useRpcClientContext()
- const [, force] = useState(0)
- // Why: an absent entry at mount is almost always the open racing the render, not a
- // dead host — seed amber; a failed open notifies 'disconnected' moments later.
- const [state, setState] = useState(() =>
- hostId ? (ctx.getKnownState(hostId) ?? 'connecting') : 'disconnected'
- )
- const clientRef = useRef(null)
- const clientHostIdRef = useRef(hostId)
- const acquisitionRef = useRef({})
-
- useEffect(() => {
- if (!hostId) {
- clientRef.current = null
- clientHostIdRef.current = undefined
- setState('disconnected')
- return
- }
- clientHostIdRef.current = hostId
- let cancelled = false
- // Subscribe before acquire so any state change during open is captured.
- const unsub = ctx.subscribeHostState(hostId, (next) => {
- if (cancelled) {
- return
- }
- setState(next)
- // Why: async open and forceReconnect swap the client object; re-read each state change so screens never drive a stale one.
- const found = ctx.getAllClients().find((entry) => entry.hostId === hostId)
- if (found && found.client !== clientRef.current) {
- clientRef.current = found.client
- force((n) => n + 1)
- } else if (!found && clientRef.current) {
- // Why: disconnect/forget deletes the entry; never retain a dead client (STA-1511).
- clientRef.current = null
- force((n) => n + 1)
- }
- })
- const initial = ctx.acquire(hostId, acquisitionRef.current)
- clientRef.current = initial
- setState(ctx.getKnownState(hostId) ?? 'connecting')
- if (initial) {
- // Why: two cached hosts can both be connected, so equal state values cannot reveal the replacement client.
- force((n) => n + 1)
- }
- return () => {
- cancelled = true
- unsub()
- ctx.release(hostId, acquisitionRef.current)
- clientRef.current = null
- clientHostIdRef.current = undefined
- }
- }, [ctx, hostId])
-
- // Why: Expo can reuse the screen before effects bind the next host; never expose the prior host's client or state in that render.
- const bound = clientHostIdRef.current === hostId
- const boundState = bound
- ? state
- : hostId
- ? (ctx.getKnownState(hostId) ?? 'connecting')
- : 'disconnected'
- return { client: bound ? clientRef.current : null, state: boundState }
-}
-
-// Why: host-store's removeHost() must close the live client but has no React-side handle; this hook bridges to it.
-export function useRefreshHostClient(): (hostId: string) => void {
- const ctx = useRpcClientContext()
- return ctx.refreshHostClient
-}
-
-export function useForgetHostClient(): (hostId: string) => void {
- const ctx = useRpcClientContext()
- return ctx.forgetHostClient
-}
-
-export function useDisconnectHostClient(): (hostId: string) => void {
- const ctx = useRpcClientContext()
- return ctx.disconnectHostClient
-}
-
-// Why: future-proof "Connection issues — try again" affordance.
-export function useForceReconnect(): (hostId: string) => Promise {
- const ctx = useRpcClientContext()
- return ctx.forceReconnect
-}
-
-// Why: primes already-loaded HostProfiles so the provider can skip a second loadHosts()/Keychain pass on cold start.
-export function usePrimeHosts(): (hosts: HostProfile[]) => void {
- const ctx = useRpcClientContext()
- return ctx.primeHosts
-}
diff --git a/mobile/src/transport/host-client-context-state.ts b/mobile/src/transport/host-client-context-state.ts
index 972a810af21..859e5d847f9 100644
--- a/mobile/src/transport/host-client-context-state.ts
+++ b/mobile/src/transport/host-client-context-state.ts
@@ -79,6 +79,7 @@ export function createHostClientSelectors(
return {
getKnownState,
getState: (hostId: string): ConnectionState => getKnownState(hostId) ?? 'disconnected',
+ getClientId: (hostId: string): string | null => entries.get(hostId)?.clientId ?? null,
getReconnectAttempt: (hostId: string): number =>
entries.get(hostId)?.client.getReconnectAttempt() ?? 0,
getLastConnectedAt: (hostId: string): number | null =>
diff --git a/mobile/src/transport/host-client-hooks.ts b/mobile/src/transport/host-client-hooks.ts
new file mode 100644
index 00000000000..c7f885034c2
--- /dev/null
+++ b/mobile/src/transport/host-client-hooks.ts
@@ -0,0 +1,92 @@
+import { useEffect, useRef, useState } from 'react'
+import type { RpcClient } from './rpc-client'
+import type { ConnectionState, HostProfile } from './types'
+import type { HostClientAcquisition } from './host-client-acquisition-registry'
+import { useRpcClientContext } from './client-context'
+
+// Primary hook for screens: acquires the shared client on mount, releases on unmount, re-renders on state change.
+export function useHostClient(hostId: string | undefined): {
+ client: RpcClient | null
+ clientId: string | null
+ state: ConnectionState
+} {
+ const ctx = useRpcClientContext()
+ const [, force] = useState(0)
+ const [state, setState] = useState(() =>
+ hostId ? (ctx.getKnownState(hostId) ?? 'connecting') : 'disconnected'
+ )
+ const clientRef = useRef(null)
+ const clientHostIdRef = useRef(hostId)
+ const acquisitionRef = useRef({})
+
+ useEffect(() => {
+ if (!hostId) {
+ clientRef.current = null
+ clientHostIdRef.current = undefined
+ setState('disconnected')
+ return
+ }
+ clientHostIdRef.current = hostId
+ let cancelled = false
+ const unsub = ctx.subscribeHostState(hostId, (next) => {
+ if (cancelled) {
+ return
+ }
+ setState(next)
+ const found = ctx.getAllClients().find((entry) => entry.hostId === hostId)
+ if (found && found.client !== clientRef.current) {
+ clientRef.current = found.client
+ force((n) => n + 1)
+ } else if (!found && clientRef.current) {
+ clientRef.current = null
+ force((n) => n + 1)
+ }
+ })
+ const initial = ctx.acquire(hostId, acquisitionRef.current)
+ clientRef.current = initial
+ setState(ctx.getKnownState(hostId) ?? 'connecting')
+ if (initial) {
+ force((n) => n + 1)
+ }
+ return () => {
+ cancelled = true
+ unsub()
+ ctx.release(hostId, acquisitionRef.current)
+ clientRef.current = null
+ clientHostIdRef.current = undefined
+ }
+ }, [ctx, hostId])
+
+ const bound = clientHostIdRef.current === hostId
+ const boundClient = bound ? clientRef.current : null
+ const boundState = bound
+ ? state
+ : hostId
+ ? (ctx.getKnownState(hostId) ?? 'connecting')
+ : 'disconnected'
+ return {
+ client: boundClient,
+ clientId: boundClient && hostId ? ctx.getClientId(hostId) : null,
+ state: boundState
+ }
+}
+
+export function useRefreshHostClient(): (hostId: string) => void {
+ return useRpcClientContext().refreshHostClient
+}
+
+export function useForgetHostClient(): (hostId: string) => void {
+ return useRpcClientContext().forgetHostClient
+}
+
+export function useDisconnectHostClient(): (hostId: string) => void {
+ return useRpcClientContext().disconnectHostClient
+}
+
+export function useForceReconnect(): (hostId: string) => Promise {
+ return useRpcClientContext().forceReconnect
+}
+
+export function usePrimeHosts(): (hosts: HostProfile[]) => void {
+ return useRpcClientContext().primeHosts
+}
diff --git a/mobile/src/transport/host-entry-opener.ts b/mobile/src/transport/host-entry-opener.ts
index 6e29f55d985..03d6363c7c7 100644
--- a/mobile/src/transport/host-entry-opener.ts
+++ b/mobile/src/transport/host-entry-opener.ts
@@ -12,6 +12,7 @@ import type { ConnectionState, HostProfile } from './types'
export type HostClientStoreEntry = {
client: RpcClient
+ clientId: string
state: ConnectionState
refCount: number
unsubState: () => void
@@ -130,6 +131,7 @@ export async function openHostClientEntry(
}) ?? (() => {})
const entry: HostClientStoreEntry = {
client,
+ clientId: host.deviceToken,
state: client.getState(),
refCount: state.pendingAcquisitions.get(hostId) ?? 0,
unsubState,
diff --git a/mobile/src/transport/rpc-client-context-contract.ts b/mobile/src/transport/rpc-client-context-contract.ts
index 9eb9efd4444..54e25973c7f 100644
--- a/mobile/src/transport/rpc-client-context-contract.ts
+++ b/mobile/src/transport/rpc-client-context-contract.ts
@@ -18,6 +18,7 @@ export type RpcClientContextValue = {
disconnectHostClient: (hostId: string) => void
getState: (hostId: string) => ConnectionState
getKnownState: (hostId: string) => ConnectionState | null
+ getClientId: (hostId: string) => string | null
getReconnectAttempt: (hostId: string) => number
getLastConnectedAt: (hostId: string) => number | null
getActivePath: (hostId: string) => MobileConnectionPath
From 34222e01376d164530cd7f2e1bdaa04731b15db0 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Thu, 3 Sep 2026 20:10:01 -0700
Subject: [PATCH 18/43] perf(orchestration): project explicit columns so the
graph publish stops recompiling SQL (#18420)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* perf(orchestration): cache the prepared statements the graph publish recompiles
SyncDatabase refuses to cache any `SELECT *` — node:sqlite can build the first
row after a schema change from stale column names — so every wildcard read in
the orchestration DB recompiles its SQL on each call. The graph publish runs
that fan-out once per pane, ~0.7 times a second, forever.
Add a per-connection prepared-statement cache scoped to the orchestration DB,
whose schema is frozen in the constructor (createTables/migrate/trigger) and
whose resets are DELETE-only, and route the buildByPaneKey -> getForHandle ->
getRecent path through it. 5 publishes over 2 panes: 30 compilations -> 2.
* perf(orchestration): project explicit columns so the existing cache covers the hot path
Replaces the branch's second statement cache. The six graph-publish reads were
uncacheable only because they were spelled `SELECT *` / `SELECT t.*`, which
SyncDatabase refuses to cache (node:sqlite can build the first row after a schema
change from stale column names). Spelling the projection out from type-checked
column tuples makes them cacheable by the SyncDatabase LRU that is already merged,
already bounded, and already clears on DDL — so the WeakMap and its documented
cross-connection ALTER hazard both go away.
Drift is caught at build time: `satisfies readonly (keyof Row)[]` plus an
`Exclude extends never` assertion pins list vs type at tsc,
and a PRAGMA table_info test against a freshly migrated OrchestrationDb pins list
vs schema.
Same win, verified: 6 compilations per publish -> 2 total then 0, identical to the
WeakMap branch; 92/96/91 us CPU per 2-pane publish before, 11-12 us after on both.
---
.../db/dispatch-context/dispatch-lookup.ts | 54 +++----
.../db/hot-path-statement-compilation.test.ts | 148 ++++++++++++++++++
.../orchestration/db/row-column-lists.test.ts | 57 +++++++
.../orchestration/db/row-column-lists.ts | 82 ++++++++++
.../orchestration/db/runs/run-lookup.ts | 19 +--
.../orchestration/db/tasks/task-store.ts | 42 ++---
6 files changed, 346 insertions(+), 56 deletions(-)
create mode 100644 src/main/runtime/orchestration/db/hot-path-statement-compilation.test.ts
create mode 100644 src/main/runtime/orchestration/db/row-column-lists.test.ts
create mode 100644 src/main/runtime/orchestration/db/row-column-lists.ts
diff --git a/src/main/runtime/orchestration/db/dispatch-context/dispatch-lookup.ts b/src/main/runtime/orchestration/db/dispatch-context/dispatch-lookup.ts
index ae1cd4f45d1..c96238f13ee 100644
--- a/src/main/runtime/orchestration/db/dispatch-context/dispatch-lookup.ts
+++ b/src/main/runtime/orchestration/db/dispatch-context/dispatch-lookup.ts
@@ -6,6 +6,23 @@ import {
paneKeyMatchSuffix
} from '../pane-key-match'
import type { OrchestrationDb } from '../orchestration-db'
+import { DISPATCH_CONTEXT_COLUMN_LIST } from '../row-column-lists'
+
+// Why: hoisted and wildcard-free so the graph-publish fan-out hits the SyncDatabase statement cache.
+const ACTIVE_DISPATCH_BY_HANDLE_SQL =
+ // Why: newest-first like the pane lookups below — an unordered LIMIT 1 could pin a stale row if a handle ever has two active dispatches.
+ `SELECT ${DISPATCH_CONTEXT_COLUMN_LIST} FROM dispatch_contexts
+ WHERE assignee_handle = ? AND status IN ('pending', 'dispatched')
+ ORDER BY rowid DESC LIMIT 1`
+const ACTIVE_DISPATCH_BY_PANE_KEY_SQL = `SELECT ${DISPATCH_CONTEXT_COLUMN_LIST} FROM dispatch_contexts
+ WHERE assignee_pane_key = ? AND status IN ('pending', 'dispatched')
+ ORDER BY rowid DESC LIMIT 1`
+const ACTIVE_DISPATCH_BY_PANE_SUFFIX_SQL = `SELECT ${DISPATCH_CONTEXT_COLUMN_LIST} FROM dispatch_contexts
+ WHERE assignee_pane_key IS NOT NULL
+ AND status IN ('pending', 'dispatched') AND instr(assignee_pane_key, ':') > 1
+ AND ${DISPATCH_PANE_KEY_MATCH_SUFFIX_SQL} = ?
+ ORDER BY rowid DESC LIMIT 1`
+const LATEST_DISPATCH_BY_HANDLE_SQL = `SELECT ${DISPATCH_CONTEXT_COLUMN_LIST} FROM dispatch_contexts WHERE assignee_handle = ? ORDER BY rowid DESC LIMIT 1`
export function getActiveDispatchForTerminal(
this: OrchestrationDb,
@@ -116,14 +133,9 @@ export function findActiveDispatchForAssignee(
assigneeHandle: string,
assigneePaneKey?: string
): DispatchContextRow | undefined {
- const byHandle = this.db
- .prepare(
- // Why: newest-first like the pane lookups below — an unordered LIMIT 1 could pin a stale row if a handle ever has two active dispatches.
- `SELECT * FROM dispatch_contexts
- WHERE assignee_handle = ? AND status IN ('pending', 'dispatched')
- ORDER BY rowid DESC LIMIT 1`
- )
- .get(assigneeHandle) as DispatchContextRow | undefined
+ const byHandle = this.db.prepare(ACTIVE_DISPATCH_BY_HANDLE_SQL).get(assigneeHandle) as
+ | DispatchContextRow
+ | undefined
if (byHandle) {
return byHandle
}
@@ -132,13 +144,9 @@ export function findActiveDispatchForAssignee(
return undefined
}
- const exactPane = this.db
- .prepare(
- `SELECT * FROM dispatch_contexts
- WHERE assignee_pane_key = ? AND status IN ('pending', 'dispatched')
- ORDER BY rowid DESC LIMIT 1`
- )
- .get(assigneePaneKey) as DispatchContextRow | undefined
+ const exactPane = this.db.prepare(ACTIVE_DISPATCH_BY_PANE_KEY_SQL).get(assigneePaneKey) as
+ | DispatchContextRow
+ | undefined
if (exactPane) {
return exactPane
}
@@ -146,13 +154,7 @@ export function findActiveDispatchForAssignee(
return undefined
}
return this.db
- .prepare(
- `SELECT * FROM dispatch_contexts
- WHERE assignee_pane_key IS NOT NULL
- AND status IN ('pending', 'dispatched') AND instr(assignee_pane_key, ':') > 1
- AND ${DISPATCH_PANE_KEY_MATCH_SUFFIX_SQL} = ?
- ORDER BY rowid DESC LIMIT 1`
- )
+ .prepare(ACTIVE_DISPATCH_BY_PANE_SUFFIX_SQL)
.get(paneKeyMatchSuffix(assigneePaneKey)) as DispatchContextRow | undefined
}
@@ -160,11 +162,9 @@ export function getLatestDispatchForTerminal(
this: OrchestrationDb,
handle: string
): DispatchContextRow | undefined {
- return this.db
- .prepare(
- 'SELECT * FROM dispatch_contexts WHERE assignee_handle = ? ORDER BY rowid DESC LIMIT 1'
- )
- .get(handle) as DispatchContextRow | undefined
+ return this.db.prepare(LATEST_DISPATCH_BY_HANDLE_SQL).get(handle) as
+ | DispatchContextRow
+ | undefined
}
export type DispatchLookupMethods = {
diff --git a/src/main/runtime/orchestration/db/hot-path-statement-compilation.test.ts b/src/main/runtime/orchestration/db/hot-path-statement-compilation.test.ts
new file mode 100644
index 00000000000..26f82410cfe
--- /dev/null
+++ b/src/main/runtime/orchestration/db/hot-path-statement-compilation.test.ts
@@ -0,0 +1,148 @@
+import { mkdtempSync, rmSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import { RuntimeAgentOrchestrationProjection } from '../../runtime-agent-orchestration-projection'
+import type { OrchestrationCompatibilityTerminalAuthority } from '../../runtime-terminal-contracts'
+import type { RuntimeLeafRecord } from '../../runtime-terminal-state-records'
+import { OrchestrationDb } from '../db'
+import { createRootDispatch } from './root-dispatch-test-fixture'
+
+const COORDINATOR_HANDLE = 'term_coordinator'
+const COORDINATOR_PANE = 'tab_c:leaf_c'
+const WORKER_HANDLE = 'term_worker'
+const WORKER_PANE = 'tab_w:leaf_w'
+const IDLE_HANDLE = 'term_idle'
+const IDLE_PANE = 'tab_i:leaf_i'
+
+// Why: mirrors SyncDatabase's `isStatementCacheable` — aggregate `(*)` is fine, any other `*` is not.
+const WILDCARD_PROJECTION = /(? {
+ for (const db of openDatabases.splice(0)) {
+ try {
+ db.close()
+ } catch {
+ // already closed by the test
+ }
+ }
+ for (const directory of temporaryDirectories.splice(0)) {
+ rmSync(directory, { recursive: true, force: true })
+ }
+})
+
+function openDatabase(path: string): OrchestrationDb {
+ const db = new OrchestrationDb(path)
+ openDatabases.push(db)
+ return db
+}
+
+function temporaryDatabasePath(): string {
+ const directory = mkdtempSync(join(tmpdir(), 'orca-orchestration-hot-path-'))
+ temporaryDirectories.push(directory)
+ return join(directory, 'orchestration.db')
+}
+
+/** Counts real SQL compilations by wrapping the node:sqlite handle SyncDatabase prepares against. */
+function trackCompiledSql(db: OrchestrationDb): string[] {
+ const inner = (db.db as unknown as { db: { prepare(sql: string): unknown } }).db
+ const original = inner.prepare.bind(inner)
+ const compiled: string[] = []
+ inner.prepare = (sql: string) => {
+ compiled.push(sql)
+ return original(sql)
+ }
+ return compiled
+}
+
+function seedDispatchedWorker(db: OrchestrationDb): void {
+ const run = db.createRun({
+ objective: 'demo',
+ coordinatorHandle: COORDINATOR_HANDLE,
+ coordinatorPaneKey: COORDINATOR_PANE
+ })
+ const task = db.createTask({
+ spec: 'ship the thing',
+ runId: run.id,
+ createdByTerminalHandle: COORDINATOR_HANDLE,
+ createdByPaneKey: COORDINATOR_PANE,
+ createdByProcessIncarnation: 'inc_1',
+ createdByRunGeneration: run.consumer_generation
+ })
+ createRootDispatch(db, task.id, WORKER_HANDLE, WORKER_PANE)
+}
+
+function buildProjection(db: OrchestrationDb): RuntimeAgentOrchestrationProjection {
+ const leaves = [{ ptyId: 'pty_w' }, { ptyId: 'pty_i' }] as unknown as RuntimeLeafRecord[]
+ const handleByLeaf = new Map([
+ [leaves[0] as RuntimeLeafRecord, WORKER_HANDLE],
+ [leaves[1] as RuntimeLeafRecord, IDLE_HANDLE]
+ ])
+ const paneByLeaf = new Map([
+ [leaves[0] as RuntimeLeafRecord, WORKER_PANE],
+ [leaves[1] as RuntimeLeafRecord, IDLE_PANE]
+ ])
+ return new RuntimeAgentOrchestrationProjection({
+ getDb: () => db,
+ getLeaves: () => leaves,
+ getPtys: () => [],
+ issueLeafHandle: (leaf) => handleByLeaf.get(leaf) ?? '',
+ issuePtyHandle: () => '',
+ makePaneKey: (leaf) => paneByLeaf.get(leaf) ?? '',
+ getWorktreeId: () => null,
+ getHandleForPaneKey: (paneKey) => (paneKey === COORDINATOR_PANE ? COORDINATOR_HANDLE : null),
+ getPaneKey: (handle) => (handle === COORDINATOR_HANDLE ? COORDINATOR_PANE : null),
+ getDispatchAuthority: (handle) =>
+ handle === COORDINATOR_HANDLE
+ ? ({
+ paneKey: COORDINATOR_PANE,
+ processIncarnation: 'inc_1'
+ } as OrchestrationCompatibilityTerminalAuthority)
+ : null
+ })
+}
+
+describe('orchestration hot-path statement compilation', () => {
+ it('compiles each hot-path SQL exactly once across repeated graph publishes', () => {
+ const db = openDatabase(':memory:')
+ seedDispatchedWorker(db)
+ const projection = buildProjection(db)
+ const compiled = trackCompiledSql(db)
+
+ const publishes = [projection.buildByPaneKey()]
+ const compiledByFirstPublish = [...compiled]
+ for (let publish = 0; publish < 4; publish += 1) {
+ publishes.push(projection.buildByPaneKey())
+ }
+
+ const compilationsPerSql = new Map