diff --git a/.github/actions/install-node-dependencies/action.yml b/.github/actions/install-node-dependencies/action.yml
index 46edfc54111..e36ec4c65d8 100644
--- a/.github/actions/install-node-dependencies/action.yml
+++ b/.github/actions/install-node-dependencies/action.yml
@@ -39,6 +39,9 @@ runs:
with:
install: false
+ # Why both lockfiles: setup-node keys the pnpm store on the root lockfile alone, so
+ # jobs that also install mobile restored a store with none of the React Native tree
+ # in it and re-downloaded the lot on every run.
- name: Setup Node.js
id: default-node
if: inputs.node-version == ''
@@ -46,6 +49,9 @@ runs:
with:
node-version-file: package.json
cache: pnpm
+ cache-dependency-path: |
+ pnpm-lock.yaml
+ mobile/pnpm-lock.yaml
- name: Setup requested Node.js
id: requested-node
@@ -54,6 +60,9 @@ runs:
with:
node-version: ${{ inputs.node-version }}
cache: pnpm
+ cache-dependency-path: |
+ pnpm-lock.yaml
+ mobile/pnpm-lock.yaml
- name: Validate native runtime
shell: bash
diff --git a/.github/workflows/cloud-operate-relay-production-rehome-job.yml b/.github/workflows/cloud-operate-relay-production-rehome-job.yml
index 1ceadcece12..682953af5e7 100644
--- a/.github/workflows/cloud-operate-relay-production-rehome-job.yml
+++ b/.github/workflows/cloud-operate-relay-production-rehome-job.yml
@@ -26,6 +26,9 @@ permissions:
defaults:
run:
+ # `shell: bash` adds pipefail; without it `node ... | tee` reports tee's exit code and a
+ # thrown inspect/apply passed green (Aug 28-29 and Sep 3 2026 runs).
+ shell: bash
working-directory: cloud
jobs:
diff --git a/.github/workflows/cloud-prove-relay-asia-staging.yml b/.github/workflows/cloud-prove-relay-asia-staging.yml
index 9a66e967b57..56677a98600 100644
--- a/.github/workflows/cloud-prove-relay-asia-staging.yml
+++ b/.github/workflows/cloud-prove-relay-asia-staging.yml
@@ -55,9 +55,11 @@ jobs:
- name: Validate the exact staging proof request
shell: bash
+ env:
+ CONFIRMATION: ${{ inputs.confirmation }}
run: |
set -euo pipefail
- test "${{ inputs.confirmation }}" = PROVE_ASIA_STAGING
+ test "${CONFIRMATION}" = PROVE_ASIA_STAGING
[[ "${IMAGE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]
[[ "${INITIAL_SELECTOR_GENERATION}" =~ ^[1-9][0-9]*$ ]]
[[ "${PROMOTE_ATTEMPT_ID}" =~ ^[A-Za-z0-9_-]{8,128}$ ]]
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index bde0b5e05d8..ed37cefb434 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -28,6 +28,7 @@ jobs:
outputs:
should_run: ${{ steps.filter.outputs.should_run }}
native_cache_changed: ${{ steps.filter.outputs.native_cache_changed }}
+ mobile_dependencies: ${{ steps.filter.outputs.mobile_dependencies }}
static_analysis: ${{ steps.filter.outputs.static_analysis }}
typecheck: ${{ steps.filter.outputs.typecheck }}
git_compatibility: ${{ steps.filter.outputs.git_compatibility }}
@@ -95,6 +96,25 @@ jobs:
- name: Enforce type-aware code-quality baseline
run: pnpm run audit:code-quality:type-aware
+ # Why: the changed-code gate lints mobile files too, and its type-aware pass
+ # resolves types from mobile/node_modules. Mobile is a separate pnpm project,
+ # so the root install above leaves it empty and every mobile type degrades to
+ # an `error` type — reported as phantom findings against the changed lines.
+ # Why no --ignore-scripts, unlike the root install: mobile's postinstall generates
+ # the gitignored terminal/mermaid webview engine modules that tracked source imports,
+ # and skipping it degrades those very types the step exists to resolve. The drift
+ # guard mirrors the root install so a stale mobile lockfile fails by name — mobile's
+ # lockfile carries patchedDependencies that a silent rewrite would drop.
+ - name: Install mobile dependencies
+ if: needs.code_paths.outputs.mobile_dependencies == 'true'
+ working-directory: mobile
+ run: |
+ pnpm install --frozen-lockfile
+ if [ "$(git -C "$GITHUB_WORKSPACE" rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; then
+ git -C "$GITHUB_WORKSPACE" diff --exit-code -- \
+ mobile/package.json mobile/pnpm-lock.yaml mobile/pnpm-workspace.yaml
+ fi
+
- name: Enforce changed-code quality
run: pnpm run check:code-quality:changed -- "${{ github.event.pull_request.base.sha }}"
@@ -360,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/.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/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.
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/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-ops/src/incident-monitor.test.ts b/cloud/apps/relay-ops/src/incident-monitor.test.ts
index 51153bb63e1..ea5ad55b645 100644
--- a/cloud/apps/relay-ops/src/incident-monitor.test.ts
+++ b/cloud/apps/relay-ops/src/incident-monitor.test.ts
@@ -123,6 +123,19 @@ describe('incident monitor evaluator', () => {
})
})
+ // Why: sweeps no longer reach the retry wrapper, so any exhaustion left in this
+ // counter is a request path that terminally failed. It must still freeze.
+ it('freezes on a single exhausted request-path transaction', () => {
+ const sample = healthySample()
+ sample.sources['relay-logs']!.signals['relay.postgres_retry_exhausted'] = signal(1)
+ expect(evaluateIncidentSample(sample, startedAt)).toMatchObject({
+ status: 'freeze',
+ failures: [
+ expect.objectContaining({ signal: 'relay.postgres_retry_exhausted', threshold: 0 })
+ ]
+ })
+ })
+
it('allows missing auth readiness and legacy existing-only connections', () => {
const sample = healthySample()
const legacySelector = {
diff --git a/cloud/apps/relay-ops/src/incident-monitor.ts b/cloud/apps/relay-ops/src/incident-monitor.ts
index 6073e351511..a1936f5df6c 100644
--- a/cloud/apps/relay-ops/src/incident-monitor.ts
+++ b/cloud/apps/relay-ops/src/incident-monitor.ts
@@ -38,6 +38,20 @@ export const INCIDENT_MONITOR_THRESHOLDS = {
// margin; relayPostgresRetryExhausted below stays at zero tolerance, so any
// transaction that terminally fails still freezes the gate.
relayPostgresRetries: 300,
+ // Why: this bar stays at zero. Cell-inventory contention reaches the retry
+ // wrapper from exactly two kinds of caller, and neither is a sweep tick that
+ // can shrug the failure off:
+ // - request paths, which take a wait bounded at CELL_INVENTORY_LOCK_TIMEOUT_MS
+ // (assignment, control activation, activity, admin drain/evacuate/supersede);
+ // - sweep-reachable code that a request also enters, which keeps the pool
+ // lock_timeout so it cannot fail faster than before this change: the
+ // completeEvacuation site that waits, reconcileReservationAccounting, and
+ // placement re-entered from evacuateDeadCells.
+ // Sweep-only sites take the inventory NOWAIT, so their contention becomes
+ // database_lock_unavailable, which is not a retryable abort and never reaches
+ // this counter. The relay's cell-inventory-lock census test holds that split.
+ // Splitting the metric by the phase label PR #423 put on the log payload would
+ // need a labelled log-based metric, which this signal's counter does not carry.
relayPostgresRetryExhausted: 0,
// Why: public admission is a per-instance semaphore, so fleet assignment capacity is
// concurrency x instances. A floor of 1 let the 2026-08-04 collapse from five instances
diff --git a/cloud/apps/relay/src/assignment-store.ts b/cloud/apps/relay/src/assignment-store.ts
index 0b2b1ef72a9..96d66eb7dc2 100644
--- a/cloud/apps/relay/src/assignment-store.ts
+++ b/cloud/apps/relay/src/assignment-store.ts
@@ -31,7 +31,12 @@ import {
} from './assignment-connection-headroom-query.js'
import { AssignmentIdentityQueue } from './assignment-identity-queue.js'
import type { RelayCellConfig } from './config.js'
-import type { RelayDatabase, RelayTransactionOptions, SqlRow } from './database.js'
+import type {
+ RelayDatabase,
+ RelayLockOptions,
+ RelayTransactionOptions,
+ SqlRow
+} from './database.js'
import type { RegionalRehomeSafetySnapshot } from './relay-observability.js'
import {
combineRegionalRehomeSafety,
@@ -316,6 +321,25 @@ const ACTIVITY_REQUEST_UNITS: Record = {
}
const ASSIGNMENT_LOCK_RETRY_DEADLINE_MS = 15_000
+// Why: one global FOR UPDATE over a 23-row table serialises every director and
+// cell. At the 1s pool lock_timeout each blocked waiter also holds a pooled
+// client for a full second, so the queue converts contention into pool
+// exhaustion. The lock is held to COMMIT and the assignment path runs many
+// statements after taking it, and no hold-time telemetry existed before this
+// change, so 500ms is a first value to tune once cellInventoryHoldMsMax lands.
+export const CELL_INVENTORY_LOCK_TIMEOUT_MS = 500
+
+// The same inventory lock is taken by live requests and by background sweeps,
+// and the right failure mode differs per caller.
+export type CellInventoryLockMode =
+ // Bound the wait so a blocked request stops occupying a pooled client.
+ | 'request'
+ // Never queue: the caller handles database_lock_unavailable and moves on.
+ | 'nowait'
+ // A sweep can enter here, so keep the pool default. Failing sooner would turn
+ // ordinary contention into a 55P03 the retry wrapper reports as terminal, and
+ // one terminal failure freezes the incident gate.
+ | 'pool-default'
// Why: stranded detection (issue #225) needs a grant old enough that a real
// attach would have registered (the 90s activity lease covers dial +
// activation), yet recent enough to prove an active retry loop rather than
@@ -536,29 +560,35 @@ export class RelayAssignmentStore {
async assign(
identity: AssignmentIdentity,
preferredRegion?: RelayRegion,
- placementRegion: RelayRegion = preferredRegion ?? RELAY_DEFAULT_REGION
+ placementRegion: RelayRegion = preferredRegion ?? RELAY_DEFAULT_REGION,
+ // evacuateDeadCells re-enters placement from a sweep; it must not take the
+ // bounded wait, whose 55P03 would surface as a terminal sweep failure.
+ lockMode: CellInventoryLockMode = 'request'
): Promise {
- const sticky = await this.assignStickyWithLockRetry(identity, preferredRegion)
+ const sticky = await this.assignStickyWithLockRetry(identity, lockMode, preferredRegion)
if (sticky) return sticky
// Only placement needs the global inventory critical section; queueing those
// attempts locally avoids turning true placement bursts into NOWAIT storms.
return await this.serializeAssignment(
- async () => await this.assignWithLockRetry(identity, preferredRegion, placementRegion)
+ async () =>
+ await this.assignWithLockRetry(identity, lockMode, preferredRegion, placementRegion)
)
}
private async assignStickyWithLockRetry(
identity: AssignmentIdentity,
+ lockMode: CellInventoryLockMode,
preferredRegion?: RelayRegion
): Promise {
return await this.withAssignmentLockRetry(
async (inventoryFirst) =>
- await this.assignStickyOnce(identity, inventoryFirst, preferredRegion)
+ await this.assignStickyOnce(identity, inventoryFirst, lockMode, preferredRegion)
)
}
private async assignWithLockRetry(
identity: AssignmentIdentity,
+ lockMode: CellInventoryLockMode,
preferredRegion?: RelayRegion,
placementRegion: RelayRegion = preferredRegion ?? RELAY_DEFAULT_REGION
): Promise {
@@ -566,7 +596,13 @@ export class RelayAssignmentStore {
let inventoryScope: AssignmentInventoryScope = 'none'
while (true) {
try {
- return await this.assignOnce(identity, inventoryScope, preferredRegion, placementRegion)
+ return await this.assignOnce(
+ identity,
+ inventoryScope,
+ lockMode,
+ preferredRegion,
+ placementRegion
+ )
} catch (error) {
if (error instanceof AssignmentInventoryScopeChanged) {
inventoryScope = 'all'
@@ -604,12 +640,13 @@ export class RelayAssignmentStore {
private async assignStickyOnce(
identity: AssignmentIdentity,
inventoryFirst: boolean,
+ lockMode: CellInventoryLockMode,
preferredRegion?: RelayRegion
): Promise {
const now = this.now()
return await this.database.transaction(async (transaction) => {
const lockedCells = inventoryFirst
- ? await this.lockCellInventory(transaction)
+ ? await this.lockCellInventory(transaction, lockMode)
: undefined
const existing = await this.assignmentRow(transaction, identity, inventoryFirst)
if (!existing) return null
@@ -751,6 +788,7 @@ export class RelayAssignmentStore {
private async assignOnce(
identity: AssignmentIdentity,
inventoryScope: AssignmentInventoryScope,
+ lockMode: CellInventoryLockMode,
preferredRegion?: RelayRegion,
placementRegion: RelayRegion = preferredRegion ?? RELAY_DEFAULT_REGION
): Promise {
@@ -760,9 +798,9 @@ export class RelayAssignmentStore {
return await this.database.transaction(async (transaction) => {
let lockedCells =
inventoryScope === 'all'
- ? await this.lockCellInventory(transaction)
+ ? await this.lockCellInventory(transaction, lockMode)
: inventoryScope === 'general'
- ? await this.lockGeneralCellInventory(transaction)
+ ? await this.lockGeneralCellInventory(transaction, lockMode)
: undefined
const existing = await this.assignmentRow(
transaction,
@@ -779,7 +817,7 @@ export class RelayAssignmentStore {
let connectionHeadroomReassignment = false
let strandedReassignment = false
if (existing && !mayNormallyReassign(activity(existing), now)) {
- lockedCells ??= await this.lockCellInventory(transaction, true)
+ lockedCells ??= await this.lockCellInventory(transaction, 'nowait')
const admission = await cellAdmissionStates(transaction)
const currentRow = lockedCells.find(
(row) => text(row, 'cell_id') === text(existing, 'cell_id')
@@ -859,8 +897,8 @@ export class RelayAssignmentStore {
}
lockedCells ??= existing
- ? await this.lockCellInventory(transaction, true)
- : await this.lockGeneralCellInventory(transaction, true)
+ ? await this.lockCellInventory(transaction, 'nowait')
+ : await this.lockGeneralCellInventory(transaction, 'nowait')
const target = await this.leastLoadedCell(
transaction,
lockedCells,
@@ -2114,7 +2152,7 @@ export class RelayAssignmentStore {
ORDER BY migration.user_id, migration.relay_host_id`,
[input.cellId]
)
- const cells = await this.lockCellInventory(transaction)
+ const cells = await this.lockCellInventory(transaction, 'request')
for (const migrationRow of migrations) {
const identity = {
userId: text(migrationRow, 'user_id'),
@@ -2608,10 +2646,12 @@ export class RelayAssignmentStore {
let moved = 0
for (const row of rows) {
try {
- const assignment = await this.assign({
- userId: text(row, 'user_id'),
- relayHostId: text(row, 'relay_host_id')
- })
+ const assignment = await this.assign(
+ { userId: text(row, 'user_id'), relayHostId: text(row, 'relay_host_id') },
+ undefined,
+ undefined,
+ 'pool-default'
+ )
if (assignment.cellId !== text(row, 'cell_id')) moved++
} catch (error) {
if (!(error instanceof Error && error.message === 'relay_capacity_exhausted')) throw error
@@ -3162,7 +3202,7 @@ export class RelayAssignmentStore {
)
const requestDelta = ACTIVITY_REQUEST_UNITS[kind] * (after - before)
if (requestDelta !== 0) {
- await this.lockCellInventory(transaction)
+ await this.lockCellInventory(transaction, 'request')
await this.adjustCellReservation(transaction, text(row, 'cell_id'), requestDelta)
}
})
@@ -3223,7 +3263,7 @@ export class RelayAssignmentStore {
}
const units = ACTIVITY_REQUEST_UNITS[input.kind]
if (existing) {
- await this.lockCellInventory(transaction)
+ await this.lockCellInventory(transaction, 'request')
await this.removeActivityLease(transaction, identity, existing, now)
await this.adjustCellReservation(transaction, input.cellId, units)
}
@@ -3540,7 +3580,7 @@ export class RelayAssignmentStore {
)
await this.touchAssignment(transaction, identity, expiresAt, now)
} else {
- await this.lockCellInventory(transaction)
+ await this.lockCellInventory(transaction, 'request')
await this.adjustCellReservation(transaction, input.cellId, 1)
await this.adjustActivityCount(transaction, identity, 'control', 1, expiresAt, now)
await transaction.query(
@@ -3614,7 +3654,7 @@ export class RelayAssignmentStore {
}
if (sourceCellId === targetCellId) throw new Error('target_matches_source')
await this.lockAssignmentActivities(transaction, identity)
- const cells = await this.lockCellInventory(transaction)
+ const cells = await this.lockCellInventory(transaction, 'request')
const target = cells.find((row) => text(row, 'cell_id') === targetCellId)
if (!target || integer(target, 'enabled') !== 1) throw new Error('target_cell_unavailable')
if (!(await this.cellIsLive(transaction, targetCellId, now))) {
@@ -3822,7 +3862,7 @@ export class RelayAssignmentStore {
let lockedCells: SqlRow[] | undefined
if (inventoryFirst) {
try {
- lockedCells = await this.lockCellInventory(transaction)
+ lockedCells = await this.lockCellInventory(transaction, 'request')
} catch (error) {
if (isDatabaseLockTimeout(error)) {
throw new Error('database_lock_unavailable')
@@ -3863,7 +3903,7 @@ export class RelayAssignmentStore {
if (activityUnitsForCell(activityLeases, input.sourceCellId) > 0) {
throw new Error('migration_source_still_active')
}
- const cells = lockedCells ?? (await this.lockCellInventory(transaction, true))
+ const cells = lockedCells ?? (await this.lockCellInventory(transaction, 'nowait'))
const source = cells.find((cell) => text(cell, 'cell_id') === input.sourceCellId)
const target = cells.find((cell) => text(cell, 'cell_id') === input.targetCellId)
if (!source || integer(source, 'enabled') !== 0) {
@@ -3967,7 +4007,7 @@ export class RelayAssignmentStore {
const now = this.now()
return await this.database.transaction(async (transaction) => {
const lockedCells = inventoryFirst
- ? await this.lockCellInventory(transaction)
+ ? await this.lockCellInventory(transaction, 'request')
: undefined
const assignment = await this.assignmentRow(transaction, identity, inventoryFirst)
const existing = (
@@ -4041,7 +4081,7 @@ export class RelayAssignmentStore {
) {
throw new Error('migration_activity_topology_mismatch')
}
- const cells = lockedCells ?? (await this.lockCellInventory(transaction, true))
+ const cells = lockedCells ?? (await this.lockCellInventory(transaction, 'nowait'))
const source = cells.find((cell) => text(cell, 'cell_id') === input.sourceCellId)
const currentTarget = cells.find(
(cell) => text(cell, 'cell_id') === input.currentTargetCellId
@@ -4458,7 +4498,7 @@ export class RelayAssignmentStore {
throw new Error('migration_activity_topology_mismatch')
}
}
- if (obsoleteLeases.length > 0) await this.lockCellInventory(transaction)
+ if (obsoleteLeases.length > 0) await this.lockCellInventory(transaction, 'request')
for (const lease of obsoleteLeases) {
await this.removeActivityLease(transaction, identity, lease, now)
}
@@ -4634,7 +4674,7 @@ export class RelayAssignmentStore {
) {
throw new Error('migration_activity_lease_shape_mismatch')
}
- await this.lockCellInventory(transaction)
+ await this.lockCellInventory(transaction, 'request')
await this.adjustCellReservation(
transaction,
input.currentTargetCellId,
@@ -4723,7 +4763,7 @@ export class RelayAssignmentStore {
if (this.requireLiveCells) {
let cells: SqlRow[]
try {
- cells = await this.lockCellInventory(transaction, true)
+ cells = await this.lockCellInventory(transaction, 'nowait')
} catch (error) {
if (isDatabaseLockUnavailable(error)) {
// Mixed-version workers may still hold a cell-first lock; defer
@@ -4779,7 +4819,7 @@ export class RelayAssignmentStore {
)
if (!targetIsActive) throw new Error('migration_target_not_active')
const lease = activityLeaseById(activityLeases, migrationActivityId(assignmentEpoch))
- if (lease && !cellsLocked) await this.lockCellInventory(transaction)
+ if (lease && !cellsLocked) await this.lockCellInventory(transaction, 'pool-default')
if (lease) await this.removeActivityLease(transaction, identity, lease, now)
await transaction.query(
`UPDATE relay_assignment_migrations SET completed_at = ?, updated_at = ?
@@ -4801,7 +4841,7 @@ export class RelayAssignmentStore {
const sourceCellId = text(assignment, 'cell_id')
if (sourceCellId === targetCellId) throw new Error('target_matches_source')
await this.lockAssignmentActivities(transaction, identity)
- const cells = await this.lockCellInventory(transaction)
+ const cells = await this.lockCellInventory(transaction, 'request')
const admission = await cellAdmissionStates(transaction)
const targetRow = cells.find(
(row) =>
@@ -5051,7 +5091,13 @@ export class RelayAssignmentStore {
}
this.pendingRegionalRehomeDisableLog = null
const candidateSkips: RegionalRehomeCandidateSkip[] = []
+ // A Postgres transaction is unusable after a NOWAIT abort, so a contended
+ // tick abandons the candidate it stopped on plus every one behind it.
+ let candidatesTotal = 0
+ let candidatesFinished = 0
const claimResult = await this.database.transaction(async (transaction) => {
+ candidatesTotal = 0
+ candidatesFinished = 0
candidateSkips.length = 0
await this.initializeRegionalRehomeControl(transaction, now)
const control = (
@@ -5122,6 +5168,7 @@ export class RelayAssignmentStore {
)
)[0]
if (retry) {
+ candidatesTotal = 1
const fleetSafety = await this.lockedRegionalRehomeFleetSafety(transaction, now)
if (
!(await this.regionalRehomeSafetyAllowsClaim(
@@ -5190,6 +5237,7 @@ export class RelayAssignmentStore {
)
)[0]
if (redrain) {
+ candidatesTotal = 1
const fleetSafety = await this.lockedRegionalRehomeFleetSafety(transaction, now)
if (
!(await this.regionalRehomeSafetyAllowsClaim(
@@ -5253,6 +5301,7 @@ export class RelayAssignmentStore {
LIMIT 10`,
[preferenceCutoff, now - this.heartbeatTtlMs, now]
)
+ candidatesTotal = candidates.length
for (const candidate of candidates) {
const claimed = await this.startRegionalRehomeCandidate(transaction, {
identity: {
@@ -5268,6 +5317,7 @@ export class RelayAssignmentStore {
now,
skips: candidateSkips
})
+ candidatesFinished++
if (!claimed) continue
await this.markRegionalRehomeDispatchClaimed(
transaction,
@@ -5283,6 +5333,21 @@ export class RelayAssignmentStore {
await this.markRegionalRehomeTickSkipped(transaction, now, intervalMs)
}
return null
+ }).catch((error: unknown): RegionalRehomeAttempt | null => {
+ // Only inventory contention is swallowed here; every other failure keeps
+ // its existing propagation and its dispatch-failure accounting.
+ if (!isDatabaseLockUnavailable(error)) throw error
+ // The dispatch tick runs every second; losing one to inventory contention
+ // costs a second of latency and never loses durable rehome state. The
+ // rolled-back transaction never disabled anything, so its pending disable
+ // log would describe a decision that did not happen.
+ candidateSkips.length = 0
+ this.pendingRegionalRehomeDisableLog = null
+ warnSweepCellInventoryBusy(
+ 'claim-regional-rehome',
+ Math.max(1, candidatesTotal - candidatesFinished)
+ )
+ return null
})
const pendingDisableLog = this.pendingRegionalRehomeDisableLog
this.pendingRegionalRehomeDisableLog = null
@@ -5343,7 +5408,7 @@ export class RelayAssignmentStore {
}
const activityLeases = await this.lockAssignmentActivities(transaction, input.identity)
assertAssignmentActivityCounts(assignment, activityLeases, 0)
- const cells = await this.lockCellInventory(transaction)
+ const cells = await this.lockCellInventory(transaction, 'nowait')
const admission = await cellAdmissionStates(transaction)
const regions = new Map(
(await transaction.query(`SELECT cell_id, region FROM relay_cell_regions`)).map((row) => [
@@ -5630,7 +5695,7 @@ export class RelayAssignmentStore {
transaction: RelayDatabase,
now: number
): Promise {
- const cells = await this.lockCellInventory(transaction)
+ const cells = await this.lockCellInventory(transaction, 'nowait')
const admission = await cellAdmissionStates(transaction)
const regions = new Map(
(await transaction.query(`SELECT cell_id, region FROM relay_cell_regions`)).map((row) => [
@@ -5874,6 +5939,7 @@ export class RelayAssignmentStore {
[...quarantined, limit]
)
let completed = 0
+ let inventoryBusy = 0
for (const candidate of candidates) {
// One poisoned row must not stall every later candidate: an invariant
// throw here blocked fleet completions head-of-line in production.
@@ -5890,9 +5956,14 @@ export class RelayAssignmentStore {
if (changed) completed++
this.regionalRehomeCandidateQuarantine.delete(attemptId)
} catch (error) {
+ if (isDatabaseLockUnavailable(error)) {
+ inventoryBusy++
+ continue
+ }
this.recordRegionalRehomeCandidateFailure('complete', attemptId, now, error)
}
}
+ warnSweepCellInventoryBusy('complete-ready-regional-rehomes', inventoryBusy)
return completed
}
@@ -6112,7 +6183,7 @@ export class RelayAssignmentStore {
leases,
migration
)
- const cells = await this.lockCellInventory(transaction)
+ const cells = await this.lockCellInventory(transaction, 'nowait')
const target = cells.find((cell) => text(cell, 'cell_id') === targetCellId)
const admission = await cellAdmissionStates(transaction)
if (
@@ -6261,6 +6332,7 @@ export class RelayAssignmentStore {
[now - REGIONAL_REHOME_MAX_REFRESH_MS, ...quarantined, limit]
)
let aborted = 0
+ let inventoryBusy = 0
for (const candidate of candidates) {
const identity = {
userId: text(candidate, 'user_id'),
@@ -6318,7 +6390,7 @@ export class RelayAssignmentStore {
integer(lease, 'expires_at') > now
)
if (targetActive) return false
- const cells = await this.lockCellInventory(transaction)
+ const cells = await this.lockCellInventory(transaction, 'nowait')
const source = cells.find((cell) => text(cell, 'cell_id') === sourceCellId)
const admission = await cellAdmissionStates(transaction)
if (
@@ -6376,10 +6448,12 @@ export class RelayAssignmentStore {
})
this.regionalRehomeCandidateQuarantine.delete(attemptId)
} catch (error) {
- this.recordRegionalRehomeCandidateFailure('abort', attemptId, now, error)
+ if (isDatabaseLockUnavailable(error)) inventoryBusy++
+ else this.recordRegionalRehomeCandidateFailure('abort', attemptId, now, error)
}
if (changed) aborted++
}
+ warnSweepCellInventoryBusy('abort-expired-regional-rehomes', inventoryBusy)
return aborted
}
@@ -6396,6 +6470,7 @@ export class RelayAssignmentStore {
[now, now, abandonedBefore, abandonedBefore]
)
let aborted = 0
+ let inventoryBusy = 0
for (const candidate of candidates) {
const didAbort = await this.database.transaction(async (transaction) => {
const identity = {
@@ -6480,7 +6555,7 @@ export class RelayAssignmentStore {
]
.map((activityId) => activityLeaseById(activityLeases, activityId))
.filter((lease): lease is SqlRow => lease !== undefined)
- if (obsoleteLeases.length > 0) await this.lockCellInventory(transaction)
+ if (obsoleteLeases.length > 0) await this.lockCellInventory(transaction, 'nowait')
for (const lease of obsoleteLeases) {
await this.removeActivityLease(transaction, identity, lease, now)
}
@@ -6498,7 +6573,7 @@ export class RelayAssignmentStore {
)
return true
}
- const cells = await this.lockCellInventory(transaction)
+ const cells = await this.lockCellInventory(transaction, 'nowait')
const sourceCellId = text(row, 'source_cell_id')
const admissionRows = await transaction.query(
`SELECT cell_id, admission_state, updated_at FROM relay_cell_admission
@@ -6595,9 +6670,15 @@ export class RelayAssignmentStore {
[now, now, identity.userId, identity.relayHostId, assignmentEpoch]
)
return true
+ }).catch((error: unknown): boolean => {
+ // Expiry is durable; another director settling this row is not a failure.
+ if (!isDatabaseLockUnavailable(error)) throw error
+ inventoryBusy++
+ return false
})
if (didAbort) aborted++
}
+ warnSweepCellInventoryBusy('abort-expired-evacuations', inventoryBusy)
return aborted
}
@@ -6665,7 +6746,7 @@ export class RelayAssignmentStore {
const activityLeases = await this.lockAssignmentActivities(transaction, identity, true)
const lease = activityLeaseById(activityLeases, text(candidate, 'activity_id'))
if (!lease || integer(lease, 'expires_at') > now) return false
- await this.lockCellInventory(transaction, true)
+ await this.lockCellInventory(transaction, 'nowait')
await this.removeActivityLease(transaction, identity, lease, now)
return true
})
@@ -6709,7 +6790,7 @@ export class RelayAssignmentStore {
[now],
{ failIfUnavailable: true }
)
- if (expired.length > 0) await this.lockCellInventory(transaction, true)
+ if (expired.length > 0) await this.lockCellInventory(transaction, 'nowait')
for (const row of expired) {
await this.adjustCellReservation(transaction, text(row, 'cell_id'), -requestUnits(row))
await transaction.query(
@@ -6782,7 +6863,7 @@ export class RelayAssignmentStore {
targetCellId
]
)
- const cells = await this.lockCellInventory(transaction)
+ const cells = await this.lockCellInventory(transaction, 'pool-default')
const assignmentKeys = new Set(
assignments.map((row) =>
assignmentKey(text(row, 'user_id'), text(row, 'relay_host_id'))
@@ -6861,30 +6942,32 @@ export class RelayAssignmentStore {
private async lockCellInventory(
database: RelayDatabase,
- failIfUnavailable = false
+ mode: CellInventoryLockMode
): Promise {
// Every capacity-changing assignment takes the tiny cell inventory in one
// order; dynamically locking only the selected target allowed cross-cell cycles.
- return await database.queryLocked(
+ const rows = await database.queryLocked(
`SELECT * FROM relay_cells ORDER BY cell_id ASC`,
[],
- { failIfUnavailable }
+ cellInventoryLockOptions(mode)
)
+ return rows
}
private async lockGeneralCellInventory(
database: RelayDatabase,
- failIfUnavailable = false
+ mode: CellInventoryLockMode
): Promise {
- return await database.queryLocked(
+ const rows = await database.queryLocked(
`SELECT * FROM relay_cells
WHERE cell_id IN (
SELECT cell_id FROM relay_cell_admission WHERE admission_state = 'general'
)
ORDER BY cell_id ASC`,
[],
- { failIfUnavailable }
+ cellInventoryLockOptions(mode)
)
+ return rows
}
private async leastLoadedCell(
@@ -6892,7 +6975,7 @@ export class RelayAssignmentStore {
lockedCells: SqlRow[] | undefined,
preferredRegion: RelayRegion
): Promise {
- const rows = lockedCells ?? (await this.lockCellInventory(database))
+ const rows = lockedCells ?? (await this.lockCellInventory(database, 'pool-default'))
const regions = new Map(
(await database.query(`SELECT cell_id, region FROM relay_cell_regions`)).map((row) => [
text(row, 'cell_id'),
@@ -7507,7 +7590,7 @@ export class RelayAssignmentStore {
) {
throw new Error('activity_lease_shape_mismatch')
}
- const cells = await this.lockCellInventory(database)
+ const cells = await this.lockCellInventory(database, 'request')
await database.query(
`DELETE FROM relay_assignment_activity_leases
WHERE user_id = ? AND relay_host_id = ? AND activity_kind = 'control'
@@ -7886,6 +7969,23 @@ function isDatabaseLockUnavailable(error: unknown): boolean {
return error instanceof Error && error.message === 'database_lock_unavailable'
}
+export function cellInventoryLockOptions(mode: CellInventoryLockMode): RelayLockOptions {
+ if (mode === 'nowait') return { failIfUnavailable: true, measureHoldMs: true }
+ if (mode === 'pool-default') return { measureHoldMs: true }
+ return { lockTimeoutMs: CELL_INVENTORY_LOCK_TIMEOUT_MS, measureHoldMs: true }
+}
+
+// Background sweeps take the cell inventory NOWAIT so they never queue ahead of
+// assignment traffic. A skipped candidate is re-derived from durable state on
+// the next tick, so it is ordinary contention, not a sweep failure: one summary
+// line per tick, never an error and never a quarantine.
+function warnSweepCellInventoryBusy(sweep: string, skipped: number): void {
+ if (skipped === 0) return
+ console.warn(
+ JSON.stringify({ event: 'orca_relay_sweep_cell_inventory_busy', sweep, skipped })
+ )
+}
+
function isDatabaseLockTimeout(error: unknown): boolean {
return String((error as { code?: unknown }).code) === '55P03'
}
diff --git a/cloud/apps/relay/src/cell-inventory-hold-samples.test.ts b/cloud/apps/relay/src/cell-inventory-hold-samples.test.ts
new file mode 100644
index 00000000000..abd37dcbb29
--- /dev/null
+++ b/cloud/apps/relay/src/cell-inventory-hold-samples.test.ts
@@ -0,0 +1,70 @@
+import { describe, expect, it } from 'vitest'
+import {
+ CellInventoryHoldSamples,
+ emptyCellInventoryHoldCounts
+} from './cell-inventory-hold-samples.js'
+
+// Nearest rank, computed in integer arithmetic so it cannot inherit the float
+// error the implementation's `0.95 * n` could in principle carry.
+function nearestRankP95(sorted: number[]): number {
+ return sorted[Math.ceil((95 * sorted.length) / 100) - 1]!
+}
+
+function samplesOf(values: number[]): CellInventoryHoldSamples {
+ const samples = new CellInventoryHoldSamples()
+ for (const value of values) samples.record(value)
+ return samples
+}
+
+describe('cell inventory hold samples', () => {
+ it('reports nothing before the first hold', () => {
+ expect(new CellInventoryHoldSamples().readCounts()).toEqual(
+ emptyCellInventoryHoldCounts()
+ )
+ })
+
+ // Why: the 500ms bound will be tuned against this percentile, so an off-by-one
+ // here reads as a hold the fleet never had.
+ it('places p95 at the nearest rank for every window size', () => {
+ for (let size = 1; size <= 400; size++) {
+ const values = Array.from({ length: size }, (_, index) => index + 1)
+ const shuffled = [...values].reverse()
+
+ const counts = samplesOf(shuffled).readCounts()
+
+ expect(counts.cellInventoryHoldMsP95).toBe(nearestRankP95(values))
+ expect(counts.cellInventoryHoldMsMax).toBe(size)
+ expect(counts.cellInventoryHolds).toBe(size)
+ }
+ })
+
+ it('never reports a p95 above the max', () => {
+ for (let size = 1; size <= 200; size++) {
+ const counts = samplesOf(Array.from({ length: size }, (_, i) => i + 1)).readCounts()
+
+ expect(counts.cellInventoryHoldMsP95).toBeLessThanOrEqual(counts.cellInventoryHoldMsMax)
+ }
+ })
+
+ it('ignores a hold that is not a finite, non-negative duration', () => {
+ const samples = samplesOf([Number.NaN, Number.POSITIVE_INFINITY, -1])
+
+ expect(samples.readCounts()).toEqual(emptyCellInventoryHoldCounts())
+ })
+
+ // Why: the reservoir is bounded, so a heavy flush interval keeps the most
+ // recent holds rather than growing without limit or freezing on the oldest.
+ it('keeps the most recent holds once the reservoir is full', () => {
+ const counts = samplesOf(Array.from({ length: 2_100 }, (_, index) => index + 1)).readCounts()
+
+ expect(counts.cellInventoryHolds).toBe(2_048)
+ expect(counts.cellInventoryHoldMsMax).toBe(2_100)
+ })
+
+ it('resets the window on consume so each flush reports its own holds', () => {
+ const samples = samplesOf([5, 10])
+
+ expect(samples.consumeCounts().cellInventoryHolds).toBe(2)
+ expect(samples.consumeCounts()).toEqual(emptyCellInventoryHoldCounts())
+ })
+})
diff --git a/cloud/apps/relay/src/cell-inventory-hold-samples.ts b/cloud/apps/relay/src/cell-inventory-hold-samples.ts
new file mode 100644
index 00000000000..14941032d80
--- /dev/null
+++ b/cloud/apps/relay/src/cell-inventory-hold-samples.ts
@@ -0,0 +1,46 @@
+// Why: the cell inventory lock is held to COMMIT, and the assignment path runs
+// many statements after taking it. Tuning the request-path wait bound needs the
+// hold distribution, and no runtime metric carried it before this change.
+export type CellInventoryHoldCounts = {
+ cellInventoryHoldMsMax: number
+ cellInventoryHoldMsP95: number
+ cellInventoryHolds: number
+}
+
+// Bounded so a flush interval with heavy assignment traffic cannot grow the array
+// without limit; the reservoir keeps the most recent holds.
+const MAX_SAMPLES = 2_048
+
+export function emptyCellInventoryHoldCounts(): CellInventoryHoldCounts {
+ return { cellInventoryHoldMsMax: 0, cellInventoryHoldMsP95: 0, cellInventoryHolds: 0 }
+}
+
+export class CellInventoryHoldSamples {
+ private samples: number[] = []
+
+ record(holdMs: number): void {
+ if (!Number.isFinite(holdMs) || holdMs < 0) return
+ if (this.samples.length === MAX_SAMPLES) this.samples.shift()
+ this.samples.push(holdMs)
+ }
+
+ consumeCounts(): CellInventoryHoldCounts {
+ const counts = this.readCounts()
+ this.samples = []
+ return counts
+ }
+
+ readCounts(): CellInventoryHoldCounts {
+ if (this.samples.length === 0) return emptyCellInventoryHoldCounts()
+ const sorted = [...this.samples].sort((left, right) => left - right)
+ return {
+ cellInventoryHoldMsMax: round(sorted[sorted.length - 1]!),
+ cellInventoryHoldMsP95: round(sorted[Math.ceil(0.95 * sorted.length) - 1] ?? 0),
+ cellInventoryHolds: sorted.length
+ }
+ }
+}
+
+function round(value: number): number {
+ return Number(value.toFixed(3))
+}
diff --git a/cloud/apps/relay/src/cell-inventory-lock-census.test.ts b/cloud/apps/relay/src/cell-inventory-lock-census.test.ts
new file mode 100644
index 00000000000..b2b5b65684c
--- /dev/null
+++ b/cloud/apps/relay/src/cell-inventory-lock-census.test.ts
@@ -0,0 +1,205 @@
+import { readFileSync } from 'node:fs'
+import { describe, expect, it } from 'vitest'
+import { cellInventoryLockOptions, type CellInventoryLockMode } from './assignment-store.js'
+
+// Which entry points can reach a call site. A site a sweep can enter must never
+// take the bounded wait: its 55P03 becomes a terminal transaction failure, and
+// the incident monitor freezes on a single one.
+type Reachability = 'request' | 'sweep' | 'both' | 'orphan'
+
+// 'caller' is not a CellInventoryLockMode: those sites take the mode threaded
+// from `assign`, which is 'request' for a client and 'pool-default' for the
+// evacuateDeadCells sweep.
+type CensusMode = CellInventoryLockMode | 'caller'
+
+type CensusEntry = { method: string; mode: CensusMode; reach: Reachability }
+
+// Every lockCellInventory / lockGeneralCellInventory call site in
+// assignment-store.ts, in source order. A new site fails this test until it is
+// classified here, which is the point.
+const CENSUS: CensusEntry[] = [
+ { method: 'assignStickyOnce', mode: 'caller', reach: 'both' },
+ { method: 'assignOnce', mode: 'caller', reach: 'both' },
+ { method: 'assignOnce', mode: 'caller', reach: 'both' },
+ { method: 'assignOnce', mode: 'nowait', reach: 'both' },
+ { method: 'assignOnce', mode: 'nowait', reach: 'both' },
+ { method: 'assignOnce', mode: 'nowait', reach: 'both' },
+ { method: 'refreshDrainMigrationLeasesOnce', mode: 'request', reach: 'request' },
+ // Reachable from neither: changeActivity has no production callers, only tests.
+ { method: 'changeActivity', mode: 'request', reach: 'orphan' },
+ { method: 'acquireActivity', mode: 'request', reach: 'request' },
+ { method: 'activateControl', mode: 'request', reach: 'request' },
+ { method: 'startEvacuation', mode: 'request', reach: 'request' },
+ { method: 'completeEvacuationFromDeadSourceOnce', mode: 'request', reach: 'request' },
+ { method: 'completeEvacuationFromDeadSourceOnce', mode: 'nowait', reach: 'request' },
+ { method: 'supersedeRegisteredEvacuationOnce', mode: 'request', reach: 'request' },
+ { method: 'supersedeRegisteredEvacuationOnce', mode: 'nowait', reach: 'request' },
+ { method: 'prepareRegisteredCellSupersession', mode: 'request', reach: 'request' },
+ { method: 'prepareRegisteredCellSupersession', mode: 'request', reach: 'request' },
+ { method: 'completeEvacuation', mode: 'nowait', reach: 'both' },
+ { method: 'completeEvacuation', mode: 'pool-default', reach: 'both' },
+ { method: 'rebalanceDormant', mode: 'request', reach: 'request' },
+ { method: 'startRegionalRehomeCandidate', mode: 'nowait', reach: 'sweep' },
+ { method: 'lockedRegionalRehomeFleetSafety', mode: 'nowait', reach: 'sweep' },
+ { method: 'completeRegionalRehomeCandidate', mode: 'nowait', reach: 'sweep' },
+ { method: 'abortExpiredRegionalRehomes', mode: 'nowait', reach: 'sweep' },
+ { method: 'abortExpiredEvacuations', mode: 'nowait', reach: 'sweep' },
+ { method: 'abortExpiredEvacuations', mode: 'nowait', reach: 'sweep' },
+ { method: 'releaseExpiredActivityLeases', mode: 'nowait', reach: 'sweep' },
+ { method: 'releaseExpiredActivity', mode: 'nowait', reach: 'sweep' },
+ { method: 'reconcileReservationAccounting', mode: 'pool-default', reach: 'both' },
+ { method: 'leastLoadedCell', mode: 'pool-default', reach: 'both' },
+ { method: 'removeSupersededSameCellControls', mode: 'request', reach: 'request' }
+]
+
+// The background sweeps, and nothing else. A method reachable from one of these
+// can be entered by a sweep tick, whatever else can also enter it. Both lists are
+// read from source, so a new sweep step or a new route widens the derivation here
+// instead of silently widening what a bounded wait can be entered from.
+const SWEEP_ENTRY_FILES = ['./assignment-cleanup-steps.ts', './regional-rehome-worker.ts']
+const REQUEST_ENTRY_FILES = [
+ './app.ts',
+ './relay-server.ts',
+ './host-session-registry.ts',
+ './cell-admission-startup.ts'
+]
+
+const DECLARATION = /^ {2}(?:private |public )?(?:static )?(?:async )?([A-Za-z_][\w]*)[(<]/
+
+function storeSource(): string[] {
+ return readFileSync(new URL('./assignment-store.ts', import.meta.url), 'utf8').split('\n')
+}
+
+function entryPoints(files: string[]): string[] {
+ return files.flatMap((file) =>
+ [
+ ...readFileSync(new URL(file, import.meta.url), 'utf8').matchAll(
+ /assignments\.([A-Za-z_][\w]*)\(/g
+ )
+ ].map((call) => call[1]!)
+ )
+}
+
+// Same-class call graph: store methods only ever reach each other through `this.`.
+function storeCallGraph(lines: string[]): Map> {
+ const bounds: { name: string; start: number }[] = []
+ lines.forEach((line, index) => {
+ const declaration = DECLARATION.exec(line)
+ if (declaration) bounds.push({ name: declaration[1]!, start: index })
+ })
+ const callees = new Map>()
+ bounds.forEach((method, index) => {
+ const end = bounds[index + 1]?.start ?? lines.length
+ const names = callees.get(method.name) ?? new Set()
+ for (const call of lines.slice(method.start, end).join('\n').matchAll(
+ /this\.([A-Za-z_][\w]*)\s*\(/g
+ )) {
+ names.add(call[1]!)
+ }
+ callees.set(method.name, names)
+ })
+ return callees
+}
+
+function closure(callees: Map>, roots: string[]): Set {
+ const reached = new Set()
+ const pending = [...roots]
+ while (pending.length > 0) {
+ const name = pending.pop()!
+ if (reached.has(name)) continue
+ reached.add(name)
+ for (const callee of callees.get(name) ?? []) if (!reached.has(callee)) pending.push(callee)
+ }
+ return reached
+}
+
+// Why: a hand-written reachability column is a claim, not a check. Derive both
+// directions, so a new sweep edge into a bounded site fails here instead of in
+// production, and so 'sweep' and 'both' stop being asserted by hand.
+function derivedReachability(lines: string[]): (method: string) => Reachability {
+ const callees = storeCallGraph(lines)
+ const sweep = closure(callees, entryPoints(SWEEP_ENTRY_FILES))
+ const request = closure(callees, entryPoints(REQUEST_ENTRY_FILES))
+ return (method) =>
+ sweep.has(method)
+ ? request.has(method)
+ ? 'both'
+ : 'sweep'
+ : request.has(method)
+ ? 'request'
+ : 'orphan'
+}
+
+function readCallSites(): { method: string; mode: CensusMode }[] {
+ const sites: { method: string; mode: CensusMode }[] = []
+ let method = ''
+ for (const line of storeSource()) {
+ const declaration = DECLARATION.exec(line)
+ if (declaration) method = declaration[1]!
+ if (/private async lock(General)?CellInventory\(/.test(line)) continue
+ const call = /lock(?:General)?CellInventory\(\s*\w+\s*,\s*(?:'([a-z-]+)'|(\w+))\s*\)/.exec(line)
+ if (!call) continue
+ sites.push({ method, mode: (call[1] ?? 'caller') as CensusMode })
+ }
+ return sites
+}
+
+describe('cell inventory lock call-site census', () => {
+ it('classifies every call site exactly as recorded', () => {
+ expect(readCallSites()).toEqual(
+ CENSUS.map(({ method, mode }) => ({ method, mode }))
+ )
+ })
+
+ it('leaves no call site taking the inventory without naming a mode', () => {
+ const source = readFileSync(new URL('./assignment-store.ts', import.meta.url), 'utf8')
+ const unclassified = source
+ .split('\n')
+ .filter((line) => /lock(?:General)?CellInventory\(\s*\w+\s*\)/.test(line))
+ .filter((line) => !line.includes('private async'))
+
+ expect(unclassified).toEqual([])
+ })
+
+ it('derives the same reachability the census claims', () => {
+ const reachOf = derivedReachability(storeSource())
+
+ expect(readCallSites().map(({ method }) => reachOf(method))).toEqual(
+ CENSUS.map((entry) => entry.reach)
+ )
+ })
+
+ // Why: this is the whole point of the classification. A shorter wait on a
+ // sweep-reachable site turns contention into a terminal transaction failure,
+ // and relayPostgresRetryExhausted freezes the incident gate at zero.
+ // Why: the hold distribution is what the 500ms bound will be tuned against, so
+ // a mode that stops asking for it goes unmeasured in exactly the lane that
+ // matters. Nothing else in the suite reads the pool-default branch.
+ it('measures the hold in every lock mode', () => {
+ const modes: CellInventoryLockMode[] = ['request', 'nowait', 'pool-default']
+
+ expect(modes.map((mode) => cellInventoryLockOptions(mode).measureHoldMs)).toEqual([
+ true,
+ true,
+ true
+ ])
+ })
+
+ it('never puts a sweep-reachable site on the bounded wait', () => {
+ const reachOf = derivedReachability(storeSource())
+ const bounded = readCallSites().filter(
+ (site) => site.mode === 'request' && ['sweep', 'both'].includes(reachOf(site.method))
+ )
+
+ expect(bounded).toEqual([])
+ })
+
+ it('routes every sweep-only site to NOWAIT so it can skip the tick', () => {
+ const reachOf = derivedReachability(storeSource())
+ const queueing = readCallSites().filter(
+ (site) => reachOf(site.method) === 'sweep' && site.mode !== 'nowait'
+ )
+
+ expect(queueing).toEqual([])
+ })
+})
diff --git a/cloud/apps/relay/src/cell-inventory-lock-contention.test.ts b/cloud/apps/relay/src/cell-inventory-lock-contention.test.ts
new file mode 100644
index 00000000000..783d8a62e8b
--- /dev/null
+++ b/cloud/apps/relay/src/cell-inventory-lock-contention.test.ts
@@ -0,0 +1,541 @@
+import { readFileSync } from 'node:fs'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+
+const fakes = vi.hoisted(() => ({
+ statements: [] as string[],
+ query: vi.fn(async (sql: string) => {
+ fakes.statements.push(sql)
+ return { rows: [], rowCount: 0 }
+ }),
+ release: vi.fn(),
+ end: vi.fn(async () => undefined)
+}))
+
+vi.mock('pg', () => ({
+ default: {
+ Pool: class {
+ totalCount = 1
+ idleCount = 1
+ waitingCount = 0
+ end = fakes.end
+ on = vi.fn()
+ connect = vi.fn(async () => ({ query: fakes.query, release: fakes.release }))
+ }
+ }
+}))
+
+const { CELL_INVENTORY_LOCK_TIMEOUT_MS, RelayAssignmentStore } = await import(
+ './assignment-store.js'
+)
+const { consumeRelayCellInventoryHold, openInMemoryRelayDatabase, openRelayDatabase, POSTGRES_LOCK_TIMEOUT_MS } =
+ await import('./database.js')
+const RESTORE = `SET LOCAL lock_timeout = '${POSTGRES_LOCK_TIMEOUT_MS}ms'`
+type RelayDatabase = import('./database.js').RelayDatabase
+type RelayLockOptions = import('./database.js').RelayLockOptions
+type RelayTransactionOptions = import('./database.js').RelayTransactionOptions
+type SqlRow = import('./database.js').SqlRow
+
+const CELL_INVENTORY_SQL = 'SELECT * FROM relay_cells ORDER BY cell_id ASC'
+
+// The assignment path locks the general-admission subset; both forms are the
+// same ordered scan of the same 23-row table and share its lock queue.
+function locksCellInventory(sql: string): boolean {
+ return sql.trim().startsWith('SELECT * FROM relay_cells') && sql.includes('ORDER BY cell_id ASC')
+}
+const CELLS = [
+ { id: 'cell-a', url: 'https://relay-a.example.com', capacityRequests: 10 },
+ { id: 'cell-b', url: 'https://relay-b.example.com', capacityRequests: 10 }
+]
+const identity = { userId: 'user-a', relayHostId: 'host000000000001' }
+
+async function openFakePostgres(): Promise {
+ const database = await openRelayDatabase({
+ databaseUrl: 'postgresql://relay:secret@127.0.0.1:5432/relay',
+ dataDir: './unused'
+ })
+ fakes.statements.length = 0
+ return database
+}
+
+afterEach(() => {
+ fakes.statements.length = 0
+ fakes.query.mockReset()
+ fakes.query.mockImplementation(async (sql: string) => {
+ fakes.statements.push(sql)
+ return { rows: [], rowCount: 0 }
+ })
+})
+
+describe('bounded cell-inventory lock wait', () => {
+ // Why: a bound at or above the pool default would fence nothing, and one far
+ // below the hold time would convert ordinary contention into terminal failures.
+ it('keeps the request bound strictly inside the pool default', () => {
+ expect(CELL_INVENTORY_LOCK_TIMEOUT_MS).toBe(500)
+ expect(CELL_INVENTORY_LOCK_TIMEOUT_MS).toBeLessThan(POSTGRES_LOCK_TIMEOUT_MS)
+ })
+
+ // Why: SET LOCAL lasts to COMMIT. Left in place it would govern every later
+ // locked statement in the transaction and misattribute their 55P03s.
+ it('restores the pool default before the next statement in the transaction', async () => {
+ const database = await openFakePostgres()
+
+ await database.transaction(async (transaction) => {
+ await transaction.queryLocked(CELL_INVENTORY_SQL, [], { lockTimeoutMs: 150 })
+ await transaction.queryLocked('SELECT * FROM relay_assignments', [])
+ })
+
+ expect(fakes.statements).toEqual([
+ 'BEGIN',
+ "SET LOCAL lock_timeout = '150ms'",
+ `${CELL_INVENTORY_SQL} FOR UPDATE`,
+ RESTORE,
+ 'SELECT * FROM relay_assignments FOR UPDATE',
+ 'COMMIT'
+ ])
+ await database.close()
+ })
+
+ it('restores the pool default when the bounded lock itself times out', async () => {
+ const database = await openFakePostgres()
+ fakes.query.mockImplementation(async (sql: string) => {
+ fakes.statements.push(sql)
+ if (sql.includes('FOR UPDATE')) {
+ throw Object.assign(new Error('lock timeout'), { code: '55P03' })
+ }
+ return { rows: [], rowCount: 0 }
+ })
+
+ await expect(
+ database.transaction(async (transaction) => {
+ await transaction.queryLocked(CELL_INVENTORY_SQL, [], { lockTimeoutMs: 150 })
+ })
+ ).rejects.toMatchObject({ code: '55P03' })
+
+ // The retry wrapper makes three attempts; each one must leave the default back.
+ expect(fakes.statements.filter((sql) => sql.startsWith('SET LOCAL'))).toEqual(
+ Array.from({ length: 3 }, () => ["SET LOCAL lock_timeout = '150ms'", RESTORE]).flat()
+ )
+ await database.close()
+ })
+
+ it('rejects a lock bound that is not a positive whole number of milliseconds', async () => {
+ const database = await openFakePostgres()
+
+ for (const lockTimeoutMs of [0, -1, 1.5, Number.NaN]) {
+ await expect(
+ database.transaction(
+ async (transaction) =>
+ await transaction.queryLocked(CELL_INVENTORY_SQL, [], { lockTimeoutMs })
+ )
+ ).rejects.toThrow('invalid_lock_timeout')
+ }
+ await database.close()
+ })
+
+ it('skips the timeout for a NOWAIT lock, which never queues', async () => {
+ const database = await openFakePostgres()
+
+ await database.transaction(async (transaction) => {
+ await transaction.queryLocked(CELL_INVENTORY_SQL, [], {
+ failIfUnavailable: true,
+ lockTimeoutMs: 150
+ })
+ })
+
+ expect(fakes.statements.filter((sql) => sql.startsWith('SET LOCAL'))).toEqual([])
+ await database.close()
+ })
+
+ it('skips the timeout outside a transaction, where SET LOCAL cannot survive', async () => {
+ const database = await openFakePostgres()
+
+ await database.queryLocked(CELL_INVENTORY_SQL, [], { lockTimeoutMs: 150 })
+
+ expect(fakes.statements).toEqual([`${CELL_INVENTORY_SQL} FOR UPDATE`])
+ await database.close()
+ })
+
+ it('ignores the timeout on SQLite, which has no SET LOCAL', async () => {
+ const database = await openInMemoryRelayDatabase()
+
+ const rows = await database.transaction(
+ async (transaction) =>
+ await transaction.queryLocked(CELL_INVENTORY_SQL, [], { lockTimeoutMs: 150 })
+ )
+
+ expect(rows).toEqual([])
+ await database.close()
+ })
+
+ // Why: testing the helper alone would pass with the store still queueing for
+ // the pool's one-second default.
+ // Why: testing the helper alone would pass with the request path still queueing
+ // for the pool's full second.
+ it('never lets a request path take the unbounded wait', async () => {
+ const database = await openInMemoryRelayDatabase()
+ const probe = new InventoryLockProbe(database)
+ const store = new RelayAssignmentStore(probe, () => 1_000)
+ await store.reconcileCells(CELLS)
+ probe.inventoryLocks.length = 0
+
+ // Assignment takes the general-admission subset; evacuation takes them all.
+ await store.assign(identity)
+ const generalLocks = probe.inventoryLocks.length
+ await store.startEvacuation(identity, 'cell-b')
+
+ expect(generalLocks).toBeGreaterThan(0)
+ expect(probe.inventoryLocks.length).toBeGreaterThan(generalLocks)
+ for (const options of probe.inventoryLocks) {
+ const bounded = options?.lockTimeoutMs === CELL_INVENTORY_LOCK_TIMEOUT_MS
+ expect(bounded || options?.failIfUnavailable === true).toBe(true)
+ }
+ await database.close()
+ })
+
+ // Why: evacuateDeadCells re-enters placement from a sweep. A 55P03 there would
+ // be reported as a terminal sweep failure and freeze the incident gate.
+ it('keeps the pool default when a sweep re-enters placement', async () => {
+ const requestModes = await recordAssignInventoryModes(async (store) => {
+ await store.assign(identity)
+ })
+ const sweepModes = await recordAssignInventoryModes(async (store) => {
+ await store.assign(identity, undefined, undefined, 'pool-default')
+ })
+
+ // The inventory-first retry is the lane that carries the caller's mode.
+ expect(requestModes).toContain(CELL_INVENTORY_LOCK_TIMEOUT_MS)
+ expect(sweepModes).not.toContain(CELL_INVENTORY_LOCK_TIMEOUT_MS)
+ expect(sweepModes.filter((mode) => mode === 'nowait').length).toBe(
+ requestModes.filter((mode) => mode === 'nowait').length
+ )
+ })
+
+ it('sends the sweep that re-enters placement down the unbounded lane', async () => {
+ const database = await openInMemoryRelayDatabase()
+ const probe = new InventoryLockProbe(database)
+ let now = 1_000
+ const store = new RelayAssignmentStore(probe, () => now, {
+ requireLiveCells: true,
+ heartbeatTtlMs: 45_000
+ })
+ await store.reconcileCells(CELLS)
+ for (const cell of CELLS) {
+ await store.recordCellHeartbeat({
+ cellId: cell.id,
+ cellUrl: cell.url,
+ cellIncarnation: `1111111${cell.id.slice(-1)}-1111-4111-8111-111111111111`,
+ startedAt: 50,
+ ready: true,
+ observedRequests: 0
+ })
+ }
+ await store.assign(identity)
+ // Let every heartbeat lapse so the sweep sees the assigned cell as dead.
+ now += 45_001
+ probe.inventoryLocks.length = 0
+ probe.failActivityLockOnce = true
+
+ await store.evacuateDeadCells()
+
+ expect(probe.inventoryLocks).not.toEqual([])
+ for (const options of probe.inventoryLocks) {
+ expect(options?.lockTimeoutMs).toBeUndefined()
+ }
+ await database.close()
+ })
+
+ // Why: the SQLite hold test cannot reach PostgresDatabase.transaction, which is
+ // the only path production ever takes.
+ it('records the hold on the PostgreSQL transaction path', async () => {
+ const database = await openFakePostgres()
+
+ await database.transaction(async (transaction) => {
+ await transaction.queryLocked(CELL_INVENTORY_SQL, [], {
+ lockTimeoutMs: 150,
+ measureHoldMs: true
+ })
+ })
+
+ expect(consumeRelayCellInventoryHold(database).cellInventoryHolds).toBe(1)
+ await database.close()
+ })
+
+ it('records no hold for a PostgreSQL transaction that took no measured lock', async () => {
+ const database = await openFakePostgres()
+
+ await database.transaction(async (transaction) => {
+ await transaction.queryLocked(CELL_INVENTORY_SQL, [], { lockTimeoutMs: 150 })
+ })
+
+ expect(consumeRelayCellInventoryHold(database).cellInventoryHolds).toBe(0)
+ await database.close()
+ })
+
+ // Why: index.ts boots a server on import, so its wiring can only be read. An
+ // unspread hold metric is invisible: the flush simply omits the fields.
+ it('spreads the hold counts into the runtime metrics flush', () => {
+ const source = readFileSync(new URL('./index.ts', import.meta.url), 'utf8')
+ const flush = /observability\.start\(\(\) => \(\{([^}]*)\}\)\)/.exec(source)
+
+ expect(flush?.[1]).toContain('...consumeRelayCellInventoryHold(database)')
+ })
+
+ // Why: 500ms is a first value, not a measurement. Tuning it needs the hold
+ // distribution, which no runtime metric carried.
+ it('reports how long the inventory lock was held to COMMIT', async () => {
+ const database = await openInMemoryRelayDatabase()
+ const store = new RelayAssignmentStore(database, () => 1_000)
+ await store.reconcileCells(CELLS)
+ consumeRelayCellInventoryHold(database)
+
+ await store.assign(identity)
+
+ const counts = consumeRelayCellInventoryHold(database)
+ expect(counts.cellInventoryHolds).toBeGreaterThan(0)
+ expect(counts.cellInventoryHoldMsMax).toBeGreaterThanOrEqual(counts.cellInventoryHoldMsP95)
+ expect(counts.cellInventoryHoldMsMax).toBeGreaterThan(0)
+ // Consuming resets the window so the next flush reports its own holds.
+ expect(consumeRelayCellInventoryHold(database).cellInventoryHolds).toBe(0)
+ await database.close()
+ })
+})
+
+// Why: the incident monitor freezes at zero exhausted transactions. A sweep that
+// steps aside must not spend the retry budget or report a terminal failure.
+describe('sweep lock skips stay off the transaction retry counters', () => {
+ it('reports neither a retry nor an exhaustion when NOWAIT finds the lock held', async () => {
+ const database = await openFakePostgres()
+ fakes.query.mockImplementation(async (sql: string) => {
+ fakes.statements.push(sql)
+ if (sql.includes('FOR UPDATE NOWAIT')) {
+ throw Object.assign(new Error('could not obtain lock'), { code: '55P03' })
+ }
+ return { rows: [], rowCount: 0 }
+ })
+ const events: string[] = []
+ const warn = vi.spyOn(console, 'warn').mockImplementation((line: unknown) => {
+ try {
+ events.push(String((JSON.parse(line as string) as { event?: unknown }).event))
+ } catch {
+ // non-JSON lines are not transaction telemetry
+ }
+ })
+
+ try {
+ await expect(
+ database.transaction(async (transaction) => {
+ await transaction.queryLocked(CELL_INVENTORY_SQL, [], { failIfUnavailable: true })
+ })
+ ).rejects.toThrow('database_lock_unavailable')
+ } finally {
+ warn.mockRestore()
+ }
+
+ expect(events).not.toContain('orca_relay_postgres_transaction_retry')
+ expect(events).not.toContain('orca_relay_postgres_transaction_exhausted')
+ expect(fakes.statements.filter((sql) => sql === 'BEGIN')).toHaveLength(1)
+ await database.close()
+ })
+})
+
+describe('background sweeps skip a contended cell inventory', () => {
+ it('takes the inventory NOWAIT and skips the tick instead of queueing', async () => {
+ const database = await openInMemoryRelayDatabase()
+ const probe = new InventoryLockProbe(database)
+ let now = 1_000
+ const store = new RelayAssignmentStore(probe, () => now)
+ await store.reconcileCells(CELLS)
+ const assignment = await store.assign(identity)
+ await store.activateControl(identity, {
+ cellId: assignment.cellId,
+ assignmentEpoch: assignment.assignmentEpoch,
+ generation: 1
+ })
+ await store.startEvacuation(identity, 'cell-b')
+ now += 24 * 60 * 60_000
+ probe.inventoryLocks.length = 0
+ probe.failNoWait = true
+ const warnings = collectWarnings('orca_relay_sweep_cell_inventory_busy')
+
+ let aborted: number
+ try {
+ aborted = await store.abortExpiredEvacuations()
+ } finally {
+ warnings.restore()
+ }
+
+ expect(aborted).toBe(0)
+ expect(probe.inventoryLocks).not.toEqual([])
+ expect(probe.inventoryLocks.every((options) => options?.failIfUnavailable === true)).toBe(
+ true
+ )
+ expect(warnings.entries).toEqual([
+ { event: 'orca_relay_sweep_cell_inventory_busy', sweep: 'abort-expired-evacuations', skipped: 1 }
+ ])
+ await database.close()
+ })
+
+ // Why: a summary line on every quiet tick would bury the contended ones.
+ it('says nothing on a tick that skipped no candidate', async () => {
+ const database = await openInMemoryRelayDatabase()
+ let now = 1_000
+ const store = new RelayAssignmentStore(database, () => now)
+ await store.reconcileCells(CELLS)
+ const assignment = await store.assign(identity)
+ await store.activateControl(identity, {
+ cellId: assignment.cellId,
+ assignmentEpoch: assignment.assignmentEpoch,
+ generation: 1
+ })
+ await store.startEvacuation(identity, 'cell-b')
+ now += 24 * 60 * 60_000
+ const warnings = collectWarnings('orca_relay_sweep_cell_inventory_busy')
+
+ let aborted: number
+ try {
+ aborted = await store.abortExpiredEvacuations()
+ } finally {
+ warnings.restore()
+ }
+
+ expect(aborted).toBe(1)
+ expect(warnings.entries).toEqual([])
+ await database.close()
+ })
+
+ it('still aborts the expired evacuation once the inventory is free', async () => {
+ const database = await openInMemoryRelayDatabase()
+ const probe = new InventoryLockProbe(database)
+ let now = 1_000
+ const store = new RelayAssignmentStore(probe, () => now)
+ await store.reconcileCells(CELLS)
+ const assignment = await store.assign(identity)
+ await store.activateControl(identity, {
+ cellId: assignment.cellId,
+ assignmentEpoch: assignment.assignmentEpoch,
+ generation: 1
+ })
+ await store.startEvacuation(identity, 'cell-b')
+ now += 24 * 60 * 60_000
+
+ expect(await store.abortExpiredEvacuations()).toBe(1)
+ await database.close()
+ })
+})
+
+// Returns each inventory lock the run took, as its bound or 'nowait'.
+async function recordAssignInventoryModes(
+ drive: (store: InstanceType) => Promise
+): Promise<(number | 'nowait' | 'pool-default')[]> {
+ const database = await openInMemoryRelayDatabase()
+ const probe = new InventoryLockProbe(database)
+ const store = new RelayAssignmentStore(probe, () => 1_000)
+ await store.reconcileCells(CELLS)
+ probe.inventoryLocks.length = 0
+ probe.failActivityLockOnce = true
+ await drive(store)
+ await database.close()
+ return probe.inventoryLocks.map((options) =>
+ options?.failIfUnavailable ? 'nowait' : (options?.lockTimeoutMs ?? 'pool-default')
+ )
+}
+
+function collectWarnings(event: string) {
+ const entries: Record[] = []
+ const original = console.warn
+ console.warn = (line: unknown, ...rest: unknown[]) => {
+ try {
+ const parsed = JSON.parse(line as string) as Record
+ if (parsed.event === event) return void entries.push(parsed)
+ } catch {
+ // fall through to the real console for non-JSON lines
+ }
+ original(line, ...rest)
+ }
+ return { entries, restore: () => (console.warn = original) }
+}
+
+const ACTIVITY_LEASE_SQL = 'SELECT * FROM relay_assignment_activity_leases'
+
+class InventoryLockProbe implements RelayDatabase {
+ readonly inventoryLocks: (RelayLockOptions | undefined)[] = []
+ failNoWait = false
+ // Forces the next assign attempt down its inventory-first retry, the only lane
+ // that reaches the threaded lock mode.
+ failActivityLockOnce = false
+
+ constructor(private readonly delegate: RelayDatabase) {}
+
+ async query(sql: string, params?: unknown[]): Promise {
+ return await this.delegate.query(sql, params)
+ }
+
+ async queryLocked(
+ sql: string,
+ params?: unknown[],
+ options?: RelayLockOptions
+ ): Promise {
+ if (locksCellInventory(sql)) {
+ this.inventoryLocks.push(options)
+ if (this.failNoWait && options?.failIfUnavailable) {
+ throw new Error('database_lock_unavailable')
+ }
+ }
+ if (this.failActivityLockOnce && sql.trim().startsWith(ACTIVITY_LEASE_SQL) && options?.failIfUnavailable) {
+ this.failActivityLockOnce = false
+ throw new Error('database_lock_unavailable')
+ }
+ return await this.delegate.queryLocked(sql, params, options)
+ }
+
+ async transaction(
+ operation: (transaction: RelayDatabase) => Promise,
+ options?: RelayTransactionOptions
+ ): Promise {
+ return await this.delegate.transaction(
+ async (transaction) => await operation(new InventoryLockProbeTransaction(transaction, this)),
+ options
+ )
+ }
+
+ async close(): Promise {}
+}
+
+class InventoryLockProbeTransaction implements RelayDatabase {
+ constructor(
+ private readonly delegate: RelayDatabase,
+ private readonly probe: InventoryLockProbe
+ ) {}
+
+ async query(sql: string, params?: unknown[]): Promise {
+ return await this.delegate.query(sql, params)
+ }
+
+ async queryLocked(
+ sql: string,
+ params?: unknown[],
+ options?: RelayLockOptions
+ ): Promise {
+ if (locksCellInventory(sql)) {
+ this.probe.inventoryLocks.push(options)
+ if (this.probe.failNoWait && options?.failIfUnavailable) {
+ throw new Error('database_lock_unavailable')
+ }
+ }
+ if (
+ this.probe.failActivityLockOnce &&
+ sql.trim().startsWith(ACTIVITY_LEASE_SQL) &&
+ options?.failIfUnavailable
+ ) {
+ this.probe.failActivityLockOnce = false
+ throw new Error('database_lock_unavailable')
+ }
+ return await this.delegate.queryLocked(sql, params, options)
+ }
+
+ async transaction(operation: (transaction: RelayDatabase) => Promise): Promise {
+ return await operation(this)
+ }
+
+ async close(): Promise {}
+}
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/database.ts b/cloud/apps/relay/src/database.ts
index f7208863f42..326ab010ccb 100644
--- a/cloud/apps/relay/src/database.ts
+++ b/cloud/apps/relay/src/database.ts
@@ -1,4 +1,5 @@
import { mkdirSync } from 'node:fs'
+import { performance } from 'node:perf_hooks'
import { join } from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import pg from 'pg'
@@ -8,9 +9,37 @@ import {
type PostgresPoolPressureCounts
} from './postgres-pool-pressure.js'
import { applyPostgresSchema } from './postgres-schema-startup.js'
+import {
+ CellInventoryHoldSamples,
+ emptyCellInventoryHoldCounts,
+ type CellInventoryHoldCounts
+} from './cell-inventory-hold-samples.js'
+
+export const POSTGRES_LOCK_TIMEOUT_MS = 1_000
+
+function setLocalLockTimeout(milliseconds: number): string {
+ if (!Number.isInteger(milliseconds) || milliseconds < 1) {
+ throw new Error('invalid_lock_timeout')
+ }
+ return `SET LOCAL lock_timeout = '${milliseconds}ms'`
+}
export type SqlRow = Record
-export type RelayLockOptions = { failIfUnavailable?: boolean }
+export type RelayLockOptions = {
+ failIfUnavailable?: boolean
+ // Only honoured inside a transaction: SET LOCAL is a no-op in autocommit.
+ lockTimeoutMs?: number
+ // Report how long this lock is held to COMMIT. The hold, not the wait, is what
+ // forms the queue, and nothing measured it before.
+ measureHoldMs?: boolean
+}
+
+// A transaction that can report how long it held a measured lock before COMMIT.
+type HoldMeasuringTransaction = { consumeHoldMs(): number | undefined }
+
+function measuredHoldMs(transaction: unknown): number | undefined {
+ return (transaction as HoldMeasuringTransaction).consumeHoldMs?.()
+}
export type RelayTransactionOptions = { reportRetries?: boolean }
export interface RelayDatabase {
@@ -612,9 +641,23 @@ function postgresTransactionErrorPhase(error: unknown): string {
class SqliteTransaction implements RelayDatabase {
readonly dialect = 'sqlite' as const
+ private heldFromMs: number | undefined
constructor(protected readonly database: DatabaseSync) {}
+ consumeHoldMs(): number | undefined {
+ if (this.heldFromMs === undefined) return undefined
+ const holdMs = performance.now() - this.heldFromMs
+ this.heldFromMs = undefined
+ return holdMs
+ }
+
+ protected noteHeld(options: RelayLockOptions): void {
+ if (options.measureHoldMs && this.heldFromMs === undefined) {
+ this.heldFromMs = performance.now()
+ }
+ }
+
async query(sql: string, params: unknown[] = []): Promise {
const statement = this.database.prepare(sql)
const bound = params.map((value) => (value === undefined ? null : value)) as never[]
@@ -626,9 +669,11 @@ class SqliteTransaction implements RelayDatabase {
async queryLocked(
sql: string,
params: unknown[] = [],
- _options: RelayLockOptions = {}
+ options: RelayLockOptions = {}
): Promise {
- return await this.query(sql, params)
+ const rows = await this.query(sql, params)
+ this.noteHeld(options)
+ return rows
}
async transaction(
@@ -643,6 +688,11 @@ class SqliteTransaction implements RelayDatabase {
class SqliteDatabase extends SqliteTransaction {
private tail: Promise = Promise.resolve()
+ private readonly holds = new CellInventoryHoldSamples()
+
+ consumeHoldCounts(): CellInventoryHoldCounts {
+ return this.holds.consumeCounts()
+ }
override async query(sql: string, params: unknown[] = []): Promise {
await this.tail
@@ -655,9 +705,11 @@ class SqliteDatabase extends SqliteTransaction {
this.tail = new Promise((resolve) => (release = resolve))
await previous
this.database.exec('BEGIN IMMEDIATE')
+ const transaction = new SqliteTransaction(this.database)
try {
- const result = await operation(new SqliteTransaction(this.database))
+ const result = await operation(transaction)
this.database.exec('COMMIT')
+ this.holds.record(measuredHoldMs(transaction) ?? Number.NaN)
return result
} catch (error) {
this.database.exec('ROLLBACK')
@@ -675,9 +727,17 @@ class SqliteDatabase extends SqliteTransaction {
class PostgresTransaction implements RelayDatabase {
readonly dialect = 'postgres' as const
+ private heldFromMs: number | undefined
constructor(protected readonly client: pg.PoolClient) {}
+ consumeHoldMs(): number | undefined {
+ if (this.heldFromMs === undefined) return undefined
+ const holdMs = performance.now() - this.heldFromMs
+ this.heldFromMs = undefined
+ return holdMs
+ }
+
async query(sql: string, params: unknown[] = []): Promise {
try {
const result = await this.client.query(postgresSql(sql), params)
@@ -693,11 +753,21 @@ class PostgresTransaction implements RelayDatabase {
params: unknown[] = [],
options: RelayLockOptions = {}
): Promise {
+ // SET LOCAL lasts to COMMIT, so a bound left in place would silently govern
+ // every later locked statement in the transaction and misattribute its 55P03s.
+ const bounded = options.lockTimeoutMs !== undefined && !options.failIfUnavailable
try {
- return await this.query(
+ // A blocked waiter holds its pooled client for the whole lock_timeout, so
+ // hot tiny-table locks bound their own wait well under the pool default.
+ if (bounded) await this.query(setLocalLockTimeout(options.lockTimeoutMs!))
+ const rows = await this.query(
`${sql} FOR UPDATE${options.failIfUnavailable ? ' NOWAIT' : ''}`,
params
)
+ if (options.measureHoldMs && this.heldFromMs === undefined) {
+ this.heldFromMs = performance.now()
+ }
+ return rows
} catch (error) {
if (
options.failIfUnavailable &&
@@ -706,6 +776,10 @@ class PostgresTransaction implements RelayDatabase {
throw new Error('database_lock_unavailable')
}
throw error
+ } finally {
+ // Restore on the error path too: the transaction may still be retried or
+ // continue with unrelated locks after a caught lock failure.
+ if (bounded) await this.query(setLocalLockTimeout(POSTGRES_LOCK_TIMEOUT_MS)).catch(() => undefined)
}
}
@@ -723,7 +797,6 @@ const POSTGRES_TRANSACTION_ATTEMPTS = 3
const POSTGRES_RETRY_MAX_DELAY_MS = 25
const POSTGRES_CONNECTION_TIMEOUT_MS = 2_000
const POSTGRES_STATEMENT_TIMEOUT_MS = 5_000
-const POSTGRES_LOCK_TIMEOUT_MS = 1_000
const POSTGRES_IDLE_TRANSACTION_TIMEOUT_MS = 5_000
function retryablePostgresTransactionError(error: unknown): boolean {
@@ -749,6 +822,11 @@ async function waitForPostgresRetry(random: () => number = Math.random): Promise
class PostgresDatabase implements RelayDatabase {
readonly dialect = 'postgres' as const
private readonly pressure: PostgresPoolPressure
+ private readonly holds = new CellInventoryHoldSamples()
+
+ consumeHoldCounts(): CellInventoryHoldCounts {
+ return this.holds.consumeCounts()
+ }
constructor(private readonly pool: pg.Pool) {
this.pressure = new PostgresPoolPressure(pool)
@@ -770,6 +848,8 @@ class PostgresDatabase implements RelayDatabase {
options: RelayLockOptions = {}
): Promise {
try {
+ // No transaction here, so options.lockTimeoutMs cannot apply: SET LOCAL
+ // would be discarded at the autocommit boundary before the lock is taken.
return await this.query(
`${sql} FOR UPDATE${options.failIfUnavailable ? ' NOWAIT' : ''}`,
params
@@ -791,10 +871,12 @@ class PostgresDatabase implements RelayDatabase {
): Promise {
for (let attempt = 1; attempt <= POSTGRES_TRANSACTION_ATTEMPTS; attempt++) {
const client = await this.pressure.connect()
+ const transaction = new PostgresTransaction(client)
try {
await client.query('BEGIN')
- const result = await operation(new PostgresTransaction(client))
+ const result = await operation(transaction)
await client.query('COMMIT')
+ this.holds.record(measuredHoldMs(transaction) ?? Number.NaN)
return result
} catch (error) {
await client.query('ROLLBACK').catch(() => undefined)
@@ -852,6 +934,13 @@ export function consumeRelayDatabasePoolPressure(
: emptyPostgresPoolPressureCounts()
}
+export function consumeRelayCellInventoryHold(
+ database: RelayDatabase
+): CellInventoryHoldCounts {
+ const holder = database as { consumeHoldCounts?: () => CellInventoryHoldCounts }
+ return holder.consumeHoldCounts?.() ?? emptyCellInventoryHoldCounts()
+}
+
export function readRelayDatabasePoolPressure(
database: RelayDatabase
): PostgresPoolPressureCounts {
diff --git a/cloud/apps/relay/src/index.ts b/cloud/apps/relay/src/index.ts
index 635f3ae9b38..541884362c2 100644
--- a/cloud/apps/relay/src/index.ts
+++ b/cloud/apps/relay/src/index.ts
@@ -10,12 +10,14 @@ import {
roleOwnsAssignmentMaintenance
} from './cell-admission-startup.js'
import {
+ consumeRelayCellInventoryHold,
consumeRelayDatabasePoolPressure,
openRelayDatabase,
readRelayDatabasePoolPressure
} from './database.js'
import { runAssignmentCleanup } from './assignment-cleanup-steps.js'
import { runRelayBackgroundOperation } from './relay-background-operation.js'
+import { jitteredSweepIntervalMs } from './relay-sweep-schedule.js'
import { observedRelayRequests } from './relay-observability.js'
import { startRegionalRehomeWorker } from './regional-rehome-worker.js'
import { createRelayServer } from './relay-server.js'
@@ -54,7 +56,7 @@ const cleanupTimer = setInterval(
const assignmentCleanupTimer = roleOwnsAssignmentMaintenance(config.role)
? setInterval(() => {
void runAssignmentCleanup(assignments)
- }, 30_000)
+ }, jitteredSweepIntervalMs(30_000))
: null
const inventorySnapshotTimer = roleOwnsAssignmentMaintenance(config.role)
? setInterval(() => {
@@ -78,7 +80,8 @@ inventorySnapshotTimer?.unref()
migrationInventoryTimer?.unref()
observability.start(() => ({
...runtimeCounts(),
- ...consumeRelayDatabasePoolPressure(database)
+ ...consumeRelayDatabasePoolPressure(database),
+ ...consumeRelayCellInventoryHold(database)
}))
const regionalRehomeWorker = startRegionalRehomeWorker(config, assignments, {
safetySnapshot: () => ({
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)
)
}
diff --git a/cloud/apps/relay/src/regional-rehome-store.test.ts b/cloud/apps/relay/src/regional-rehome-store.test.ts
index 43f293b1131..26f711189ee 100644
--- a/cloud/apps/relay/src/regional-rehome-store.test.ts
+++ b/cloud/apps/relay/src/regional-rehome-store.test.ts
@@ -5,7 +5,12 @@ import {
REGIONAL_REHOME_QUARANTINE_MS,
REGIONAL_REHOME_REDRAIN_SEND_LIMIT
} from './assignment-store.js'
-import { openInMemoryRelayDatabase, type RelayDatabase, type SqlRow } from './database.js'
+import {
+ openInMemoryRelayDatabase,
+ type RelayDatabase,
+ type RelayLockOptions,
+ type SqlRow
+} from './database.js'
import {
REGIONAL_REHOME_SQL_FAILURES_LIMIT,
REGIONAL_REHOME_SQL_FAILURES_PER_CELL_LIMIT
@@ -556,6 +561,341 @@ describe('regional rehome assignment state', () => {
await context.database.close()
})
+ it('skips a rehome dispatch tick on a contended cell inventory', async () => {
+ const probe = new CellInventoryLockProbe()
+ const context = await setup({ wrap: (database) => probe.wrap(database) })
+ const identity = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' }
+ await activatePreferredSource(context, identity)
+ probe.reset()
+ probe.failNoWait = true
+ const busy = collectEventWarnings('orca_relay_sweep_cell_inventory_busy')
+
+ let attempt: unknown
+ try {
+ attempt = await context.store.claimRegionalRehome()
+ } finally {
+ busy.restore()
+ }
+
+ expect(attempt).toBeNull()
+ expect(probe.locks).not.toEqual([])
+ expect(probe.locks.every((options) => options?.failIfUnavailable === true)).toBe(true)
+ expect(busy.entries).toEqual([
+ {
+ event: 'orca_relay_sweep_cell_inventory_busy',
+ sweep: 'claim-regional-rehome',
+ skipped: 1
+ }
+ ])
+
+ probe.failNoWait = false
+ expect(await context.store.claimRegionalRehome()).toMatchObject({
+ sourceCellId: source.id,
+ targetCellId: target.id
+ })
+ await context.database.close()
+ })
+
+ // Why: the redrain lane reaches the inventory through the fleet-safety read
+ // rather than through candidate selection, so it needs its own coverage.
+ // Why: one contended candidate must cost its own tick, not the whole page. The
+ // sweeps are explicitly per-candidate isolated for exactly this reason.
+ it('completes the candidates behind a contended one', async () => {
+ const probe = new CellInventoryLockProbe()
+ const context = await setup({ wrap: (database) => probe.wrap(database) })
+ const identities = [
+ { userId: 'user-1', relayHostId: 'abcdefghijklmnop' },
+ { userId: 'user-2', relayHostId: 'ponmlkjihgfedcba' }
+ ]
+ for (const identity of identities) {
+ // Dispatch is rate limited, so each claim needs its own interval.
+ context.advance(60_000)
+ await freshHeartbeats(context)
+ const sourceControl = await activatePreferredSource(context, identity)
+ const attempt = await context.store.claimRegionalRehome()
+ await context.store.recordRegionalRehomeDrainReceipt(attempt!.attemptId, 'accepted')
+ await context.store.activateControl(identity, {
+ cellId: target.id,
+ assignmentEpoch: 2,
+ generation: 1
+ })
+ await context.store.markMigrationTargetRegistered(identity, {
+ cellId: target.id,
+ assignmentEpoch: 2
+ })
+ await context.store.releaseActivity(identity, sourceControl)
+ }
+ probe.reset()
+ probe.failNoWaitTimes = 1
+ const busy = collectEventWarnings('orca_relay_sweep_cell_inventory_busy')
+
+ let completed: number
+ try {
+ completed = await context.store.completeReadyRegionalRehomes()
+ } finally {
+ busy.restore()
+ }
+
+ expect(completed).toBe(1)
+ expect(busy.entries).toEqual([
+ {
+ event: 'orca_relay_sweep_cell_inventory_busy',
+ sweep: 'complete-ready-regional-rehomes',
+ skipped: 1
+ }
+ ])
+ await context.database.close()
+ })
+
+ // Why: with `continue` replaced by `break` a single contended candidate drops
+ // the rest of the page. Two in a row prove the sweep resumes, not just that it
+ // survived one, and that the summary counts both.
+ it('completes a candidate behind two contended ones', async () => {
+ const probe = new CellInventoryLockProbe()
+ const context = await setup({ wrap: (database) => probe.wrap(database) })
+ const identities = [
+ { userId: 'user-1', relayHostId: 'abcdefghijklmnop' },
+ { userId: 'user-2', relayHostId: 'ponmlkjihgfedcba' },
+ { userId: 'user-3', relayHostId: 'aaaabbbbccccdddd' }
+ ]
+ for (const identity of identities) {
+ // Dispatch is rate limited, so each claim needs its own interval.
+ context.advance(60_000)
+ await freshHeartbeats(context)
+ const sourceControl = await activatePreferredSource(context, identity)
+ const attempt = await context.store.claimRegionalRehome()
+ await context.store.recordRegionalRehomeDrainReceipt(attempt!.attemptId, 'accepted')
+ await context.store.activateControl(identity, {
+ cellId: target.id,
+ assignmentEpoch: 2,
+ generation: 1
+ })
+ await context.store.markMigrationTargetRegistered(identity, {
+ cellId: target.id,
+ assignmentEpoch: 2
+ })
+ await context.store.releaseActivity(identity, sourceControl)
+ }
+ probe.reset()
+ probe.failNoWaitTimes = 2
+ const busy = collectEventWarnings('orca_relay_sweep_cell_inventory_busy')
+
+ let completed: number
+ try {
+ completed = await context.store.completeReadyRegionalRehomes()
+ } finally {
+ busy.restore()
+ }
+
+ expect(completed).toBe(1)
+ expect(busy.entries).toEqual([
+ {
+ event: 'orca_relay_sweep_cell_inventory_busy',
+ sweep: 'complete-ready-regional-rehomes',
+ skipped: 2
+ }
+ ])
+ await context.database.close()
+ })
+
+ // Why: only inventory contention is ordinary. Every other failure must keep its
+ // existing propagation and its dispatch-failure accounting.
+ it('propagates a claim failure that is not inventory contention', async () => {
+ const probe = new CellInventoryLockProbe()
+ const context = await setup({ wrap: (database) => probe.wrap(database) })
+ await activatePreferredSource(context, { userId: 'user-1', relayHostId: 'abcdefghijklmnop' })
+ probe.reset()
+ probe.failWith = new Error('relay_capacity_exhausted')
+ const busy = collectEventWarnings('orca_relay_sweep_cell_inventory_busy')
+
+ try {
+ await expect(context.store.claimRegionalRehome()).rejects.toThrow(
+ 'relay_capacity_exhausted'
+ )
+ } finally {
+ busy.restore()
+ }
+
+ expect(busy.entries).toEqual([])
+ await context.database.close()
+ })
+
+ // Why: the transaction dies at the first contended candidate, so every
+ // candidate behind it is abandoned too. Reporting one would understate the tick.
+ it('reports every candidate the contended tick abandoned', async () => {
+ const probe = new CellInventoryLockProbe()
+ const context = await setup({ wrap: (database) => probe.wrap(database) })
+ await activatePreferredSource(context, { userId: 'user-1', relayHostId: 'abcdefghijklmnop' })
+ await activatePreferredSource(context, { userId: 'user-2', relayHostId: 'ponmlkjihgfedcba' })
+ await activatePreferredSource(context, { userId: 'user-3', relayHostId: 'aaaabbbbccccdddd' })
+ probe.reset()
+ probe.failNoWait = true
+ const busy = collectEventWarnings('orca_relay_sweep_cell_inventory_busy')
+
+ try {
+ expect(await context.store.claimRegionalRehome()).toBeNull()
+ } finally {
+ busy.restore()
+ }
+
+ expect(busy.entries).toEqual([
+ {
+ event: 'orca_relay_sweep_cell_inventory_busy',
+ sweep: 'claim-regional-rehome',
+ skipped: 3
+ }
+ ])
+ await context.database.close()
+ })
+
+ it('skips a redrain tick on a contended cell inventory', async () => {
+ const probe = new CellInventoryLockProbe()
+ const context = await setup({ wrap: (database) => probe.wrap(database) })
+ const identity = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' }
+ await activatePreferredSource(context, identity)
+ const attempt = await context.store.claimRegionalRehome()
+ await context.store.recordRegionalRehomeDrainReceipt(attempt!.attemptId, 'accepted')
+ await context.store.activateControl(identity, {
+ cellId: target.id,
+ assignmentEpoch: 2,
+ generation: 1
+ })
+ await context.store.markMigrationTargetRegistered(identity, {
+ cellId: target.id,
+ assignmentEpoch: 2
+ })
+ context.advance(60 * 60_000 + 1)
+ await freshHeartbeats(context)
+ probe.reset()
+ probe.failNoWait = true
+ const busy = collectEventWarnings('orca_relay_sweep_cell_inventory_busy')
+
+ let redrain: unknown
+ try {
+ redrain = await context.store.claimRegionalRehome()
+ } finally {
+ busy.restore()
+ }
+
+ expect(redrain).toBeNull()
+ expect(probe.locks).not.toEqual([])
+ expect(probe.locks.every((options) => options?.failIfUnavailable === true)).toBe(true)
+ expect(busy.entries).toEqual([
+ {
+ event: 'orca_relay_sweep_cell_inventory_busy',
+ sweep: 'claim-regional-rehome',
+ skipped: 1
+ }
+ ])
+
+ probe.failNoWait = false
+ expect(await context.store.claimRegionalRehome()).toMatchObject({
+ attemptId: attempt!.attemptId,
+ sendAttempts: 2
+ })
+ await context.database.close()
+ })
+
+ it('skips a completion tick on a contended cell inventory without quarantining it', async () => {
+ const probe = new CellInventoryLockProbe()
+ const context = await setup({ wrap: (database) => probe.wrap(database) })
+ const identity = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' }
+ const sourceControl = await activatePreferredSource(context, identity)
+ const attempt = await context.store.claimRegionalRehome()
+ await context.store.recordRegionalRehomeDrainReceipt(attempt!.attemptId, 'accepted')
+ await context.store.activateControl(identity, {
+ cellId: target.id,
+ assignmentEpoch: 2,
+ generation: 1
+ })
+ await context.store.markMigrationTargetRegistered(identity, {
+ cellId: target.id,
+ assignmentEpoch: 2
+ })
+ await context.store.releaseActivity(identity, sourceControl)
+ probe.reset()
+ probe.failNoWait = true
+ const busy = collectEventWarnings('orca_relay_sweep_cell_inventory_busy')
+ const failures = collectCandidateFailureWarnings()
+
+ let completed: number
+ try {
+ completed = await context.store.completeReadyRegionalRehomes()
+ } finally {
+ failures.restore()
+ busy.restore()
+ }
+
+ expect(completed).toBe(0)
+ expect(probe.locks).not.toEqual([])
+ expect(probe.locks.every((options) => options?.failIfUnavailable === true)).toBe(true)
+ expect(failures.entries).toEqual([])
+ expect(busy.entries).toEqual([
+ {
+ event: 'orca_relay_sweep_cell_inventory_busy',
+ sweep: 'complete-ready-regional-rehomes',
+ skipped: 1
+ }
+ ])
+
+ probe.failNoWait = false
+ expect(await context.store.completeReadyRegionalRehomes()).toBe(1)
+ await context.database.close()
+ })
+
+ // Why: a contended inventory is another director settling the same row, not a
+ // poisoned candidate. Quarantining on it would exclude a healthy attempt from
+ // the sweep's LIMIT pages for 15 minutes.
+ it('skips an abort tick on a contended cell inventory without quarantining it', async () => {
+ const probe = new CellInventoryLockProbe()
+ const context = await setup({ wrap: (database) => probe.wrap(database) })
+ const identity = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' }
+ const sourceControl = await activatePreferredSource(context, identity)
+ const attempt = await context.store.claimRegionalRehome()
+ await context.store.recordRegionalRehomeDrainReceipt(attempt!.attemptId, 'accepted')
+ const targetControl = await context.store.activateControl(identity, {
+ cellId: target.id,
+ assignmentEpoch: 2,
+ generation: 1
+ })
+ await context.store.markMigrationTargetRegistered(identity, {
+ cellId: target.id,
+ assignmentEpoch: 2
+ })
+ await context.store.releaseActivity(identity, sourceControl)
+ await context.store.releaseActivity(identity, targetControl)
+ context.advance(24 * 60 * 60_000)
+ await heartbeat(context.store, source, sourceIncarnation, 1, 2)
+ probe.reset()
+ probe.failNoWait = true
+ const busy = collectEventWarnings('orca_relay_sweep_cell_inventory_busy')
+ const failures = collectCandidateFailureWarnings()
+
+ let aborted: number
+ try {
+ aborted = await context.store.abortExpiredRegionalRehomes()
+ } finally {
+ failures.restore()
+ busy.restore()
+ }
+
+ expect(aborted).toBe(0)
+ expect(probe.locks).not.toEqual([])
+ expect(probe.locks.every((options) => options?.failIfUnavailable === true)).toBe(true)
+ expect(failures.entries).toEqual([])
+ expect(busy.entries).toEqual([
+ {
+ event: 'orca_relay_sweep_cell_inventory_busy',
+ sweep: 'abort-expired-regional-rehomes',
+ skipped: 1
+ }
+ ])
+
+ probe.failNoWait = false
+ expect(await context.store.abortExpiredRegionalRehomes()).toBe(1)
+ await context.database.close()
+ })
+
it('rolls back an inactive registered target only after the 24-hour bound', async () => {
const context = await setup()
const identity = { userId: 'user-1', relayHostId: 'abcdefghijklmnop' }
@@ -1208,10 +1548,12 @@ function collectDisableWarnings() {
}
}
-async function setup(options: { sourceProtocol?: number } = {}) {
+async function setup(
+ options: { sourceProtocol?: number; wrap?: (database: RelayDatabase) => RelayDatabase } = {}
+) {
let clock = 1_000_000
const database = await openInMemoryRelayDatabase()
- const store = new RelayAssignmentStore(database, () => clock, {
+ const store = new RelayAssignmentStore(options.wrap?.(database) ?? database, () => clock, {
requireLiveCells: true,
heartbeatTtlMs: 45_000
})
@@ -1397,3 +1739,43 @@ async function heartbeat(
}
})
}
+
+class CellInventoryLockProbe {
+ readonly locks: (RelayLockOptions | undefined)[] = []
+ failNoWait = false
+ // Contends the first N candidates only, so the sweep must carry on past them.
+ failNoWaitTimes = 0
+ failWith: Error | null = null
+
+ reset(): void {
+ this.locks.length = 0
+ }
+
+ wrap(database: RelayDatabase): RelayDatabase {
+ const probe = this
+ const decorate = (delegate: RelayDatabase): RelayDatabase => ({
+ query: async (sql, params) => await delegate.query(sql, params),
+ queryLocked: async (sql, params, options) => {
+ if (sql.trim() === 'SELECT * FROM relay_cells ORDER BY cell_id ASC') {
+ probe.locks.push(options)
+ if (probe.failWith) throw probe.failWith
+ if (options?.failIfUnavailable && probe.failNoWaitTimes > 0) {
+ probe.failNoWaitTimes--
+ throw new Error('database_lock_unavailable')
+ }
+ if (probe.failNoWait && options?.failIfUnavailable) {
+ throw new Error('database_lock_unavailable')
+ }
+ }
+ return await delegate.queryLocked(sql, params, options)
+ },
+ transaction: async (operation, options) =>
+ await delegate.transaction(
+ async (transaction) => await operation(decorate(transaction)),
+ options
+ ),
+ close: async () => undefined
+ })
+ return decorate(database)
+ }
+}
diff --git a/cloud/apps/relay/src/regional-rehome-worker.ts b/cloud/apps/relay/src/regional-rehome-worker.ts
index 97c63a61025..47a2748cff4 100644
--- a/cloud/apps/relay/src/regional-rehome-worker.ts
+++ b/cloud/apps/relay/src/regional-rehome-worker.ts
@@ -3,6 +3,7 @@ import type { RelayAssignmentStore } from './assignment-store.js'
import type { RelayConfig } from './config.js'
import { googleMetadataIdentityToken } from './google-metadata-identity-token.js'
import type { RegionalRehomeSafetySnapshot } from './relay-observability.js'
+import { jitteredSweepIntervalMs } from './relay-sweep-schedule.js'
type RegionalRehomeWorkerOptions = {
fetch?: typeof fetch
@@ -10,6 +11,7 @@ type RegionalRehomeWorkerOptions = {
now?: () => number
intervalMs?: number
requestTimeoutMs?: number
+ random?: () => number
safetySnapshot?: () => RegionalRehomeSafetySnapshot
}
@@ -109,7 +111,10 @@ export function startRegionalRehomeWorker(
inFlight = false
}
}
- const timer = setInterval(() => void run(), options.intervalMs ?? 1_000)
+ const timer = setInterval(
+ () => void run(),
+ options.intervalMs ?? jitteredSweepIntervalMs(1_000, options.random)
+ )
timer.unref()
void run()
return {
diff --git a/cloud/apps/relay/src/relay-observability.ts b/cloud/apps/relay/src/relay-observability.ts
index 6125ede8d1a..2266217d607 100644
--- a/cloud/apps/relay/src/relay-observability.ts
+++ b/cloud/apps/relay/src/relay-observability.ts
@@ -1,6 +1,7 @@
import { monitorEventLoopDelay, performance } from 'node:perf_hooks'
import type { RelayRegion } from '@orca-cloud/relay-contract'
import type { ControlRenewalOutcome } from './assignment-store.js'
+import type { CellInventoryHoldCounts } from './cell-inventory-hold-samples.js'
import type { PostgresPoolPressureCounts } from './postgres-pool-pressure.js'
import type { RelayReadinessObservation } from './relay-readiness.js'
@@ -20,7 +21,9 @@ export function observedRelayRequests(counts: RelayRuntimeCounts): number {
return counts.preAuthConnections + counts.controls + counts.splices + counts.pendingSplices
}
-export type RelayProcessCounts = RelayRuntimeCounts & PostgresPoolPressureCounts
+export type RelayProcessCounts = RelayRuntimeCounts &
+ PostgresPoolPressureCounts &
+ Partial
export type RegionalRehomeRuntimeSafety = {
observedAt: number
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-sweep-schedule.test.ts b/cloud/apps/relay/src/relay-sweep-schedule.test.ts
new file mode 100644
index 00000000000..d5ef450cc43
--- /dev/null
+++ b/cloud/apps/relay/src/relay-sweep-schedule.test.ts
@@ -0,0 +1,55 @@
+import { readFileSync } from 'node:fs'
+import { describe, expect, it, vi } from 'vitest'
+import { startRegionalRehomeWorker } from './regional-rehome-worker.js'
+import { jitteredSweepIntervalMs, SWEEP_JITTER_FRACTION } from './relay-sweep-schedule.js'
+
+describe('sweep schedule jitter', () => {
+ it('spreads instances across a bounded window above the base period', () => {
+ expect(jitteredSweepIntervalMs(30_000, () => 0)).toBe(30_000)
+ expect(jitteredSweepIntervalMs(30_000, () => 0.5)).toBe(33_000)
+ // Math.random() never returns 1, so the open bound is the real ceiling.
+ expect(jitteredSweepIntervalMs(30_000, () => 0.999)).toBeLessThan(36_000)
+ })
+
+ // Why: a shorter period would raise the very lock traffic the offset spreads.
+ it('never schedules a sweep sooner than its base period', () => {
+ for (const random of [0, 0.25, 0.5, 0.75, 0.999]) {
+ expect(jitteredSweepIntervalMs(1_000, () => random)).toBeGreaterThanOrEqual(1_000)
+ }
+ expect(SWEEP_JITTER_FRACTION).toBeGreaterThan(0)
+ })
+
+ it('jitters the regional rehome dispatch tick, which every director runs each second', () => {
+ const timers: number[] = []
+ const setIntervalSpy = vi
+ .spyOn(globalThis, 'setInterval')
+ .mockImplementation(((_handler: unknown, delayMs?: number) => {
+ timers.push(delayMs ?? 0)
+ return { unref: () => undefined, [Symbol.dispose]: () => undefined } as never
+ }) as never)
+
+ try {
+ startRegionalRehomeWorker(
+ {
+ role: 'director',
+ rehomeAudience: 'https://rehome.example.test',
+ rehomeDirectorServiceAccount: 'rehome@example.test'
+ } as never,
+ { claimRegionalRehome: async () => null } as never,
+ { random: () => 0.5, safetySnapshot: () => ({}) as never }
+ )
+ } finally {
+ setIntervalSpy.mockRestore()
+ }
+
+ expect(timers).toEqual([1_100])
+ })
+
+ // Why: index.ts boots a server on import, so its wiring can only be read.
+ it('jitters the director assignment cleanup tick', () => {
+ const source = readFileSync(new URL('./index.ts', import.meta.url), 'utf8')
+ const cleanup = /runAssignmentCleanup\(assignments\)\s*\},\s*([^\n]*?)\)\n/.exec(source)
+
+ expect(cleanup?.[1]).toBe('jitteredSweepIntervalMs(30_000)')
+ })
+})
diff --git a/cloud/apps/relay/src/relay-sweep-schedule.ts b/cloud/apps/relay/src/relay-sweep-schedule.ts
new file mode 100644
index 00000000000..f73e6b69ead
--- /dev/null
+++ b/cloud/apps/relay/src/relay-sweep-schedule.ts
@@ -0,0 +1,13 @@
+// Why: every director instance boots from the same rollout, so its periodic
+// sweeps land on the same wall-clock second across instances and pile onto the
+// one global cell-inventory lock together. A per-process offset spreads the
+// arrivals; the sweeps are idempotent, so a slightly longer period is free.
+export const SWEEP_JITTER_FRACTION = 0.2
+
+export function jitteredSweepIntervalMs(
+ baseMs: number,
+ random: () => number = Math.random
+): number {
+ // Only ever longer: a shorter period would raise the very load being spread.
+ return baseMs + Math.floor(random() * baseMs * SWEEP_JITTER_FRACTION)
+}
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/)
+ })
+})
diff --git a/cloud/dev/scripts/relay-regional-rehome-workflow.test.mjs b/cloud/dev/scripts/relay-regional-rehome-workflow.test.mjs
index 5eab757255a..e31403f6dd6 100644
--- a/cloud/dev/scripts/relay-regional-rehome-workflow.test.mjs
+++ b/cloud/dev/scripts/relay-regional-rehome-workflow.test.mjs
@@ -191,3 +191,10 @@ test('director rollout has a strict one-time identity bootstrap', () => {
assert.ok(candidateProof > 0 && candidateProof < trafficMove)
assert.equal(script.indexOf('verifyRehomeDisabled', trafficMove), -1)
})
+
+test('rehome job pipes every control result through tee under pipefail', () => {
+ const job = workflow('operate-relay-production-rehome-job.yml')
+ // Without `shell: bash` the step exit code is tee's, so a thrown inspect/apply passes green.
+ assert.match(job, /defaults:\n run:\n(?: #.*\n)* shell: bash\n/)
+ assert.ok((job.match(/\| tee "\$\{RUNNER_TEMP\}/g) ?? []).length >= 5)
+})
diff --git a/cloud/dev/scripts/relay-repository.mjs b/cloud/dev/scripts/relay-repository.mjs
index bf41bed8012..7e8b01e4799 100644
--- a/cloud/dev/scripts/relay-repository.mjs
+++ b/cloud/dev/scripts/relay-repository.mjs
@@ -15,9 +15,16 @@ export function relayWorkflowFile(name) {
return `${RELAY_WORKFLOW_FILE_PREFIX}${name}`
}
+// Repository-relative path for a repository that renames its copies with `prefix`. Terraform's
+// trusted prefix is a variable and need not be this checkout's, so callers rendering a
+// workflow_ref from Terraform pass it in rather than assuming the local one.
+export function prefixedRelayWorkflowPath(prefix, name) {
+ return `.github/workflows/${prefix}${name}`
+}
+
// Repository-relative path, the shape GitHub reports in workflow_ref and evidence payloads.
export function relayWorkflowPath(name) {
- return `.github/workflows/${relayWorkflowFile(name)}`
+ return prefixedRelayWorkflowPath(RELAY_WORKFLOW_FILE_PREFIX, name)
}
export function relayWorkflowUrl(name) {
diff --git a/cloud/dev/scripts/relay-repository.test.mjs b/cloud/dev/scripts/relay-repository.test.mjs
index 56383db4a1e..cf33f869773 100644
--- a/cloud/dev/scripts/relay-repository.test.mjs
+++ b/cloud/dev/scripts/relay-repository.test.mjs
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'
import {
RELAY_GITHUB_REPOSITORY,
RELAY_WORKFLOW_FILE_PREFIX,
+ prefixedRelayWorkflowPath,
readRelayWorkflow,
relayWorkflowFile,
relayWorkflowPath,
@@ -21,6 +22,8 @@ test('workflow identity is derived, never restated', () => {
assert.equal(relayWorkflowFile('deploy-relay-staging.yml'), `${RELAY_WORKFLOW_FILE_PREFIX}deploy-relay-staging.yml`)
assert.equal(relayWorkflowPath('deploy-relay-staging.yml'), `.github/workflows/${relayWorkflowFile('deploy-relay-staging.yml')}`)
assert.ok(relayWorkflowUrl('deploy-relay-staging.yml').pathname.endsWith(relayWorkflowPath('deploy-relay-staging.yml')))
+ // A caller rendering Terraform's trusted ref supplies that prefix instead of this checkout's.
+ assert.equal(prefixedRelayWorkflowPath('cloud-', 'deploy-relay-staging.yml'), '.github/workflows/cloud-deploy-relay-staging.yml')
assert.match(readRelayWorkflow('deploy-relay-staging.yml'), /^name:/m)
assert.match(RELAY_GITHUB_REPOSITORY, /^[\w.-]+\/[\w.-]+$/)
})
diff --git a/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs b/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs
index 75bbb9ac8d5..0e777b06c38 100644
--- a/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs
+++ b/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs
@@ -39,14 +39,17 @@ const launchDigest = '5aedbca5c86de24c8b4d4bf7e3b444b76c712f281ede916cb9d90f70ca
// so a file-wide count no longer isolates Asia.
const asiaCells = ['production-gce-c27', 'production-gce-c28', 'production-gce-c29']
-function productionCell(cellId) {
- const start = productionTfvars.indexOf(`"${cellId}"`)
+function cellBlock(tfvars, cellId) {
+ const start = tfvars.indexOf(`"${cellId}"`)
assert.notEqual(start, -1, `${cellId} is missing`)
- return productionTfvars.slice(start, productionTfvars.indexOf('\n }', start))
+ return tfvars.slice(start, tfvars.indexOf('\n }', start))
}
+const productionCell = (cellId) => cellBlock(productionTfvars, cellId)
+
+// Scoped to C4 by name: staging C3 serves this digest too since its 2026-09-03 re-pin.
test('pins staging C4 and all production Asia cells to the same launch image', () => {
- assert.equal(stagingTfvars.match(new RegExp(launchDigest, 'g'))?.length, 1)
+ assert.match(cellBlock(stagingTfvars, 'staging-gce-c4'), new RegExp(`relay@sha256:${launchDigest}"`))
for (const cellId of asiaCells) {
assert.match(productionCell(cellId), new RegExp(`relay@sha256:${launchDigest}"`), cellId)
}
diff --git a/cloud/dev/scripts/relay-staging-deploy-identity.test.mjs b/cloud/dev/scripts/relay-staging-deploy-identity.test.mjs
index fece62f9ea2..8afbfb5ca94 100644
--- a/cloud/dev/scripts/relay-staging-deploy-identity.test.mjs
+++ b/cloud/dev/scripts/relay-staging-deploy-identity.test.mjs
@@ -2,11 +2,7 @@ import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { readWorkflow, workflowFiles } from './cloud-sql-rollout-lock-census.mjs'
-import {
- RELAY_WORKFLOW_FILE_PREFIX,
- relayWorkflowFile,
- relayWorkflowPath
-} from './relay-repository.mjs'
+import { prefixedRelayWorkflowPath, relayWorkflowFile } from './relay-repository.mjs'
const identity = readFileSync(
new URL('../../infra/terraform/relay-staging-deploy-iam.tf', import.meta.url),
@@ -128,8 +124,11 @@ test('the rendered attribute condition stays inside the provider limit', () => {
`assertion.repository_id == '${variableDefault('github_repo_id')}'`,
`assertion.repository_owner_id == '${variableDefault('github_owner_id')}'`
]
+ // The prefix is the Terraform variable, not this checkout's own workflow filenames: the
+ // condition names the files as the trusted repository carries them.
+ const prefix = variableDefault('github_workflow_file_prefix')
const workflowRefs = providerWorkflowFiles().map(
- (file) => `${repository}/${relayWorkflowPath(file)}@refs/heads/main`
+ (file) => `${repository}/${prefixedRelayWorkflowPath(prefix, file)}@refs/heads/main`
)
const rendered = [
...claims,
@@ -138,9 +137,7 @@ test('the rendered attribute condition stays inside the provider limit', () => {
`(${workflowRefs.map((ref) => `assertion.workflow_ref == '${ref}'`).join(' || ')})`
].join(' && ')
assert.ok(rendered.length < 4096, `rendered condition is ${rendered.length} characters`)
- // 797 is the private repository's rendered length. This copy prefixes every workflow filename,
- // which is the only difference, so the pin still moves the moment a workflow is added or dropped.
- assert.equal(rendered.length, 797 + workflowRefs.length * RELAY_WORKFLOW_FILE_PREFIX.length)
+ assert.equal(rendered.length, 791)
})
// Why: the census is the point. A binding added here without a workflow step behind it, or one
diff --git a/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs b/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs
index f25e442d8c0..1d3f3ce4d79 100644
--- a/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs
+++ b/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs
@@ -13,13 +13,13 @@ const EXPECTED_CONDITIONS = {
staging: {
relay: {
github_staging_relay_capacity:
- "assertion.ref == 'refs/heads/main' && assertion.environment == 'staging' && ((assertion.repository == 'stablyai/orca-cloud' && assertion.repository_id == '1273841466' && assertion.repository_owner_id == '127256420' && (assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/bootstrap-relay-staging-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/prove-relay-staging-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/recover-relay-staging-c4-image.yml@refs/heads/main')) || (assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-bootstrap-relay-staging-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-prove-relay-staging-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-recover-relay-staging-c4-image.yml@refs/heads/main')))",
+ "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'staging' && (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-bootstrap-relay-staging-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-prove-relay-staging-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-recover-relay-staging-c4-image.yml@refs/heads/main')",
github_staging_relay_deploy:
- "assertion.ref == 'refs/heads/main' && assertion.environment == 'staging' && ((assertion.repository == 'stablyai/orca-cloud' && assertion.repository_id == '1273841466' && assertion.repository_owner_id == '127256420' && (assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/bootstrap-relay-staging-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-staging-gce-candidate.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-staging.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/operate-relay-asia-admission.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/power-relay-staging.yml@refs/heads/main')) || (assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-bootstrap-relay-staging-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-staging-gce-candidate.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-staging.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-asia-admission.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-power-relay-staging.yml@refs/heads/main')))",
+ "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'staging' && (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-bootstrap-relay-staging-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-staging-gce-candidate.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-staging.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-asia-admission.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-power-relay-staging.yml@refs/heads/main')",
github_relay_asia_topology:
- "assertion.ref == 'refs/heads/main' && assertion.environment == 'staging' && assertion.event_name == 'workflow_dispatch' && ((assertion.repository == 'stablyai/orca-cloud' && assertion.repository_id == '1273841466' && assertion.repository_owner_id == '127256420' && assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-asia-topology.yml@refs/heads/main') || (assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-asia-topology.yml@refs/heads/main'))",
+ "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'staging' && assertion.event_name == 'workflow_dispatch' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-asia-topology.yml@refs/heads/main'",
github_relay_asia_proof:
- "assertion.ref == 'refs/heads/main' && assertion.environment == 'staging' && assertion.event_name == 'workflow_dispatch' && ((assertion.repository == 'stablyai/orca-cloud' && assertion.repository_id == '1273841466' && assertion.repository_owner_id == '127256420' && assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/prove-relay-asia-staging.yml@refs/heads/main') || (assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-prove-relay-asia-staging.yml@refs/heads/main'))",
+ "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'staging' && assertion.event_name == 'workflow_dispatch' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-prove-relay-asia-staging.yml@refs/heads/main'",
},
// The relay root creates this provider only in production, so staging has exactly one
// definition and it lives here.
@@ -31,15 +31,15 @@ const EXPECTED_CONDITIONS = {
production: {
relay: {
github:
- "assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && ((assertion.repository == 'stablyai/orca-cloud' && assertion.repository_id == '1273841466' && assertion.repository_owner_id == '127256420' && ((assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-fence-broker.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-director.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-multi-target.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/operate-relay-asia-admission.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/publish-relay-production.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/operate-relay-production-rehome.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca-cloud/.github/workflows/operate-relay-production-rehome-job.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-same-cap.yml@refs/heads/main' && (assertion.job_workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-same-cap-job.yml@refs/heads/main' || assertion.job_workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-same-cap.yml@refs/heads/main')))) || (assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && ((assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-fence-broker.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-director.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-multi-target.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-asia-admission.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-publish-relay-production.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome-job.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main' && (assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml@refs/heads/main' || assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main')))))",
+ "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && ((assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-fence-broker.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-director.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-multi-target.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-asia-admission.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-publish-relay-production.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome-job.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main' && (assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml@refs/heads/main' || assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main')))",
github_monitor:
- "assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && ((assertion.repository == 'stablyai/orca-cloud' && assertion.repository_id == '1273841466' && assertion.repository_owner_id == '127256420' && assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/monitor-relay-production.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca-cloud/.github/workflows/monitor-relay-production-job.yml@refs/heads/main') || (assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-monitor-relay-production.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-monitor-relay-production-job.yml@refs/heads/main'))",
+ "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-monitor-relay-production.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-monitor-relay-production-job.yml@refs/heads/main'",
github_fence:
- "assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && ((assertion.repository == 'stablyai/orca-cloud' && assertion.repository_id == '1273841466' && assertion.repository_owner_id == '127256420' && assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-multi-target.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-multi-target.yml@refs/heads/main') || (assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-multi-target.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-multi-target.yml@refs/heads/main'))",
+ "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-multi-target.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-multi-target.yml@refs/heads/main'",
github_production_relay_capacity:
- "assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && ((assertion.repository == 'stablyai/orca-cloud' && assertion.repository_id == '1273841466' && assertion.repository_owner_id == '127256420' && ((assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-capacity.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-capacity-job.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-same-cap.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-production-same-cap-job.yml@refs/heads/main'))) || (assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && ((assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-capacity.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-capacity-job.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml@refs/heads/main'))))",
+ "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && ((assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-capacity.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-capacity-job.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml@refs/heads/main'))",
github_relay_asia_topology:
- "assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && assertion.event_name == 'workflow_dispatch' && ((assertion.repository == 'stablyai/orca-cloud' && assertion.repository_id == '1273841466' && assertion.repository_owner_id == '127256420' && assertion.workflow_ref == 'stablyai/orca-cloud/.github/workflows/deploy-relay-asia-topology.yml@refs/heads/main') || (assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-asia-topology.yml@refs/heads/main'))",
+ "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && assertion.event_name == 'workflow_dispatch' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-asia-topology.yml@refs/heads/main'",
},
apps: {
github_production_app_deploy:
@@ -48,20 +48,21 @@ const EXPECTED_CONDITIONS = {
},
}
-// Every repository the relay root accepts while the public extraction runs, with the workflow-ref
-// head each one contributes. The apps root is not part of the dual accept.
-const ACCEPTED_REPOSITORIES = [
- {
- claims:
- "assertion.repository == 'stablyai/orca-cloud' && assertion.repository_id == '1273841466' && assertion.repository_owner_id == '127256420'",
- workflowHead: 'stablyai/orca-cloud/.github/workflows/'
- },
- {
+// The one repository each root trusts, with the workflow-ref head it contributes. The relay root
+// moved to the public repository, where the workflow files carry the `cloud-` prefix; the apps
+// root still deploys from the private one.
+const ROOT_REPOSITORIES = {
+ relay: {
claims:
"assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420'",
workflowHead: 'stablyai/orca/.github/workflows/cloud-'
+ },
+ apps: {
+ claims:
+ "assertion.repository == 'stablyai/orca-cloud' && assertion.repository_id == '1273841466' && assertion.repository_owner_id == '127256420'",
+ workflowHead: 'stablyai/orca-cloud/.github/workflows/'
}
-]
+}
// [root, provider, condition] for every provider the environment creates, across all roots.
async function flatten(environment) {
@@ -103,9 +104,7 @@ for (const environment of Object.keys(EXPECTED_CONDITIONS)) {
test(`${environment} pins repository, branch, and environment on every provider`, async () => {
for (const [root, provider, condition] of await flatten(environment)) {
for (const pin of [
- "assertion.repository == 'stablyai/orca-cloud'",
- "assertion.repository_id == '1273841466'",
- "assertion.repository_owner_id == '127256420'",
+ ROOT_REPOSITORIES[root].claims,
"assertion.ref == 'refs/heads/main'",
`assertion.environment == '${environment}'`
]) {
@@ -131,27 +130,23 @@ for (const environment of Object.keys(EXPECTED_CONDITIONS)) {
})
}
-// Why: the dual accept is only safe if each OR arm carries its own repository claims. An arm that
-// inherited them, or a workflow ref that named the other repository, would let one repository's
-// workflows run under the other's proof.
+// Why: the cutover left one arm per relay provider. A leftover `stablyai/orca-cloud` claim or
+// workflow ref would keep trusting a repository whose relay workflows are retired, and an unprefixed
+// ref would name a file the public repository does not have.
for (const environment of Object.keys(EXPECTED_CONDITIONS)) {
- test(`${environment} admits both repositories through every relay provider`, async () => {
+ test(`${environment} admits only the public repository through every relay provider`, async () => {
+ const { claims, workflowHead } = ROOT_REPOSITORIES.relay
const rendered = await renderAttributeConditions(environment)
for (const [provider, condition] of Object.entries(rendered.relay)) {
- assert.ok(
- condition.startsWith("assertion.ref == 'refs/heads/main' && "),
- `${provider} does not lead with the repository-independent claims`
- )
+ assert.ok(condition.startsWith(`${claims} && `), `${provider} does not lead with the claims`)
+ assert.doesNotMatch(condition, /stablyai\/orca-cloud|1273841466/, `${provider} keeps an old arm`)
const refs = [...condition.matchAll(/(?:job_)?workflow_ref == '([^']+)'/g)].map(
(match) => match[1]
)
- const perRepository = ACCEPTED_REPOSITORIES.map((repository) => {
- assert.ok(condition.includes(`(${repository.claims} && `), `${provider} misses an arm`)
- return refs.filter((ref) => ref.startsWith(repository.workflowHead)).length
- })
- assert.equal(refs.length, perRepository[0] + perRepository[1], `${provider} names a stray ref`)
- assert.equal(perRepository[0], perRepository[1], `${provider} arms are not the same size`)
- assert.ok(perRepository[0] > 0, `${provider} names no workflow`)
+ assert.ok(refs.length > 0, `${provider} names no workflow`)
+ for (const ref of refs) {
+ assert.ok(ref.startsWith(workflowHead), `${provider} names a stray ref ${ref}`)
+ }
}
})
}
diff --git a/cloud/infra/terraform/README.md b/cloud/infra/terraform/README.md
index 2e2b9b02011..8aa7aa0a95c 100644
--- a/cloud/infra/terraform/README.md
+++ b/cloud/infra/terraform/README.md
@@ -37,35 +37,26 @@ identity (`google_service_account.github_deploy`, its provider, and its bindings
with production-only counts; staging's copies are declared by `infra/terraform-apps`. An untargeted
plan is orderable again; the `Plan:` line still reflects the standing cell-template drift backlog.
-### Dual-accept Workload Identity during the public extraction
+### Workload Identity trusts the public repository
-While the relay source moves to the public `stablyai/orca` repository, every relay Workload
-Identity provider accepts the same workflows from both repositories. `github_accepted_repositories`
-lists the extra repositories; `relay-github-workflow-trust.tf` renders one parenthesised OR arm per
-accepted repository, each arm carrying that repository's own `repository`, `repository_id`, and
-`repository_owner_id` claims plus its exact workflow refs. `ref`, `environment`, and `event_name`
-stay outside the OR. Workflow files keep their names in the private repo and take the
-`workflow_file_prefix` (`cloud-`) in the public one.
+The cutover closed on 2026-09-03. Every relay Workload Identity provider now accepts exactly one
+repository, `stablyai/orca` (`1183888342`, owner `127256420`), and every workflow ref it names is
+built from `github_workflow_file_prefix` (`cloud-`), which is the rename the public repo applies to
+the workflow files it carries. `github_repo`, `github_repo_id`, and that prefix are set in both
+`environments/*.tfvars` as well as defaulted here, and `github_accepted_repositories` is empty.
+Nothing in this root trusts `stablyai/orca-cloud` any more; the apps and foundation roots still do,
+because the app workflows still live there.
-Adding a repository is a tfvars edit: no provider block changes, and the rendered strings are
-pinned by `dev/scripts/workload-identity-attribute-conditions.test.mjs`. An empty list renders
-byte-identically to the single-repository form, which is what makes the arms reviewable against
-the pre-extraction condition.
+`github_accepted_repositories` stays available for the next repository move. Each entry renders its
+own parenthesised OR arm in `relay-github-workflow-trust.tf`, carrying that repository's own
+`repository`, `repository_id`, and `repository_owner_id` claims plus its exact workflow refs, while
+`ref`, `environment`, and `event_name` stay outside the OR. An empty list renders byte-identically
+to the single-repository form, so adding and removing a repository is a tfvars edit with no provider
+block change. The rendered strings are pinned by
+`dev/scripts/workload-identity-attribute-conditions.test.mjs`.
-Closing the cutover is an owner step, in this order:
-
-1. Retire the private workflows, so nothing runs from `stablyai/orca-cloud` any more.
-2. Point `github_owner`, `github_repo`, `github_repo_id`, and `github_owner_id` at
- `stablyai/orca` (`1183888342`, owner `127256420`), and set `workflow_file_prefix` for it by
- moving the surviving entry's prefix onto the primary: the public files keep the `cloud-` names,
- so the primary prefix becomes `cloud-` unless the files are renamed back.
-3. Empty `github_accepted_repositories` in both `environments/*.tfvars`.
-4. Re-render and update the pinned conditions, then apply. Each provider goes back to a single
- arm, and `google_service_account_iam_member.github_accepted_repository_workload_identity_user`
- is destroyed as the primary `attribute.repository` binding takes over.
-
-Step 2 and step 3 must land in the same apply: dropping the accepted entry before repointing the
-primary would revoke the public repository mid-flight.
+Repointing the primary and emptying the list must land in the same apply: dropping the accepted
+entry before repointing the primary would revoke the surviving repository mid-flight.
### `ORCA_RELAY_IMAGE_DIGEST` is not Terraform-owned
diff --git a/cloud/infra/terraform/environments/production.tfvars b/cloud/infra/terraform/environments/production.tfvars
index 60d36e3d832..8e442c75900 100644
--- a/cloud/infra/terraform/environments/production.tfvars
+++ b/cloud/infra/terraform/environments/production.tfvars
@@ -5,19 +5,11 @@ region = "us-central1"
artifact_repository_id = "orca-cloud"
-# Dual accept while the relay source moves to the public stablyai/orca repository: the same
-# workflows are trusted from both repos, and the public copies carry a `cloud-` file prefix.
-# Remove this entry once the private workflows are retired and point github_owner/github_repo,
-# github_repo_id, and github_owner_id at the surviving repository.
-github_accepted_repositories = [
- {
- owner = "stablyai"
- repo = "orca"
- repo_id = "1183888342"
- owner_id = "127256420"
- workflow_file_prefix = "cloud-"
- }
-]
+# The relay source lives in the public stablyai/orca repository, where the workflows carry a
+# `cloud-` file prefix. github_owner and github_owner_id keep their defaults.
+github_repo = "orca"
+github_repo_id = "1183888342"
+github_workflow_file_prefix = "cloud-"
# Our first-party auth service. auth.onorca.dev is PropelAuth's prod domain, so
# our service lives at login.onorca.dev (desktop points ORCA_CLOUD_API_URL here).
diff --git a/cloud/infra/terraform/environments/staging.tfvars b/cloud/infra/terraform/environments/staging.tfvars
index 3ee108fe874..4a32458fcd5 100644
--- a/cloud/infra/terraform/environments/staging.tfvars
+++ b/cloud/infra/terraform/environments/staging.tfvars
@@ -5,19 +5,11 @@ region = "us-central1"
artifact_repository_id = "orca-cloud"
-# Dual accept while the relay source moves to the public stablyai/orca repository: the same
-# workflows are trusted from both repos, and the public copies carry a `cloud-` file prefix.
-# Remove this entry once the private workflows are retired and point github_owner/github_repo,
-# github_repo_id, and github_owner_id at the surviving repository.
-github_accepted_repositories = [
- {
- owner = "stablyai"
- repo = "orca"
- repo_id = "1183888342"
- owner_id = "127256420"
- workflow_file_prefix = "cloud-"
- }
-]
+# The relay source lives in the public stablyai/orca repository, where the workflows carry a
+# `cloud-` file prefix. github_owner and github_owner_id keep their defaults.
+github_repo = "orca"
+github_repo_id = "1183888342"
+github_workflow_file_prefix = "cloud-"
auth_base_url = "https://auth-staging.onorca.dev"
@@ -67,7 +59,7 @@ relay_gce_cells = {
boot_disk_gb = 30
boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-7"
capacity_requests = 4000
- image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:9fba2a189ab3fa29853800830e77c7551ab3aaa8f43f3cd9adbdea28b876a8b9"
+ image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:5aedbca5c86de24c8b4d4bf7e3b444b76c712f281ede916cb9d90f70cad1e563"
initially_enabled = false
connection_hard_cap = 1000
connection_unobserved_bound = 60
diff --git a/cloud/infra/terraform/relay-shared.tf b/cloud/infra/terraform/relay-shared.tf
index d4f0e5dac15..b1afc4d1890 100644
--- a/cloud/infra/terraform/relay-shared.tf
+++ b/cloud/infra/terraform/relay-shared.tf
@@ -14,16 +14,16 @@ locals {
"assertion.repository_owner_id == '${var.github_owner_id}'",
]
- # Dual accept during the public extraction: the primary repository first, then every repository
- # var.github_accepted_repositories adds. Each one renders its own OR arm in every provider
- # condition, so both repos can run the same workflows through the same identities. A repository
- # that imports these workflows may rename the files, hence the per-repository prefix.
+ # The primary repository first, then every repository var.github_accepted_repositories adds.
+ # Each one renders its own OR arm in every provider condition, so a repository move can trust
+ # both repos at once. A repository that imports these workflows may rename the files, hence the
+ # per-repository prefix; the primary's is var.github_workflow_file_prefix.
relay_github_accepted_repositories = concat([{
owner = var.github_owner
repo = var.github_repo
repo_id = var.github_repo_id
owner_id = var.github_owner_id
- workflow_file_prefix = ""
+ workflow_file_prefix = var.github_workflow_file_prefix
}], var.github_accepted_repositories)
relay_github_single_repository = length(local.relay_github_accepted_repositories) == 1
diff --git a/cloud/infra/terraform/variables.tf b/cloud/infra/terraform/variables.tf
index c3e06ee280f..68f6c555bd3 100644
--- a/cloud/infra/terraform/variables.tf
+++ b/cloud/infra/terraform/variables.tf
@@ -22,14 +22,14 @@ variable "github_owner" {
variable "github_repo" {
type = string
description = "GitHub repo allowed to deploy through Workload Identity Federation."
- default = "orca-cloud"
+ default = "orca"
}
# Numeric IDs survive a rename or transfer of the repository; every provider pins them next to the name.
variable "github_repo_id" {
type = string
description = "Numeric GitHub repository ID of github_owner/github_repo."
- default = "1273841466"
+ default = "1183888342"
validation {
condition = can(regex("^[0-9]+$", var.github_repo_id))
@@ -48,12 +48,24 @@ variable "github_owner_id" {
}
}
-# Additional repositories whose identical workflows the same identities must accept while the
-# public extraction runs. Each entry renders its own OR arm in every provider condition, so the
-# private repo keeps working while the public one takes over. `workflow_file_prefix` is the rename
-# the importing repository applies to the workflow files it copies. Empty is the steady state:
-# the final step of the cutover is to empty this list again and point github_owner/github_repo,
-# github_repo_id, and github_owner_id at the surviving repository.
+# The rename the relay repository applies to the workflow files it carries. The public repo keeps
+# the workflows under `cloud-` names, so every relay workflow_ref is built from this head.
+variable "github_workflow_file_prefix" {
+ type = string
+ description = "Filename prefix on github_owner/github_repo's copies of the relay workflows."
+ default = "cloud-"
+
+ validation {
+ condition = can(regex("^[a-z0-9-]*$", var.github_workflow_file_prefix))
+ error_message = "github_workflow_file_prefix must be lowercase letters, digits, or hyphens."
+ }
+}
+
+# Additional repositories whose identical workflows the same identities must accept during a
+# repository move. Each entry renders its own OR arm in every provider condition, so both repos
+# can run the same workflows through the same identities. `workflow_file_prefix` is the rename the
+# importing repository applies to the workflow files it copies. Empty is the steady state, and is
+# where the public extraction left it: stablyai/orca is now the primary and only repository.
variable "github_accepted_repositories" {
type = list(object({
owner = string
diff --git a/config/i18next.config.ts b/config/i18next.config.ts
index 577375bd2e9..87e302ac213 100644
--- a/config/i18next.config.ts
+++ b/config/i18next.config.ts
@@ -16,7 +16,7 @@ export default defineConfig({
],
output,
defaultNS: false,
- functions: ['t', '*.t', 'translate', 'translateMain'],
+ functions: ['t', '*.t', 'translate', 'translateMain', 'translateSearchKeyword'],
useTranslationNames: ['useTranslation'],
sort: true,
disablePlurals: true,
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/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":"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","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": "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",
+- "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