From a1f198be0d96c7152997a1fd178ad4f201fa7e67 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:31:39 +0000 Subject: [PATCH 01/80] Update README downloads badge --- docs/assets/readme-downloads.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg index 17c8d9e6926..90293d0a7ea 100644 --- a/docs/assets/readme-downloads.svg +++ b/docs/assets/readme-downloads.svg @@ -1,5 +1,5 @@ - - downloads: 32m + + downloads: 33m @@ -15,7 +15,7 @@ downloads downloads - 32m - 32m + 33m + 33m From 9940355f6eec66c71ccd211ad7e78c517b66d3d6 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:33:35 -0700 Subject: [PATCH 02/80] Improve worktree palette layout with flex-1 titles (#17273) Adds flex-1 to palette open tab titles so they expand to fill available space, making better use of the palette's horizontal layout. --- src/renderer/src/components/WorktreeJumpPalette.tsx | 2 +- .../worktree-jump-palette-interleaved-sections.test.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx index d729bc3cbef..b6408593ed3 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.tsx @@ -568,7 +568,7 @@ function PaletteOpenTabPrimaryLine({
diff --git a/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx b/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx index 62055b5027b..854412b5107 100644 --- a/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx +++ b/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx @@ -401,6 +401,7 @@ describe('WorktreeJumpPalette interleaved primary sections', () => { const title = row?.querySelector('[data-slot="palette-open-tab-title"]') const worktree = row?.querySelector('[data-slot="palette-open-tab-worktree"]') expect(title?.textContent).toBe(longTitle) + expect(title?.classList.contains('flex-1')).toBe(true) expect(worktree?.textContent).toBe('user-support') expect(worktree?.compareDocumentPosition(title ?? document.createElement('span'))).toBe( Node.DOCUMENT_POSITION_PRECEDING From 7c0c9cee5717a032e9d97beeb5f240310a564954 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:41:33 -0700 Subject: [PATCH 03/80] perf(markdown): skip closed-search update renders (#17162) * perf(markdown): skip closed-search update renders * test(markdown): stress closed-search updates --- .../editor/useRichMarkdownSearch.test.tsx | 72 +++++++++++++++++++ .../editor/useRichMarkdownSearch.ts | 4 +- 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/components/editor/useRichMarkdownSearch.test.tsx diff --git a/src/renderer/src/components/editor/useRichMarkdownSearch.test.tsx b/src/renderer/src/components/editor/useRichMarkdownSearch.test.tsx new file mode 100644 index 00000000000..d2be40e647a --- /dev/null +++ b/src/renderer/src/components/editor/useRichMarkdownSearch.test.tsx @@ -0,0 +1,72 @@ +// @vitest-environment happy-dom + +import { act, renderHook } from '@testing-library/react' +import type { Editor } from '@tiptap/react' +import { describe, expect, it, vi } from 'vitest' +import { useRichMarkdownSearch } from './useRichMarkdownSearch' + +function createEditor(): { editor: Editor; emitUpdate: () => void } { + const updateListeners = new Set<() => void>() + const transaction = { + setMeta: vi.fn().mockReturnThis() + } + const editor = { + commands: { focus: vi.fn() }, + off: vi.fn((event: string, listener: () => void) => { + if (event === 'update') { + updateListeners.delete(listener) + } + }), + on: vi.fn((event: string, listener: () => void) => { + if (event === 'update') { + updateListeners.add(listener) + } + }), + registerPlugin: vi.fn(), + state: { doc: {}, tr: transaction }, + unregisterPlugin: vi.fn(), + view: { dispatch: vi.fn() } + } as unknown as Editor + return { + editor, + emitUpdate: () => updateListeners.forEach((listener) => listener()) + } +} + +describe('useRichMarkdownSearch', () => { + it('rerenders for editor updates only while search is open', () => { + const closedUpdateCount = 100 + const { editor, emitUpdate } = createEditor() + const rootRef = { current: document.createElement('div') } + const scrollContainerRef = { current: document.createElement('div') } + let renderCount = 0 + const hook = renderHook(() => { + renderCount += 1 + return useRichMarkdownSearch({ editor, rootRef, scrollContainerRef }) + }) + + const closedRenderCount = renderCount + for (let index = 0; index < closedUpdateCount; index += 1) { + act(emitUpdate) + } + + expect(renderCount - closedRenderCount).toBe(0) + expect(editor.on).not.toHaveBeenCalledWith('update', expect.any(Function)) + + act(() => hook.result.current.openSearch()) + + const openRenderCount = renderCount + expect(editor.on).toHaveBeenCalledWith('update', expect.any(Function)) + act(emitUpdate) + expect(renderCount - openRenderCount).toBe(1) + + act(() => hook.result.current.searchActions.closeSearch()) + + const reclosedRenderCount = renderCount + for (let index = 0; index < closedUpdateCount; index += 1) { + act(emitUpdate) + } + expect(renderCount - reclosedRenderCount).toBe(0) + expect(editor.off).toHaveBeenCalledWith('update', expect.any(Function)) + }) +}) diff --git a/src/renderer/src/components/editor/useRichMarkdownSearch.ts b/src/renderer/src/components/editor/useRichMarkdownSearch.ts index 035081665ee..2508505b85b 100644 --- a/src/renderer/src/components/editor/useRichMarkdownSearch.ts +++ b/src/renderer/src/components/editor/useRichMarkdownSearch.ts @@ -234,7 +234,7 @@ export function useRichMarkdownSearch({ }, [editor]) useEffect(() => { - if (!editor) { + if (!editor || !isSearchOpen) { return } @@ -242,7 +242,7 @@ export function useRichMarkdownSearch({ return () => { editor.off('update', handleEditorUpdate) } - }, [editor, handleEditorUpdate]) + }, [editor, handleEditorUpdate, isSearchOpen]) useEffect(() => { if (!isSearchOpen) { From 11d8673112df388b5d010eb95d2ea67b326bf38e Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:42:50 -0700 Subject: [PATCH 04/80] test(cross-version-wire): derive skew expectations from the baseline under test (#17178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(cross-version-wire): derive skew expectations from the baseline under test The cross-version wire job pairs current code against whichever release tag is newest, so a hand-written "the old side does not have X" assertion expires by itself: v1.4.192 was the first tag containing the SnapshotStart `terminalOwner` field, and cutting it turned the new-client/old-server pairing red on unrelated pull requests with no code change anywhere. Read what each build publishes from that build. Each host is now paired against a client of its own version to produce a reference, and the skewed pairings are compared against that reference, so the expectation is whatever the release actually shipped. The same class of assertion in the agent-session suite — "the old build advertises no structured capability and registers no structured method" — becomes "each build's advertisement agrees with what it registers", and the "client too old to know this capability" is derived by removing the capability from the baseline's own list. The guard is unchanged in strength: a field the old host still publishes may not be dropped, skew may not change what a host puts on the wire, and a new pairing asserts the oracle still stalls when a peer cannot decode an opcode the other side sends. * test(cross-version-wire): exercise release structured methods * test(cross-version-wire): load the registered method manifest * test(cross-version-wire): assert execution, not registration, on both host gates The release-shaped checkout gate accepted any reply that was not method_not_found, so a registered-but-throwing handler passed it. The capability gate asserted a shared host spy had been called at all, so the second method mapped to that spy could stop reaching the host unnoticed. * test(cross-version): make the release-shaped skew cover the whole agent-session manifest The release-shaped checkout is the only place the "registered means usable" claim is executable today — the baseline release registers none of these methods — and it was exercising one of sixteen. A handler registered and returning an execution error passed the suite. - Declare each method's result in the manifest, so "answered" is the contract rather than "did not say method_not_found". - Give each build a seam to install a host into its own module slot; a release checkout has its own copy, so the working tree's host was never this dispatcher's, and every host-backed method answered structured_agent_session_unsupported — the capability gate's own words. - Run one execution contract over both skews instead of two divergent loops. - Pair the AI Vault never-called spy with a positive control; renaming the runtime method it watches left it green. --------- Co-authored-by: Brennan Benson --- docs/reference/remote-wire-compatibility.md | 43 ++- ...ss-version-agent-session-wire.unit.test.ts | 306 ++++++++++++++---- .../cross-version-terminal-wire.unit.test.ts | 113 +++++-- .../published-field-shape.ts | 38 +++ .../published-field-shape.unit.test.ts | 50 +++ .../terminal-skew-journey.ts | 57 +++- .../versioned-agent-session-wire.ts | 92 +++--- .../versioned-terminal-wire.ts | 34 ++ 8 files changed, 577 insertions(+), 156 deletions(-) create mode 100644 tests/e2e/cross-version-wire/published-field-shape.ts create mode 100644 tests/e2e/cross-version-wire/published-field-shape.unit.test.ts diff --git a/docs/reference/remote-wire-compatibility.md b/docs/reference/remote-wire-compatibility.md index 423149950ce..1fceb67d5d5 100644 --- a/docs/reference/remote-wire-compatibility.md +++ b/docs/reference/remote-wire-compatibility.md @@ -95,16 +95,51 @@ negotiated capabilities differ from the contract. Adding an optional field keeps green (Rule 1); making a client depend on that field turns the new-client/old-host pairing red. +### Never write down what the old side has + +The baseline is whichever release tag is newest, so it moves on every cut. An +expectation of the form "the old side does not have X" — a `not.toHaveProperty`, a +`not.toContain`, a hard-coded field list — stops being true the first time a release +ships X. The suite then reddens on whatever pull request is in flight, with no code +change anywhere, and the job trains people to ignore it. That is worse than no test, +because a rolling baseline eventually contains every additive field the wire has, and +adding one is the sanctioned way to evolve it. + +Derive the expectation from the baseline that was actually checked out: + +- for a published frame, pair each build against a client of its own version and + compare the skewed pairing against that same-version reference, so the expectation + is whatever that build publishes today (`publishedFieldNames` in + `tests/e2e/cross-version-wire/published-field-shape.ts`); +- for a negotiated surface, read the old build's advertised capabilities and + registered method names from its checkout, and assert they agree with each other + rather than asserting the old build lacks them; +- for a "client too old to know X", derive that client's advertised list by removing + X from the baseline's own list, so the gate stays exercised after X ships. + +Name the direction in the assertion. `new client against old server` and `old client +against new server` fail for different reasons, and the host is the only side that +authors a published frame — the terminal `terminalOwner` false positive on 2026-08-29 +was misread as a new client sending an unknown field when the old server was +publishing it. Two things are still safe to state literally: the current build's own +contract, and an invariant that holds for every version. + +Pinning a legacy ref is the fallback when a contract genuinely needs a release from +before a feature shipped, as `cross-version-browser-placement.unit.test.ts` does with +`LEGACY_BROWSER_PLACEMENT_RELEASE_REF`. It does not rot on a cut, but it is +hand-maintained, so prefer deriving. + `tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts` pairs the same two builds over the structured `agentSession.*` surface. Because a released build cannot name a capability string its own source never contains, the old side's advertised list and registered method names are read from the extracted checkout rather than hand-written. It covers the three skews that surface can fail on: -- an old client — advertising only what the baseline build defines — is told the whole - surface does not exist and reaches no host method; -- a new client against the old dispatcher gets `method_not_found` on every method, and - can see the absence during negotiation instead of by calling; +- an old client — advertising the baseline's list minus this capability — is told the + whole surface does not exist and reaches no host method; +- a new client against the old dispatcher always gets an answer rather than silence, + and `method_not_found` for every method that release does not register, so the + absence is visible during negotiation instead of by calling; - a cursor survives a host restart: the client's fence is refused as stale with the live one attached, and resuming from the held cursor replays only what it missed. diff --git a/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts b/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts index 093547102c6..0ce15ee6563 100644 --- a/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts +++ b/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts @@ -36,27 +36,69 @@ const WORKSPACE = 'workspace-1' const THREAD = '019fd532-7c11-7a90-b6de-4e1a2c3d5f60' const NOW = 1_800_000_000_000 -/** Every method the structured surface publishes, paired with the host method it - * must reach — a gate that hides one method and leaks another is the bug. */ -const STRUCTURED_CALLS: { method: string; hostMethod: string | null }[] = [ - { method: 'agentSession.createSupport', hostMethod: null }, - { method: 'agentSession.create', hostMethod: 'attach' }, - { method: 'agentSession.ensure', hostMethod: 'attach' }, - { method: 'agentSession.send', hostMethod: 'send' }, - { method: 'agentSession.cancel', hostMethod: 'cancel' }, - { method: 'agentSession.close', hostMethod: 'close' }, - { method: 'agentSession.respondToApproval', hostMethod: 'respondToPrompt' }, - { method: 'agentSession.respondToQuestion', hostMethod: 'respondToPrompt' }, - { method: 'agentSession.setOption', hostMethod: 'setOption' }, - { method: 'agentSession.handoffStatus', hostMethod: 'handoffStatus' }, - { method: 'agentSession.options', hostMethod: 'readOptions' }, - { method: 'agentSession.hold', hostMethod: 'hold' }, - { method: 'agentSession.release', hostMethod: 'release' }, - { method: 'agentSession.history', hostMethod: 'history' }, +/** Every method the structured surface publishes: the host method it must reach, + * and the result it must hand back. A gate that hides one method and leaks + * another is the bug; so is a method that is registered and answers with an + * error, which is why `result` is declared per method rather than inferred from + * "did not say method_not_found". `result` is omitted only where the method + * legitimately answers with no reply at all. */ +const STRUCTURED_CALLS: { + method: string + hostMethod: string | null + result?: Record +}[] = [ + { method: 'agentSession.createSupport', hostMethod: null, result: { supported: true } }, + { + method: 'agentSession.create', + hostMethod: 'attach', + result: { ok: true, replayed: false, value: { sessionId: SESSION } } + }, + { + method: 'agentSession.ensure', + hostMethod: 'attach', + result: { ok: true, replayed: false, value: { sessionId: SESSION } } + }, + { method: 'agentSession.send', hostMethod: 'send', result: { ok: true, replayed: false } }, + { method: 'agentSession.cancel', hostMethod: 'cancel', result: { ok: true, replayed: false } }, + { method: 'agentSession.close', hostMethod: 'close', result: { ok: true } }, + { + method: 'agentSession.respondToApproval', + hostMethod: 'respondToPrompt', + result: { ok: true, replayed: false } + }, + { + method: 'agentSession.respondToQuestion', + hostMethod: 'respondToPrompt', + result: { ok: true, replayed: false } + }, + { + method: 'agentSession.setOption', + hostMethod: 'setOption', + result: { ok: true, replayed: false } + }, + { + method: 'agentSession.handoffStatus', + hostMethod: 'handoffStatus', + result: { owner: 'native' } + }, + { + method: 'agentSession.options', + hostMethod: 'readOptions', + result: { current: { model: 'gpt-live' } } + }, + { method: 'agentSession.hold', hostMethod: 'hold', result: { held: true } }, + { method: 'agentSession.release', hostMethod: 'release', result: { released: true } }, + { + method: 'agentSession.history', + hostMethod: 'history', + result: { ok: true, page: { items: [] } } + }, + // A subscription that opens with nothing to say answers with no reply at all, + // so reaching the host is the only signal that the gate opened. { method: 'agentSession.subscribe', hostMethod: 'subscribe' }, // Teardown runs through the runtime's subscription registry rather than the // host, so its reply is the only signal that the gate opened. - { method: 'agentSession.unsubscribe', hostMethod: null } + { method: 'agentSession.unsubscribe', hostMethod: null, result: { unsubscribed: true } } ] let baselineRef: string @@ -199,6 +241,24 @@ function runtimeStub(): unknown { } } +/** + * What a client too old to know the structured surface advertises: the baseline's + * own list, minus the capability. Derived rather than assumed to be the baseline's + * list as-is — the baseline is the newest release tag, so the day a release ships + * this capability the list would contain it and the gate below would stop being + * exercised at all, on a pull request that changed nothing. + */ +function legacyClientCapabilities(): string[] { + return baseline.capabilities.filter( + (capability) => capability !== STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY + ) +} + +/** The structured methods the baseline release actually registers, read from it. */ +function baselineStructuredMethods(): string[] { + return baseline.methodNames.filter((name) => name.startsWith('agentSession.')) +} + /** Every reply one call produced. Streaming methods answer more than once, and a * refusal has to arrive as a reply rather than as silence. */ async function callBuild( @@ -219,6 +279,75 @@ async function callBuild( return replies } +/** The host every skew installs to drive the surface: enough of the real host's + * shape for each handler to run, and a spy per method so "which call reached the + * host" is answerable per call rather than per suite. */ +function structuredHostStub(): Record> { + return { + attach: vi.fn(async () => ({ ok: true, replayed: false, value: { sessionId: SESSION } })), + send: vi.fn(async () => ({ ok: true, replayed: false })), + cancel: vi.fn(async () => ({ ok: true, replayed: false })), + close: vi.fn(async () => undefined), + hold: vi.fn(async () => undefined), + release: vi.fn(() => undefined), + respondToPrompt: vi.fn(async () => ({ ok: true, replayed: false })), + setOption: vi.fn(async () => ({ ok: true, replayed: false })), + requestHandoff: vi.fn(async () => ({ status: { owner: 'native' } })), + handoffStatus: vi.fn(async () => ({ owner: 'native' })), + readOptions: vi.fn(async () => ({ models: [], current: { model: 'gpt-live' } })), + history: vi.fn(() => ({ ok: true, page: { items: [] } })), + subscribe: vi.fn(() => () => undefined), + unsubscribe: vi.fn() + } +} + +/** + * The one thing this suite exists to guarantee, written once and applied per + * build: every method the manifest declares is not merely registered but reaches + * its host method on this call, answers, and answers with its declared result. + * + * Written as a helper rather than inline because a build passing it is the claim, + * and each skew that registers the surface owes the same claim — a check that + * covers one method leaves the rest registered-but-unusable behind a green suite. + */ +async function expectDeclaredSurfaceExecutes( + build: AgentSessionWireBuild, + hostCalls: Record>, + clientCapabilities: readonly string[] +): Promise { + for (const { method, hostMethod, result } of STRUCTURED_CALLS) { + // Two methods share one host method, so "has been called" would already be + // true from the earlier one: only this call's own delta pins the pairing. + const before = hostMethod ? hostCalls[hostMethod].mock.calls.length : 0 + const replies = await callBuild(build, method, paramsFor(method), { + clientKind: 'runtime', + clientCapabilities + }) + if (hostMethod) { + expect( + hostCalls[hostMethod].mock.calls.length - before, + `${build.label}: ${method} did not reach the host` + ).toBe(1) + } + for (const reply of replies) { + expect( + reply, + `${build.label}: ${method} was refused: ${JSON.stringify(reply)}` + ).toMatchObject({ ok: true }) + } + if (result) { + // The declared answer, not merely a non-refusal: a handler that is + // registered and returns an execution error, or hands back someone else's + // envelope, fails here rather than passing as "reached the host". + expect(replies, `${build.label}: ${method} must answer exactly once`).toHaveLength(1) + expect(replies[0], `${build.label}: ${method} answered off-contract`).toMatchObject({ + ok: true, + result + }) + } + } +} + describe('cross-version structured agent sessions', () => { it( 'skews current code against a real published release', @@ -239,22 +368,7 @@ describe('cross-version structured agent sessions', () => { beforeEach(() => { operations = 0 - hostCalls = { - attach: vi.fn(async () => ({ ok: true, replayed: false, value: { sessionId: SESSION } })), - send: vi.fn(async () => ({ ok: true, replayed: false })), - cancel: vi.fn(async () => ({ ok: true, replayed: false })), - close: vi.fn(async () => undefined), - hold: vi.fn(async () => undefined), - release: vi.fn(() => undefined), - respondToPrompt: vi.fn(async () => ({ ok: true, replayed: false })), - setOption: vi.fn(async () => ({ ok: true, replayed: false })), - requestHandoff: vi.fn(async () => ({ status: { owner: 'native' } })), - handoffStatus: vi.fn(async () => ({ owner: 'native' })), - readOptions: vi.fn(async () => ({ models: [], current: { model: 'gpt-live' } })), - history: vi.fn(() => ({ ok: true, page: { items: [] } })), - subscribe: vi.fn(() => () => undefined), - unsubscribe: vi.fn() - } + hostCalls = structuredHostStub() setStructuredAgentSessionHost(hostCalls as unknown as StructuredAgentSessionHost) }) @@ -263,12 +377,13 @@ describe('cross-version structured agent sessions', () => { }) it('is told the whole surface does not exist, and reaches no host method', async () => { - // The old build cannot name the capability, so its clients never send it. - expect(baseline.capabilities).not.toContain(STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY) + // Anti-vacuous: the old client still advertises a real list, so the refusal + // below is the capability gate answering, not an empty negotiation. + expect(legacyClientCapabilities().length).toBeGreaterThan(0) for (const { method } of STRUCTURED_CALLS) { const replies = await callBuild(current, method, paramsFor(method), { clientKind: 'runtime', - clientCapabilities: baseline.capabilities + clientCapabilities: legacyClientCapabilities() }) expect(replies, `${method} must answer exactly once`).toHaveLength(1) expect(replies[0]).toMatchObject({ @@ -282,59 +397,91 @@ describe('cross-version structured agent sessions', () => { }) it('is served the same calls once it advertises the capability', async () => { - for (const { method, hostMethod } of STRUCTURED_CALLS) { - const replies = await callBuild(current, method, paramsFor(method), { - clientKind: 'runtime', - clientCapabilities: [ - ...baseline.capabilities, - STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY - ] - }) - // A subscription that opens with nothing to say answers with no reply at - // all, so reaching the host is the signal that the gate opened. - if (hostMethod) { - expect(hostCalls[hostMethod], `${method} did not reach the host`).toHaveBeenCalled() - } else { - expect(replies[0], `${method} was refused`).toMatchObject({ ok: true }) - } - for (const reply of replies) { - expect(reply, `${method} was refused: ${JSON.stringify(reply)}`).toMatchObject({ - ok: true - }) - } - } + await expectDeclaredSurfaceExecutes(current, hostCalls, [ + ...legacyClientCapabilities(), + STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY + ]) }) }) describe('a new client against an old host', () => { - it('finds no structured method registered on the old build', () => { - expect(baseline.methodNames.filter((name) => name.startsWith('agentSession.'))).toEqual([]) + it('registers the whole surface on the new build', () => { + expect(current.capabilities).toContain(STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY) expect(current.methodNames.filter((name) => name.startsWith('agentSession.'))).toHaveLength( STRUCTURED_CALLS.length ) }) it('can detect the absence during negotiation instead of by calling', () => { - expect(current.capabilities).toContain(STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY) - expect(baseline.capabilities).not.toContain(STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY) + // The invariant that survives a release cut: each build's advertised list and + // its registered methods agree. "The old build has neither" is only true + // until a release ships the surface, and pinning it turns this red on the cut + // rather than on a change. + expect(baseline.capabilities.includes(STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY)).toBe( + baselineStructuredMethods().length > 0 + ) // Additive surface: bumping the protocol number would strand every paired // device on this release rather than degrade one feature. expect(current.protocolVersion).toBe(baseline.protocolVersion) }) - it('gets a clean method_not_found from the old dispatcher rather than silence', async () => { + it('gets a clean answer from the old dispatcher rather than silence', async () => { + const registered = new Set(baselineStructuredMethods()) for (const { method } of STRUCTURED_CALLS) { const replies = await callBuild(baseline, method, paramsFor(method), { clientKind: 'runtime', clientCapabilities: current.capabilities }) + // Silence is the failure mode a new client cannot recover from, whatever + // the old build knows; the refusal code is only asserted for the methods + // that release genuinely does not have. expect(replies, `${method} must answer exactly once`).toHaveLength(1) - expect(replies[0], `${method} on the old host`).toMatchObject({ - ok: false, - error: { code: 'method_not_found' } - }) + if (!registered.has(method)) { + expect(replies[0], `${method} on the old host`).toMatchObject({ + ok: false, + error: { code: 'method_not_found' } + }) + } else { + expect(replies[0], `${method} is registered on the old host`).not.toMatchObject({ + ok: false, + error: { code: 'method_not_found' } + }) + } } }) + + it( + 'executes every method a release-shaped checkout registers', + async () => { + // The stand-in for the release that ships this surface: the same source, + // read the way a release checkout reads it rather than through the test + // runner's module graph. It is the only place the "registered means + // usable" claim is executable today, because the baseline registers none + // of these methods — so it has to carry the whole manifest, not a sample. + const releasedCurrent = await loadAgentSessionWireBuild('HEAD') + expect(releasedCurrent.capabilities).toContain(STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY) + expect( + releasedCurrent.methodNames.filter((name) => name.startsWith('agentSession.')) + ).toHaveLength(STRUCTURED_CALLS.length) + // Each build owns its own host slot, so the one the suite installed in + // current source is not this dispatcher's. Installing here is also the + // anti-vacuous guard: without it every host-backed method answers + // `structured_agent_session_unsupported`, the same words the capability + // gate uses, and the run would read as a refusal rather than a miss. + const hostCalls = structuredHostStub() + await releasedCurrent.installStructuredHost(hostCalls) + try { + await expectDeclaredSurfaceExecutes( + releasedCurrent, + hostCalls, + releasedCurrent.capabilities + ) + } finally { + await releasedCurrent.installStructuredHost(null) + } + }, + SUITE_TIMEOUT_MS + ) }) describe('an old client against a structured-owned AI Vault row', () => { @@ -429,7 +576,7 @@ describe('cross-version structured agent sessions', () => { {}, { clientKind: 'runtime', - clientCapabilities: baseline.capabilities + clientCapabilities: legacyClientCapabilities() }, runtime ) @@ -474,7 +621,7 @@ describe('cross-version structured agent sessions', () => { params, { clientKind: 'runtime', - clientCapabilities: baseline.capabilities + clientCapabilities: legacyClientCapabilities() }, runtime ) @@ -487,7 +634,7 @@ describe('cross-version structured agent sessions', () => { current, 'session.tabs.createTerminal', { worktree: `id:${WORKSPACE}`, command: `codex resume '${THREAD}'` }, - { clientKind: 'runtime', clientCapabilities: baseline.capabilities }, + { clientKind: 'runtime', clientCapabilities: legacyClientCapabilities() }, runtime ) )[0] @@ -498,12 +645,29 @@ describe('cross-version structured agent sessions', () => { current, 'terminal.send', { terminal: 'terminal-1', text: `codex resume '${THREAD}'`, enter: true }, - { clientKind: 'runtime', clientCapabilities: baseline.capabilities }, + { clientKind: 'runtime', clientCapabilities: legacyClientCapabilities() }, runtime ) )[0] ).toMatchObject({ ok: false, error: { code: 'agent_session_conflict' } }) expect(createMobileSessionTerminal).not.toHaveBeenCalled() + + // The positive control for the three refusals above: the same client, the + // same method, a command that is not this thread's resume, and it lands. + // Without it, a stub whose shape drifted from the runtime would satisfy + // "was never called" by never being reachable at all. + expect( + ( + await callBuild( + current, + 'session.tabs.createTerminal', + { worktree: `id:${WORKSPACE}`, command: 'echo unrelated' }, + { clientKind: 'runtime', clientCapabilities: legacyClientCapabilities() }, + runtime + ) + )[0] + ).toMatchObject({ ok: true }) + expect(createMobileSessionTerminal).toHaveBeenCalledTimes(1) }) }) diff --git a/tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts b/tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts index 54d303ac871..6b3a7abe5b6 100644 --- a/tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts +++ b/tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts @@ -1,6 +1,18 @@ +// Cross-version coverage for the remote terminal stream, paired in both skew +// directions: current working tree against the newest published release. +// +// What each build publishes is read from that build, never written down here. The +// baseline is whichever release tag is newest, so a list of "fields the old side +// does not have yet" stops being true the moment a release ships one of them — the +// suite then reddens on whatever pull request is in flight, with no code change +// anywhere. Every version-dependent expectation below therefore comes from a +// same-version reference pairing of the build that publishes the frame. + import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { comparePublishedFields, publishedFieldNames } from './published-field-shape' import { resolveBaselineReleaseRef, selectLatestStableReleaseTag } from './release-checkout' import { + CrossVersionJourneyStall, JOURNEY_INPUTS, JOURNEY_STEPS, runTerminalSkewJourney, @@ -8,6 +20,7 @@ import { } from './terminal-skew-journey' import { loadTerminalWireBuild, + withoutOpcodeSupport, WORKING_TREE, type TerminalWireBuild } from './versioned-terminal-wire' @@ -42,11 +55,17 @@ const EXPECTED_JOURNEY_FRAMES = [ let baselineRef: string let current: TerminalWireBuild let baseline: TerminalWireBuild +/** What a current host publishes to a client of its own version. */ +let currentReference: JourneyRecord +/** What the baseline host publishes to a client of its own version. */ +let baselineReference: JourneyRecord beforeAll(async () => { baselineRef = resolveBaselineReleaseRef() current = await loadTerminalWireBuild(WORKING_TREE) baseline = await loadTerminalWireBuild(baselineRef) + currentReference = await runTerminalSkewJourney({ hostBuild: current, clientBuild: current }) + baselineReference = await runTerminalSkewJourney({ hostBuild: baseline, clientBuild: baseline }) }, SUITE_TIMEOUT_MS) afterEach(() => { @@ -114,20 +133,26 @@ describe('cross-version remote terminal wire', () => { SUITE_TIMEOUT_MS ) - it( - 'current client against current server completes the journey', - async () => { - const record = await runTerminalSkewJourney({ hostBuild: current, clientBuild: current }) - expectJourneyActuallyRan(record) - expectWireCompatible(record) - expect(record.snapshotStarts).toEqual([ - expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' }), - expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' }), - expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' }) - ]) - }, - SUITE_TIMEOUT_MS - ) + it('current client against current server completes the journey, and is the reference for a current host', () => { + expectJourneyActuallyRan(currentReference) + expectWireCompatible(currentReference) + // Current code's own contract in both roles, so it is safe to state literally. + expect(currentReference.snapshotStarts).toEqual([ + expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' }), + expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' }), + expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' }) + ]) + }) + + it('old client against old server completes the journey, and is the reference for an old host', () => { + expect(baselineReference.hostRevision).toBe(baseline.revision) + expect(baselineReference.clientRevision).toBe(baseline.revision) + expectJourneyActuallyRan(baselineReference) + expectWireCompatible(baselineReference) + // Anti-vacuous: a reference read from a pairing that published nothing would + // make every comparison against it trivially true. + expect(publishedFieldNames(baselineReference.snapshotStarts).length).toBeGreaterThan(4) + }) it( 'old client against new server completes the journey', @@ -136,11 +161,10 @@ describe('cross-version remote terminal wire', () => { expect(record.clientRevision).toBe(baseline.revision) expectJourneyActuallyRan(record) expectWireCompatible(record) - expect(record.snapshotStarts).toEqual([ - expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' }), - expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' }), - expect.objectContaining({ alternateScreen: false, terminalOwner: 'shell' }) - ]) + // Direction: the NEW host publishes here, and the old client only reads. Skew + // must not change what that host puts on the wire, so the expectation is the + // current host's own reference — whatever fields it carries today. + expect(record.snapshotStarts).toEqual(currentReference.snapshotStarts) }, SUITE_TIMEOUT_MS ) @@ -152,10 +176,53 @@ describe('cross-version remote terminal wire', () => { expect(record.hostRevision).toBe(baseline.revision) expectJourneyActuallyRan(record) expectWireCompatible(record) - for (const start of record.snapshotStarts) { - expect(start).not.toHaveProperty('terminalOwner') - expect(start).not.toHaveProperty('alternateScreen') - } + // Direction: the OLD host publishes here, and the new client only reads. Which + // optional fields that release shipped is a property of the release, so it is + // read from the baseline's own pairing rather than named here. + expect(record.snapshotStarts).toEqual(baselineReference.snapshotStarts) + }, + SUITE_TIMEOUT_MS + ) + + it('adds SnapshotStart fields rather than dropping ones the old host still publishes', () => { + const skew = comparePublishedFields({ + older: publishedFieldNames(baselineReference.snapshotStarts), + newer: publishedFieldNames(currentReference.snapshotStarts) + }) + // Rule 1 is additive-only. A field the old host still publishes is one an old + // client may still read, so dropping it breaks that client with no opcode + // change for the decoder check to catch. + expect( + skew.removed, + `current code stopped publishing SnapshotStart fields ${baselineRef} still publishes ` + + `(it added: ${skew.added.join(', ') || 'nothing'})` + ).toEqual([]) + }) + + it( + 'still fails a pairing whose peer cannot decode an opcode the other side sends', + async () => { + // The regression case for the guard itself: relaxing a stale field list must + // not relax the real incompatibility. A short barrier only bounds a stall + // that is already certain — the frame either arrives at once, or never. + const inputOpcode = Number(current.codec.TerminalStreamOpcode.Input) + const stall = await runTerminalSkewJourney({ + hostBuild: withoutOpcodeSupport(current, 'Input'), + clientBuild: current, + barrierTimeoutMs: 2_000 + }).then( + () => null, + (error: unknown) => error + ) + + expect(stall).toBeInstanceOf(CrossVersionJourneyStall) + const stalled = stall as CrossVersionJourneyStall + expect(stalled.step).toBe('input-reaches-process') + expect(stalled.record.completed).not.toContain('input-reaches-process') + expect(stalled.record.inputAtProcess).toEqual([]) + expect(stalled.record.rejected).toContainEqual( + expect.objectContaining({ direction: 'client-to-host', rawOpcode: inputOpcode }) + ) }, SUITE_TIMEOUT_MS ) diff --git a/tests/e2e/cross-version-wire/published-field-shape.ts b/tests/e2e/cross-version-wire/published-field-shape.ts new file mode 100644 index 00000000000..726824add2e --- /dev/null +++ b/tests/e2e/cross-version-wire/published-field-shape.ts @@ -0,0 +1,38 @@ +/** + * Reading the field shape of a published frame from the frame itself, so a + * cross-version expectation can be stated against the build that produced it. + * + * The baseline this suite pairs against is whichever release tag is newest, and + * that moves on every cut. An expectation written as a literal list of fields the + * old side does or does not have therefore expires by itself: the first release + * containing an already-merged optional field turns the assertion red on whatever + * pull request happens to be in flight, with no code change anywhere. + */ + +export type PublishedFieldSkew = { + /** Names only the newer side publishes — additive, and safe under Rule 1. */ + added: string[] + /** Names the older side still publishes and the newer side dropped — a break. */ + removed: string[] +} + +/** Sorted union of the keys across one published frame sequence. */ +export function publishedFieldNames(payloads: Record[]): string[] { + return [...new Set(payloads.flatMap((payload) => Object.keys(payload)))].sort() +} + +/** + * Which field names the two sides disagree on, by direction. Both sides are read + * from a real pairing; neither is a list this file knows. + */ +export function comparePublishedFields(args: { + older: string[] + newer: string[] +}): PublishedFieldSkew { + const older = new Set(args.older) + const newer = new Set(args.newer) + return { + added: [...newer].filter((name) => !older.has(name)).sort(), + removed: [...older].filter((name) => !newer.has(name)).sort() + } +} diff --git a/tests/e2e/cross-version-wire/published-field-shape.unit.test.ts b/tests/e2e/cross-version-wire/published-field-shape.unit.test.ts new file mode 100644 index 00000000000..53d9874f940 --- /dev/null +++ b/tests/e2e/cross-version-wire/published-field-shape.unit.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest' +import { comparePublishedFields, publishedFieldNames } from './published-field-shape' + +describe('published field shape', () => { + it('unions the keys across a frame sequence, so a field only one frame carries counts', () => { + expect( + publishedFieldNames([ + { kind: 'scrollback', seq: 0 }, + { kind: 'scrollback', seq: 27, requestId: 1 }, + { kind: 'scrollback', seq: 27 } + ]) + ).toEqual(['kind', 'requestId', 'seq']) + }) + + it('reads an empty sequence as no fields, which is what the anti-vacuous check tests', () => { + expect(publishedFieldNames([])).toEqual([]) + }) + + it('reports an added field as added and nothing as removed', () => { + expect( + comparePublishedFields({ + older: ['cols', 'kind', 'rows'], + newer: ['alternateScreen', 'cols', 'kind', 'rows', 'terminalOwner'] + }) + ).toEqual({ added: ['alternateScreen', 'terminalOwner'], removed: [] }) + }) + + it('reports a field the newer side stopped publishing as removed', () => { + expect( + comparePublishedFields({ + older: ['cols', 'kind', 'rows', 'source'], + newer: ['cols', 'kind', 'rows'] + }) + ).toEqual({ added: [], removed: ['source'] }) + }) + + it('is silent when both sides publish the same names in a different order', () => { + expect(comparePublishedFields({ older: ['rows', 'cols'], newer: ['cols', 'rows'] })).toEqual({ + added: [], + removed: [] + }) + }) + + it('does not repeat a name a caller passed twice', () => { + expect(comparePublishedFields({ older: [], newer: ['seq', 'seq'] })).toEqual({ + added: ['seq'], + removed: [] + }) + }) +}) diff --git a/tests/e2e/cross-version-wire/terminal-skew-journey.ts b/tests/e2e/cross-version-wire/terminal-skew-journey.ts index e5a84fc1683..136c8ee0179 100644 --- a/tests/e2e/cross-version-wire/terminal-skew-journey.ts +++ b/tests/e2e/cross-version-wire/terminal-skew-journey.ts @@ -63,14 +63,20 @@ function nameOpcode(build: TerminalWireBuild, opcode: number): string { return typeof name === 'string' ? name : `Opcode${opcode}` } -async function barrier(label: string, predicate: () => boolean): Promise { - try { - await vi.waitFor(() => expect(predicate()).toBe(true), { - timeout: BARRIER_TIMEOUT_MS, - interval: 5 - }) - } catch { - throw new Error(`Cross-version journey stalled at barrier: ${label}`) +/** + * A pairing that never advanced. Carries the partial record so a caller can read + * what the wire actually did — which frames the receiving decoder refused, and + * which steps completed before the stall. + */ +export class CrossVersionJourneyStall extends Error { + readonly step: JourneyStep + readonly record: JourneyRecord + + constructor(step: JourneyStep, detail: string, record: JourneyRecord) { + super(`Cross-version journey stalled at ${step}: ${detail}`) + this.name = 'CrossVersionJourneyStall' + this.step = step + this.record = record } } @@ -86,8 +92,11 @@ async function barrier(label: string, predicate: () => boolean): Promise { export async function runTerminalSkewJourney(args: { hostBuild: TerminalWireBuild clientBuild: TerminalWireBuild + /** Only lower this for a pairing whose stall is the expected outcome. */ + barrierTimeoutMs?: number }): Promise { const { hostBuild, clientBuild } = args + const barrierTimeoutMs = args.barrierTimeoutMs ?? BARRIER_TIMEOUT_MS const hostStub: HostTerminalRuntimeStub = createHostTerminalRuntimeStub({ terminalHandle: TERMINAL_HANDLE, initialBuffer: INITIAL_BUFFER @@ -114,6 +123,21 @@ export async function runTerminalSkewJourney(args: { missingRuntimeMethods: hostStub.missingRuntimeMethods } + const barrier = async ( + step: JourneyStep, + detail: string, + predicate: () => boolean + ): Promise => { + try { + await vi.waitFor(() => expect(predicate()).toBe(true), { + timeout: barrierTimeoutMs, + interval: 5 + }) + } catch { + throw new CrossVersionJourneyStall(step, detail, record) + } + } + // Name opcodes with whichever build knows more of them, so an unknown opcode in // the journey reads as `Opcode17` instead of silently borrowing a wrong name. const namingBuild = @@ -168,26 +192,27 @@ export async function runTerminalSkewJourney(args: { try { let terminal = await subscribe() - await barrier('subscribe: client never saw a `subscribed` event', () => subscribedCount >= 1) + await barrier('subscribe', 'client never saw a `subscribed` event', () => subscribedCount >= 1) record.subscribedEvents = link.connections.flatMap((connection) => connection.events.filter((event) => event.type === 'subscribed') ) record.completed.push('subscribe') await barrier( - 'first-snapshot: client never rendered the initial buffer snapshot', + 'first-snapshot', + 'client never rendered the initial buffer snapshot', () => record.snapshotsRendered.length >= 1 ) record.completed.push('first-snapshot') terminal.sendInput(FIRST_INPUT) - await barrier('input-reaches-process: host never wrote the client input to the PTY', () => + await barrier('input-reaches-process', 'host never wrote the client input to the PTY', () => hostStub.writtenInput.includes(FIRST_INPUT) ) record.completed.push('input-reaches-process') hostStub.emitOutput(LIVE_OUTPUT) - await barrier('live-output: client never rendered host output', () => + await barrier('live-output', 'client never rendered host output', () => record.dataRendered.join('').includes(LIVE_OUTPUT.trim()) ) record.completed.push('live-output') @@ -203,7 +228,8 @@ export async function runTerminalSkewJourney(args: { const closesBeforeDrop = record.transportCloses link.disconnect() await barrier( - 'transport-drop: client never observed the transport close', + 'transport-drop', + 'client never observed the transport close', () => record.transportCloses > closesBeforeDrop ) record.completed.push('transport-drop') @@ -211,7 +237,8 @@ export async function runTerminalSkewJourney(args: { const subscribedBeforeReconnect = subscribedCount terminal = await subscribe() await barrier( - 'resubscribe: client never re-established the stream after reconnect', + 'resubscribe', + 'client never re-established the stream after reconnect', () => subscribedCount > subscribedBeforeReconnect ) record.subscribedEvents = link.connections.flatMap((connection) => @@ -220,7 +247,7 @@ export async function runTerminalSkewJourney(args: { record.completed.push('resubscribe') terminal.sendInput(SECOND_INPUT) - await barrier('input-after-reconnect: host never wrote post-reconnect input to the PTY', () => + await barrier('input-after-reconnect', 'host never wrote post-reconnect input to the PTY', () => hostStub.writtenInput.includes(SECOND_INPUT) ) record.completed.push('input-after-reconnect') diff --git a/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts b/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts index 420efcbd994..1b0dc297f81 100644 --- a/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts +++ b/tests/e2e/cross-version-wire/versioned-agent-session-wire.ts @@ -1,9 +1,6 @@ -import { readFileSync, readdirSync } from 'node:fs' -import { join } from 'node:path' import { importReleaseCheckoutModule, materializeReleaseCheckout, - REPO_ROOT, type ReleaseCheckout } from './release-checkout' @@ -16,6 +13,11 @@ import { export const WORKING_TREE = 'working-tree' as const +/** Each build owns its own copy of the module-level host slot, so a host installed + * in current source is invisible to a release checkout's dispatcher. */ +const STRUCTURED_HOST_REGISTRY = + '/src/main/native-chat/agent-session-wire/structured-agent-session-registry.ts' + export type RpcReply = { id: string ok: boolean @@ -53,36 +55,34 @@ export type AgentSessionWireBuild = { /** A dispatcher carrying a method set this build really ships, so an * unknown-method answer is about the method and not an empty registry. */ createDispatcher: (runtime: unknown) => AgentSessionDispatcher + /** Put a host in *this* build's slot. Loaded on call so a release that predates + * the surface stays loadable, and throws rather than no-opping so a build with + * no slot cannot read as a surface that answered. */ + installStructuredHost: (host: unknown) => Promise } type DispatcherModule = { RpcDispatcher: new (options: { runtime: unknown; methods: unknown[] }) => AgentSessionDispatcher } -// A dotted literal in a `name:` position. Deliberately loose: over-matching only -// makes "this build registers no agentSession method" a stronger claim. -const METHOD_NAME = /\bname:\s*'([A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z0-9]+)+)'/g - -/** - * Method names declared under `runtime/rpc/methods`, scanned rather than imported: - * a released build's method manifest reaches Electron, which cannot load here. - */ -function scanMethodNames(root: string): string[] { - const names = new Set() - const walk = (directory: string): void => { - for (const entry of readdirSync(directory, { withFileTypes: true })) { - const full = join(directory, entry.name) - if (entry.isDirectory()) { - walk(full) - } else if (entry.isFile() && entry.name.endsWith('.ts')) { - for (const match of readFileSync(full, 'utf8').matchAll(METHOD_NAME)) { - names.add(match[1]!) - } +function registeredMethodNames(methods: readonly unknown[]): string[] { + return methods + .flatMap((method) => { + if (!method || typeof method !== 'object') { + return [] } - } + const name = Reflect.get(method, 'name') + return typeof name === 'string' ? [name] : [] + }) + .sort() +} + +function applyStructuredHost(module: Record, label: string, host: unknown): void { + const install = module.setStructuredAgentSessionHost + if (typeof install !== 'function') { + throw new Error(`Build ${label} publishes no structured agent-session host registry`) } - walk(join(root, 'src', 'main', 'runtime', 'rpc', 'methods')) - return [...names].sort() + ;(install as (next: unknown) => void)(host) } function capabilityStrings(module: Record): readonly string[] { @@ -94,52 +94,58 @@ function capabilityStrings(module: Record): readonly string[] { } async function loadWorkingTreeBuild(): Promise { - const [protocol, dispatcher, structured, aiVault, sessionTabs, terminal] = await Promise.all([ + const [protocol, dispatcher, methodRegistry] = await Promise.all([ import('../../../src/shared/protocol-version'), import('../../../src/main/runtime/rpc/dispatcher'), - import('../../../src/main/runtime/rpc/methods/structured-agent-session'), - import('../../../src/main/runtime/rpc/methods/ai-vault'), - import('../../../src/main/runtime/rpc/methods/session-tabs'), - import('../../../src/main/runtime/rpc/methods/terminal') + import('../../../src/main/runtime/rpc/methods') ]) const module = dispatcher as unknown as DispatcherModule + const methods = methodRegistry.ALL_RPC_METHODS as unknown[] return { label: WORKING_TREE, revision: WORKING_TREE, capabilities: capabilityStrings(protocol as unknown as Record), protocolVersion: protocol.RUNTIME_PROTOCOL_VERSION, - methodNames: scanMethodNames(REPO_ROOT), + methodNames: registeredMethodNames(methods), createDispatcher: (runtime) => new module.RpcDispatcher({ runtime, - methods: [ - ...(structured.STRUCTURED_AGENT_SESSION_METHODS as unknown[]), - ...(aiVault.AI_VAULT_METHODS as unknown[]), - ...(sessionTabs.SESSION_TAB_METHODS as unknown[]), - ...(terminal.TERMINAL_METHODS as unknown[]) - ] - }) + methods + }), + installStructuredHost: async (host) => { + const registry = + await import('../../../src/main/native-chat/agent-session-wire/structured-agent-session-registry') + applyStructuredHost(registry as unknown as Record, WORKING_TREE, host) + } } } async function loadReleaseBuild(checkout: ReleaseCheckout): Promise { - const [protocol, dispatcher, terminalMethods] = await Promise.all([ + const [protocol, dispatcher, methodRegistry] = await Promise.all([ importReleaseCheckoutModule(checkout, '/src/shared/protocol-version.ts'), importReleaseCheckoutModule(checkout, '/src/main/runtime/rpc/dispatcher.ts'), - importReleaseCheckoutModule(checkout, '/src/main/runtime/rpc/methods/terminal.ts') + importReleaseCheckoutModule(checkout, '/src/main/runtime/rpc/methods/index.ts') ]) const module = dispatcher as unknown as DispatcherModule + const methods = methodRegistry.ALL_RPC_METHODS as unknown[] return { label: checkout.ref, revision: checkout.commit, capabilities: capabilityStrings(protocol), protocolVersion: protocol.RUNTIME_PROTOCOL_VERSION as number, - methodNames: scanMethodNames(checkout.root), + methodNames: registeredMethodNames(methods), createDispatcher: (runtime) => new module.RpcDispatcher({ runtime, - methods: terminalMethods.TERMINAL_METHODS as unknown[] - }) + methods + }), + installStructuredHost: async (host) => { + applyStructuredHost( + await importReleaseCheckoutModule(checkout, STRUCTURED_HOST_REGISTRY), + checkout.ref, + host + ) + } } } diff --git a/tests/e2e/cross-version-wire/versioned-terminal-wire.ts b/tests/e2e/cross-version-wire/versioned-terminal-wire.ts index 08e721900d6..421f78cdb88 100644 --- a/tests/e2e/cross-version-wire/versioned-terminal-wire.ts +++ b/tests/e2e/cross-version-wire/versioned-terminal-wire.ts @@ -149,3 +149,37 @@ export async function loadTerminalWireBuild(ref: string): Promise` rather than borrowing a name this build lost. + TerminalStreamOpcode: Object.fromEntries( + Object.entries(build.codec.TerminalStreamOpcode).filter( + ([name, value]) => name !== opcodeName && value !== opcodeName + ) + ), + decodeTerminalStreamFrame: (bytes) => { + const frame = build.codec.decodeTerminalStreamFrame(bytes) + return frame && frame.opcode === opcode ? null : frame + } + } + } +} From eb00123a8159d51f6d278a91be33f909e819a4d8 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:43:27 -0700 Subject: [PATCH 05/80] perf(markdown): skip unmatched list tokenizer scans (#17134) --- package.json | 1 + pnpm-lock.yaml | 3 + .../editor/rich-markdown-extensions.ts | 5 +- .../rich-markdown-list-tokenizers.test.ts | 62 +++++++++++++++++++ .../editor/rich-markdown-ordered-list.ts | 17 +++++ .../editor/rich-markdown-task-list.ts | 4 ++ 6 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts create mode 100644 src/renderer/src/components/editor/rich-markdown-ordered-list.ts diff --git a/package.json b/package.json index 7d0cc956642..ef3dae2bf17 100644 --- a/package.json +++ b/package.json @@ -189,6 +189,7 @@ "@tiptap/extension-details": "^3.22.5", "@tiptap/extension-image": "^3.22.5", "@tiptap/extension-link": "^3.22.5", + "@tiptap/extension-list": "^3.22.5", "@tiptap/extension-mathematics": "3.22.5", "@tiptap/extension-placeholder": "^3.22.5", "@tiptap/extension-table": "3.22.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75b6ec89192..1a3585ccabf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -158,6 +158,9 @@ importers: '@tiptap/extension-link': specifier: ^3.22.5 version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) + '@tiptap/extension-list': + specifier: ^3.22.5 + version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) '@tiptap/extension-mathematics': specifier: 3.22.5 version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(katex@0.16.45) diff --git a/src/renderer/src/components/editor/rich-markdown-extensions.ts b/src/renderer/src/components/editor/rich-markdown-extensions.ts index ede658b577c..55cc01550fa 100644 --- a/src/renderer/src/components/editor/rich-markdown-extensions.ts +++ b/src/renderer/src/components/editor/rich-markdown-extensions.ts @@ -33,6 +33,7 @@ import { createRichMarkdownAnnotationHighlightExtension } from './rich-markdown- import type { RichMarkdownEditorCodec } from './rich-markdown-source-transport' import { createRichMarkdownHtmlSuperscriptLink } from './rich-markdown-html-superscript-link' import type { RichMarkdownHtmlSuperscriptLinkContext } from './rich-markdown-html-superscript-link-context' +import { RichMarkdownOrderedList } from './rich-markdown-ordered-list' import { RichMarkdownTaskList } from './rich-markdown-task-list' const lowlight = createLowlight(common) @@ -70,7 +71,8 @@ export function createRichMarkdownExtensions({ StarterKit.configure({ link: false, code: false, - codeBlock: false + codeBlock: false, + orderedList: false }), RichMarkdownCode, CodeBlockLowlight.extend({ @@ -195,6 +197,7 @@ export function createRichMarkdownExtensions({ }).configure({ allowBase64: true }), + RichMarkdownOrderedList, RichMarkdownTaskList, TaskItem.configure({ nested: true diff --git a/src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts b/src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts new file mode 100644 index 00000000000..1b1f645d126 --- /dev/null +++ b/src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts @@ -0,0 +1,62 @@ +import type { MarkdownLexerConfiguration, MarkdownTokenizer } from '@tiptap/core' +import { OrderedList } from '@tiptap/extension-list' +import TaskList from '@tiptap/extension-task-list' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { RichMarkdownOrderedList } from './rich-markdown-ordered-list' +import { RichMarkdownTaskList } from './rich-markdown-task-list' + +const lexer: MarkdownLexerConfiguration = { + inlineTokens: (src) => [{ type: 'text', raw: src, text: src }], + blockTokens: (src) => [{ type: 'paragraph', raw: src, text: src }] +} + +function getTokenizer(extension: typeof OrderedList | typeof TaskList): MarkdownTokenizer { + return extension.config.markdownTokenizer as MarkdownTokenizer +} + +afterEach(() => { + vi.restoreAllMocks() +}) + +describe('rich markdown list tokenizers', () => { + it.each([ + ['ordered', RichMarkdownOrderedList, OrderedList], + ['task', RichMarkdownTaskList, TaskList] + ] as const)('skips the %s base tokenizer for nonmatching source', (_name, guarded, base) => { + const baseTokenizer = getTokenizer(base) + const tokenize = vi.spyOn(baseTokenizer, 'tokenize') + const source = `# Heading\n\n${'Paragraph content.\n'.repeat(10_000)}` + + expect(getTokenizer(guarded).tokenize(source, [], lexer)).toBeUndefined() + expect(tokenize).not.toHaveBeenCalled() + }) + + it.each([ + ['ordered', RichMarkdownOrderedList, OrderedList, '3. third\n4. fourth\n'], + ['task', RichMarkdownTaskList, TaskList, '- [x] done\n- [ ] todo\n'] + ] as const)( + 'calls the %s base tokenizer once for matching source', + (_name, guarded, base, source) => { + const tokenize = vi.spyOn(getTokenizer(base), 'tokenize') + + expect(getTokenizer(guarded).tokenize(source, [], lexer)).toBeTruthy() + expect(tokenize).toHaveBeenCalledOnce() + } + ) + + it('preserves nested ordered-list tokens', () => { + const source = '3. parent\n 1. child\n 2. child two\n4. sibling\n' + + expect(getTokenizer(RichMarkdownOrderedList).tokenize(source, [], lexer)).toEqual( + getTokenizer(OrderedList).tokenize(source, [], lexer) + ) + }) + + it('preserves nested task-list tokens', () => { + const source = '- [ ] parent\n - [x] child\n- [x] sibling\n' + + expect(getTokenizer(RichMarkdownTaskList).tokenize(source, [], lexer)).toEqual( + getTokenizer(TaskList).tokenize(source, [], lexer) + ) + }) +}) diff --git a/src/renderer/src/components/editor/rich-markdown-ordered-list.ts b/src/renderer/src/components/editor/rich-markdown-ordered-list.ts new file mode 100644 index 00000000000..40a1ef4ee34 --- /dev/null +++ b/src/renderer/src/components/editor/rich-markdown-ordered-list.ts @@ -0,0 +1,17 @@ +import type { MarkdownTokenizer } from '@tiptap/core' +import { OrderedList } from '@tiptap/extension-list' + +const baseTokenizer = OrderedList.config.markdownTokenizer as MarkdownTokenizer + +export const RichMarkdownOrderedList = OrderedList.extend({ + markdownTokenizer: { + ...baseTokenizer, + tokenize(src, tokens, lexer) { + // Why: the base tokenizer scans the full remaining source before rejecting a non-list. + if (typeof baseTokenizer.start === 'function' && baseTokenizer.start(src) !== 0) { + return undefined + } + return baseTokenizer.tokenize(src, tokens, lexer) + } + } +}) diff --git a/src/renderer/src/components/editor/rich-markdown-task-list.ts b/src/renderer/src/components/editor/rich-markdown-task-list.ts index 9b59e4af5ea..5bb0d07fea2 100644 --- a/src/renderer/src/components/editor/rich-markdown-task-list.ts +++ b/src/renderer/src/components/editor/rich-markdown-task-list.ts @@ -27,6 +27,10 @@ export const RichMarkdownTaskList = TaskList.extend({ markdownTokenizer: { ...baseTokenizer, tokenize(src, tokens, lexer) { + // Why: the base tokenizer scans the full remaining source before rejecting a non-list. + if (typeof baseTokenizer.start === 'function' && baseTokenizer.start(src) !== 0) { + return undefined + } const token = baseTokenizer.tokenize(src, tokens, lexer) if (token) { normalizeTaskListToken(token, lexer) From 0bf5361c9257c81ec9e93f2517f62b8ee1c36a86 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:52:16 -0700 Subject: [PATCH 06/80] perf(markdown): update code highlighting incrementally (#17147) --- package.json | 1 + pnpm-lock.yaml | 3 + .../editor/rich-markdown-extensions.ts | 4 +- .../editor/rich-markdown-lowlight.test.ts | 467 ++++++++++++++++++ .../editor/rich-markdown-lowlight.ts | 219 ++++++++ 5 files changed, 692 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/components/editor/rich-markdown-lowlight.test.ts create mode 100644 src/renderer/src/components/editor/rich-markdown-lowlight.ts diff --git a/package.json b/package.json index ef3dae2bf17..59b7dcef7fd 100644 --- a/package.json +++ b/package.json @@ -185,6 +185,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@tiptap/extension-code-block": "^3.22.5", "@tiptap/extension-code-block-lowlight": "^3.22.5", "@tiptap/extension-details": "^3.22.5", "@tiptap/extension-image": "^3.22.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a3585ccabf..d063a91690a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,6 +146,9 @@ importers: '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) + '@tiptap/extension-code-block': + specifier: ^3.22.5 + version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5) '@tiptap/extension-code-block-lowlight': specifier: ^3.22.5 version: 3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/extension-code-block@3.22.5(@tiptap/core@3.22.5(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5))(@tiptap/pm@3.22.5)(highlight.js@11.11.1)(lowlight@3.3.0) diff --git a/src/renderer/src/components/editor/rich-markdown-extensions.ts b/src/renderer/src/components/editor/rich-markdown-extensions.ts index 55cc01550fa..91d32faa42b 100644 --- a/src/renderer/src/components/editor/rich-markdown-extensions.ts +++ b/src/renderer/src/components/editor/rich-markdown-extensions.ts @@ -3,7 +3,6 @@ import StarterKit from '@tiptap/starter-kit' import Link from '@tiptap/extension-link' import { Code } from '@tiptap/extension-code' import Image from '@tiptap/extension-image' -import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' import Placeholder from '@tiptap/extension-placeholder' import TaskItem from '@tiptap/extension-task-item' import { Table } from '@tiptap/extension-table' @@ -34,6 +33,7 @@ import type { RichMarkdownEditorCodec } from './rich-markdown-source-transport' import { createRichMarkdownHtmlSuperscriptLink } from './rich-markdown-html-superscript-link' import type { RichMarkdownHtmlSuperscriptLinkContext } from './rich-markdown-html-superscript-link-context' import { RichMarkdownOrderedList } from './rich-markdown-ordered-list' +import { RichMarkdownCodeBlockLowlight } from './rich-markdown-lowlight' import { RichMarkdownTaskList } from './rich-markdown-task-list' const lowlight = createLowlight(common) @@ -75,7 +75,7 @@ export function createRichMarkdownExtensions({ orderedList: false }), RichMarkdownCode, - CodeBlockLowlight.extend({ + RichMarkdownCodeBlockLowlight.extend({ addNodeView() { // Why: RichMarkdownCodeBlock never reads getPos, so it must not re-render // just because earlier edits shifted this block's document position. diff --git a/src/renderer/src/components/editor/rich-markdown-lowlight.test.ts b/src/renderer/src/components/editor/rich-markdown-lowlight.test.ts new file mode 100644 index 00000000000..4800a430acf --- /dev/null +++ b/src/renderer/src/components/editor/rich-markdown-lowlight.test.ts @@ -0,0 +1,467 @@ +// @vitest-environment happy-dom + +import { Editor, type JSONContent } from '@tiptap/core' +import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' +import { TextSelection, type Plugin } from '@tiptap/pm/state' +import type { Decoration, DecorationSet } from '@tiptap/pm/view' +import StarterKit from '@tiptap/starter-kit' +import { common, createLowlight } from 'lowlight' +import { afterEach, describe, expect, it } from 'vitest' +import { RichMarkdownCodeBlockLowlight } from './rich-markdown-lowlight' + +type Lowlight = ReturnType +type Counters = { highlight: number; highlightAuto: number; listLanguages: number } +type RuntimeKeyedPlugin = Plugin & { key?: string } +type DecorationWithAttrs = Decoration & { + type: { attrs?: { class?: string } } +} + +function observedLowlight(counters: Counters): Lowlight { + const base = createLowlight(common) + return { + ...base, + highlight: (...args) => { + counters.highlight += 1 + return base.highlight(...args) + }, + highlightAuto: (...args) => { + counters.highlightAuto += 1 + return base.highlightAuto(...args) + }, + listLanguages: () => { + counters.listLanguages += 1 + return base.listLanguages() + } + } +} + +function documentWithCodeBlocks(count = 3): JSONContent { + return { + type: 'doc', + content: Array.from({ length: count }, (_, index) => [ + { type: 'paragraph', content: [{ type: 'text', text: `paragraph ${index}` }] }, + { + type: 'codeBlock', + attrs: { language: index === 1 ? 'js' : index === 2 ? 'unknown' : 'typescript' }, + content: [{ type: 'text', text: `const value${index}: number = ${index}` }] + } + ]).flat() + } +} + +function createEditor({ + incremental, + counters, + content = documentWithCodeBlocks(), + defaultLanguage = null +}: { + incremental: boolean + counters: Counters + content?: JSONContent + defaultLanguage?: string | null +}): Editor { + const extension = incremental ? RichMarkdownCodeBlockLowlight : CodeBlockLowlight + return new Editor({ + element: document.createElement('div'), + extensions: [ + StarterKit.configure({ codeBlock: false, trailingNode: false }), + extension.configure({ lowlight: observedLowlight(counters), defaultLanguage }) + ], + content + }) +} + +function nodePositions(editor: Editor, name: string): number[] { + const positions: number[] = [] + editor.state.doc.descendants((node, pos) => { + if (node.type.name === name) { + positions.push(pos) + } + }) + return positions +} + +function decorationSnapshot(editor: Editor, incremental: boolean): unknown[] { + const keyPrefix = incremental ? 'richMarkdownLowlight$' : 'lowlight$' + const plugin = editor.state.plugins.find((candidate) => + (candidate as RuntimeKeyedPlugin).key?.startsWith(keyPrefix) + ) + const decorations = plugin?.getState(editor.state) as DecorationSet | undefined + if (!decorations) { + const keys = editor.state.plugins.map((candidate) => (candidate as RuntimeKeyedPlugin).key) + throw new Error(`Missing ${keyPrefix} plugin: ${JSON.stringify(keys)}`) + } + return decorations.find().map((decoration) => { + const typed = decoration as DecorationWithAttrs + return [decoration.from, decoration.to, typed.type.attrs?.class] + }) +} + +function expectParity(stock: Editor, incremental: Editor): void { + expect(incremental.state.doc.toJSON()).toEqual(stock.state.doc.toJSON()) + expect(decorationSnapshot(incremental, true)).toEqual(decorationSnapshot(stock, false)) +} + +function resetCounters(counters: Counters): void { + counters.highlight = 0 + counters.highlightAuto = 0 + counters.listLanguages = 0 +} + +describe('incremental rich markdown lowlight', () => { + const editors: Editor[] = [] + + afterEach(() => { + for (const editor of editors) { + editor.destroy() + } + editors.length = 0 + }) + + function createPair(content?: JSONContent, defaultLanguage: string | null = null) { + const stockCounters = { highlight: 0, highlightAuto: 0, listLanguages: 0 } + const incrementalCounters = { highlight: 0, highlightAuto: 0, listLanguages: 0 } + const stock = createEditor({ + incremental: false, + counters: stockCounters, + content, + defaultLanguage + }) + const incremental = createEditor({ + incremental: true, + counters: incrementalCounters, + content, + defaultLanguage + }) + editors.push(stock, incremental) + return { stock, incremental, stockCounters, incrementalCounters } + } + + it('matches stock decorations for named, aliased, and unknown languages', () => { + const { stock, incremental, stockCounters, incrementalCounters } = createPair() + + expectParity(stock, incremental) + expect(stockCounters).toEqual({ highlight: 2, highlightAuto: 1, listLanguages: 3 }) + expect(incrementalCounters).toEqual({ highlight: 2, highlightAuto: 1, listLanguages: 1 }) + }) + + it('matches stock decorations when a default language is configured', () => { + const content = documentWithCodeBlocks(1) + const codeBlock = content.content?.[1] + if (!codeBlock?.attrs) { + throw new Error('Missing code block') + } + codeBlock.attrs.language = null + const { stock, incremental, stockCounters, incrementalCounters } = createPair( + content, + 'typescript' + ) + + expectParity(stock, incremental) + expect(stockCounters.highlight).toBe(1) + expect(incrementalCounters.highlight).toBe(1) + }) + + it('maps decorations without highlighting after a prose edit', () => { + const { stock, incremental, stockCounters, incrementalCounters } = createPair() + resetCounters(stockCounters) + resetCounters(incrementalCounters) + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'paragraph')[0] + 1 + editor.view.dispatch(editor.state.tr.insertText('x', pos)) + } + + expectParity(stock, incremental) + expect(stockCounters.highlight + stockCounters.highlightAuto).toBe(0) + expect(incrementalCounters.highlight + incrementalCounters.highlightAuto).toBe(0) + }) + + it('highlights only the edited code block', () => { + const { stock, incremental, stockCounters, incrementalCounters } = createPair() + resetCounters(stockCounters) + resetCounters(incrementalCounters) + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[0] + 1 + const tr = editor.state.tr.setSelection(TextSelection.create(editor.state.doc, pos)) + editor.view.dispatch(tr.insertText('x')) + } + + expectParity(stock, incremental) + expect(stockCounters.highlight + stockCounters.highlightAuto).toBe(3) + expect(incrementalCounters.highlight + incrementalCounters.highlightAuto).toBe(1) + }) + + it('highlights only the code blocks touched by a multi-step transaction', () => { + const { stock, incremental, stockCounters, incrementalCounters } = createPair() + resetCounters(stockCounters) + resetCounters(incrementalCounters) + + for (const editor of [stock, incremental]) { + const [first, , third] = nodePositions(editor, 'codeBlock').map((pos) => pos + 1) + const tr = editor.state.tr.insertText('z', third).insertText('a', first) + tr.setSelection(TextSelection.create(tr.doc, first)) + editor.view.dispatch(tr) + } + + expectParity(stock, incremental) + expect(stockCounters.highlight + stockCounters.highlightAuto).toBe(3) + expect(incrementalCounters.highlight + incrementalCounters.highlightAuto).toBe(2) + }) + + it('falls back safely for a language attribute change', () => { + const { stock, incremental } = createPair() + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[0] + const node = editor.state.doc.nodeAt(pos) + const tr = editor.state.tr.setNodeMarkup(pos, undefined, { + ...node?.attrs, + language: 'javascript' + }) + tr.setSelection(TextSelection.create(tr.doc, pos + 1)) + editor.view.dispatch(tr) + } + + expectParity(stock, incremental) + }) + + it('preserves parity when code blocks are inserted and deleted', () => { + const { stock, incremental } = createPair() + + for (const editor of [stock, incremental]) { + const end = editor.state.doc.content.size + const codeBlock = editor.schema.nodes.codeBlock.create( + { language: 'typescript' }, + editor.schema.text('let inserted = true') + ) + editor.view.dispatch(editor.state.tr.insert(end, codeBlock)) + } + expectParity(stock, incremental) + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[1] + const node = editor.state.doc.nodeAt(pos) + if (!node) { + throw new Error('Missing code block') + } + editor.view.dispatch(editor.state.tr.delete(pos, pos + node.nodeSize)) + } + expectParity(stock, incremental) + }) + + it('preserves parity through undo and redo', () => { + const { stock, incremental } = createPair() + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[0] + 1 + const tr = editor.state.tr.setSelection(TextSelection.create(editor.state.doc, pos)) + editor.view.dispatch(tr.insertText('undoable')) + } + expectParity(stock, incremental) + + expect(stock.commands.undo()).toBe(true) + expect(incremental.commands.undo()).toBe(true) + expectParity(stock, incremental) + + expect(stock.commands.redo()).toBe(true) + expect(incremental.commands.redo()).toBe(true) + expectParity(stock, incremental) + }) + + it('preserves parity when paragraphs and code blocks are converted', () => { + const { stock, incremental } = createPair() + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'paragraph')[0] + 1 + editor.view.dispatch( + editor.state.tr.setSelection(TextSelection.create(editor.state.doc, pos)) + ) + expect(editor.commands.setCodeBlock({ language: 'typescript' })).toBe(true) + } + expectParity(stock, incremental) + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[0] + 1 + editor.view.dispatch( + editor.state.tr.setSelection(TextSelection.create(editor.state.doc, pos)) + ) + expect(editor.commands.setParagraph()).toBe(true) + } + expectParity(stock, incremental) + }) + + it('preserves parity for paste and deletion inside a code block', () => { + const { stock, incremental } = createPair() + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[0] + 2 + editor.view.dispatch( + editor.state.tr.setSelection(TextSelection.create(editor.state.doc, pos)) + ) + expect(editor.view.pasteText('pasted text')).toBe(true) + } + expectParity(stock, incremental) + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[0] + 1 + editor.view.dispatch(editor.state.tr.delete(pos, pos + 6)) + } + expectParity(stock, incremental) + }) + + it('preserves parity when a code block is split and joined', () => { + const { stock, incremental } = createPair() + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[0] + 8 + editor.view.dispatch(editor.state.tr.split(pos)) + } + expectParity(stock, incremental) + + for (const editor of [stock, incremental]) { + const joinPos = nodePositions(editor, 'codeBlock')[1] + editor.view.dispatch(editor.state.tr.join(joinPos)) + } + expectParity(stock, incremental) + }) + + it('preserves parity when a code block moves in one transaction', () => { + const { stock, incremental } = createPair() + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[0] + const node = editor.state.doc.nodeAt(pos) + if (!node) { + throw new Error('Missing code block') + } + const tr = editor.state.tr.delete(pos, pos + node.nodeSize) + const insertedAt = tr.doc.content.size + tr.insert(insertedAt, node) + tr.setSelection(TextSelection.create(tr.doc, insertedAt + 1)) + editor.view.dispatch(tr) + } + + expectParity(stock, incremental) + }) + + it('preserves parity for code blocks nested in list items', () => { + const content: JSONContent = { + type: 'doc', + content: [ + { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [ + { type: 'paragraph', content: [{ type: 'text', text: 'item' }] }, + { + type: 'codeBlock', + attrs: { language: 'typescript' }, + content: [{ type: 'text', text: 'const nested = true' }] + } + ] + } + ] + }, + { type: 'paragraph', content: [{ type: 'text', text: 'after' }] } + ] + } + const { stock, incremental } = createPair(content) + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[0] + 1 + const tr = editor.state.tr.setSelection(TextSelection.create(editor.state.doc, pos)) + editor.view.dispatch(tr.insertText('x')) + } + + expectParity(stock, incremental) + }) + + it('preserves parity after whole-document replacement', () => { + const { stock, incremental } = createPair() + + expect(stock.commands.setContent(documentWithCodeBlocks(5))).toBe(true) + expect(incremental.commands.setContent(documentWithCodeBlocks(5))).toBe(true) + + expectParity(stock, incremental) + }) + + it('does not highlight for selection-only transactions', () => { + const { stock, incremental, stockCounters, incrementalCounters } = createPair() + resetCounters(stockCounters) + resetCounters(incrementalCounters) + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[0] + 1 + editor.view.dispatch( + editor.state.tr.setSelection(TextSelection.create(editor.state.doc, pos)) + ) + } + + expectParity(stock, incremental) + expect(stockCounters.highlight + stockCounters.highlightAuto).toBe(0) + expect(incrementalCounters.highlight + incrementalCounters.highlightAuto).toBe(0) + }) + + it('replaces the stock plugin without dropping the code-block paste handler', () => { + const counters = { highlight: 0, highlightAuto: 0, listLanguages: 0 } + const editor = createEditor({ incremental: true, counters }) + editors.push(editor) + const keys = editor.state.plugins.map((plugin) => (plugin as RuntimeKeyedPlugin).key) + + expect(RichMarkdownCodeBlockLowlight.options).toEqual(CodeBlockLowlight.options) + expect(keys.some((key) => key?.startsWith('richMarkdownLowlight$'))).toBe(true) + expect(keys.some((key) => key?.startsWith('lowlight$'))).toBe(false) + expect(keys.some((key) => key?.startsWith('codeBlockVSCodeHandler$'))).toBe(true) + }) + + it('preserves parity at adjacent code-block boundaries', () => { + const content: JSONContent = { + type: 'doc', + content: documentWithCodeBlocks(3).content?.filter((node) => node.type === 'codeBlock') + } + const { stock, incremental } = createPair(content) + + for (const editor of [stock, incremental]) { + const firstPos = nodePositions(editor, 'codeBlock')[0] + const first = editor.state.doc.nodeAt(firstPos) + if (!first) { + throw new Error('Missing first code block') + } + editor.view.dispatch(editor.state.tr.insertText('tail', firstPos + first.nodeSize - 1)) + } + expectParity(stock, incremental) + + for (const editor of [stock, incremental]) { + const firstPos = nodePositions(editor, 'codeBlock')[0] + const first = editor.state.doc.nodeAt(firstPos) + if (!first) { + throw new Error('Missing first code block') + } + editor.view.dispatch(editor.state.tr.delete(firstPos, firstPos + first.nodeSize)) + } + expectParity(stock, incremental) + }) + + it('rehighlights one of 533 code blocks after a local code edit', () => { + const { stock, incremental, stockCounters, incrementalCounters } = createPair( + documentWithCodeBlocks(533) + ) + resetCounters(stockCounters) + resetCounters(incrementalCounters) + + for (const editor of [stock, incremental]) { + const pos = nodePositions(editor, 'codeBlock')[0] + 1 + const tr = editor.state.tr.setSelection(TextSelection.create(editor.state.doc, pos)) + editor.view.dispatch(tr.insertText('x')) + } + + expectParity(stock, incremental) + expect(stockCounters.highlight + stockCounters.highlightAuto).toBe(533) + expect(incrementalCounters.highlight + incrementalCounters.highlightAuto).toBe(1) + expect(incrementalCounters.listLanguages).toBe(0) + }) +}) diff --git a/src/renderer/src/components/editor/rich-markdown-lowlight.ts b/src/renderer/src/components/editor/rich-markdown-lowlight.ts new file mode 100644 index 00000000000..c6411b51f2d --- /dev/null +++ b/src/renderer/src/components/editor/rich-markdown-lowlight.ts @@ -0,0 +1,219 @@ +import { getChangedRanges } from '@tiptap/core' +import { CodeBlock } from '@tiptap/extension-code-block' +import CodeBlockLowlight, { + type CodeBlockLowlightOptions +} from '@tiptap/extension-code-block-lowlight' +import type { Node as ProseMirrorNode } from '@tiptap/pm/model' +import { Plugin, PluginKey, type Transaction } from '@tiptap/pm/state' +import { Decoration, DecorationSet } from '@tiptap/pm/view' +import type { createLowlight } from 'lowlight' + +type Lowlight = ReturnType +type HighlightNode = ReturnType['children'][number] +type CodeBlockAt = { node: ProseMirrorNode; pos: number } +type DocumentRange = { from: number; to: number } +type HighlightSpan = { text: string; classes: string[] } + +function parseHighlightNodes(nodes: HighlightNode[], classes: string[] = []): HighlightSpan[] { + return nodes.flatMap((node) => { + if (node.type === 'text') { + return [{ text: node.value, classes }] + } + if (node.type !== 'element') { + return [] + } + + const className = node.properties.className + const ownClasses = Array.isArray(className) + ? className.map(String) + : typeof className === 'string' + ? [className] + : [] + return parseHighlightNodes(node.children, [...classes, ...ownClasses]) + }) +} + +function highlightBlock( + block: CodeBlockAt, + lowlight: Lowlight, + languages: ReadonlySet, + defaultLanguage: string | null | undefined +): Decoration[] { + const language = (block.node.attrs.language as string | null | undefined) || defaultLanguage + const highlighted = + language && (languages.has(language) || lowlight.registered(language)) + ? lowlight.highlight(language, block.node.textContent) + : lowlight.highlightAuto(block.node.textContent) + const decorations: Decoration[] = [] + let from = block.pos + 1 + + for (const span of parseHighlightNodes(highlighted.children)) { + const to = from + span.text.length + if (span.classes.length > 0) { + decorations.push(Decoration.inline(from, to, { class: span.classes.join(' ') })) + } + from = to + } + + return decorations +} + +function findAllCodeBlocks(doc: ProseMirrorNode, name: string): CodeBlockAt[] { + const blocks: CodeBlockAt[] = [] + doc.descendants((node, pos) => { + if (node.type.name === name) { + blocks.push({ node, pos }) + return false + } + return true + }) + return blocks +} + +function findCodeBlocksNearRanges( + doc: ProseMirrorNode, + name: string, + ranges: DocumentRange[] +): CodeBlockAt[] { + const blocks = new Map() + const addResolvedBlocks = (position: number): void => { + // Why: zero-width inserts and deletes have no nodesBetween span. + const $position = doc.resolve(Math.max(0, Math.min(doc.content.size, position))) + for (let depth = $position.depth; depth > 0; depth -= 1) { + const node = $position.node(depth) + if (node.type.name === name) { + blocks.set($position.before(depth), node) + break + } + } + if ($position.nodeBefore?.type.name === name) { + blocks.set($position.pos - $position.nodeBefore.nodeSize, $position.nodeBefore) + } + if ($position.nodeAfter?.type.name === name) { + blocks.set($position.pos, $position.nodeAfter) + } + } + + for (const range of ranges) { + const from = Math.max(0, Math.min(doc.content.size, range.from)) + const to = Math.max(from, Math.min(doc.content.size, range.to)) + if (from < to) { + doc.nodesBetween(from, to, (node, pos) => { + if (node.type.name === name) { + blocks.set(pos, node) + return false + } + return true + }) + } + addResolvedBlocks(from) + if (to !== from) { + addResolvedBlocks(to) + } + } + return [...blocks].map(([pos, node]) => ({ node, pos })) +} + +function createDecorations( + doc: ProseMirrorNode, + name: string, + lowlight: Lowlight, + languages: ReadonlySet, + defaultLanguage: string | null | undefined +): DecorationSet { + return DecorationSet.create( + doc, + findAllCodeBlocks(doc, name).flatMap((block) => + highlightBlock(block, lowlight, languages, defaultLanguage) + ) + ) +} + +function updateDecorations( + transaction: Transaction, + decorationSet: DecorationSet, + name: string, + lowlight: Lowlight, + languages: ReadonlySet, + defaultLanguage: string | null | undefined +): DecorationSet { + if (!transaction.docChanged) { + return decorationSet + } + + const changes = getChangedRanges(transaction) + if (changes.length === 0) { + // Why: attribute-only steps can change a code language without exposing a mapped range. + return createDecorations(transaction.doc, name, lowlight, languages, defaultLanguage) + } + + // Why: mapped decorations stay valid outside code blocks touched by the transaction. + let next = decorationSet.map(transaction.mapping, transaction.doc) + const oldBlocks = findCodeBlocksNearRanges( + transaction.before, + name, + changes.map((change) => change.oldRange) + ) + const staleDecorations = new Set() + for (const block of oldBlocks) { + const from = transaction.mapping.map(block.pos, -1) + const to = transaction.mapping.map(block.pos + block.node.nodeSize, 1) + for (const decoration of next.find(Math.min(from, to), Math.max(from, to))) { + staleDecorations.add(decoration) + } + } + if (staleDecorations.size > 0) { + next = next.remove([...staleDecorations]) + } + + const newBlocks = findCodeBlocksNearRanges( + transaction.doc, + name, + changes.map((change) => change.newRange) + ) + return next.add( + transaction.doc, + newBlocks.flatMap((block) => highlightBlock(block, lowlight, languages, defaultLanguage)) + ) +} + +function createRichMarkdownLowlightPlugin({ + name, + lowlight, + defaultLanguage +}: { + name: string + lowlight: Lowlight + defaultLanguage: string | null | undefined +}): Plugin { + const languages = new Set(lowlight.listLanguages()) + const key = new PluginKey('richMarkdownLowlight') + const plugin = new Plugin({ + key, + state: { + init: (_, { doc }) => createDecorations(doc, name, lowlight, languages, defaultLanguage), + apply: (transaction, decorationSet) => + updateDecorations(transaction, decorationSet, name, lowlight, languages, defaultLanguage) + }, + props: { + decorations: (state) => key.getState(state) + } + }) + return plugin +} + +export const RichMarkdownCodeBlockLowlight = CodeBlock.extend({ + addOptions() { + return { ...CodeBlockLowlight.options } + }, + addProseMirrorPlugins() { + return [ + ...(this.parent?.() ?? []), + createRichMarkdownLowlightPlugin({ + name: this.name, + lowlight: this.options.lowlight as Lowlight, + defaultLanguage: this.options.defaultLanguage + }) + ] + } +}) From 78bba0772a1a3ea0c8cd898c2693d1be0efc4a41 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:59:45 -0700 Subject: [PATCH 07/80] perf(markdown): cache repeated syntax highlighting (#17158) --- .../editor/rich-markdown-extensions.ts | 3 +- .../rich-markdown-lowlight-cache.test.ts | 117 ++++++++++++++++ .../editor/rich-markdown-lowlight-cache.ts | 127 ++++++++++++++++++ 3 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/components/editor/rich-markdown-lowlight-cache.test.ts create mode 100644 src/renderer/src/components/editor/rich-markdown-lowlight-cache.ts diff --git a/src/renderer/src/components/editor/rich-markdown-extensions.ts b/src/renderer/src/components/editor/rich-markdown-extensions.ts index 91d32faa42b..9876f2871cc 100644 --- a/src/renderer/src/components/editor/rich-markdown-extensions.ts +++ b/src/renderer/src/components/editor/rich-markdown-extensions.ts @@ -35,8 +35,9 @@ import type { RichMarkdownHtmlSuperscriptLinkContext } from './rich-markdown-htm import { RichMarkdownOrderedList } from './rich-markdown-ordered-list' import { RichMarkdownCodeBlockLowlight } from './rich-markdown-lowlight' import { RichMarkdownTaskList } from './rich-markdown-task-list' +import { createCachedLowlight } from './rich-markdown-lowlight-cache' -const lowlight = createLowlight(common) +const lowlight = createCachedLowlight(createLowlight(common)) const RichMarkdownLink = Link.extend({ // Why: link's priority must stay below code's default 100 so Markdown diff --git a/src/renderer/src/components/editor/rich-markdown-lowlight-cache.test.ts b/src/renderer/src/components/editor/rich-markdown-lowlight-cache.test.ts new file mode 100644 index 00000000000..fd944cc7c69 --- /dev/null +++ b/src/renderer/src/components/editor/rich-markdown-lowlight-cache.test.ts @@ -0,0 +1,117 @@ +import { common, createLowlight } from 'lowlight' +import { describe, expect, it, vi } from 'vitest' +import { createCachedLowlight } from './rich-markdown-lowlight-cache' + +const CODE = 'export function handler(input: string): string { return input.trim() }' + +describe('createCachedLowlight', () => { + it('reuses one exact-language result across 533 identical code blocks', () => { + const lowlight = createLowlight(common) + const highlight = vi.spyOn(lowlight, 'highlight') + const cached = createCachedLowlight(lowlight) + + for (let index = 0; index < 533; index += 1) { + cached.highlight('typescript', CODE) + } + + expect(highlight).toHaveBeenCalledOnce() + }) + + it('matches lowlight for exact-language and automatic highlighting', () => { + const stock = createLowlight(common) + const cached = createCachedLowlight(createLowlight(common)) + + expect(cached.highlight('typescript', CODE, { prefix: 'syntax-' })).toEqual( + stock.highlight('typescript', CODE, { prefix: 'syntax-' }) + ) + expect( + cached.highlightAuto(CODE, { + prefix: 'syntax-', + subset: ['javascript', 'typescript'] + }) + ).toEqual( + stock.highlightAuto(CODE, { + prefix: 'syntax-', + subset: ['javascript', 'typescript'] + }) + ) + }) + + it('preserves language registration', () => { + const cached = createCachedLowlight(createLowlight()) + + cached.register('typescript', common.typescript) + + expect(cached.listLanguages()).toEqual(['typescript']) + expect(cached.registered('typescript')).toBe(true) + expect(cached.highlight('typescript', CODE).children).not.toHaveLength(0) + }) + + it('keys explicit language, automatic mode, and supported options independently', () => { + const lowlight = createLowlight(common) + const highlight = vi.spyOn(lowlight, 'highlight') + const highlightAuto = vi.spyOn(lowlight, 'highlightAuto') + const cached = createCachedLowlight(lowlight) + + cached.highlight('typescript', CODE, { prefix: 'first-' }) + cached.highlight('typescript', CODE, { prefix: 'second-' }) + cached.highlight('typescript', CODE, { prefix: 'first-' }) + cached.highlightAuto(CODE, { subset: ['typescript'] }) + cached.highlightAuto(CODE, { subset: ['javascript'] }) + cached.highlightAuto(CODE, { subset: ['typescript'] }) + + expect(highlight).toHaveBeenCalledTimes(2) + expect(highlightAuto).toHaveBeenCalledTimes(2) + }) + + it('preserves registration APIs and invalidates results after registry changes', () => { + const lowlight = createLowlight(common) + const reference = createLowlight(common) + const highlight = vi.spyOn(lowlight, 'highlight') + const cached = createCachedLowlight(lowlight) + + cached.highlight('javascript', CODE) + cached.highlight('javascript', CODE) + cached.registerAlias('javascript', 'cached-js') + reference.registerAlias('javascript', 'cached-js') + + expect(cached.registered('cached-js')).toBe(true) + expect(cached.highlight('cached-js', CODE)).toEqual(reference.highlight('cached-js', CODE)) + cached.highlight('javascript', CODE) + expect(highlight).toHaveBeenCalledTimes(3) + }) + + it('evicts least-recently-used results by entry count', () => { + const lowlight = createLowlight(common) + const highlight = vi.spyOn(lowlight, 'highlight') + const cached = createCachedLowlight(lowlight, { + maxEntries: 2, + maxSourceCharacters: 100 + }) + + cached.highlight('typescript', 'first') + cached.highlight('typescript', 'second') + cached.highlight('typescript', 'first') + cached.highlight('typescript', 'third') + cached.highlight('typescript', 'second') + + expect(highlight).toHaveBeenCalledTimes(4) + }) + + it('bounds retained source text and does not retain oversized blocks', () => { + const lowlight = createLowlight(common) + const highlight = vi.spyOn(lowlight, 'highlight') + const cached = createCachedLowlight(lowlight, { + maxEntries: 10, + maxSourceCharacters: 5 + }) + + cached.highlight('typescript', '123') + cached.highlight('typescript', '456') + cached.highlight('typescript', '123') + cached.highlight('typescript', '123456') + cached.highlight('typescript', '123456') + + expect(highlight).toHaveBeenCalledTimes(5) + }) +}) diff --git a/src/renderer/src/components/editor/rich-markdown-lowlight-cache.ts b/src/renderer/src/components/editor/rich-markdown-lowlight-cache.ts new file mode 100644 index 00000000000..d2c5882438f --- /dev/null +++ b/src/renderer/src/components/editor/rich-markdown-lowlight-cache.ts @@ -0,0 +1,127 @@ +import type { AutoOptions, createLowlight, LanguageFn, Options } from 'lowlight' + +type Lowlight = ReturnType +type HighlightResult = ReturnType + +type HighlightCacheEntry = { + result: HighlightResult + sourceCharacters: number +} + +type HighlightCacheLimits = { + maxEntries: number + maxSourceCharacters: number +} + +// Source text bounds retained HASTs; the entry cap also covers empty and tiny blocks. +const DEFAULT_CACHE_LIMITS: HighlightCacheLimits = { + maxEntries: 256, + maxSourceCharacters: 128 * 1024 +} + +function prefixKey(options: Readonly | null | undefined): string | null { + return options?.prefix ?? null +} + +function highlightKey( + language: string, + value: string, + options: Readonly | null | undefined +): string { + return JSON.stringify(['language', language, prefixKey(options), value]) +} + +function highlightAutoKey( + value: string, + options: Readonly | null | undefined +): string { + return JSON.stringify(['auto', prefixKey(options), options?.subset ?? null, value]) +} + +export function createCachedLowlight( + lowlight: Lowlight, + limits: HighlightCacheLimits = DEFAULT_CACHE_LIMITS +): Lowlight { + const cache = new Map() + let retainedSourceCharacters = 0 + + const clear = (): void => { + cache.clear() + retainedSourceCharacters = 0 + } + + const getOrHighlight = ( + createKey: () => string, + sourceCharacters: number, + highlight: () => HighlightResult + ): HighlightResult => { + if ( + limits.maxEntries <= 0 || + limits.maxSourceCharacters <= 0 || + sourceCharacters > limits.maxSourceCharacters + ) { + return highlight() + } + + const key = createKey() + const cached = cache.get(key) + if (cached) { + cache.delete(key) + cache.set(key, cached) + return cached.result + } + const result = highlight() + cache.set(key, { result, sourceCharacters }) + retainedSourceCharacters += sourceCharacters + while ( + cache.size > limits.maxEntries || + retainedSourceCharacters > limits.maxSourceCharacters + ) { + const oldest = cache.entries().next() + if (oldest.done) { + break + } + retainedSourceCharacters -= oldest.value[1].sourceCharacters + cache.delete(oldest.value[0]) + } + return result + } + + const register = (( + grammarsOrName: Readonly> | string, + grammar?: LanguageFn + ): undefined => { + clear() + return typeof grammarsOrName === 'string' + ? lowlight.register(grammarsOrName, grammar as LanguageFn) + : lowlight.register(grammarsOrName) + }) as Lowlight['register'] + + const registerAlias = (( + aliasesOrName: Readonly> | string, + alias?: readonly string[] | string + ): undefined => { + clear() + return typeof aliasesOrName === 'string' + ? lowlight.registerAlias(aliasesOrName, alias as readonly string[] | string) + : lowlight.registerAlias(aliasesOrName) + }) as Lowlight['registerAlias'] + + return { + ...lowlight, + highlight: (language, value, options) => + getOrHighlight( + () => highlightKey(language, value, options), + value.length, + () => lowlight.highlight(language, value, options) + ), + highlightAuto: (value, options) => + getOrHighlight( + () => highlightAutoKey(value, options), + value.length, + () => lowlight.highlightAuto(value, options) + ), + register, + registerAlias + } +} From b17f60d744c05a58ee054260878af1dd8cdd3afb Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:13:26 -0700 Subject: [PATCH 08/80] build: upgrade to pnpm 12 (#17156) --- .../install-node-dependencies/action.yml | 7 +- .github/workflows/adhoc-mac-build.yml | 5 +- .github/workflows/daemon-relocation-spike.yml | 5 +- .github/workflows/daily-mac-build.yml | 5 +- .github/workflows/dev-channel-win-build.yml | 5 +- .github/workflows/golden-e2e-experiment.yml | 5 +- .github/workflows/hourly-mac-build.yml | 5 +- .../workflows/linux-wayland-gpu-sandbox.yml | 12 +- .github/workflows/mobile-android-release.yml | 5 +- .github/workflows/mobile-ios-release.yml | 5 +- .github/workflows/mobile.yml | 5 +- .github/workflows/release-cut.yml | 15 +- .github/workflows/release-mac-build.yml | 5 +- .github/workflows/terminal-ime-e2e.yml | 5 +- .github/workflows/terminal-perf.yml | 5 +- .github/workflows/win-crash-survival-e2e.yml | 5 +- .github/workflows/win-update-e2e.yml | 5 +- .github/workflows/win-update-survival-e2e.yml | 5 +- .../workflows/windows-signing-rehearsal.yml | 5 +- .../windows-terminal-restart-e2e.yml | 5 +- .npmrc | 2 - .../docker/headless-pairing/Dockerfile.build | 3 +- .../@vscode__windows-process-tree@0.8.0.patch | 2 +- .../@xterm__addon-webgl@0.20.0-beta.286.patch | 12 +- config/scripts/computer-e2e-workflow.test.mjs | 2 +- config/scripts/ensure-native-runtime.mjs | 3 +- .../install-node-dependencies-action.test.mjs | 10 +- ...package-electron-runtime-contract.test.mjs | 17 +-- .../scripts/pr-workflow-parallelism.test.mjs | 20 ++- config/scripts/regenerate-xterm-patches.mjs | 17 ++- .../scripts/regenerate-xterm-patches.test.mjs | 25 ++-- config/scripts/verify-telemetry-constants.mjs | 2 +- ...ndows-process-tree-patch-contract.test.mjs | 7 +- docs/reference/windows-process-enumeration.md | 2 +- mobile/.npmrc | 1 - mobile/package.json | 8 - mobile/pnpm-lock.yaml | 4 +- mobile/pnpm-workspace.yaml | 5 + package.json | 36 +---- pnpm-lock.yaml | 138 ++++++++++++++---- pnpm-workspace.yaml | 40 +++++ ...inal-windows-codex-multiline-paste.spec.ts | 2 +- 42 files changed, 277 insertions(+), 200 deletions(-) delete mode 100644 .npmrc delete mode 100644 mobile/.npmrc diff --git a/.github/actions/install-node-dependencies/action.yml b/.github/actions/install-node-dependencies/action.yml index 8b7801881eb..f06f0ffa8d8 100644 --- a/.github/actions/install-node-dependencies/action.yml +++ b/.github/actions/install-node-dependencies/action.yml @@ -28,10 +28,9 @@ runs: steps: # setup-node needs pnpm on PATH to locate and restore its store. - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Setup Node.js id: default-node @@ -88,7 +87,7 @@ runs: pnpm install --frozen-lockfile --ignore-scripts # Job containers can run composite steps from a source mirror without .git. if [ "$(git -C "$GITHUB_WORKSPACE" rev-parse --is-inside-work-tree 2>/dev/null)" = true ]; then - git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml + git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml fi # Why cached: `--ignore-scripts` leaves node-pty without build/Release, so diff --git a/.github/workflows/adhoc-mac-build.yml b/.github/workflows/adhoc-mac-build.yml index 552444046ce..59d705bf67e 100644 --- a/.github/workflows/adhoc-mac-build.yml +++ b/.github/workflows/adhoc-mac-build.yml @@ -171,10 +171,9 @@ jobs: persist-credentials: false - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/daemon-relocation-spike.yml b/.github/workflows/daemon-relocation-spike.yml index 161c4894138..2ffd4a58661 100644 --- a/.github/workflows/daemon-relocation-spike.yml +++ b/.github/workflows/daemon-relocation-spike.yml @@ -43,10 +43,9 @@ jobs: node-version-file: package.json - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/daily-mac-build.yml b/.github/workflows/daily-mac-build.yml index b439b7caad4..45130e932d8 100644 --- a/.github/workflows/daily-mac-build.yml +++ b/.github/workflows/daily-mac-build.yml @@ -145,10 +145,9 @@ jobs: - name: Setup pnpm if: steps.freshness.outputs.should_build == 'true' - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Setup Node.js if: steps.freshness.outputs.should_build == 'true' diff --git a/.github/workflows/dev-channel-win-build.yml b/.github/workflows/dev-channel-win-build.yml index 35abbf1c495..e16a50f1c3c 100644 --- a/.github/workflows/dev-channel-win-build.yml +++ b/.github/workflows/dev-channel-win-build.yml @@ -191,10 +191,9 @@ jobs: # pnpm must be on PATH before setup-node so setup-node can locate the store. - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/golden-e2e-experiment.yml b/.github/workflows/golden-e2e-experiment.yml index 3216fb66a72..d46c80033fa 100644 --- a/.github/workflows/golden-e2e-experiment.yml +++ b/.github/workflows/golden-e2e-experiment.yml @@ -43,10 +43,9 @@ jobs: node-version-file: package.json - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false # Why: Linux golden E2E uses the same native install path as PR/release CI, # which needs pnpm to bypass its non-executable bundled gyp_main.py. diff --git a/.github/workflows/hourly-mac-build.yml b/.github/workflows/hourly-mac-build.yml index d4dc4d3e391..ac3af92a3bc 100644 --- a/.github/workflows/hourly-mac-build.yml +++ b/.github/workflows/hourly-mac-build.yml @@ -135,10 +135,9 @@ jobs: - name: Setup pnpm if: steps.freshness.outputs.should_build == 'true' - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Setup Node.js if: steps.freshness.outputs.should_build == 'true' diff --git a/.github/workflows/linux-wayland-gpu-sandbox.yml b/.github/workflows/linux-wayland-gpu-sandbox.yml index 96256d5193c..15af59da8a5 100644 --- a/.github/workflows/linux-wayland-gpu-sandbox.yml +++ b/.github/workflows/linux-wayland-gpu-sandbox.yml @@ -24,10 +24,9 @@ jobs: node-version-file: package.json - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false # Why: mirrors pr.yml so native module rebuilds do not use pnpm's # non-executable bundled gyp_main.py on Linux runners. @@ -44,12 +43,9 @@ jobs: fi - name: Install dependencies - # Why: pnpm 10.24's frozen headless fast path can fail on fresh Ubuntu - # runners while creating the root node_modules. Use the normal resolver - # path, then verify package metadata stayed unchanged. run: | - pnpm install --no-frozen-lockfile --prefer-frozen-lockfile=false - git diff --exit-code package.json pnpm-lock.yaml + pnpm install --frozen-lockfile + git diff --exit-code package.json pnpm-lock.yaml pnpm-workspace.yaml - name: Start Weston run: | diff --git a/.github/workflows/mobile-android-release.yml b/.github/workflows/mobile-android-release.yml index 6dfc99b8273..35e900dc31c 100644 --- a/.github/workflows/mobile-android-release.yml +++ b/.github/workflows/mobile-android-release.yml @@ -44,10 +44,9 @@ jobs: node-version: 24 - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/mobile-ios-release.yml b/.github/workflows/mobile-ios-release.yml index 31fe546fbf0..934b3f694a3 100644 --- a/.github/workflows/mobile-ios-release.yml +++ b/.github/workflows/mobile-ios-release.yml @@ -64,10 +64,9 @@ jobs: node-version: 24 - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index ce968e54fab..59f6cf20bf4 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -51,10 +51,9 @@ jobs: working-directory: mobile - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false # Why: the mobile typecheck imports shared types from ../src/shared, and # some of those files import runtime deps (tweetnacl, ws) resolved from diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index 1d65ad89a21..f4db35bc314 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -864,10 +864,9 @@ jobs: node-version-file: package.json - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false # Why: Linux terminal golden E2E uses the same native install path as # release CI, which needs pnpm to bypass its non-executable gyp_main.py. @@ -1080,10 +1079,9 @@ jobs: node-version-file: package.json - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false # Why: keep the non-blocking evidence lane on the same Linux native # install path as the blocking golden and release build jobs. @@ -1195,10 +1193,9 @@ jobs: # pnpm must be on PATH before setup-node so setup-node can locate the store for caching. - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/release-mac-build.yml b/.github/workflows/release-mac-build.yml index 54fbc65d9c8..f97ac0e5537 100644 --- a/.github/workflows/release-mac-build.yml +++ b/.github/workflows/release-mac-build.yml @@ -38,10 +38,9 @@ jobs: ref: refs/tags/${{ inputs.tag }} - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/terminal-ime-e2e.yml b/.github/workflows/terminal-ime-e2e.yml index 5cf83589b02..83f5b839828 100644 --- a/.github/workflows/terminal-ime-e2e.yml +++ b/.github/workflows/terminal-ime-e2e.yml @@ -41,10 +41,9 @@ jobs: node-version-file: package.json - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Use external node-gyp to avoid pnpm bundled copy run: | diff --git a/.github/workflows/terminal-perf.yml b/.github/workflows/terminal-perf.yml index 6c3866a8ea0..38d0a25bbb7 100644 --- a/.github/workflows/terminal-perf.yml +++ b/.github/workflows/terminal-perf.yml @@ -73,10 +73,9 @@ jobs: node-version-file: package.json - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false # Why: this scheduled/manual workflow uses the same native install path as # PR and E2E CI, which needs pnpm to bypass its bundled gyp_main.py. diff --git a/.github/workflows/win-crash-survival-e2e.yml b/.github/workflows/win-crash-survival-e2e.yml index 13fae0c4126..f3d22cc1227 100644 --- a/.github/workflows/win-crash-survival-e2e.yml +++ b/.github/workflows/win-crash-survival-e2e.yml @@ -46,10 +46,9 @@ jobs: # Why: setup-node can restore pnpm's content-addressed store only after # the pnpm binary exists, avoiding repeat dependency downloads per run. - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/win-update-e2e.yml b/.github/workflows/win-update-e2e.yml index 604ccd92b14..2a1bde70924 100644 --- a/.github/workflows/win-update-e2e.yml +++ b/.github/workflows/win-update-e2e.yml @@ -85,10 +85,9 @@ jobs: node-version-file: package.json - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false # Why: the harness only needs its runtime deps (the Playwright Electron # driver); it drives already-built installer artifacts, so no app build. diff --git a/.github/workflows/win-update-survival-e2e.yml b/.github/workflows/win-update-survival-e2e.yml index f3d3c21bff7..e7ed41125e9 100644 --- a/.github/workflows/win-update-survival-e2e.yml +++ b/.github/workflows/win-update-survival-e2e.yml @@ -58,10 +58,9 @@ jobs: node-version-file: package.json - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/windows-signing-rehearsal.yml b/.github/workflows/windows-signing-rehearsal.yml index 18aadd7d933..54b751908dc 100644 --- a/.github/workflows/windows-signing-rehearsal.yml +++ b/.github/workflows/windows-signing-rehearsal.yml @@ -46,10 +46,9 @@ jobs: persist-credentials: false - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/windows-terminal-restart-e2e.yml b/.github/workflows/windows-terminal-restart-e2e.yml index 3a5b4a35715..abb8dc89aa8 100644 --- a/.github/workflows/windows-terminal-restart-e2e.yml +++ b/.github/workflows/windows-terminal-restart-e2e.yml @@ -32,10 +32,9 @@ jobs: # Why: pnpm must exist before setup-node resolves its dependency cache. - name: Setup pnpm - uses: pnpm/action-setup@v6 + uses: pnpm/setup@v2 with: - version: 10.24.0 - run_install: false + install: false - name: Setup Node.js uses: actions/setup-node@v6 diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 313e39e8526..00000000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -shamefully-hoist=true -minimum-release-age=4320 diff --git a/config/docker/headless-pairing/Dockerfile.build b/config/docker/headless-pairing/Dockerfile.build index 900607a7a5c..19f76290c75 100644 --- a/config/docker/headless-pairing/Dockerfile.build +++ b/config/docker/headless-pairing/Dockerfile.build @@ -26,6 +26,7 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* RUN corepack enable \ - && corepack prepare pnpm@10.24.0 --activate + && corepack prepare pnpm@12.0.0 --activate \ + && pnpm --version WORKDIR /workspace diff --git a/config/patches/@vscode__windows-process-tree@0.8.0.patch b/config/patches/@vscode__windows-process-tree@0.8.0.patch index 65cf2be5ce4..10780f5288a 100644 --- a/config/patches/@vscode__windows-process-tree@0.8.0.patch +++ b/config/patches/@vscode__windows-process-tree@0.8.0.patch @@ -10,7 +10,7 @@ index 855bd4b86f0a3c18c7594212c0e42b6e35bc4001..33774e7ae296f0de39dd94156673c9e7 ], "conditions": [ ['OS=="win"', { -@@ -15,14 +14,13 @@ +@@ -15,12 +14,11 @@ "src/process_commandline.cc" ], - "include_dirs": [], diff --git a/config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch b/config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch index dece243837f..de8de45664f 100644 --- a/config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch +++ b/config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch @@ -1,4 +1,4 @@ -diff -ruN a/lib/addon-webgl.js b/lib/addon-webgl.js +diff --git a/lib/addon-webgl.js b/lib/addon-webgl.js --- a/lib/addon-webgl.js +++ b/lib/addon-webgl.js @@ -1,2 +1,15 @@ @@ -20,7 +20,7 @@ diff -ruN a/lib/addon-webgl.js b/lib/addon-webgl.js + outColor = texture(u_texture[0], v_texcoord); + } ${e4} else { outColor = vec4(0.0, 0.0, 0.0, 0.0); } +}`})(s2.TextureAtlas.maxAtlasPages),this._logService)),this._register((0,a.toDisposable)(()=>h2.deleteProgram(this._program))),this._projectionLocation=(0,n.throwIfFalsy)(h2.getUniformLocation(this._program,"u_projection")),this._resolutionLocation=(0,n.throwIfFalsy)(h2.getUniformLocation(this._program,"u_resolution")),this._textureLocation=(0,n.throwIfFalsy)(h2.getUniformLocation(this._program,"u_texture"));const T2=this._vertexArrayObject=h2.createVertexArray();this._register((0,a.toDisposable)(()=>h2.deleteVertexArray(T2))),h2.bindVertexArray(T2);const l2=new Float32Array([0,0,1,0,0,1,1,1]),d2=h2.createBuffer();this._register((0,a.toDisposable)(()=>h2.deleteBuffer(d2))),h2.bindBuffer(h2.ARRAY_BUFFER,d2),h2.bufferData(h2.ARRAY_BUFFER,l2,h2.STATIC_DRAW),h2.enableVertexAttribArray(0),h2.vertexAttribPointer(0,2,this._gl.FLOAT,false,0,0);const c=new Uint8Array([0,1,2,3]),_=h2.createBuffer();this._register((0,a.toDisposable)(()=>h2.deleteBuffer(_))),h2.bindBuffer(h2.ELEMENT_ARRAY_BUFFER,_),h2.bufferData(h2.ELEMENT_ARRAY_BUFFER,c,h2.STATIC_DRAW),this._attributesBuffer=(0,n.throwIfFalsy)(h2.createBuffer()),this._register((0,a.toDisposable)(()=>h2.deleteBuffer(this._attributesBuffer))),h2.bindBuffer(h2.ARRAY_BUFFER,this._attributesBuffer),h2.enableVertexAttribArray(2),h2.vertexAttribPointer(2,2,h2.FLOAT,false,44,0),h2.vertexAttribDivisor(2,1),h2.enableVertexAttribArray(3),h2.vertexAttribPointer(3,2,h2.FLOAT,false,44,2*Float32Array.BYTES_PER_ELEMENT),h2.vertexAttribDivisor(3,1),h2.enableVertexAttribArray(4),h2.vertexAttribPointer(4,1,h2.FLOAT,false,44,4*Float32Array.BYTES_PER_ELEMENT),h2.vertexAttribDivisor(4,1),h2.enableVertexAttribArray(5),h2.vertexAttribPointer(5,2,h2.FLOAT,false,44,5*Float32Array.BYTES_PER_ELEMENT),h2.vertexAttribDivisor(5,1),h2.enableVertexAttribArray(6),h2.vertexAttribPointer(6,2,h2.FLOAT,false,44,7*Float32Array.BYTES_PER_ELEMENT),h2.vertexAttribDivisor(6,1),h2.enableVertexAttribArray(1),h2.vertexAttribPointer(1,2,h2.FLOAT,false,44,9*Float32Array.BYTES_PER_ELEMENT),h2.vertexAttribDivisor(1,1),h2.useProgram(this._program);const Q=new Int32Array(s2.TextureAtlas.maxAtlasPages);for(let t4=0;t4h2.deleteTexture(e4.texture))),h2.activeTexture(h2.TEXTURE0+t4),h2.bindTexture(h2.TEXTURE_2D,e4.texture),h2.texParameteri(h2.TEXTURE_2D,h2.TEXTURE_WRAP_S,h2.CLAMP_TO_EDGE),h2.texParameteri(h2.TEXTURE_2D,h2.TEXTURE_WRAP_T,h2.CLAMP_TO_EDGE),h2.texImage2D(h2.TEXTURE_2D,0,h2.RGBA,1,1,0,h2.RGBA,h2.UNSIGNED_BYTE,new Uint8Array([255,0,0,255])),this._atlasTextures[t4]=e4};h2.enable(h2.BLEND),h2.blendFunc(h2.SRC_ALPHA,h2.ONE_MINUS_SRC_ALPHA),this.handleResize()}beginFrame(){if(!this._atlas)return true;const _g=this._atlas._clearModelGeneration||0;return this._lastSeenClearModelGeneration!==_g&&(this._lastSeenClearModelGeneration=_g,true)}updateCell(t3,e3,i3,s3,r2,a2,o2,n2,L2){this._updateCell(this._vertices.attributes,t3,e3,i3,s3,r2,a2,o2,n2,L2)}_updateCell(t3,e3,i3,s3,a2,o2,d2,c,_,Q){;h=11*(i3*this._terminal.cols+e3),s3!==r.NULL_CELL_CODE&&void 0!==s3?this._atlas&&(L=c&&c.length>1?this._atlas.getRasterizedGlyphCombinedChar(c,a2,o2,d2,false,this._terminal.element):this._atlas.getRasterizedGlyph(s3,a2,o2,d2,false,this._terminal.element),T=Math.floor((this._dimensions.device.cell.width-this._dimensions.device.char.width)/2),a2!==Q&&L.offset.x>T?(l=L.offset.x-T,t3[h]=-(L.offset.x-l)+this._dimensions.device.char.left,t3[h+1]=-L.offset.y+this._dimensions.device.char.top,t3[h+2]=(L.size.x-l)/this._dimensions.device.canvas.width,t3[h+3]=L.size.y/this._dimensions.device.canvas.height,t3[h+4]=L.texturePage,t3[h+5]=L.texturePositionClipSpace.x+l/this._atlas.pages[L.texturePage].canvas.width,t3[h+6]=L.texturePositionClipSpace.y,t3[h+7]=L.sizeClipSpace.x-l/this._atlas.pages[L.texturePage].canvas.width,t3[h+8]=L.sizeClipSpace.y):(t3[h]=-L.offset.x+this._dimensions.device.char.left,t3[h+1]=-L.offset.y+this._dimensions.device.char.top,t3[h+2]=L.size.x/this._dimensions.device.canvas.width,t3[h+3]=L.size.y/this._dimensions.device.canvas.height,t3[h+4]=L.texturePage,t3[h+5]=L.texturePositionClipSpace.x,t3[h+6]=L.texturePositionClipSpace.y,t3[h+7]=L.sizeClipSpace.x,t3[h+8]=L.sizeClipSpace.y),this._optionsService.rawOptions.rescaleOverlappingGlyphs&&(0,n.allowRescaling)(s3,_,L.size.x,this._dimensions.device.cell.width)&&(t3[h+2]=(this._dimensions.device.cell.width-1)/this._dimensions.device.canvas.width)):t3.fill(0,h,h+11-1-2)}clear(){const t3=this._terminal,e3=t3.cols*t3.rows*11;this._vertices.count!==e3?this._vertices.attributes=new Float32Array(e3):this._vertices.attributes.fill(0);let i3=0;for(;i3this._atlasTextures.length&&!this._pageOverflowWarned&&(this._pageOverflowWarned=true,console.warn("xterm webgl: atlas page count ("+this._atlas.pages.length+") exceeds the renderer texture capacity ("+this._atlasTextures.length+"), some glyphs will not render correctly"));for(let t4=0;t4T2.deleteProgram(this._program))),this._projectionLocation=(0,a.throwIfFalsy)(T2.getUniformLocation(this._program,"u_projection"));const l2=this._vertexArrayObject=T2.createVertexArray();this._register((0,s2.toDisposable)(()=>T2.deleteVertexArray(l2))),T2.bindVertexArray(l2);const d2=new Float32Array([0,0,1,0,0,1,1,1]),c2=T2.createBuffer();this._register((0,s2.toDisposable)(()=>T2.deleteBuffer(c2))),T2.bindBuffer(T2.ARRAY_BUFFER,c2),T2.bufferData(T2.ARRAY_BUFFER,d2,T2.STATIC_DRAW),T2.enableVertexAttribArray(3),T2.vertexAttribPointer(3,2,this._gl.FLOAT,false,0,0);const _2=new Uint8Array([0,1,2,3]),Q2=T2.createBuffer();this._register((0,s2.toDisposable)(()=>T2.deleteBuffer(Q2))),T2.bindBuffer(T2.ELEMENT_ARRAY_BUFFER,Q2),T2.bufferData(T2.ELEMENT_ARRAY_BUFFER,_2,T2.STATIC_DRAW),this._attributesBuffer=(0,a.throwIfFalsy)(T2.createBuffer()),this._register((0,s2.toDisposable)(()=>T2.deleteBuffer(this._attributesBuffer))),T2.bindBuffer(T2.ARRAY_BUFFER,this._attributesBuffer),T2.enableVertexAttribArray(0),T2.vertexAttribPointer(0,2,T2.FLOAT,false,o,0),T2.vertexAttribDivisor(0,1),T2.enableVertexAttribArray(1),T2.vertexAttribPointer(1,2,T2.FLOAT,false,o,2*Float32Array.BYTES_PER_ELEMENT),T2.vertexAttribDivisor(1,1),T2.enableVertexAttribArray(2),T2.vertexAttribPointer(2,4,T2.FLOAT,false,o,4*Float32Array.BYTES_PER_ELEMENT),T2.vertexAttribDivisor(2,1),this._updateCachedColors(L2.colors),this._register(this._themeService.onChangeColors(t4=>{;this._updateCachedColors(t4),this._updateViewportRectangle()}))}renderBackgrounds(){this._renderVertices(this._vertices)}renderCursor(){this._renderVertices(this._verticesCursor)}_renderVertices(t3){const e3=this._gl;e3.useProgram(this._program),e3.bindVertexArray(this._vertexArrayObject),e3.uniformMatrix4fv(this._projectionLocation,false,r.PROJECTION_MATRIX),e3.bindBuffer(e3.ARRAY_BUFFER,this._attributesBuffer),e3.bufferData(e3.ARRAY_BUFFER,t3.attributes,e3.DYNAMIC_DRAW),e3.drawElementsInstanced(this._gl.TRIANGLE_STRIP,4,e3.UNSIGNED_BYTE,0,t3.count)}handleResize(){this._updateViewportRectangle()}setDimensions(t3){this._dimensions=t3}_updateCachedColors(t3){;this._bgFloat=this._colorToFloat32Array(t3.background),this._cursorFloat=this._colorToFloat32Array(t3.cursor)}_updateViewportRectangle(){this._addRectangleFloat(this._vertices.attributes,0,0,0,this._terminal.cols*this._dimensions.device.cell.width,this._terminal.rows*this._dimensions.device.cell.height,this._bgFloat)}updateBackgrounds(t3){const e3=this._terminal,i3=this._vertices;let s3,r2,a2,o2,n2,L2,h2,T2,l2,d2,c2,_2=1;for(s3=0;s3>24&255)/255,d=(L>>16&255)/255,c=(L>>8&255)/255,_=1,this._addRectangle(t3.attributes,e3,h,T,(o2-a2)*this._dimensions.device.cell.width,this._dimensions.device.cell.height,l,d,c,_)}_addRectangle(t3,e3,i3,s3,r2,a2,o2,n2,L2,h2){;t3[e3]=i3/this._dimensions.device.canvas.width,t3[e3+1]=s3/this._dimensions.device.canvas.height,t3[e3+2]=r2/this._dimensions.device.canvas.width,t3[e3+3]=a2/this._dimensions.device.canvas.height,t3[e3+4]=o2,t3[e3+5]=n2,t3[e3+6]=L2,t3[e3+7]=h2}_addRectangleFloat(t3,e3,i3,s3,r2,a2,o2){;t3[e3]=i3/this._dimensions.device.canvas.width,t3[e3+1]=s3/this._dimensions.device.canvas.height,t3[e3+2]=r2/this._dimensions.device.canvas.width,t3[e3+3]=a2/this._dimensions.device.canvas.height,t3[e3+4]=o2[0],t3[e3+5]=o2[1],t3[e3+6]=o2[2],t3[e3+7]=o2[3]}_colorToFloat32Array(t3){return new Float32Array([(t3.rgba>>24&255)/255,(t3.rgba>>16&255)/255,(t3.rgba>>8&255)/255,(255&t3.rgba)/255])}}e2.RectangleRenderer=Q},208(t2,e2,i2){;Object.defineProperty(e2,"__esModule",{value:true}),e2.RenderModel=e2.COMBINED_CHAR_BIT_MASK=void 0;const s2=i2(948);e2.COMBINED_CHAR_BIT_MASK=2147483648,e2.RenderModel=class{constructor(){;this.cells=new Uint32Array(0),this.lineLengths=new Uint32Array(0),this.selection=(0,s2.createSelectionRenderModel)()}resize(t3,e3){const i3=t3*e3*4;i3!==this.cells.length&&(this.cells=new Uint32Array(i3),this.lineLengths=new Uint32Array(e3))}clear(){;this.cells.fill(0,0),this.lineLengths.fill(0,0)}}},115(t2,e2,i2){;Object.defineProperty(e2,"__esModule",{value:true}),e2.TextureAtlas=void 0;const s2=i2(0),r=i2(183),a=i2(959),o=i2(993),n=i2(836),L=i2(930),h=i2(917),T=i2(564),l=i2(414),d={texturePage:0,texturePosition:{x:0,y:0},texturePositionClipSpace:{x:0,y:0},offset:{x:0,y:0},size:{x:0,y:0},sizeClipSpace:{x:0,y:0}};let c;class _{get pages(){return this._pages}constructor(t3,e3,i3,s3){;this._document=t3,this._config=e3,this._unicodeService=i3,this._logService=s3,this._didWarmUp=false,this._cacheMap=new n.FourKeyMap,this._cacheMapCombined=new n.FourKeyMap,this._pages=[],this._activePages=[],this._workBoundingBox={top:0,left:0,bottom:0,right:0},this._workAttributeData=new h.AttributeData,this._textureSize=512,this._onAddTextureAtlasCanvas=new l.Emitter,this.onAddTextureAtlasCanvas=this._onAddTextureAtlasCanvas.event,this._onRemoveTextureAtlasCanvas=new l.Emitter,this.onRemoveTextureAtlasCanvas=this._onRemoveTextureAtlasCanvas.event,this._requestClearModel=false,this._clearModelGeneration=0,this._createNewPage(),this._tmpCanvas=p(t3,4*this._config.deviceCellWidth+4,this._config.deviceCellHeight+4),this._tmpCtx=(0,a.throwIfFalsy)(this._tmpCanvas.getContext("2d",{alpha:this._config.allowTransparency,willReadFrequently:true}))}dispose(){this._tmpCanvas.remove();for(const t3 of this.pages)t3.canvas.remove();this._onAddTextureAtlasCanvas.dispose(),this._onRemoveTextureAtlasCanvas.dispose()}warmUp(){this._didWarmUp||(this._doWarmUp(),this._didWarmUp=true)}_doWarmUp(){const t3=new L.IdleTaskQueue(this._logService);for(let e3=33;e3<126;e3++)t3.enqueue(()=>{if(!this._cacheMap.get(e3,T.DEFAULT_COLOR,T.DEFAULT_COLOR,T.DEFAULT_EXT)){const t4=this._drawToCache(e3,T.DEFAULT_COLOR,T.DEFAULT_COLOR,T.DEFAULT_EXT,false,void 0);this._cacheMap.set(e3,T.DEFAULT_COLOR,T.DEFAULT_COLOR,T.DEFAULT_EXT,t4)}})}beginFrame(){const t3=this._requestClearModel;return this._requestClearModel=false,t3}clearTexture(){if(this._pages.some(t3=>t3._glyphs.length>0||0!==t3.currentRow.x||0!==t3.currentRow.y)){this._evictAllPages();this._createNewPage()}}_evictAllPages(){for(const t3 of this._pages)this._onRemoveTextureAtlasCanvas.fire(t3.canvas),t3.canvas.remove();this._pages.length=0,this._activePages.length=0,this._overflowSizePage=void 0,this._cacheMap.clear(),this._cacheMapCombined.clear(),this._didWarmUp=false,this._requestClearModel=true,this._clearModelGeneration++}_createNewPage(){if(_.maxAtlasPages&&this._pages.length>=_.maxAtlasPages){const t4=this._pages.filter(t5=>2*t5.canvas.width<=(_.maxTextureSize||4096)).sort((t5,e4)=>e4.canvas.width!==t5.canvas.width?e4.canvas.width-t5.canvas.width:e4.percentageUsed-t5.percentageUsed);let e3=-1,i3=0;for(let s4=0;s4t5.canvas.width!==s3[0].canvas.width))this._evictAllPages();else{const r2=s3.map(t5=>t5.glyphs[0].texturePage).sort((t5,e4)=>t5-e4),a2=this.pages.length-s3.length,o2=this._mergePages(s3,a2);o2.version=++Q.nextVersion;for(let t5=r2.length-1;t5>=0;t5--)this._deletePage(r2[t5]);this.pages.push(o2),this._requestClearModel=true,this._clearModelGeneration++,this._onAddTextureAtlasCanvas.fire(o2.canvas)}}const t3=new Q(this._document,this._textureSize);return this._pages.push(t3),this._activePages.push(t3),this._onAddTextureAtlasCanvas.fire(t3.canvas),t3}_mergePages(t3,e3){const i3=2*t3[0].canvas.width,s3=new Q(this._document,i3,t3);for(const[r2,a2]of t3.entries()){const t4=r2*a2.canvas.width%i3,o2=Math.floor(r2/2)*a2.canvas.height;s3.ctx.drawImage(a2.canvas,t4,o2);for(const s4 of a2.glyphs)s4.texturePage=e3,s4.sizeClipSpace.x=s4.size.x/i3,s4.sizeClipSpace.y=s4.size.y/i3,s4.texturePosition.x+=t4,s4.texturePosition.y+=o2,s4.texturePositionClipSpace.x=s4.texturePosition.x/i3,s4.texturePositionClipSpace.y=s4.texturePosition.y/i3;this._onRemoveTextureAtlasCanvas.fire(a2.canvas);const n2=this._activePages.indexOf(a2);-1!==n2&&this._activePages.splice(n2,1)}return s3}_deletePage(t3){this._pages.splice(t3,1);for(let e3=t3;e3=this._config.colors.ansi.length)throw new Error("No color found for idx "+t3);return this._config.colors.ansi[t3]}_getBackgroundColor(t3,e3,i3,s3){if(this._config.allowTransparency)return o.NULL_COLOR;let r2;switch(t3){case 16777216:case 33554432:r2=this._getColorFromAnsiIndex(e3);break;case 50331648:const t4=h.AttributeData.toColorRGB(e3);r2=o.channels.toColor(t4[0],t4[1],t4[2]);break;default:r2=i3?o.color.opaque(this._config.colors.foreground):this._config.colors.background}return this._config.allowTransparency||(r2=o.color.opaque(r2)),r2}_getForegroundColor(t3,e3,i3,r2,a2,n2,L2,T2,l2,d2){const c2=this._getMinimumContrastColor(t3,e3,i3,r2,a2,n2,L2,l2,T2,d2);if(c2)return c2;let _2;switch(a2){case 16777216:case 33554432:;this._config.drawBoldTextInBrightColors&&l2&&n2<8&&(n2+=8),_2=this._getColorFromAnsiIndex(n2);break;case 50331648:const t4=h.AttributeData.toColorRGB(n2);_2=o.channels.toColor(t4[0],t4[1],t4[2]);break;default:_2=L2?this._config.colors.background:this._config.colors.foreground}return this._config.allowTransparency&&(_2=o.color.opaque(_2)),T2&&(_2=o.color.multiplyOpacity(_2,s2.DIM_OPACITY)),_2}_resolveBackgroundRgba(t3,e3,i3){switch(t3){case 16777216:case 33554432:return this._getColorFromAnsiIndex(e3).rgba;case 50331648:return e3<<8;default:return i3?this._config.colors.foreground.rgba:this._config.colors.background.rgba}}_resolveForegroundRgba(t3,e3,i3,s3){switch(t3){case 16777216:case 33554432:return this._config.drawBoldTextInBrightColors&&s3&&e3<8&&(e3+=8),this._getColorFromAnsiIndex(e3).rgba;case 50331648:return e3<<8;default:return i3?this._config.colors.background.rgba:this._config.colors.foreground.rgba}}_getMinimumContrastColor(t3,e3,i3,s3,r2,a2,n2,L2,h2,T2){if(1===this._config.minimumContrastRatio||T2)return;const l2=this._getContrastCache(h2),d2=l2.getColor(t3,s3);if(void 0!==d2)return d2??void 0;const c2=this._resolveBackgroundRgba(e3,i3,n2),_2=this._resolveForegroundRgba(r2,a2,n2,L2),Q2=o.rgba.ensureContrastRatio(c2,_2,this._config.minimumContrastRatio/(h2?2:1));if(!Q2)return void l2.setColor(t3,s3,null);const u2=o.channels.toColor(Q2>>24&255,Q2>>16&255,Q2>>8&255);return l2.setColor(t3,s3,u2),u2}_getContrastCache(t3){return t3?this._config.colors.halfContrastCache:this._config.colors.contrastCache}_drawToCache(t3,e3,i3,o2,n2,L2){const T2="number"==typeof t3?String.fromCharCode(t3):t3;L2&&this._tmpCanvas.parentElement!==L2&&(this._tmpCanvas.style.display="none",L2.append(this._tmpCanvas));const l2=Math.min(this._config.deviceCellWidth*Math.max(T2.length,2)+4,this._config.deviceMaxTextureSize);this._tmpCanvas.width{orcaProbeWeight=String(orcaProbeWeight);let orcaActualWeightToken=this._tmpCtx.font.match(/(?:^|\s)(normal|bold|[1-9]\d{0,3})(?=\s+\d+(?:\.\d+)?px(?:\s|$))/)?.[1]??"400",orcaActualWeight=orcaActualWeightToken==="normal"?"400":orcaActualWeightToken==="bold"?"700":orcaActualWeightToken;if(/^(?:[1-8]\d{2}|900)$/.test(orcaProbeWeight)&&orcaActualWeight!==orcaProbeWeight){this.fontProbeMismatchCount=(this.fontProbeMismatchCount||0)+1;this.fontProbeLastMismatch={desired:orcaProbeWeight,actual:this._tmpCtx.font};try{globalThis.__orcaAtlasFontProbe&&globalThis.__orcaAtlasFontProbe(this.fontProbeLastMismatch)}catch(orcaFontProbeError){}}})(R),this._tmpCtx.textBaseline=s2.TEXT_BASELINE;const A=1===T2.length&&(0,a.isPowerlineGlyph)(T2.charCodeAt(0)),Z=1===T2.length&&(0,a.isRestrictedPowerlineGlyph)(T2.charCodeAt(0)),D=this._getForegroundColor(e3,b,x,i3,w,m,g,M,p2,(0,a.treatGlyphAsBackgroundColor)(T2.charCodeAt(0)));this._tmpCtx.fillStyle=D.css;const S=Z?0:4;let O=false;if(false!==this._config.customGlyphs){const t4=this._workAttributeData.getUnderlineVariantOffset();O=(0,r.tryDrawCustomGlyph)(this._tmpCtx,T2,S,S,this._config.deviceCellWidth,this._config.deviceCellHeight,this._config.deviceCharWidth,this._config.deviceCharHeight,this._config.fontSize,this._config.devicePixelRatio,this._logService,E.css,t4)}let I,W=!A;if(I="number"==typeof t3?this._unicodeService.wcwidth(t3):this._unicodeService.getStringCellWidth(t3),v){this._tmpCtx.save();const t4=Math.max(1,Math.floor(this._config.fontSize*this._config.devicePixelRatio/15)),e4=t4%2==1?.5:0;if(this._tmpCtx.lineWidth=t4,this._workAttributeData.isUnderlineColorDefault())this._tmpCtx.strokeStyle=this._tmpCtx.fillStyle;else if(this._workAttributeData.isUnderlineColorRGB())W=false,this._tmpCtx.strokeStyle=`rgb(${h.AttributeData.toColorRGB(this._workAttributeData.getUnderlineColor()).join(",")})`;else{W=false;let t5=this._workAttributeData.getUnderlineColor();this._config.drawBoldTextInBrightColors&&this._workAttributeData.isBold()&&t5<8&&(t5+=8),this._tmpCtx.strokeStyle=this._getColorFromAnsiIndex(t5).css};this._tmpCtx.fillStyle=this._tmpCtx.strokeStyle,this._tmpCtx.beginPath();const i4=S,s3=Math.ceil(S+this._config.deviceCharHeight)-e4-(n2?2*t4:0),r2=s3+2*t4;let o3=this._workAttributeData.getUnderlineVariantOffset(),L3=0,l3=0;for(let e5=0;e5=t4?2*t4-o3:t4-o3;false==!(o3>=t4)||0===_2?(this._tmpCtx.setLineDash([Math.round(t4),Math.round(t4)]),this._tmpCtx.moveTo(h2+_2,s3),this._tmpCtx.lineTo(T3,s3)):(this._tmpCtx.setLineDash([Math.round(t4),Math.round(t4)]),this._tmpCtx.moveTo(h2,s3),this._tmpCtx.lineTo(h2+_2,s3),this._tmpCtx.moveTo(h2+_2+t4,s3),this._tmpCtx.lineTo(T3,s3)),o3=(0,a.computeNextVariantOffset)(T3-h2,t4,o3);break;case 5:const Q2=.6,u2=.3,p3=T3-h2,g2=Math.floor(Q2*p3),M2=Math.floor(u2*p3),y2=p3-g2-M2;this._tmpCtx.setLineDash([g2,M2,y2]),this._tmpCtx.moveTo(h2,s3),this._tmpCtx.lineTo(T3,s3);break;default:;this._tmpCtx.moveTo(h2,s3),this._tmpCtx.lineTo(T3,s3)};n3||this._tmpCtx.stroke(),this._tmpCtx.restore()}if(this._tmpCtx.restore(),!O&&this._config.fontSize>=12&&!this._config.allowTransparency&&" "!==T2){;this._tmpCtx.save(),this._tmpCtx.textBaseline="alphabetic";const e5=this._tmpCtx.measureText(T2);if(this._tmpCtx.restore(),"actualBoundingBoxDescent"in e5&&e5.actualBoundingBoxDescent>0){this._tmpCtx.save();const e6=new Path2D;e6.rect(i4,L3-Math.ceil(t4/2),this._config.deviceCellWidth*I,l3-L3+Math.ceil(t4/2)),this._tmpCtx.clip(e6),this._tmpCtx.lineWidth=3*this._config.devicePixelRatio,this._tmpCtx.strokeStyle=E.css,this._tmpCtx.strokeText(T2,S,S+this._config.deviceCharHeight),this._tmpCtx.restore()}}}if(C){const t4=Math.max(1,Math.floor(this._config.fontSize*this._config.devicePixelRatio/15)),e4=t4%2==1?.5:0;this._tmpCtx.lineWidth=t4,this._tmpCtx.strokeStyle=this._tmpCtx.fillStyle,this._tmpCtx.beginPath(),this._tmpCtx.moveTo(S,S+e4),this._tmpCtx.lineTo(S+this._config.deviceCharWidth*I,S+e4),this._tmpCtx.stroke()}if(O||this._tmpCtx.fillText(T2,S,S+this._config.deviceCharHeight),"_"===T2&&!this._config.allowTransparency){let t4=u(this._tmpCtx.getImageData(S,S,this._config.deviceCellWidth,this._config.deviceCellHeight),E,D,W);if(t4)for(let e4=1;e4<=5&&(this._tmpCtx.save(),this._tmpCtx.fillStyle=E.css,this._tmpCtx.fillRect(0,0,this._tmpCanvas.width,this._tmpCanvas.height),this._tmpCtx.restore(),this._tmpCtx.fillText(T2,S,S+this._config.deviceCharHeight-e4),t4=u(this._tmpCtx.getImageData(S,S,this._config.deviceCellWidth,this._config.deviceCellHeight),E,D,W),t4);e4++);}if(f){const t4=Math.max(1,Math.floor(this._config.fontSize*this._config.devicePixelRatio/10)),e4=this._tmpCtx.lineWidth%2==1?.5:0;this._tmpCtx.lineWidth=t4,this._tmpCtx.strokeStyle=this._tmpCtx.fillStyle,this._tmpCtx.beginPath(),this._tmpCtx.moveTo(S,S+Math.floor(this._config.deviceCharHeight/2)-e4),this._tmpCtx.lineTo(S+this._config.deviceCharWidth*I,S+Math.floor(this._config.deviceCharHeight/2)-e4),this._tmpCtx.stroke()}this._tmpCtx.restore();const N=this._tmpCtx.getImageData(0,0,this._tmpCanvas.width,this._tmpCanvas.height);let P;if(P=this._config.allowTransparency?(function(t4){for(let e4=0;e40)return false;return true})(N):u(N,E,D,W),P)return d;const B=this._findGlyphBoundingBox(N,this._workBoundingBox,l2,Z,O,S);let $,F;for(;;){if(0===this._activePages.length){const t4=this._createNewPage();$=t4,F=t4.currentRow,F.height=B.size.y;break};$=this._activePages[this._activePages.length-1],F=$.currentRow;for(const t4 of this._activePages)B.size.y<=t4.currentRow.height&&($=t4,F=t4.currentRow);for(let t4=this._activePages.length-1;t4>=0;t4--)for(const e4 of this._activePages[t4].fixedRows)e4.height<=F.height&&B.size.y<=e4.height&&($=this._activePages[t4],F=e4);if(B.size.x>this._textureSize){;this._overflowSizePage||(_.maxAtlasPages&&this._pages.length>=_.maxAtlasPages&&this._evictAllPages(),this._overflowSizePage=new Q(this._document,this._config.deviceMaxTextureSize),this.pages.push(this._overflowSizePage),this._requestClearModel=true,this._clearModelGeneration++,this._onAddTextureAtlasCanvas.fire(this._overflowSizePage.canvas)),$=this._overflowSizePage,F=this._overflowSizePage.currentRow,F.x+B.size.x>=$.canvas.width&&(F.x=0,F.y+=F.height,F.height=0);break}if(F.y+B.size.y>=$.canvas.height||F.height>B.size.y+2){let t4=false;if($.currentRow.y+$.currentRow.height+B.size.y>=$.canvas.height){let e4;for(const t5 of this._activePages)if(t5.currentRow.y+t5.currentRow.height+B.size.y=_.maxAtlasPages&&F.y+B.size.y<=$.canvas.height&&F.height>=B.size.y&&F.x+B.size.x<=$.canvas.width)t4=true;else{const e5=this._createNewPage();$=e5,F=e5.currentRow,F.height=B.size.y,t4=true}}t4||($.currentRow.height>0&&$.fixedRows.push($.currentRow),F={x:0,y:$.currentRow.y+$.currentRow.height,height:B.size.y},$.fixedRows.push(F),$.currentRow={x:0,y:F.y+F.height,height:0})}if(F.x+B.size.x<=$.canvas.width)break;F===$.currentRow?(F.x=0,F.y+=F.height,F.height=0):$.fixedRows.splice($.fixedRows.indexOf(F),1)}return B.texturePage=this._pages.indexOf($),B.texturePosition.x=F.x,B.texturePosition.y=F.y,B.texturePositionClipSpace.x=F.x/$.canvas.width,B.texturePositionClipSpace.y=F.y/$.canvas.height,B.sizeClipSpace.x/=$.canvas.width,B.sizeClipSpace.y/=$.canvas.height,F.height=Math.max(F.height,B.size.y),F.x+=B.size.x,$.ctx.putImageData(N,B.texturePosition.x-this._workBoundingBox.left,B.texturePosition.y-this._workBoundingBox.top,this._workBoundingBox.left,this._workBoundingBox.top,B.size.x,B.size.y),$.addGlyph(B),$.version=++Q.nextVersion,B}_findGlyphBoundingBox(t3,e3,i3,s3,r2,a2){e3.top=0;const o2=s3?this._config.deviceCellHeight:this._tmpCanvas.height,n2=s3?this._config.deviceCellWidth:i3;let L2=false;for(let i4=0;i4=a2;i4--){for(let s4=0;s4=0;i4--){for(let s4=0;s4>>24,a2=e3.rgba>>>16&255,o2=e3.rgba>>>8&255,n2=i3.rgba>>>24,L2=i3.rgba>>>16&255,h2=i3.rgba>>>8&255,T2=Math.floor((Math.abs(r2-n2)+Math.abs(a2-L2)+Math.abs(o2-h2))/12);let l2=true;for(let e4=0;e4this._handleColorChange())),this._cellColorResolver=new s2.CellColorResolver(this._terminal,this._optionsService,this._model.selection,this._decorationService,this._coreBrowserService,this._themeService),this._core=this._terminal._core,this._renderLayers=[new _.LinkRenderLayer(this._core.screenElement,2,this._terminal,this._core.linkifier,this._coreBrowserService,d2,this._themeService)],this.dimensions=(0,g.createRenderDimensions)(),this._devicePixelRatio=this._coreBrowserService.dpr,this._updateDimensions(),this._updateCursorBlink(),this._register(d2.onOptionChange(()=>this._handleOptionsChanged())),this._textBlinkStateManager=this._register(new T.TextBlinkStateManager(()=>this._requestRedrawViewport(),this._coreBrowserService,this._optionsService)),this._resetBlinkingRowState(),this._deviceMaxTextureSize=this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE),this._register((0,u.addDisposableListener)(this._canvas,"webglcontextlost",t4=>{;this._logService.debug("webglcontextlost event received"),t4.preventDefault(),this._contextRestorationTimeout=setTimeout(()=>{;this._contextRestorationTimeout=void 0,this._logService.warn("webgl context not restored; firing onContextLoss"),this._onContextLoss.fire(t4)},3e3)})),this._register((0,u.addDisposableListener)(this._canvas,"webglcontextrestored",t4=>{;this._logService.warn("webglcontextrestored event received"),clearTimeout(this._contextRestorationTimeout),this._contextRestorationTimeout=void 0,(0,r.removeTerminalFromCache)(this._terminal),this._initializeWebGLState(),this._requestRedrawViewport()})),this._observerDisposable.value=(0,o.observeDevicePixelDimensions)(this._canvas,this._coreBrowserService.window,(t4,e4)=>this._setCanvasDevicePixelDimensions(t4,e4)),this._register(this._coreBrowserService.onWindowChange(t4=>{this._observerDisposable.value=(0,o.observeDevicePixelDimensions)(this._canvas,t4,(t5,e4)=>this._setCanvasDevicePixelDimensions(t5,e4))})),this._register((0,u.addDisposableListener)(this._coreBrowserService.mainDocument,"mousedown",()=>this._cursorBlinkStateManager.value?.restartBlinkAnimation())),this._core.screenElement.appendChild(this._canvas),[this._rectangleRenderer.value,this._glyphRenderer.value]=this._initializeWebGLState(),this._isAttached=this._core.screenElement.isConnected,this._register((0,p.toDisposable)(()=>{;clearTimeout(this._contextRestorationTimeout),this._contextRestorationTimeout=void 0;for(const t4 of this._renderLayers)t4.dispose();this._canvas.parentElement?.removeChild(this._canvas),(0,r.removeTerminalFromCache)(this._terminal)}))}get textureAtlas(){return this._charAtlas?.pages[0].canvas}_handleColorChange(){;this._refreshCharAtlas(),this._clearModel(true)}handleDevicePixelRatioChange(){this._devicePixelRatio!==this._coreBrowserService.dpr&&(this._devicePixelRatio=this._coreBrowserService.dpr,this.handleResize(this._terminal.cols,this._terminal.rows))}handleResize(t3,e3){;this._updateDimensions(),this._model.resize(this._terminal.cols,this._terminal.rows),this._resetBlinkingRowState();for(const t4 of this._renderLayers)t4.resize(this._terminal,this.dimensions);this._canvas.width=this.dimensions.device.canvas.width,this._canvas.height=this.dimensions.device.canvas.height,this._canvas.style.width=`${this.dimensions.css.canvas.width}px`,this._canvas.style.height=`${this.dimensions.css.canvas.height}px`,this._core.screenElement.style.width=`${this.dimensions.css.canvas.width}px`,this._core.screenElement.style.height=`${this.dimensions.css.canvas.height}px`,this._rectangleRenderer.value?.setDimensions(this.dimensions),this._rectangleRenderer.value?.handleResize(),this._glyphRenderer.value?.setDimensions(this.dimensions),this._glyphRenderer.value?.handleResize(),this._refreshCharAtlas(),this._clearModel(false),this._onRequestRedraw.fire({start:0,end:this._terminal.rows-1,sync:true})}handleCharSizeChanged(){this.handleResize(this._terminal.cols,this._terminal.rows)}handleBlur(){for(const t3 of this._renderLayers)t3.handleBlur(this._terminal);this._cursorBlinkStateManager.value?.pause(),this._requestRedrawViewport()}handleFocus(){for(const t3 of this._renderLayers)t3.handleFocus(this._terminal);this._cursorBlinkStateManager.value?.resume(),this._requestRedrawViewport()}handleViewportVisibilityChange(t3){this._textBlinkStateManager.setViewportVisible(t3)}handleSelectionChanged(t3,e3,i3){for(const s3 of this._renderLayers)s3.handleSelectionChanged(this._terminal,t3,e3,i3);this._model.selection.update(this._core,t3,e3,i3),this._requestRedrawViewport()}handleCursorMove(){for(const t3 of this._renderLayers)t3.handleCursorMove(this._terminal);this._cursorBlinkStateManager.value?.restartBlinkAnimation()}_handleOptionsChanged(){;this._updateDimensions(),this._refreshCharAtlas(),this._updateCursorBlink()}_initializeWebGLState(){return this._rectangleRenderer.value=new d.RectangleRenderer(this._terminal,this._gl,this.dimensions,this._themeService,this._logService),this._glyphRenderer.value=new l.GlyphRenderer(this._terminal,this._gl,this.dimensions,this._optionsService,this._logService),this.handleCharSizeChanged(),[this._rectangleRenderer.value,this._glyphRenderer.value]}_refreshCharAtlas(){if(this.dimensions.device.char.width<=0&&this.dimensions.device.char.height<=0)return void(this._isAttached=false);const t3=(0,r.acquireTextureAtlas)(this._terminal,this._optionsService.rawOptions,this._themeService.colors,this.dimensions.device.cell.width,this.dimensions.device.cell.height,this.dimensions.device.char.width,this.dimensions.device.char.height,this._coreBrowserService.dpr,this._deviceMaxTextureSize,this._customGlyphs);this._charAtlas!==t3&&(this._onChangeTextureAtlas.fire(t3.pages[0].canvas),this._charAtlasDisposable.value=(0,p.combinedDisposable)(Q.EventUtils.forward(t3.onAddTextureAtlasCanvas,this._onAddTextureAtlasCanvas),Q.EventUtils.forward(t3.onRemoveTextureAtlasCanvas,this._onRemoveTextureAtlasCanvas))),this._charAtlas=t3,this._charAtlas.warmUp(),this._glyphRenderer.value?.setAtlas(this._charAtlas)}_clearModel(t3){;this._model.clear(),t3&&this._glyphRenderer.value?.clear()}clearTextureAtlas(){;this._charAtlas?.clearTexture(),this._clearModel(true),this._requestRedrawViewport()}clear(){this._clearModel(true);for(const t3 of this._renderLayers)t3.reset(this._terminal);this._resetBlinkingRowState(),this._textBlinkStateManager.setNeedsBlinkInViewport(false),this._cursorBlinkStateManager.value?.restartBlinkAnimation(),this._updateCursorBlink()}renderRows(t3,e3){if(!this._isAttached){if(!(this._core.screenElement?.isConnected&&this._charSizeService.width&&this._charSizeService.height))return;this._updateDimensions(),this._refreshCharAtlas(),this._isAttached=true}for(const i4 of this._renderLayers)i4.handleGridChanged(this._terminal,t3,e3);if(!this._glyphRenderer.value||!this._rectangleRenderer.value)return;this._glyphRenderer.value.beginFrame()?(this._clearModel(true),this._updateModel(0,this._terminal.rows-1)):this._updateModel(t3,e3);let i3=false,s3=0;for(;this._charAtlas&&s3++<32&&this._glyphRenderer.value.beginFrame();)i3=true,this._clearModel(true),this._updateModel(0,this._terminal.rows-1);i3&&this._glyphRenderer.value.invalidateAtlasTextures(),this._rectangleRenderer.value.renderBackgrounds(),this._glyphRenderer.value.render(this._model),this._cursorBlinkStateManager.value&&!this._cursorBlinkStateManager.value.isCursorVisible||this._rectangleRenderer.value.renderCursor()}_updateCursorBlink(){;this._coreService.decPrivateModes.cursorBlink??this._terminal.options.cursorBlink?this._cursorBlinkStateManager.value=new a.CursorBlinkStateManager(()=>{this._requestRedrawCursor()},this._coreBrowserService):this._cursorBlinkStateManager.clear(),this._requestRedrawCursor()}_updateModel(t3,e3){const i3=this._core;let s3,r2,a2,o2,n2,L2,T2,l2,d2,_2,Q2,u2,p2,g2,M2,f,C,m=this._workCell;t3=v(t3,i3.rows-1,0),e3=v(e3,i3.rows-1,0);const w=this._coreService.decPrivateModes.cursorStyle??i3.options.cursorStyle??"block",x=this._terminal.buffer.active.baseY+this._terminal.buffer.active.cursorY,b=x-i3.buffer.ydisp,E=Math.min(this._terminal.buffer.active.cursorX,i3.cols-1);let R=-1;const k=this._coreService.isCursorInitialized&&!this._coreService.isCursorHidden&&(!this._cursorBlinkStateManager.value||this._cursorBlinkStateManager.value.isCursorVisible);this._model.cursor=void 0;let A=false;for(r2=t3;r2<=e3;r2++){a2=r2+i3.buffer.ydisp;const t4=i3.buffer.lines.get(a2);if(!t4){for(this._model.lineLengths[r2]=0,f=0;f=T2,d2=f,n2.length>0&&f===n2[0][0]&&l2){_2=n2.shift();const t5=this._model.selection.isCellSelected(this._terminal,_2[0],a2);for(M2=_2[0]+1;M2<_2[1];M2++)l2&&=t5===this._model.selection.isCellSelected(this._terminal,M2,a2);l2&&=!Q2||E<_2[0]||E>=_2[1],l2?(L2=true,m=new y(m,o2.translateToString(true,_2[0],_2[1]),_2[1]-_2[0]),d2=_2[1]-1):T2=_2[1]}if(u2=m.getChars(),p2=m.getCode(),M2=4*(r2*i3.cols+f),!e4&&m.isBlink()&&(e4=true),this._cellColorResolver.resolve(m,f,a2,this.dimensions.device.cell.width,this.dimensions.device.cell.height),k&&a2===x&&(f===E&&(this._model.cursor={x:E,y:b,width:m.getWidth(),style:this._coreBrowserService.isFocused?w:i3.options.cursorInactiveStyle,cursorWidth:i3.options.cursorWidth,dpr:this._devicePixelRatio},R=E+m.getWidth()-1),f>=E&&f<=R&&(this._coreBrowserService.isFocused&&"block"===w||false===this._coreBrowserService.isFocused&&"block"===i3.options.cursorInactiveStyle)&&(this._cellColorResolver.result.fg=50331648|this._themeService.colors.cursorAccent.rgba>>8&16777215,this._cellColorResolver.result.bg=50331648|this._themeService.colors.cursor.rgba>>8&16777215)),this._textBlinkStateManager.isEnabled&&!this._textBlinkStateManager.isBlinkOn&&m.isBlink()&&(this._cellColorResolver.result.fg|=1073741824),p2!==h.NULL_CELL_CODE&&(this._model.lineLengths[r2]=f+1),(this._model.cells[M2]!==p2||this._model.cells[M2+1]!==this._cellColorResolver.result.bg||this._model.cells[M2+2]!==this._cellColorResolver.result.fg||this._model.cells[M2+3]!==this._cellColorResolver.result.ext)&&(A=true,u2.length>1&&(p2|=c.COMBINED_CHAR_BIT_MASK),this._model.cells[M2]=p2,this._model.cells[M2+1]=this._cellColorResolver.result.bg,this._model.cells[M2+2]=this._cellColorResolver.result.fg,this._model.cells[M2+3]=this._cellColorResolver.result.ext,g2=m.getWidth(),this._glyphRenderer.value.updateCell(f,r2,p2,this._cellColorResolver.result.bg,this._cellColorResolver.result.fg,this._cellColorResolver.result.ext,u2,g2,s3),L2)){for(m=this._workCell,f++;f<=d2;f++)C=4*(r2*i3.cols+f),this._nullModelCell(f,r2,C,this._cellColorResolver.result.bg,this._cellColorResolver.result.fg,this._cellColorResolver.result.ext);f--}}this._setRowBlinkState(r2,e4)};A&&this._rectangleRenderer.value.updateBackgrounds(this._model),this._rectangleRenderer.value.updateCursor(this._model),this._updateTextBlinkState()}_nullModelCell(t3,e3,i3,s3,r2,a2){;this._glyphRenderer.value.updateCell(t3,e3,h.NULL_CELL_CODE,s3,r2,a2,h.NULL_CELL_CHAR,0,0),this._model.cells[i3]=h.NULL_CELL_CODE,this._model.cells[i3+1]=s3,this._model.cells[i3+2]=r2,this._model.cells[i3+3]=a2}_resetBlinkingRowState(){;this._rowHasBlinkingCells=new Array(this._terminal.rows).fill(false),this._rowHasBlinkingCellsCount=0}_setRowBlinkState(t3,e3){this._rowHasBlinkingCells[t3]!==e3&&(this._rowHasBlinkingCells[t3]=e3,this._rowHasBlinkingCellsCount+=e3?1:-1)}_updateTextBlinkState(){this._textBlinkStateManager.setNeedsBlinkInViewport(this._rowHasBlinkingCellsCount>0)}_updateDimensions(){this._charSizeService.width&&this._charSizeService.height&&(this.dimensions.device.char.width=Math.floor(this._charSizeService.width*this._devicePixelRatio),this.dimensions.device.char.height=Math.ceil(this._charSizeService.height*this._devicePixelRatio),this.dimensions.device.cell.height=Math.floor(this.dimensions.device.char.height*this._optionsService.rawOptions.lineHeight),this.dimensions.device.char.top=1===this._optionsService.rawOptions.lineHeight?0:Math.round((this.dimensions.device.cell.height-this.dimensions.device.char.height)/2),this.dimensions.device.cell.width=this.dimensions.device.char.width+Math.round(this._optionsService.rawOptions.letterSpacing),this.dimensions.device.char.left=Math.floor(this._optionsService.rawOptions.letterSpacing/2),this.dimensions.device.canvas.height=this._terminal.rows*this.dimensions.device.cell.height,this.dimensions.device.canvas.width=this._terminal.cols*this.dimensions.device.cell.width,this.dimensions.css.canvas.height=Math.round(this.dimensions.device.canvas.height/this._devicePixelRatio),this.dimensions.css.canvas.width=Math.round(this.dimensions.device.canvas.width/this._devicePixelRatio),this.dimensions.css.cell.height=this.dimensions.device.cell.height/this._devicePixelRatio,this.dimensions.css.cell.width=this.dimensions.device.cell.width/this._devicePixelRatio)}_setCanvasDevicePixelDimensions(t3,e3){;this._canvas.width===t3&&this._canvas.height===e3||(this._canvas.width=t3,this._canvas.height=e3,this._onRequestRedraw.fire({start:0,end:this._terminal.rows-1,sync:true}))}_requestRedrawViewport(){this._onRequestRedraw.fire({start:0,end:this._terminal.rows-1})}_requestRedrawCursor(){const t3=this._terminal.buffer.active.cursorY;this._onRequestRedraw.fire({start:t3,end:t3})}}e2.WebglRenderer=M;class y extends n.AttributeData{constructor(t3,e3,i3){;super(),this.content=0,this.combinedData="",this.fg=t3.fg,this.bg=t3.bg,this.combinedData=e3,this._width=i3}isCombined(){return 2097152}getWidth(){return this._width}getChars(){return this.combinedData}getCode(){return 2097151}setFromCharData(t3){throw new Error("not implemented")}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}}function v(t3,e3,i3=0){return Math.max(Math.min(t3,e3),i3)}e2.JoinedCellData=y},719(t2,e2,i2){;Object.defineProperty(e2,"__esModule",{value:true}),e2.GLTexture=e2.PROJECTION_MATRIX=void 0,e2.createProgram=function(t3,e3,i3,a){const o=(0,s2.throwIfFalsy)(t3.createProgram());if(t3.attachShader(o,(0,s2.throwIfFalsy)(r(t3,t3.VERTEX_SHADER,e3,a))),t3.attachShader(o,(0,s2.throwIfFalsy)(r(t3,t3.FRAGMENT_SHADER,i3,a))),t3.linkProgram(o),t3.getProgramParameter(o,t3.LINK_STATUS))return o;a.error(t3.getProgramInfoLog(o)),t3.deleteProgram(o)},e2.createShader=r,e2.expandFloat32Array=function(t3,e3){const i3=Math.min(2*t3.length,e3),s3=new Float32Array(i3);for(let e4=0;e4{const e3=[],i3=[3/8,2/8,3/8],s3=[0,3/8,5/8];for(let r2=0;r2<3;r2++){const a=t3>>2*r2&1,o=t3>>2*r2+1&1;a&&o?e3.push(`M0,${s3[r2]} L1,${s3[r2]} L1,${s3[r2]+i3[r2]} L0,${s3[r2]+i3[r2]} Z`):a?e3.push(`M0,${s3[r2]} L0.5,${s3[r2]} L0.5,${s3[r2]+i3[r2]} L0,${s3[r2]+i3[r2]} Z`):o&&e3.push(`M0.5,${s3[r2]} L1,${s3[r2]} L1,${s3[r2]+i3[r2]} L0.5,${s3[r2]+i3[r2]} Z`)}return e3.join(" ")}}}function r(t3){const e3=[],i3=.15,s3=.015,r2=.075,a=.0375,o=.95;if(64&t3){const t4=.175,a2=.2125,n=.25,L=.2+s3,h=o-i3-s3;e3.push(`M${L},${a2} L${L+r2},${t4} L${h-r2},${t4} L${h},${a2} L${h-r2},${n} L${L+r2},${n} Z`)}if(32&t3){const t4=o-i3,s4=.875,r3=o,n=.265,L=.485;e3.push(`M${s4},${n} L${r3},${n+a} L${r3},${L-a} L${s4},${L} L${t4},${L-a} L${t4},${n+a} Z`)}if(16&t3){const t4=o-i3,s4=.875,r3=o,n=.515,L=.735;e3.push(`M${s4},${n} L${r3},${n+a} L${r3},${L-a} L${s4},${L} L${t4},${L-a} L${t4},${n+a} Z`)}if(8&t3){const t4=.75,a2=.7875,n=.825,L=.2+s3,h=o-i3-s3;e3.push(`M${L},${a2} L${L+r2},${t4} L${h-r2},${t4} L${h},${a2} L${h-r2},${n} L${L+r2},${n} Z`)}if(4&t3){const t4=.05,i4=.125,s4=.2,r3=.515,o2=.735;e3.push(`M${i4},${r3} L${s4},${r3+a} L${s4},${o2-a} L${i4},${o2} L${t4},${o2-a} L${t4},${r3+a} Z`)}if(2&t3){const t4=.05,i4=.125,s4=.2,r3=.265,o2=.485;e3.push(`M${i4},${r3} L${s4},${r3+a} L${s4},${o2-a} L${i4},${o2} L${t4},${o2-a} L${t4},${r3+a} Z`)}if(1&t3){const t4=.4625,a2=.5,n=.5375,L=.2+s3,h=o-i3-s3;e3.push(`M${L},${a2} L${L+r2},${t4} L${h-r2},${t4} L${h},${a2} L${h-r2},${n} L${L+r2},${n} Z`)}return e3.join(" ")};Object.defineProperty(e2,"__esModule",{value:true}),e2.blockPatternCodepoints=e2.customGlyphDefinitions=void 0,(function(t3){;t3.LINE_H=Object.freeze({type:2,data:"M0,.5 L1,.5",strokeWidth:1}),t3.LINE_V=Object.freeze({type:2,data:"M.5,0 L.5,1",strokeWidth:1}),t3.FADE_RIGHT=Object.freeze({type:2,data:"M0,.5 L.28,.5 M.32,.5 L.52,.5 M.60,.5 L.72,.5 M.84,.5 L.90,.5",strokeWidth:1}),t3.FADE_LEFT=Object.freeze({type:2,data:"M.10,.5 L.16,.5 M.28,.5 L.40,.5 M.48,.5 L.68,.5 M.72,.5 L1,.5",strokeWidth:1}),t3.FADE_DOWN=Object.freeze({type:2,data:"M.5,0 L.5,.28 M.5,.32 L.5,.52 M.5,.60 L.5,.72 M.5,.84 L.5,.90",strokeWidth:1}),t3.FADE_UP=Object.freeze({type:2,data:"M.5,.10 L.5,.16 M.5,.28 L.5,.40 M.5,.48 L.5,.68 M.5,.72 L.5,1",strokeWidth:1}),t3.CURVE_DOWN_RIGHT=Object.freeze({type:2,data:(t4,e3)=>`M.5,1 L.5,${.5+e3/.15*.5} C.5,${.5+e3/.15*.5},.5,.5,1,.5`,strokeWidth:1}),t3.CURVE_DOWN_LEFT=Object.freeze({type:2,data:(t4,e3)=>`M.5,1 L.5,${.5+e3/.15*.5} C.5,${.5+e3/.15*.5},.5,.5,0,.5`,strokeWidth:1}),t3.CURVE_UP_RIGHT=Object.freeze({type:2,data:(t4,e3)=>`M.5,0 L.5,${.5-e3/.15*.5} C.5,${.5-e3/.15*.5},.5,.5,1,.5`,strokeWidth:1}),t3.CURVE_UP_LEFT=Object.freeze({type:2,data:(t4,e3)=>`M.5,0 L.5,${.5-e3/.15*.5} C.5,${.5-e3/.15*.5},.5,.5,0,.5`,strokeWidth:1}),t3.NODE_FILL=Object.freeze({type:3,data:"M.85,.5 A.35,.175,0,1,1,.15,.5 A.35,.175,0,1,1,.85,.5"}),t3.NODE_STROKE=Object.freeze({type:3,data:"M.85,.5 A.35,.175,0,1,1,.15,.5 A.35,.175,0,1,1,.85,.5",strokeWidth:1}),t3.NODE_LINE_RIGHT=Object.freeze({type:3,data:"M1,.5 L.85,.5",strokeWidth:1}),t3.NODE_LINE_LEFT=Object.freeze({type:3,data:"M0,.5 L.15,.5",strokeWidth:1}),t3.NODE_LINE_DOWN=Object.freeze({type:3,data:"M.5,1 L.5,.7",strokeWidth:1}),t3.NODE_LINE_UP=Object.freeze({type:3,data:"M.5,0 L.5,.3",strokeWidth:1})})(i2||(i2={})),e2.customGlyphDefinitions={"\u2500":{type:2,data:"M0,.5 L1,.5",strokeWidth:1},"\u2501":{type:2,data:"M0,.5 L1,.5",strokeWidth:3},"\u2502":{type:2,data:"M.5,0 L.5,1",strokeWidth:1},"\u2503":{type:2,data:"M.5,0 L.5,1",strokeWidth:3},"\u2504":{type:2,data:"M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5",strokeWidth:1},"\u2505":{type:2,data:"M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5",strokeWidth:3},"\u2506":{type:2,data:"M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333",strokeWidth:1},"\u2507":{type:2,data:"M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333",strokeWidth:3},"\u2508":{type:2,data:"M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5",strokeWidth:1},"\u2509":{type:2,data:"M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5",strokeWidth:3},"\u250A":{type:2,data:"M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95",strokeWidth:1},"\u250B":{type:2,data:"M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95",strokeWidth:3},"\u250C":{type:2,data:"M0.5,1 L.5,.5 L1,.5",strokeWidth:1},"\u250D":[{type:2,data:"M.5,.5 L.5,1",strokeWidth:1},{type:2,data:"M.5,.5 L1,.5",strokeWidth:3}],"\u250E":[{type:2,data:"M.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,.5 L.5,1",strokeWidth:3}],"\u250F":{type:2,data:"M0.5,1 L.5,.5 L1,.5",strokeWidth:3},"\u2510":{type:2,data:"M0,.5 L.5,.5 L.5,1",strokeWidth:1},"\u2511":[{type:2,data:"M.5,.5 L.5,1",strokeWidth:1},{type:2,data:"M.5,.5 L0,.5",strokeWidth:3}],"\u2512":[{type:2,data:"M.5,.5 L0,.5",strokeWidth:1},{type:2,data:"M.5,.5 L.5,1",strokeWidth:3}],"\u2513":{type:2,data:"M0,.5 L.5,.5 L.5,1",strokeWidth:3},"\u2514":{type:2,data:"M.5,0 L.5,.5 L1,.5",strokeWidth:1},"\u2515":[{type:2,data:"M.5,.5 L.5,0",strokeWidth:1},{type:2,data:"M.5,.5 L1,.5",strokeWidth:3}],"\u2516":[{type:2,data:"M.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,.5 L.5,0",strokeWidth:3}],"\u2517":{type:2,data:"M.5,0 L.5,.5 L1,.5",strokeWidth:3},"\u2518":{type:2,data:"M.5,0 L.5,.5 L0,.5",strokeWidth:1},"\u2519":[{type:2,data:"M.5,.5 L.5,0",strokeWidth:1},{type:2,data:"M.5,.5 L0,.5",strokeWidth:3}],"\u251A":[{type:2,data:"M.5,.5 L0,.5",strokeWidth:1},{type:2,data:"M.5,.5 L.5,0",strokeWidth:3}],"\u251B":{type:2,data:"M.5,0 L.5,.5 L0,.5",strokeWidth:3},"\u251C":{type:2,data:"M.5,0 L.5,1 M.5,.5 L1,.5",strokeWidth:1},"\u251D":[{type:2,data:"M.5,0 L.5,1",strokeWidth:1},{type:2,data:"M.5,.5 L1,.5",strokeWidth:3}],"\u251E":[{type:2,data:"M0.5,1 L.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,.5 L.5,0",strokeWidth:3}],"\u251F":[{type:2,data:"M.5,0 L.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,.5 L.5,1",strokeWidth:3}],"\u2520":[{type:2,data:"M.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,0 L.5,1",strokeWidth:3}],"\u2521":[{type:2,data:"M.5,.5 L.5,1",strokeWidth:1},{type:2,data:"M.5,0 L.5,.5 L1,.5",strokeWidth:3}],"\u2522":[{type:2,data:"M.5,.5 L.5,0",strokeWidth:1},{type:2,data:"M0.5,1 L.5,.5 L1,.5",strokeWidth:3}],"\u2523":{type:2,data:"M.5,0 L.5,1 M.5,.5 L1,.5",strokeWidth:3},"\u2524":{type:2,data:"M.5,0 L.5,1 M.5,.5 L0,.5",strokeWidth:1},"\u2525":[{type:2,data:"M.5,0 L.5,1",strokeWidth:1},{type:2,data:"M.5,.5 L0,.5",strokeWidth:3}],"\u2526":[{type:2,data:"M0,.5 L.5,.5 L.5,1",strokeWidth:1},{type:2,data:"M.5,.5 L.5,0",strokeWidth:3}],"\u2527":[{type:2,data:"M.5,0 L.5,.5 L0,.5",strokeWidth:1},{type:2,data:"M.5,.5 L.5,1",strokeWidth:3}],"\u2528":[{type:2,data:"M.5,.5 L0,.5",strokeWidth:1},{type:2,data:"M.5,0 L.5,1",strokeWidth:3}],"\u2529":[{type:2,data:"M.5,.5 L.5,1",strokeWidth:1},{type:2,data:"M.5,0 L.5,.5 L0,.5",strokeWidth:3}],"\u252A":[{type:2,data:"M.5,.5 L.5,0",strokeWidth:1},{type:2,data:"M0,.5 L.5,.5 L.5,1",strokeWidth:3}],"\u252B":{type:2,data:"M.5,0 L.5,1 M.5,.5 L0,.5",strokeWidth:3},"\u252C":{type:2,data:"M0,.5 L1,.5 M.5,.5 L.5,1",strokeWidth:1},"\u252D":[{type:2,data:"M0.5,1 L.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,.5 L0,.5",strokeWidth:3}],"\u252E":[{type:2,data:"M0,.5 L.5,.5 L.5,1",strokeWidth:1},{type:2,data:"M.5,.5 L1,.5",strokeWidth:3}],"\u252F":[{type:2,data:"M.5,.5 L.5,1",strokeWidth:1},{type:2,data:"M0,.5 L1,.5",strokeWidth:3}],"\u2530":[{type:2,data:"M0,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,.5 L.5,1",strokeWidth:3}],"\u2531":[{type:2,data:"M.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M0,.5 L.5,.5 L.5,1",strokeWidth:3}],"\u2532":[{type:2,data:"M.5,.5 L0,.5",strokeWidth:1},{type:2,data:"M0.5,1 L.5,.5 L1,.5",strokeWidth:3}],"\u2533":{type:2,data:"M0,.5 L1,.5 M.5,.5 L.5,1",strokeWidth:3},"\u2534":{type:2,data:"M0,.5 L1,.5 M.5,.5 L.5,0",strokeWidth:1},"\u2535":[{type:2,data:"M.5,0 L.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,.5 L0,.5",strokeWidth:3}],"\u2536":[{type:2,data:"M.5,0 L.5,.5 L0,.5",strokeWidth:1},{type:2,data:"M.5,.5 L1,.5",strokeWidth:3}],"\u2537":[{type:2,data:"M.5,.5 L.5,0",strokeWidth:1},{type:2,data:"M0,.5 L1,.5",strokeWidth:3}],"\u2538":[{type:2,data:"M0,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,.5 L.5,0",strokeWidth:3}],"\u2539":[{type:2,data:"M.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,0 L.5,.5 L0,.5",strokeWidth:3}],"\u253A":[{type:2,data:"M.5,.5 L0,.5",strokeWidth:1},{type:2,data:"M.5,0 L.5,.5 L1,.5",strokeWidth:3}],"\u253B":{type:2,data:"M0,.5 L1,.5 M.5,.5 L.5,0",strokeWidth:3},"\u253C":{type:2,data:"M0,.5 L1,.5 M.5,0 L.5,1",strokeWidth:1},"\u253D":[{type:2,data:"M.5,0 L.5,1 M.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,.5 L0,.5",strokeWidth:3}],"\u253E":[{type:2,data:"M.5,0 L.5,1 M.5,.5 L0,.5",strokeWidth:1},{type:2,data:"M.5,.5 L1,.5",strokeWidth:3}],"\u253F":[{type:2,data:"M.5,0 L.5,1",strokeWidth:1},{type:2,data:"M0,.5 L1,.5",strokeWidth:3}],"\u2540":[{type:2,data:"M0,.5 L1,.5 M.5,.5 L.5,1",strokeWidth:1},{type:2,data:"M.5,.5 L.5,0",strokeWidth:3}],"\u2541":[{type:2,data:"M.5,.5 L.5,0 M0,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,.5 L.5,1",strokeWidth:3}],"\u2542":[{type:2,data:"M0,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,0 L.5,1",strokeWidth:3}],"\u2543":[{type:2,data:"M0.5,1 L.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,0 L.5,.5 L0,.5",strokeWidth:3}],"\u2544":[{type:2,data:"M0,.5 L.5,.5 L.5,1",strokeWidth:1},{type:2,data:"M.5,0 L.5,.5 L1,.5",strokeWidth:3}],"\u2545":[{type:2,data:"M.5,0 L.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M0,.5 L.5,.5 L.5,1",strokeWidth:3}],"\u2546":[{type:2,data:"M.5,0 L.5,.5 L0,.5",strokeWidth:1},{type:2,data:"M0.5,1 L.5,.5 L1,.5",strokeWidth:3}],"\u2547":[{type:2,data:"M.5,.5 L.5,1",strokeWidth:1},{type:2,data:"M.5,.5 L.5,0 M0,.5 L1,.5",strokeWidth:3}],"\u2548":[{type:2,data:"M.5,.5 L.5,0",strokeWidth:1},{type:2,data:"M0,.5 L1,.5 M.5,.5 L.5,1",strokeWidth:3}],"\u2549":[{type:2,data:"M.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,0 L.5,1 M.5,.5 L0,.5",strokeWidth:3}],"\u254A":[{type:2,data:"M.5,.5 L0,.5",strokeWidth:1},{type:2,data:"M.5,0 L.5,1 M.5,.5 L1,.5",strokeWidth:3}],"\u254B":{type:2,data:"M0,.5 L1,.5 M.5,0 L.5,1",strokeWidth:3},"\u254C":{type:2,data:"M.1,.5 L.4,.5 M.6,.5 L.9,.5",strokeWidth:1},"\u254D":{type:2,data:"M.1,.5 L.4,.5 M.6,.5 L.9,.5",strokeWidth:3},"\u254E":{type:2,data:"M.5,.1 L.5,.4 M.5,.6 L.5,.9",strokeWidth:1},"\u254F":{type:2,data:"M.5,.1 L.5,.4 M.5,.6 L.5,.9",strokeWidth:3},"\u2550":{type:2,data:(t3,e3)=>`M0,${.5-e3} L1,${.5-e3} M0,${.5+e3} L1,${.5+e3}`,strokeWidth:1},"\u2551":{type:2,data:(t3,e3)=>`M${.5-t3},0 L${.5-t3},1 M${.5+t3},0 L${.5+t3},1`,strokeWidth:1},"\u2552":{type:2,data:(t3,e3)=>`M.5,1 L.5,${.5-e3} L1,${.5-e3} M.5,${.5+e3} L1,${.5+e3}`,strokeWidth:1},"\u2553":{type:2,data:(t3,e3)=>`M${.5-t3},1 L${.5-t3},.5 L1,.5 M${.5+t3},.5 L${.5+t3},1`,strokeWidth:1},"\u2554":{type:2,data:(t3,e3)=>`M1,${.5-e3} L${.5-t3},${.5-e3} L${.5-t3},1 M1,${.5+e3} L${.5+t3},${.5+e3} L${.5+t3},1`,strokeWidth:1},"\u2555":{type:2,data:(t3,e3)=>`M0,${.5-e3} L.5,${.5-e3} L.5,1 M0,${.5+e3} L.5,${.5+e3}`,strokeWidth:1},"\u2556":{type:2,data:(t3,e3)=>`M${.5+t3},1 L${.5+t3},.5 L0,.5 M${.5-t3},.5 L${.5-t3},1`,strokeWidth:1},"\u2557":{type:2,data:(t3,e3)=>`M0,${.5+e3} L${.5-t3},${.5+e3} L${.5-t3},1 M0,${.5-e3} L${.5+t3},${.5-e3} L${.5+t3},1`,strokeWidth:1},"\u2558":{type:2,data:(t3,e3)=>`M.5,0 L.5,${.5+e3} L1,${.5+e3} M.5,${.5-e3} L1,${.5-e3}`,strokeWidth:1},"\u2559":{type:2,data:(t3,e3)=>`M1,.5 L${.5-t3},.5 L${.5-t3},0 M${.5+t3},.5 L${.5+t3},0`,strokeWidth:1},"\u255A":{type:2,data:(t3,e3)=>`M1,${.5-e3} L${.5+t3},${.5-e3} L${.5+t3},0 M1,${.5+e3} L${.5-t3},${.5+e3} L${.5-t3},0`,strokeWidth:1},"\u255B":{type:2,data:(t3,e3)=>`M0,${.5+e3} L.5,${.5+e3} L.5,0 M0,${.5-e3} L.5,${.5-e3}`,strokeWidth:1},"\u255C":{type:2,data:(t3,e3)=>`M0,.5 L${.5+t3},.5 L${.5+t3},0 M${.5-t3},.5 L${.5-t3},0`,strokeWidth:1},"\u255D":{type:2,data:(t3,e3)=>`M0,${.5-e3} L${.5-t3},${.5-e3} L${.5-t3},0 M0,${.5+e3} L${.5+t3},${.5+e3} L${.5+t3},0`,strokeWidth:1},"\u255E":{type:2,data:(t3,e3)=>`M.5,0 L.5,1 M.5,${.5-e3} L1,${.5-e3} M.5,${.5+e3} L1,${.5+e3}`,strokeWidth:1},"\u255F":{type:2,data:(t3,e3)=>`M${.5-t3},0 L${.5-t3},1 M${.5+t3},0 L${.5+t3},1 M${.5+t3},.5 L1,.5`,strokeWidth:1},"\u2560":{type:2,data:(t3,e3)=>`M${.5-t3},0 L${.5-t3},1 M1,${.5+e3} L${.5+t3},${.5+e3} L${.5+t3},1 M1,${.5-e3} L${.5+t3},${.5-e3} L${.5+t3},0`,strokeWidth:1},"\u2561":{type:2,data:(t3,e3)=>`M.5,0 L.5,1 M0,${.5-e3} L.5,${.5-e3} M0,${.5+e3} L.5,${.5+e3}`,strokeWidth:1},"\u2562":{type:2,data:(t3,e3)=>`M0,.5 L${.5-t3},.5 M${.5-t3},0 L${.5-t3},1 M${.5+t3},0 L${.5+t3},1`,strokeWidth:1},"\u2563":{type:2,data:(t3,e3)=>`M${.5+t3},0 L${.5+t3},1 M0,${.5+e3} L${.5-t3},${.5+e3} L${.5-t3},1 M0,${.5-e3} L${.5-t3},${.5-e3} L${.5-t3},0`,strokeWidth:1},"\u2564":{type:2,data:(t3,e3)=>`M0,${.5-e3} L1,${.5-e3} M0,${.5+e3} L1,${.5+e3} M.5,${.5+e3} L.5,1`,strokeWidth:1},"\u2565":{type:2,data:(t3,e3)=>`M0,.5 L1,.5 M${.5-t3},.5 L${.5-t3},1 M${.5+t3},.5 L${.5+t3},1`,strokeWidth:1},"\u2566":{type:2,data:(t3,e3)=>`M0,${.5-e3} L1,${.5-e3} M0,${.5+e3} L${.5-t3},${.5+e3} L${.5-t3},1 M1,${.5+e3} L${.5+t3},${.5+e3} L${.5+t3},1`,strokeWidth:1},"\u2567":{type:2,data:(t3,e3)=>`M.5,0 L.5,${.5-e3} M0,${.5-e3} L1,${.5-e3} M0,${.5+e3} L1,${.5+e3}`,strokeWidth:1},"\u2568":{type:2,data:(t3,e3)=>`M0,.5 L1,.5 M${.5-t3},.5 L${.5-t3},0 M${.5+t3},.5 L${.5+t3},0`,strokeWidth:1},"\u2569":{type:2,data:(t3,e3)=>`M0,${.5+e3} L1,${.5+e3} M0,${.5-e3} L${.5-t3},${.5-e3} L${.5-t3},0 M1,${.5-e3} L${.5+t3},${.5-e3} L${.5+t3},0`,strokeWidth:1},"\u256A":{type:2,data:(t3,e3)=>`M.5,0 L.5,1 M0,${.5-e3} L1,${.5-e3} M0,${.5+e3} L1,${.5+e3}`,strokeWidth:1},"\u256B":{type:2,data:(t3,e3)=>`M0,.5 L1,.5 M${.5-t3},0 L${.5-t3},1 M${.5+t3},0 L${.5+t3},1`,strokeWidth:1},"\u256C":{type:2,data:(t3,e3)=>`M0,${.5+e3} L${.5-t3},${.5+e3} L${.5-t3},1 M1,${.5+e3} L${.5+t3},${.5+e3} L${.5+t3},1 M0,${.5-e3} L${.5-t3},${.5-e3} L${.5-t3},0 M1,${.5-e3} L${.5+t3},${.5-e3} L${.5+t3},0`,strokeWidth:1},"\u256D":{type:2,data:(t3,e3)=>`M.5,1 L.5,${.5+e3/.15*.5} C.5,${.5+e3/.15*.5},.5,.5,1,.5`,strokeWidth:1},"\u256E":{type:2,data:(t3,e3)=>`M.5,1 L.5,${.5+e3/.15*.5} C.5,${.5+e3/.15*.5},.5,.5,0,.5`,strokeWidth:1},"\u256F":{type:2,data:(t3,e3)=>`M.5,0 L.5,${.5-e3/.15*.5} C.5,${.5-e3/.15*.5},.5,.5,0,.5`,strokeWidth:1},"\u2570":{type:2,data:(t3,e3)=>`M.5,0 L.5,${.5-e3/.15*.5} C.5,${.5-e3/.15*.5},.5,.5,1,.5`,strokeWidth:1},"\u2571":{type:2,data:"M1,0 L0,1",strokeWidth:1},"\u2572":{type:2,data:"M0,0 L1,1",strokeWidth:1},"\u2573":{type:2,data:"M1,0 L0,1 M0,0 L1,1",strokeWidth:1},"\u2574":{type:2,data:"M.5,.5 L0,.5",strokeWidth:1},"\u2575":{type:2,data:"M.5,.5 L.5,0",strokeWidth:1},"\u2576":{type:2,data:"M.5,.5 L1,.5",strokeWidth:1},"\u2577":{type:2,data:"M.5,.5 L.5,1",strokeWidth:1},"\u2578":{type:2,data:"M.5,.5 L0,.5",strokeWidth:3},"\u2579":{type:2,data:"M.5,.5 L.5,0",strokeWidth:3},"\u257A":{type:2,data:"M.5,.5 L1,.5",strokeWidth:3},"\u257B":{type:2,data:"M.5,.5 L.5,1",strokeWidth:3},"\u257C":[{type:2,data:"M.5,.5 L0,.5",strokeWidth:1},{type:2,data:"M.5,.5 L1,.5",strokeWidth:3}],"\u257D":[{type:2,data:"M.5,.5 L.5,0",strokeWidth:1},{type:2,data:"M.5,.5 L.5,1",strokeWidth:3}],"\u257E":[{type:2,data:"M.5,.5 L1,.5",strokeWidth:1},{type:2,data:"M.5,.5 L0,.5",strokeWidth:3}],"\u257F":[{type:2,data:"M.5,.5 L.5,1",strokeWidth:1},{type:2,data:"M.5,.5 L.5,0",strokeWidth:3}],"\u2580":{type:0,data:[{x:0,y:0,w:8,h:4}]},"\u2581":{type:0,data:[{x:0,y:7,w:8,h:1}]},"\u2582":{type:0,data:[{x:0,y:6,w:8,h:2}]},"\u2583":{type:0,data:[{x:0,y:5,w:8,h:3}]},"\u2584":{type:0,data:[{x:0,y:4,w:8,h:4}]},"\u2585":{type:0,data:[{x:0,y:3,w:8,h:5}]},"\u2586":{type:0,data:[{x:0,y:2,w:8,h:6}]},"\u2587":{type:0,data:[{x:0,y:1,w:8,h:7}]},"\u2588":{type:0,data:[{x:0,y:0,w:8,h:8}]},"\u2589":{type:0,data:[{x:0,y:0,w:7,h:8}]},"\u258A":{type:0,data:[{x:0,y:0,w:6,h:8}]},"\u258B":{type:0,data:[{x:0,y:0,w:5,h:8}]},"\u258C":{type:0,data:[{x:0,y:0,w:4,h:8}]},"\u258D":{type:0,data:[{x:0,y:0,w:3,h:8}]},"\u258E":{type:0,data:[{x:0,y:0,w:2,h:8}]},"\u258F":{type:0,data:[{x:0,y:0,w:1,h:8}]},"\u2590":{type:0,data:[{x:4,y:0,w:4,h:8}]},"\u2591":{type:1,data:[[1,0],[0,0]]},"\u2592":{type:1,data:[[1,0],[0,1]]},"\u2593":{type:1,data:[[1,1],[1,0]]},"\u2594":{type:0,data:[{x:0,y:0,w:8,h:1}]},"\u2595":{type:0,data:[{x:7,y:0,w:1,h:8}]},"\u2596":{type:0,data:[{x:0,y:4,w:4,h:4}]},"\u2597":{type:0,data:[{x:4,y:4,w:4,h:4}]},"\u2598":{type:0,data:[{x:0,y:0,w:4,h:4}]},"\u2599":{type:0,data:[{x:0,y:0,w:4,h:8},{x:0,y:4,w:8,h:4}]},"\u259A":{type:0,data:[{x:0,y:0,w:4,h:4},{x:4,y:4,w:4,h:4}]},"\u259B":{type:0,data:[{x:0,y:0,w:4,h:8},{x:4,y:0,w:4,h:4}]},"\u259C":{type:0,data:[{x:0,y:0,w:8,h:4},{x:4,y:0,w:4,h:8}]},"\u259D":{type:0,data:[{x:4,y:0,w:4,h:4}]},"\u259E":{type:0,data:[{x:4,y:0,w:4,h:4},{x:0,y:4,w:4,h:4}]},"\u259F":{type:0,data:[{x:4,y:0,w:4,h:8},{x:0,y:4,w:8,h:4}]},"\uE0A0":{type:5,data:{d:"M.3,1 L.03,1 L.03,.88 C.03,.82,.06,.78,.11,.73 C.15,.7,.2,.68,.28,.65 L.43,.6 C.49,.58,.53,.56,.56,.53 C.59,.5,.6,.47,.6,.43 L.6,.27 L.4,.27 L.69,.1 L.98,.27 L.78,.27 L.78,.46 C.78,.52,.76,.56,.72,.61 C.68,.66,.63,.67,.56,.7 L.48,.72 C.42,.74,.38,.76,.35,.78 C.32,.8,.31,.84,.31,.88 L.31,1 M.3,.5 L.03,.59 L.03,.09 L.3,.09 L.3,.655",type:0}},"\uE0A1":{type:5,data:{d:"M.7,.4 L.7,.47 L.2,.47 L.2,.03 L.355,.03 L.355,.4 L.705,.4 M.7,.5 L.86,.5 L.86,.95 L.69,.95 L.44,.66 L.46,.86 L.46,.95 L.3,.95 L.3,.49 L.46,.49 L.71,.78 L.69,.565 L.69,.5",type:0}},"\uE0A2":{type:5,data:{d:"M.25,.94 C.16,.94,.11,.92,.11,.87 L.11,.53 C.11,.48,.15,.455,.23,.45 L.23,.3 C.23,.25,.26,.22,.31,.19 C.36,.16,.43,.15,.51,.15 C.59,.15,.66,.16,.71,.19 C.77,.22,.79,.26,.79,.3 L.79,.45 C.87,.45,.91,.48,.91,.53 L.91,.87 C.91,.92,.86,.94,.77,.94 L.24,.94 M.53,.2 C.49,.2,.45,.21,.42,.23 C.39,.25,.38,.27,.38,.3 L.38,.45 L.68,.45 L.68,.3 C.68,.27,.67,.25,.64,.23 C.61,.21,.58,.2,.53,.2 M.58,.82 L.58,.66 C.63,.65,.65,.63,.65,.6 C.65,.58,.64,.57,.61,.56 C.58,.55,.56,.54,.52,.54 C.48,.54,.46,.55,.43,.56 C.4,.57,.39,.59,.39,.6 C.39,.63,.41,.64,.46,.66 L.46,.82 L.57,.82",type:0}},"\uE0A3":{type:5,data:{d:"M.7,.4 L.7,.47 L.2,.47 L.2,.03 L.7,.03 L.7,.1 L.355,.1 L.355,.4 L.705,.4 M.7,.5 L.86,.5 L.86,.95 L.69,.95 L.44,.66 L.46,.86 L.46,.95 L.3,.95 L.3,.49 L.46,.49 L.71,.78 L.69,.565 L.69,.5",type:0}},"\uE0B0":{type:5,data:{d:"M0,0 L1,.5 L0,1",type:0,rightPadding:2}},"\uE0B1":{type:5,data:{d:"M-1,-.5 L1,.5 L-1,1.5",type:1,leftPadding:1,rightPadding:1}},"\uE0B2":{type:5,data:{d:"M1,0 L0,.5 L1,1",type:0,leftPadding:2}},"\uE0B3":{type:5,data:{d:"M2,-.5 L0,.5 L2,1.5",type:1,leftPadding:1,rightPadding:1}},"\uE0B4":{type:5,data:{d:"M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0",type:0,rightPadding:1}},"\uE0B5":{type:5,data:{d:"M.2,1 C.422,1,.8,.826,.78,.5 C.8,.174,0.422,0,.2,0",type:1,rightPadding:1}},"\uE0B6":{type:5,data:{d:"M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0",type:0,leftPadding:1}},"\uE0B7":{type:5,data:{d:"M.8,1 C0.578,1,0.2,.826,.22,.5 C0.2,0.174,0.578,0,0.8,0",type:1,leftPadding:1}},"\uE0B8":{type:5,data:{d:"M-.5,-.5 L1.5,1.5 L-.5,1.5",type:0}},"\uE0B9":{type:5,data:{d:"M-.5,-.5 L1.5,1.5",type:1,leftPadding:1,rightPadding:1}},"\uE0BA":{type:5,data:{d:"M1.5,-.5 L-.5,1.5 L1.5,1.5",type:0}},"\uE0BB":{type:5,data:{d:"M1.5,-.5 L-.5,1.5",type:1,leftPadding:1,rightPadding:1}},"\uE0BC":{type:5,data:{d:"M1.5,-.5 L-.5,1.5 L-.5,-.5",type:0}},"\uE0BD":{type:5,data:{d:"M1.5,-.5 L-.5,1.5",type:1,leftPadding:1,rightPadding:1}},"\uE0BE":{type:5,data:{d:"M-.5,-.5 L1.5,1.5 L1.5,-.5",type:0}},"\uE0BF":{type:5,data:{d:"M-.5,-.5 L1.5,1.5",type:1,leftPadding:1,rightPadding:1}},"\uE0C0":{type:5,data:{d:"M0.8069,0.6075 Q0.7991,0.612,0.7815,0.6218 T0.7542,0.6369 T0.7282,0.6508 T0.7016,0.6644 T0.6778,0.6758 T0.6551,0.6854 T0.637,0.6911 T0.622,0.6936 Q0.6186,0.6936,0.6128,0.6942 T0.6018,0.6952 T0.5894,0.6956 T0.5757,0.6952 T0.561,0.693 T0.5451,0.6879 Q0.5342,0.6834,0.5193,0.6756 T0.4925,0.6614 T0.4673,0.6485 T0.4436,0.6389 T0.4239,0.6363 T0.4089,0.6422 Q0.4045,0.6467,0.3958,0.6554 T0.3843,0.6669 T0.3748,0.6746 T0.3613,0.683 T0.3442,0.6903 L0.3499,0.6956 Q0.3584,0.7042,0.3679,0.7177 T0.3836,0.7403 T0.4019,0.7583 T0.4295,0.7703 Q0.4688,0.7785,0.5203,0.7605 Q0.4902,0.7817,0.449,0.7911 T0.3696,0.8001 Q0.3567,0.7997,0.341,0.7942 T0.3111,0.7813 T0.2805,0.766 T0.2493,0.753 T0.218,0.7472 T0.187,0.754 Q0.1762,0.7589,0.1653,0.7681 T0.1453,0.7895 T0.1308,0.818 T0.1264,0.8507 L0.1264,0.858 Q0.1453,0.8462,0.1607,0.8433 T0.1899,0.8452 T0.2226,0.8625 Q0.2344,0.8707,0.2485,0.8752 T0.2813,0.8831 T0.3089,0.889 Q0.2981,0.8915,0.2846,0.8956 T0.2603,0.9037 T0.2368,0.9115 T0.2129,0.9174 T0.1894,0.9194 T0.1653,0.9155 Q0.1504,0.9115,0.1328,0.9196 T0.0986,0.9421 T0.0655,0.9708 T0.0317,0.9943 T0,1 Q0.001,0.94,0.0017,0.6508 T0.0024,0.2322 Q0.002,0.2109,0.0007,0.1379 T0,0.0249 Q0.0058,0.0253,0.009,0.0257 T0.0156,0.0288 T0.0203,0.0333 T0.0264,0.0422 T0.0349,0.0539 Q0.044,0.0657,0.0542,0.072 T0.0752,0.0789 T0.096,0.0775 T0.1175,0.0694 T0.1379,0.0573 T0.1579,0.0432 Q0.2141,0,0.2673,0.0318 Q0.2588,0.0318,0.2517,0.033 T0.239,0.0361 T0.2276,0.0424 T0.218,0.0504 T0.2085,0.0612 T0.1997,0.0732 T0.1899,0.0875 T0.1795,0.1028 Q0.1748,0.1093,0.1507,0.1234 T0.1186,0.1448 Q0.0999,0.1612,0.1108,0.1783 Q0.1162,0.1877,0.1238,0.1928 T0.1404,0.1979 T0.158,0.1967 T0.1775,0.1895 T0.1963,0.1791 T0.2151,0.1663 T0.2314,0.1534 T0.2458,0.141 T0.2561,0.1322 Q0.2815,0.1106,0.3017,0.101 T0.344,0.0926 T0.3919,0.1077 Q0.4099,0.1175,0.4265,0.1206 T0.4588,0.1202 T0.488,0.1098 T0.5176,0.0914 Q0.5071,0.1032,0.4963,0.1122 T0.4722,0.1285 T0.4497,0.1401 T0.4223,0.1518 T0.3943,0.1636 Q0.374,0.1726,0.3623,0.1863 T0.3482,0.2162 Q0.3445,0.2403,0.3608,0.2605 T0.4072,0.2876 Q0.4194,0.2909,0.4326,0.2899 T0.457,0.2856 T0.4805,0.2764 T0.5032,0.264 T0.5256,0.2497 T0.5478,0.2352 T0.5698,0.2222 T0.5921,0.2118 Q0.6982,0.173,0.81,0.2436 Q0.833,0.2583,0.8496,0.2674 T0.887,0.2846 T0.925,0.294 T0.9617,0.2911 T1,0.2746 Q0.9817,0.2946,0.96,0.3056 T0.9175,0.3191 T0.8731,0.3209 T0.8277,0.3164 T0.7818,0.3109 T0.7359,0.3101 T0.6907,0.3197 T0.647,0.3448 Q0.6443,0.3472,0.6333,0.3562 T0.6194,0.3674 T0.6076,0.3766 T0.594,0.386 T0.5815,0.3931 T0.5661,0.4007 T0.5501,0.4064 L0.5556,0.4121 Q0.5627,0.4198,0.5722,0.4251 T0.5926,0.4337 T0.6131,0.439 T0.6343,0.4431 T0.6521,0.4468 Q0.6762,0.4529,0.7056,0.4463 Q0.6772,0.4623,0.6543,0.471 T0.604,0.4814 Q0.5962,0.4818,0.5815,0.4825 T0.5591,0.4835 T0.5393,0.4857 T0.519,0.4908 T0.5007,0.4996 T0.4812,0.5141 T0.4627,0.5353 L0.4549,0.5451 L0.4661,0.543 Q0.4759,0.541,0.4909,0.55 T0.5315,0.5796 T0.5684,0.6083 Q0.5799,0.6161,0.5915,0.6212 T0.6181,0.6308 L0.6331,0.6353 Q0.6514,0.6418,0.7004,0.633 T0.8069,0.6075 Z",type:0}},"\uE0C1":{type:5,data:{d:"M0.011,0.0469 Q0.0152,0.0412,0.02,0.0361 T0.0292,0.0281 T0.0365,0.0253 Q0.0426,0.0257,0.0461,0.0263 T0.0534,0.03 T0.0582,0.0345 T0.0645,0.0434 T0.0723,0.0546 Q0.0807,0.0653,0.0897,0.0712 T0.1081,0.0775 T0.1255,0.0765 T0.1438,0.0695 T0.1605,0.0593 T0.1772,0.0473 Q0.2388,0,0.2927,0.0326 L0.2614,0.0767 L0.2581,0.0767 Q0.2494,0.0767,0.2444,0.0773 T0.2359,0.0789 T0.2291,0.0842 T0.2233,0.0922 T0.2146,0.1054 T0.2026,0.1236 Q0.1943,0.1354,0.1843,0.1444 T0.1675,0.1574 T0.1526,0.1654 T0.1429,0.1701 Q0.1436,0.1717,0.1446,0.1733 Q0.1504,0.1823,0.1581,0.1866 T0.1733,0.1915 T0.1909,0.1884 T0.2085,0.1809 T0.2265,0.169 T0.2427,0.1564 T0.2575,0.1436 T0.2688,0.1338 Q0.2969,0.1101,0.3195,0.0995 T0.3643,0.0901 T0.4114,0.1048 Q0.4279,0.1138,0.4426,0.1168 T0.4705,0.117 T0.4945,0.1089 T0.5186,0.0942 L0.5211,0.0926 L0.5289,0.0922 Q0.5411,0.095,0.5189,0.1203 Q0.505,0.1358,0.4913,0.1474 T0.4634,0.167 T0.4389,0.1796 T0.4121,0.1909 T0.3866,0.2011 Q0.3782,0.2047,0.3727,0.2092 Q0.3717,0.2117,0.3714,0.2141 Q0.3695,0.2263,0.3751,0.2388 T0.394,0.2616 T0.4253,0.2761 Q0.4398,0.2785,0.4513,0.2779 T0.4756,0.2724 T0.4987,0.2616 T0.5216,0.2476 T0.5453,0.2321 T0.5703,0.217 T0.5976,0.2043 Q0.7015,0.166,0.8099,0.2345 Q0.8361,0.2512,0.8572,0.2616 T0.9006,0.2779 T0.9432,0.281 T0.9819,0.2663 L0.9887,0.2671 Q1,0.272,0.9768,0.2969 Q0.9558,0.3193,0.9319,0.333 T0.8858,0.3513 T0.8391,0.3569 T0.793,0.3546 T0.7485,0.3497 T0.7065,0.3473 T0.6681,0.3522 T0.6344,0.3699 Q0.5963,0.4005,0.5847,0.4082 Q0.595,0.4139,0.6079,0.4174 T0.6367,0.4237 T0.6586,0.4278 Q0.6793,0.4331,0.7044,0.4278 Q0.7057,0.4278,0.707,0.4274 Q0.7067,0.429,0.7062,0.4313 T0.7036,0.4398 T0.6993,0.4513 T0.6931,0.4619 T0.6851,0.4698 Q0.6573,0.4857,0.6336,0.4947 T0.5831,0.5053 Q0.577,0.5057,0.5615,0.5065 T0.5394,0.508 T0.5221,0.51 T0.5042,0.5139 T0.4905,0.5204 Q0.5008,0.5232,0.515,0.5328 T0.549,0.5585 T0.5799,0.5824 Q0.5905,0.5897,0.6013,0.5946 T0.6262,0.6036 T0.6409,0.6081 Q0.6567,0.6134,0.7035,0.605 T0.8028,0.5816 L0.7974,0.5922 T0.7867,0.6134 L0.7812,0.624 Q0.7799,0.6248,0.7786,0.6252 Q0.7709,0.6297,0.7573,0.6372 T0.7346,0.6499 T0.7123,0.6621 T0.6897,0.6741 T0.6686,0.6847 T0.648,0.6941 T0.6297,0.7013 T0.6129,0.7062 T0.5995,0.7076 Q0.5992,0.7076,0.5831,0.7092 T0.5529,0.7092 T0.525,0.7019 Q0.5105,0.6958,0.4824,0.6811 T0.4356,0.6586 T0.4079,0.6533 Q0.3872,0.6741,0.3827,0.6782 Q0.3872,0.6835,0.3934,0.6929 T0.4037,0.7078 T0.4142,0.7198 T0.4284,0.7304 T0.4466,0.7365 Q0.4824,0.7443,0.5292,0.728 L0.5292,0.7294 T0.529,0.7333 T0.5284,0.7392 T0.5266,0.7463 T0.5236,0.7539 T0.5186,0.7612 T0.5111,0.7679 Q0.5086,0.77,0.5056,0.7716 Q0.4466,0.8104,0.3591,0.8091 Q0.344,0.8091,0.3246,0.802 T0.2893,0.7863 T0.2543,0.77 T0.2206,0.76 T0.1894,0.7643 Q0.1804,0.7684,0.1733,0.7753 Q0.1591,0.7892,0.1601,0.8169 Q0.1784,0.8067,0.1938,0.8053 T0.2212,0.8087 T0.2507,0.825 Q0.2614,0.8324,0.2744,0.8365 T0.3053,0.844 T0.332,0.8499 L0.303,0.894 L0.2998,0.8948 Q0.2904,0.8968,0.278,0.9009 T0.2554,0.9086 T0.233,0.9158 T0.2099,0.9213 T0.1868,0.9229 T0.1633,0.9192 Q0.1517,0.916,0.1367,0.9241 T0.1065,0.9462 T0.0753,0.9737 T0.041,0.9955 T0.0061,1 Q0,0.9984,0.0068,0.9845 Q0.009,0.9804,0.0123,0.9755 Q0.0187,0.9666,0.0261,0.9606 T0.0374,0.9555 Q0.0481,0.958,0.0629,0.949 T0.0931,0.9262 T0.1247,0.8987 T0.1597,0.8777 T0.1949,0.8752 Q0.213,0.8805,0.2343,0.8772 Q0.2249,0.8736,0.2172,0.8683 Q0.1914,0.8511,0.1738,0.8497 T0.1349,0.8617 Q0.1304,0.865,0.1273,0.8646 T0.1242,0.8605 L0.1239,0.8536 Q0.1226,0.8295,0.1354,0.8016 T0.1723,0.7496 Q0.1914,0.7308,0.2104,0.7219 Q0.2259,0.7145,0.2425,0.7141 T0.273,0.7182 T0.3022,0.7296 T0.3301,0.7439 T0.3569,0.7571 T0.3821,0.7643 Q0.373,0.7561,0.3598,0.7367 T0.3382,0.7088 L0.3327,0.7035 Q0.3282,0.699,0.3353,0.6884 T0.3517,0.6692 L0.3611,0.6607 Q0.3711,0.657,0.3751,0.6554 T0.3837,0.6507 T0.3898,0.6458 T0.3979,0.637 T0.4111,0.6236 Q0.4172,0.6175,0.4234,0.6138 Q0.4327,0.6081,0.4439,0.6077 T0.4655,0.6105 T0.4884,0.6201 T0.5113,0.633 T0.5347,0.6468 T0.5573,0.6578 Q0.5705,0.6635,0.5833,0.665 T0.6112,0.665 T0.6286,0.6635 Q0.6421,0.6631,0.6718,0.6501 Q0.626,0.6578,0.6089,0.6517 Q0.6063,0.6509,0.6002,0.6491 T0.5884,0.6454 T0.5757,0.6409 T0.561,0.6344 T0.5466,0.6256 Q0.536,0.6183,0.5108,0.5989 T0.4722,0.5712 T0.4527,0.564 L0.4418,0.5661 Q0.4308,0.5681,0.4366,0.5563 Q0.4401,0.5485,0.4476,0.5392 L0.455,0.5298 Q0.4631,0.5188,0.4714,0.51 T0.4866,0.4947 T0.5021,0.4831 T0.516,0.4749 T0.53,0.4694 T0.5428,0.4657 T0.5558,0.4637 T0.5674,0.4629 T0.5795,0.4625 Q0.5489,0.4543,0.534,0.4384 L0.5286,0.4327 Q0.5244,0.4282,0.5318,0.4174 T0.5486,0.398 L0.5579,0.3899 Q0.567,0.387,0.5749,0.3834 T0.5878,0.3772 T0.6012,0.3687 T0.6126,0.3603 T0.6268,0.3487 T0.6421,0.3361 Q0.6567,0.3242,0.6712,0.3159 T0.7009,0.3034 T0.7277,0.2973 T0.7556,0.2961 T0.7809,0.2977 T0.8074,0.301 Q0.8096,0.3014,0.8109,0.3016 T0.8143,0.302 T0.818,0.3026 Q0.8012,0.2936,0.7767,0.2781 Q0.6718,0.2117,0.5744,0.2476 Q0.5637,0.2516,0.5492,0.2604 T0.5218,0.2779 T0.4923,0.2965 T0.461,0.3124 T0.4282,0.3216 T0.394,0.3206 Q0.3637,0.3136,0.3474,0.2936 T0.3353,0.2459 Q0.3385,0.2247,0.355,0.2023 Q0.3646,0.1892,0.3763,0.179 Q0.3898,0.1672,0.4063,0.1595 Q0.3927,0.1562,0.3785,0.1485 Q0.3604,0.1387,0.3453,0.1354 T0.3161,0.1354 T0.2899,0.1458 T0.2627,0.166 Q0.262,0.1664,0.254,0.1733 T0.2414,0.1841 T0.2267,0.196 T0.2094,0.2086 T0.1915,0.2196 T0.1725,0.2288 T0.1546,0.2337 T0.1373,0.2343 T0.1223,0.228 T0.1097,0.2145 Q0.0962,0.1929,0.1258,0.1582 Q0.131,0.1517,0.1371,0.1464 Q0.1462,0.1387,0.156,0.1321 T0.1752,0.1205 T0.1878,0.1134 Q0.1891,0.1117,0.1947,0.103 T0.2035,0.0899 T0.2125,0.0781 T0.2243,0.064 Q0.1949,0.0616,0.1672,0.0828 Q0.153,0.0934,0.1412,0.1009 T0.1146,0.1146 T0.0873,0.1213 T0.0616,0.116 T0.0374,0.0962 Q0.0339,0.0909,0.0295,0.0846 T0.024,0.0767 T0.0202,0.0728 T0.0148,0.0704 T0.0068,0.0697 Q0.0003,0.0693,0.0058,0.0567 Q0.0077,0.0522,0.011,0.0469 Z",type:0}},"\uE0C2":{type:5,data:{d:"M0.1934,0.6075 Q0.2012,0.612,0.2188,0.6218 T0.2459,0.6369 T0.2718,0.6508 T0.2983,0.6644 T0.322,0.6758 T0.3447,0.6854 T0.363,0.6911 T0.3783,0.6936 Q0.3796,0.6936,0.396,0.6952 T0.4265,0.6952 T0.4548,0.6879 Q0.4656,0.6834,0.4805,0.6756 T0.5073,0.6614 T0.5327,0.6485 T0.5564,0.6389 T0.5759,0.6363 T0.5909,0.6422 Q0.5953,0.6467,0.604,0.6554 T0.6155,0.6669 T0.6251,0.6746 T0.6387,0.683 T0.6556,0.6903 L0.6498,0.6956 Q0.6414,0.7042,0.6319,0.7177 T0.6162,0.7403 T0.5979,0.7583 T0.5703,0.7703 Q0.5313,0.7785,0.4799,0.7605 Q0.5097,0.7817,0.5508,0.7911 T0.6302,0.8001 Q0.6431,0.7997,0.659,0.7942 T0.6888,0.7813 T0.7194,0.766 T0.7506,0.753 T0.7819,0.7472 T0.8131,0.754 Q0.8239,0.7589,0.8346,0.7681 T0.8544,0.7895 T0.8689,0.818 T0.8737,0.8507 L0.8733,0.858 Q0.8591,0.849,0.8474,0.845 T0.8242,0.8421 T0.8021,0.8482 T0.7772,0.8625 Q0.7653,0.8707,0.7513,0.8752 T0.7184,0.8831 T0.6908,0.889 Q0.7017,0.8915,0.7152,0.8956 T0.7396,0.9037 T0.7633,0.9115 T0.7872,0.9174 T0.8107,0.9194 T0.8347,0.9155 Q0.8493,0.9115,0.8671,0.9196 T0.9015,0.9421 T0.9345,0.9708 T0.9682,0.9943 T0.9997,1 Q0.999,0.94,0.9983,0.6508 T0.9976,0.2322 Q0.9976,0.2109,0.9992,0.1379 T1,0.0249 Q0.9949,0.0249,0.9917,0.0257 T0.9863,0.0273 T0.9817,0.0308 T0.9776,0.0357 T0.9721,0.0437 T0.9651,0.0539 Q0.956,0.0657,0.9456,0.072 T0.9247,0.0789 T0.904,0.0775 T0.8823,0.0694 T0.862,0.0573 T0.8422,0.0432 Q0.7856,0,0.7328,0.0318 Q0.7392,0.0318,0.7452,0.0324 T0.756,0.0345 T0.7657,0.0386 T0.7741,0.0437 T0.7821,0.0506 T0.7894,0.0588 T0.7966,0.0683 T0.8039,0.0787 T0.8119,0.0904 T0.8202,0.1028 Q0.8249,0.1093,0.849,0.1234 T0.8811,0.1448 Q0.8998,0.1612,0.8893,0.1783 Q0.8835,0.1877,0.8759,0.1928 T0.8593,0.1979 T0.8417,0.1967 T0.8222,0.1895 T0.8034,0.1791 T0.7848,0.1663 T0.7687,0.1534 T0.7543,0.141 T0.744,0.1322 Q0.7186,0.1106,0.6984,0.101 T0.6559,0.0926 T0.6082,0.1077 Q0.5899,0.1175,0.5733,0.1206 T0.5411,0.1202 T0.5122,0.1098 T0.4822,0.0914 Q0.4927,0.1032,0.5036,0.1122 T0.5276,0.1285 T0.5503,0.1401 T0.5777,0.1518 T0.6055,0.1636 Q0.6258,0.1726,0.6375,0.1863 T0.6519,0.2162 Q0.6556,0.2403,0.6392,0.2605 T0.5926,0.2876 Q0.5804,0.2909,0.5672,0.2899 T0.5428,0.2856 T0.5193,0.2764 T0.4966,0.264 T0.4744,0.2497 T0.4523,0.2352 T0.4301,0.2222 T0.4077,0.2118 Q0.3021,0.173,0.1903,0.2436 Q0.1669,0.2583,0.1504,0.2674 T0.1129,0.2846 T0.075,0.294 T0.0384,0.2911 T0,0.2746 Q0.021,0.297,0.0459,0.3082 T0.0953,0.3207 T0.146,0.3193 T0.1988,0.3131 T0.2508,0.3095 T0.3029,0.3176 T0.3529,0.3448 Q0.3566,0.348,0.3652,0.355 T0.3771,0.3645 T0.3877,0.3731 T0.3993,0.3815 T0.4103,0.3884 T0.4226,0.3953 T0.4352,0.4009 T0.4497,0.4064 L0.4443,0.4121 Q0.4372,0.4198,0.4277,0.4251 T0.4072,0.4337 T0.3867,0.439 T0.3656,0.4431 T0.3481,0.4468 Q0.3241,0.4529,0.2943,0.4463 Q0.3227,0.4623,0.3458,0.471 T0.3959,0.4814 Q0.404,0.4818,0.4187,0.4825 T0.4409,0.4835 T0.4605,0.4857 T0.4809,0.4908 T0.4992,0.4996 T0.5186,0.5141 T0.5371,0.5353 L0.5449,0.5451 L0.5337,0.543 Q0.5242,0.541,0.5091,0.55 T0.4683,0.5796 T0.4314,0.6083 Q0.4203,0.6161,0.4086,0.6212 T0.382,0.6308 T0.3667,0.6353 Q0.3485,0.6418,0.2995,0.633 T0.1934,0.6075 Z",type:0}},"\uE0C3":{type:5,data:{d:"M0.989,0.0469 Q0.9829,0.0379,0.9753,0.0316 T0.9635,0.0253 Q0.9574,0.0257,0.9539,0.0263 T0.9466,0.03 T0.9418,0.0345 T0.9355,0.0434 T0.9277,0.0546 Q0.9193,0.0653,0.9103,0.0712 T0.8919,0.0775 T0.8745,0.0765 T0.8564,0.0695 T0.8396,0.0593 T0.8228,0.0473 Q0.7612,0,0.7073,0.0326 L0.7386,0.0767 L0.7419,0.0767 Q0.7506,0.0767,0.7556,0.0773 T0.7641,0.0789 T0.7709,0.0842 T0.7767,0.0922 T0.7854,0.1054 T0.7974,0.1236 Q0.8057,0.1354,0.8157,0.1444 T0.8325,0.1574 T0.8474,0.1654 T0.8571,0.1701 Q0.8564,0.1717,0.8554,0.1733 Q0.8503,0.1815,0.844,0.186 T0.8301,0.1909 T0.8153,0.1903 T0.7993,0.1845 T0.7836,0.1758 T0.768,0.1648 T0.754,0.1535 T0.741,0.1425 T0.7312,0.1338 Q0.7031,0.1101,0.6805,0.0995 T0.6357,0.0901 T0.5886,0.1048 Q0.5721,0.1138,0.5574,0.1168 T0.5295,0.117 T0.5055,0.1089 T0.4814,0.0942 L0.4789,0.0926 L0.4711,0.0922 Q0.4589,0.095,0.4811,0.1203 Q0.495,0.1358,0.5087,0.1474 T0.5366,0.167 T0.5611,0.1796 T0.5879,0.1909 T0.6134,0.2011 Q0.6218,0.2047,0.6273,0.2092 Q0.6283,0.2117,0.6286,0.2141 Q0.6305,0.2263,0.6249,0.2388 T0.606,0.2616 T0.5747,0.2761 Q0.5602,0.2785,0.5487,0.2779 T0.5245,0.2724 T0.5015,0.2616 T0.4784,0.2476 T0.4547,0.2321 T0.4297,0.217 T0.4024,0.2043 Q0.2985,0.166,0.1901,0.2345 Q0.1694,0.248,0.1523,0.2569 T0.1165,0.2726 T0.0818,0.2812 T0.0494,0.2796 T0.0181,0.2663 L0.0116,0.2671 Q0,0.272,0.0232,0.2969 Q0.0468,0.3222,0.0742,0.3365 T0.1278,0.3542 T0.1807,0.3564 T0.233,0.352 T0.2815,0.3473 T0.3269,0.3509 T0.3656,0.3699 Q0.4037,0.4005,0.4153,0.4082 Q0.405,0.4139,0.3921,0.4174 T0.3633,0.4237 T0.3414,0.4278 Q0.3207,0.4331,0.2956,0.4278 Q0.2943,0.4278,0.293,0.4274 Q0.2933,0.429,0.2938,0.4313 T0.2964,0.4398 T0.3007,0.4513 T0.3069,0.4619 T0.3149,0.4698 Q0.3427,0.4857,0.3664,0.4947 T0.4169,0.5053 Q0.423,0.5057,0.4385,0.5065 T0.4606,0.508 T0.4779,0.51 T0.4958,0.5139 T0.5095,0.5204 Q0.4992,0.5232,0.485,0.5328 T0.451,0.5585 T0.4201,0.5824 Q0.4095,0.5897,0.3987,0.5946 T0.3738,0.6036 T0.3591,0.6081 Q0.3433,0.6134,0.2965,0.605 T0.1972,0.5816 L0.2026,0.5922 T0.2133,0.6134 L0.2188,0.624 Q0.2201,0.6248,0.2214,0.6252 Q0.2291,0.6297,0.2486,0.6405 T0.2786,0.657 T0.3066,0.6721 T0.3353,0.6866 T0.3598,0.6972 T0.383,0.7051 T0.4005,0.7076 Q0.4008,0.7076,0.4169,0.7092 T0.4471,0.7092 T0.475,0.7019 Q0.4895,0.6958,0.5176,0.6811 T0.5644,0.6586 T0.5921,0.6533 Q0.6128,0.6741,0.6173,0.6782 Q0.6131,0.6835,0.6068,0.6929 T0.5963,0.7078 T0.5858,0.7198 T0.5716,0.7304 T0.5534,0.7365 Q0.5179,0.7443,0.4708,0.728 L0.4708,0.7294 T0.471,0.7333 T0.4716,0.7392 T0.4734,0.7463 T0.4764,0.7539 T0.4814,0.7612 T0.4889,0.7679 Q0.4914,0.77,0.4944,0.7716 Q0.5534,0.8104,0.6409,0.8091 Q0.6521,0.8091,0.6654,0.8053 T0.6907,0.7959 T0.7157,0.7838 T0.741,0.772 T0.7652,0.7631 T0.7888,0.7594 T0.8106,0.7643 Q0.8196,0.7684,0.8267,0.7753 Q0.8409,0.7892,0.8399,0.8169 Q0.8216,0.8067,0.8062,0.8053 T0.7788,0.8087 T0.7493,0.825 Q0.7386,0.8324,0.7256,0.8365 T0.6947,0.844 T0.668,0.8499 L0.697,0.894 L0.7002,0.8948 Q0.7099,0.8968,0.7222,0.9009 T0.7446,0.9086 T0.767,0.9158 T0.7901,0.9213 T0.8132,0.9229 T0.8367,0.9192 Q0.8483,0.916,0.8633,0.9241 T0.8937,0.9462 T0.9248,0.9737 T0.959,0.9955 T0.9939,1 Q1,0.9984,0.9932,0.9845 Q0.991,0.9804,0.9877,0.9755 Q0.9813,0.9666,0.9739,0.9606 T0.9626,0.9555 Q0.9519,0.958,0.9372,0.949 T0.9071,0.9262 T0.8753,0.8987 T0.8403,0.8777 T0.8051,0.8752 Q0.787,0.8805,0.7657,0.8772 Q0.7751,0.8736,0.7828,0.8683 Q0.8086,0.8511,0.8262,0.8497 T0.8651,0.8617 Q0.8696,0.865,0.8727,0.8646 T0.8758,0.8605 L0.8761,0.8536 Q0.8774,0.8295,0.8646,0.8016 T0.8277,0.7496 Q0.8086,0.7308,0.7896,0.7219 Q0.7741,0.7145,0.7575,0.7141 T0.727,0.7182 T0.6978,0.7296 T0.6699,0.7439 T0.6433,0.7571 T0.6179,0.7643 Q0.627,0.7561,0.6402,0.7367 T0.6618,0.7088 L0.6673,0.7035 Q0.6718,0.699,0.6647,0.6884 T0.6483,0.6692 L0.6389,0.6607 Q0.6289,0.657,0.6249,0.6554 T0.6163,0.6507 T0.6102,0.6458 T0.6021,0.637 T0.5889,0.6236 Q0.5828,0.6175,0.5766,0.6138 Q0.5673,0.6081,0.5561,0.6077 T0.5345,0.6105 T0.5116,0.6201 T0.4887,0.633 T0.4653,0.6468 T0.4427,0.6578 Q0.4295,0.6635,0.4167,0.665 T0.3888,0.665 T0.3714,0.6635 Q0.3579,0.6631,0.3282,0.6501 Q0.374,0.6578,0.3911,0.6517 Q0.3937,0.6509,0.4024,0.6482 T0.4179,0.6434 T0.4348,0.6364 T0.4534,0.6256 Q0.464,0.6183,0.4892,0.5989 T0.5278,0.5712 T0.5476,0.564 L0.5582,0.5661 Q0.5692,0.5681,0.5634,0.5563 Q0.5599,0.5485,0.5524,0.5392 L0.545,0.5298 Q0.5369,0.5188,0.5286,0.51 T0.5134,0.4947 T0.4979,0.4831 T0.484,0.4749 T0.47,0.4694 T0.4572,0.4657 T0.4442,0.4637 T0.4326,0.4629 T0.4205,0.4625 Q0.4511,0.4543,0.466,0.4384 L0.4714,0.4327 Q0.4795,0.4237,0.4469,0.394 L0.4421,0.3899 Q0.433,0.387,0.4251,0.3834 T0.4122,0.3772 T0.3988,0.3687 T0.3874,0.3603 T0.3732,0.3487 T0.3579,0.3361 Q0.3433,0.3242,0.3288,0.3159 T0.2993,0.3034 T0.2725,0.2973 T0.2446,0.2961 T0.2193,0.2977 T0.1926,0.301 Q0.1855,0.3022,0.182,0.3026 Q0.1991,0.2936,0.2233,0.2781 Q0.3282,0.2117,0.4256,0.2476 Q0.4363,0.2516,0.451,0.2604 T0.4784,0.2779 T0.5077,0.2965 T0.539,0.3124 T0.5718,0.3216 T0.606,0.3206 Q0.6363,0.3136,0.6526,0.2936 T0.6647,0.2459 Q0.6615,0.2247,0.645,0.2023 Q0.6354,0.1892,0.6237,0.179 Q0.6102,0.1672,0.5937,0.1595 Q0.6073,0.1562,0.6215,0.1485 Q0.6396,0.1387,0.6547,0.1354 T0.6839,0.1354 T0.7101,0.1458 T0.7373,0.166 Q0.738,0.1664,0.746,0.1733 T0.7586,0.1841 T0.7733,0.196 T0.7907,0.2086 T0.8086,0.2196 T0.8275,0.2288 T0.8454,0.2337 T0.8627,0.2343 T0.8777,0.228 T0.8903,0.2145 Q0.9038,0.1929,0.8742,0.1582 Q0.869,0.1517,0.8629,0.1464 Q0.8538,0.1387,0.844,0.1321 T0.8248,0.1205 T0.8122,0.1134 Q0.8109,0.1117,0.8053,0.103 T0.7965,0.0899 T0.7875,0.0781 T0.7757,0.064 Q0.8051,0.0616,0.8328,0.0828 Q0.847,0.0934,0.8588,0.1009 T0.8854,0.1146 T0.9127,0.1213 T0.9384,0.116 T0.9626,0.0962 Q0.9664,0.0909,0.9706,0.0846 T0.976,0.0767 T0.9798,0.0728 T0.9852,0.0704 T0.9932,0.0697 Q0.9997,0.0693,0.9942,0.0567 Q0.9923,0.0522,0.989,0.0469 Z",type:0}},"\uE0C4":{type:5,data:{d:"M0.4116,0.0126 L0.4116,0.0776 L0.4871,0.0776 L0.4871,0.0126 L0.4116,0.0126 Z M0.4043,0 L0.4944,0 Q0.4973,0,0.4995,0.0021 T0.5017,0.0063 L0.5017,0.0839 Q0.5017,0.0864,0.4993,0.0883 T0.4944,0.0902 L0.4043,0.0902 Q0.4014,0.0902,0.3992,0.0881 T0.397,0.0839 L0.397,0.0063 Q0.397,0.0038,0.3994,0.0019 T0.4043,0 Z M0.4116,0.0126 L0.4116,0.0776 L0.4871,0.0776 L0.4871,0.0126 L0.4116,0.0126 Z M0.4043,0 L0.4944,0 Q0.4973,0,0.4995,0.0021 T0.5017,0.0063 L0.5017,0.0839 Q0.5017,0.0864,0.4993,0.0883 T0.4944,0.0902 L0.4043,0.0902 Q0.4014,0.0902,0.3992,0.0881 T0.397,0.0839 L0.397,0.0063 Q0.397,0.0038,0.3994,0.0019 T0.4043,0 Z M0.4116,0.0126 L0.4116,0.0776 L0.4871,0.0776 L0.4871,0.0126 L0.4116,0.0126 Z M0.4043,0 L0.4944,0 Q0.4973,0,0.4995,0.0021 T0.5017,0.0063 L0.5017,0.0839 Q0.5017,0.0864,0.4993,0.0883 T0.4944,0.0902 L0.4043,0.0902 Q0.4014,0.0902,0.3992,0.0881 T0.397,0.0839 L0.397,0.0063 Q0.397,0.0038,0.3994,0.0019 T0.4043,0 Z M0.9099,0.0084 L0.9099,0.0818 L0.9951,0.0818 L0.9951,0.0084 L0.9099,0.0084 Z M0.9075,0.0042 L0.9976,0.0042 Q1,0.0042,1,0.0063 L1,0.0839 Q1,0.086,0.9976,0.086 L0.9075,0.086 Q0.905,0.086,0.905,0.0839 L0.905,0.0063 Q0.905,0.0042,0.9075,0.0042 Z M0.9099,0.0084 L0.9099,0.0818 L0.9951,0.0818 L0.9951,0.0084 L0.9099,0.0084 Z M0.9075,0.0042 L0.9976,0.0042 Q1,0.0042,1,0.0063 L1,0.0839 Q1,0.086,0.9976,0.086 L0.9075,0.086 Q0.905,0.086,0.905,0.0839 L0.905,0.0063 Q0.905,0.0042,0.9075,0.0042 Z M0.9099,0.0084 L0.9099,0.0818 L0.9951,0.0818 L0.9951,0.0084 L0.9099,0.0084 Z M0.9075,0.0042 L0.9976,0.0042 Q1,0.0042,1,0.0063 L1,0.0839 Q1,0.086,0.9976,0.086 L0.9075,0.086 Q0.905,0.086,0.905,0.0839 L0.905,0.0063 Q0.905,0.0042,0.9075,0.0042 Z M0.66,0.0063 L0.7501,0.0063 L0.7501,0.0839 L0.66,0.0839 L0.66,0.0063 Z M0.66,0.0063 L0.7501,0.0063 L0.7501,0.0839 L0.66,0.0839 L0.66,0.0063 Z M0.66,0.0063 L0.7501,0.0063 L0.7501,0.0839 L0.66,0.0839 L0.66,0.0063 Z M0.5309,0.0063 L0.621,0.0063 L0.621,0.0839 L0.5309,0.0839 L0.5309,0.0063 Z M0.5309,0.0063 L0.621,0.0063 L0.621,0.0839 L0.5309,0.0839 L0.5309,0.0063 Z M0.5309,0.0063 L0.621,0.0063 L0.621,0.0839 L0.5309,0.0839 L0.5309,0.0063 Z M0.2689,0.0063 L0.359,0.0063 L0.359,0.0839 L0.2689,0.0839 L0.2689,0.0063 Z M0.2689,0.0063 L0.359,0.0063 L0.359,0.0839 L0.2689,0.0839 L0.2689,0.0063 Z M0.2689,0.0063 L0.359,0.0063 L0.359,0.0839 L0.2689,0.0839 L0.2689,0.0063 Z M0.1374,0.0063 L0.2275,0.0063 L0.2275,0.0839 L0.1374,0.0839 L0.1374,0.0063 Z M0.1374,0.0063 L0.2275,0.0063 L0.2275,0.0839 L0.1374,0.0839 L0.1374,0.0063 Z M0.1374,0.0063 L0.2275,0.0063 L0.2275,0.0839 L0.1374,0.0839 L0.1374,0.0063 Z M0,0.0063 L0.0901,0.0063 L0.0901,0.0839 L0,0.0839 L0,0.0063 Z M0,0.0063 L0.0901,0.0063 L0.0901,0.0839 L0,0.0839 L0,0.0063 Z M0,0.0063 L0.0901,0.0063 L0.0901,0.0839 L0,0.0839 L0,0.0063 Z M0.4087,0.9224 L0.4087,0.9958 L0.4939,0.9958 L0.4939,0.9224 L0.4087,0.9224 Z M0.4062,0.9182 L0.4963,0.9182 Q0.4988,0.9182,0.4988,0.9203 L0.4988,0.9979 Q0.4988,1,0.4963,1 L0.4062,1 Q0.4038,1,0.4038,0.9979 L0.4038,0.9203 Q0.4038,0.9182,0.4062,0.9182 Z M0.4087,0.9224 L0.4087,0.9958 L0.4939,0.9958 L0.4939,0.9224 L0.4087,0.9224 Z M0.4062,0.9182 L0.4963,0.9182 Q0.4988,0.9182,0.4988,0.9203 L0.4988,0.9979 Q0.4988,1,0.4963,1 L0.4062,1 Q0.4038,1,0.4038,0.9979 L0.4038,0.9203 Q0.4038,0.9182,0.4062,0.9182 Z M0.4087,0.9224 L0.4087,0.9958 L0.4939,0.9958 L0.4939,0.9224 L0.4087,0.9224 Z M0.4062,0.9182 L0.4963,0.9182 Q0.4988,0.9182,0.4988,0.9203 L0.4988,0.9979 Q0.4988,1,0.4963,1 L0.4062,1 Q0.4038,1,0.4038,0.9979 L0.4038,0.9203 Q0.4038,0.9182,0.4062,0.9182 Z M0.1374,0.9203 L0.2275,0.9203 L0.2275,0.9979 L0.1374,0.9979 L0.1374,0.9203 Z M0.1374,0.9203 L0.2275,0.9203 L0.2275,0.9979 L0.1374,0.9979 L0.1374,0.9203 Z M0.1374,0.9203 L0.2275,0.9203 L0.2275,0.9979 L0.1374,0.9979 L0.1374,0.9203 Z M0,0.9203 L0.0901,0.9203 L0.0901,0.9979 L0,0.9979 L0,0.9203 Z M0,0.9203 L0.0901,0.9203 L0.0901,0.9979 L0,0.9979 L0,0.9203 Z M0,0.9203 L0.0901,0.9203 L0.0901,0.9979 L0,0.9979 L0,0.9203 Z M0.9099,0.1158 L0.9099,0.1888 L0.9951,0.1888 L0.9951,0.1158 L0.9099,0.1158 Z M0.9075,0.1116 L0.9976,0.1116 Q1,0.1116,1,0.1137 L1,0.1909 Q1,0.193,0.9976,0.193 L0.9075,0.193 Q0.905,0.193,0.905,0.1909 L0.905,0.1137 Q0.905,0.1116,0.9075,0.1116 Z M0.7852,0.6086 L0.7852,0.682 L0.8704,0.682 L0.8704,0.6086 L0.7852,0.6086 Z M0.7828,0.6044 L0.8729,0.6044 Q0.8753,0.6044,0.8753,0.6065 L0.8753,0.6841 Q0.8753,0.6862,0.8729,0.6862 L0.7828,0.6862 Q0.7803,0.6862,0.7803,0.6841 L0.7803,0.6065 Q0.7803,0.6044,0.7828,0.6044 Z M0.66,0.1137 L0.7501,0.1137 L0.7501,0.1909 L0.66,0.1909 L0.66,0.1137 Z M0.6624,0.4102 L0.6624,0.4836 L0.7477,0.4836 L0.7477,0.4102 L0.6624,0.4102 Z M0.66,0.406 L0.7501,0.406 Q0.7526,0.406,0.7526,0.4081 L0.7526,0.4857 Q0.7526,0.4878,0.7501,0.4878 L0.66,0.4878 Q0.6576,0.4878,0.6576,0.4857 L0.6576,0.4081 Q0.6576,0.406,0.66,0.406 Z M0.6624,0.7131 L0.6624,0.7865 L0.7477,0.7865 L0.7477,0.7131 L0.6624,0.7131 Z M0.66,0.7089 L0.7501,0.7089 Q0.7526,0.7089,0.7526,0.711 L0.7526,0.7886 Q0.7526,0.7907,0.7501,0.7907 L0.66,0.7907 Q0.6576,0.7907,0.6576,0.7886 L0.6576,0.711 Q0.6576,0.7089,0.66,0.7089 Z M0.5407,0.2227 L0.5407,0.2836 L0.6113,0.2836 L0.6113,0.2227 L0.5407,0.2227 Z M0.5309,0.206 L0.621,0.206 Q0.623,0.206,0.6249,0.2068 T0.6281,0.2089 T0.6301,0.2116 T0.6308,0.2143 L0.6308,0.2919 Q0.6308,0.2957,0.6276,0.298 T0.621,0.3003 L0.5309,0.3003 Q0.5265,0.3003,0.5239,0.2976 T0.5212,0.2919 L0.5212,0.2143 Q0.5212,0.211,0.5244,0.2085 T0.5309,0.206 Z M0.5407,0.3184 L0.5407,0.3792 L0.6113,0.3792 L0.6113,0.3184 L0.5407,0.3184 Z M0.5309,0.3016 L0.621,0.3016 Q0.6254,0.3016,0.6281,0.3043 T0.6308,0.31 L0.6308,0.3876 Q0.6308,0.3914,0.6276,0.3937 T0.621,0.396 L0.5309,0.396 Q0.5265,0.396,0.5239,0.3932 T0.5212,0.3876 L0.5212,0.31 Q0.5212,0.3062,0.5244,0.3039 T0.5309,0.3016 Z M0.5309,0.4081 L0.621,0.4081 L0.621,0.4857 L0.5309,0.4857 L0.5309,0.4081 Z M0.5309,0.5059 L0.621,0.5059 L0.621,0.5835 L0.5309,0.5835 L0.5309,0.5059 Z M0.5407,0.6149 L0.5407,0.6758 L0.6113,0.6758 L0.6113,0.6149 L0.5407,0.6149 Z M0.5309,0.5982 L0.621,0.5982 Q0.6254,0.5982,0.6281,0.6009 T0.6308,0.6065 L0.6308,0.6841 Q0.6308,0.6879,0.6276,0.6902 T0.621,0.6925 L0.5309,0.6925 Q0.5265,0.6925,0.5239,0.6898 T0.5212,0.6841 L0.5212,0.6065 Q0.5212,0.6028,0.5244,0.6005 T0.5309,0.5982 Z M0.4062,0.1137 L0.4963,0.1137 L0.4963,0.1909 L0.4062,0.1909 L0.4062,0.1137 Z M0.4062,0.2143 L0.4963,0.2143 L0.4963,0.2919 L0.4062,0.2919 L0.4062,0.2143 Z M0.4184,0.4186 L0.4184,0.4753 L0.4842,0.4753 L0.4842,0.4186 L0.4184,0.4186 Z M0.4062,0.3977 L0.4963,0.3977 Q0.5017,0.3977,0.5051,0.401 T0.5085,0.4081 L0.5085,0.4857 Q0.5085,0.4904,0.5046,0.4933 T0.4963,0.4962 L0.4062,0.4962 Q0.4009,0.4962,0.3975,0.4929 T0.3941,0.4857 L0.3941,0.4081 Q0.3941,0.4035,0.398,0.4006 T0.4062,0.3977 Z M0.4062,0.5059 L0.4963,0.5059 L0.4963,0.5835 L0.4062,0.5835 L0.4062,0.5059 Z M0.4087,0.7131 L0.4087,0.7865 L0.4939,0.7865 L0.4939,0.7131 L0.4087,0.7131 Z M0.4062,0.7089 L0.4963,0.7089 Q0.4988,0.7089,0.4988,0.711 L0.4988,0.7886 Q0.4988,0.7907,0.4963,0.7907 L0.4062,0.7907 Q0.4038,0.7907,0.4038,0.7886 L0.4038,0.711 Q0.4038,0.7089,0.4062,0.7089 Z M0.2689,0.1137 L0.359,0.1137 L0.359,0.1909 L0.2689,0.1909 L0.2689,0.1137 Z M0.2689,0.2143 L0.359,0.2143 L0.359,0.2919 L0.2689,0.2919 L0.2689,0.2143 Z M0.2689,0.8154 L0.359,0.8154 L0.359,0.8926 L0.2689,0.8926 L0.2689,0.8154 Z M0.2689,0.31 L0.359,0.31 L0.359,0.3876 L0.2689,0.3876 L0.2689,0.31 Z M0.2689,0.8154 L0.359,0.8154 L0.359,0.8926 L0.2689,0.8926 L0.2689,0.8154 Z M0.2689,0.4081 L0.359,0.4081 L0.359,0.4857 L0.2689,0.4857 L0.2689,0.4081 Z M0.2689,0.5059 L0.359,0.5059 L0.359,0.5835 L0.2689,0.5835 L0.2689,0.5059 Z M0.2689,0.711 L0.359,0.711 L0.359,0.7886 L0.2689,0.7886 L0.2689,0.711 Z M0.2689,0.8154 L0.359,0.8154 L0.359,0.8926 L0.2689,0.8926 L0.2689,0.8154 Z M0.1374,0.1137 L0.2275,0.1137 L0.2275,0.1909 L0.1374,0.1909 L0.1374,0.1137 Z M0.1374,0.2143 L0.2275,0.2143 L0.2275,0.2919 L0.1374,0.2919 L0.1374,0.2143 Z M0.1374,0.8154 L0.2275,0.8154 L0.2275,0.8926 L0.1374,0.8926 L0.1374,0.8154 Z M0.1374,0.31 L0.2275,0.31 L0.2275,0.3876 L0.1374,0.3876 L0.1374,0.31 Z M0.1374,0.8154 L0.2275,0.8154 L0.2275,0.8926 L0.1374,0.8926 L0.1374,0.8154 Z M0.1374,0.4081 L0.2275,0.4081 L0.2275,0.4857 L0.1374,0.4857 L0.1374,0.4081 Z M0.1374,0.5059 L0.2275,0.5059 L0.2275,0.5835 L0.1374,0.5835 L0.1374,0.5059 Z M0.1374,0.6065 L0.2275,0.6065 L0.2275,0.6841 L0.1374,0.6841 L0.1374,0.6065 Z M0.1374,0.711 L0.2275,0.711 L0.2275,0.7886 L0.1374,0.7886 L0.1374,0.711 Z M0.1374,0.8154 L0.2275,0.8154 L0.2275,0.8926 L0.1374,0.8926 L0.1374,0.8154 Z M0,0.1137 L0.0901,0.1137 L0.0901,0.1909 L0,0.1909 L0,0.1137 Z M0,0.2143 L0.0901,0.2143 L0.0901,0.2919 L0,0.2919 L0,0.2143 Z M0,0.8154 L0.0901,0.8154 L0.0901,0.8926 L0,0.8926 L0,0.8154 Z M0,0.31 L0.0901,0.31 L0.0901,0.3876 L0,0.3876 L0,0.31 Z M0,0.8154 L0.0901,0.8154 L0.0901,0.8926 L0,0.8926 L0,0.8154 Z M0,0.4081 L0.0901,0.4081 L0.0901,0.4857 L0,0.4857 L0,0.4081 Z M0,0.5059 L0.0901,0.5059 L0.0901,0.5835 L0,0.5835 L0,0.5059 Z M0,0.6065 L0.0901,0.6065 L0.0901,0.6841 L0,0.6841 L0,0.6065 Z M0,0.711 L0.0901,0.711 L0.0901,0.7886 L0,0.7886 L0,0.711 Z M0,0.8154 L0.0901,0.8154 L0.0901,0.8926 L0,0.8926 L0,0.8154 Z",type:0}},"\uE0C5":{type:5,data:{d:"M0.5884,0.0126 L0.5884,0.0776 L0.5129,0.0776 L0.5129,0.0126 L0.5884,0.0126 Z M0.5957,0 L0.5056,0 Q0.5027,0,0.5005,0.0021 T0.4983,0.0063 L0.4983,0.0839 Q0.4983,0.0864,0.5007,0.0883 T0.5056,0.0902 L0.5957,0.0902 Q0.5986,0.0902,0.6008,0.0881 T0.603,0.0839 L0.603,0.0063 Q0.603,0.0038,0.6006,0.0019 T0.5957,0 Z M0.5884,0.0126 L0.5884,0.0776 L0.5129,0.0776 L0.5129,0.0126 L0.5884,0.0126 Z M0.5957,0 L0.5056,0 Q0.5027,0,0.5005,0.0021 T0.4983,0.0063 L0.4983,0.0839 Q0.4983,0.0864,0.5007,0.0883 T0.5056,0.0902 L0.5957,0.0902 Q0.5986,0.0902,0.6008,0.0881 T0.603,0.0839 L0.603,0.0063 Q0.603,0.0038,0.6006,0.0019 T0.5957,0 Z M0.5884,0.0126 L0.5884,0.0776 L0.5129,0.0776 L0.5129,0.0126 L0.5884,0.0126 Z M0.5957,0 L0.5056,0 Q0.5027,0,0.5005,0.0021 T0.4983,0.0063 L0.4983,0.0839 Q0.4983,0.0864,0.5007,0.0883 T0.5056,0.0902 L0.5957,0.0902 Q0.5986,0.0902,0.6008,0.0881 T0.603,0.0839 L0.603,0.0063 Q0.603,0.0038,0.6006,0.0019 T0.5957,0 Z M0.0901,0.0084 L0.0901,0.0818 L0.0049,0.0818 L0.0049,0.0084 L0.0901,0.0084 Z M0.0925,0.0042 L0.0024,0.0042 Q0,0.0042,0,0.0063 L0,0.0839 Q0,0.086,0.0024,0.086 L0.0925,0.086 Q0.095,0.086,0.095,0.0839 L0.095,0.0063 Q0.095,0.0042,0.0925,0.0042 Z M0.0901,0.0084 L0.0901,0.0818 L0.0049,0.0818 L0.0049,0.0084 L0.0901,0.0084 Z M0.0925,0.0042 L0.0024,0.0042 Q0,0.0042,0,0.0063 L0,0.0839 Q0,0.086,0.0024,0.086 L0.0925,0.086 Q0.095,0.086,0.095,0.0839 L0.095,0.0063 Q0.095,0.0042,0.0925,0.0042 Z M0.0901,0.0084 L0.0901,0.0818 L0.0049,0.0818 L0.0049,0.0084 L0.0901,0.0084 Z M0.0925,0.0042 L0.0024,0.0042 Q0,0.0042,0,0.0063 L0,0.0839 Q0,0.086,0.0024,0.086 L0.0925,0.086 Q0.095,0.086,0.095,0.0839 L0.095,0.0063 Q0.095,0.0042,0.0925,0.0042 Z M0.34,0.0063 L0.2499,0.0063 L0.2499,0.0839 L0.34,0.0839 L0.34,0.0063 Z M0.34,0.0063 L0.2499,0.0063 L0.2499,0.0839 L0.34,0.0839 L0.34,0.0063 Z M0.34,0.0063 L0.2499,0.0063 L0.2499,0.0839 L0.34,0.0839 L0.34,0.0063 Z M0.4691,0.0063 L0.379,0.0063 L0.379,0.0839 L0.4691,0.0839 L0.4691,0.0063 Z M0.4691,0.0063 L0.379,0.0063 L0.379,0.0839 L0.4691,0.0839 L0.4691,0.0063 Z M0.4691,0.0063 L0.379,0.0063 L0.379,0.0839 L0.4691,0.0839 L0.4691,0.0063 Z M0.7311,0.0063 L0.641,0.0063 L0.641,0.0839 L0.7311,0.0839 L0.7311,0.0063 Z M0.7311,0.0063 L0.641,0.0063 L0.641,0.0839 L0.7311,0.0839 L0.7311,0.0063 Z M0.7311,0.0063 L0.641,0.0063 L0.641,0.0839 L0.7311,0.0839 L0.7311,0.0063 Z M0.8626,0.0063 L0.7725,0.0063 L0.7725,0.0839 L0.8626,0.0839 L0.8626,0.0063 Z M0.8626,0.0063 L0.7725,0.0063 L0.7725,0.0839 L0.8626,0.0839 L0.8626,0.0063 Z M0.8626,0.0063 L0.7725,0.0063 L0.7725,0.0839 L0.8626,0.0839 L0.8626,0.0063 Z M1,0.0063 L0.9099,0.0063 L0.9099,0.0839 L1,0.0839 L1,0.0063 Z M1,0.0063 L0.9099,0.0063 L0.9099,0.0839 L1,0.0839 L1,0.0063 Z M1,0.0063 L0.9099,0.0063 L0.9099,0.0839 L1,0.0839 L1,0.0063 Z M0.5913,0.9224 L0.5913,0.9958 L0.5061,0.9958 L0.5061,0.9224 L0.5913,0.9224 Z M0.5938,0.9182 L0.5037,0.9182 Q0.5012,0.9182,0.5012,0.9203 L0.5012,0.9979 Q0.5012,1,0.5037,1 L0.5938,1 Q0.5962,1,0.5962,0.9979 L0.5962,0.9203 Q0.5962,0.9182,0.5938,0.9182 Z M0.5913,0.9224 L0.5913,0.9958 L0.5061,0.9958 L0.5061,0.9224 L0.5913,0.9224 Z M0.5938,0.9182 L0.5037,0.9182 Q0.5012,0.9182,0.5012,0.9203 L0.5012,0.9979 Q0.5012,1,0.5037,1 L0.5938,1 Q0.5962,1,0.5962,0.9979 L0.5962,0.9203 Q0.5962,0.9182,0.5938,0.9182 Z M0.5913,0.9224 L0.5913,0.9958 L0.5061,0.9958 L0.5061,0.9224 L0.5913,0.9224 Z M0.5938,0.9182 L0.5037,0.9182 Q0.5012,0.9182,0.5012,0.9203 L0.5012,0.9979 Q0.5012,1,0.5037,1 L0.5938,1 Q0.5962,1,0.5962,0.9979 L0.5962,0.9203 Q0.5962,0.9182,0.5938,0.9182 Z M0.8626,0.9203 L0.7725,0.9203 L0.7725,0.9979 L0.8626,0.9979 L0.8626,0.9203 Z M0.8626,0.9203 L0.7725,0.9203 L0.7725,0.9979 L0.8626,0.9979 L0.8626,0.9203 Z M0.8626,0.9203 L0.7725,0.9203 L0.7725,0.9979 L0.8626,0.9979 L0.8626,0.9203 Z M1,0.9203 L0.9099,0.9203 L0.9099,0.9979 L1,0.9979 L1,0.9203 Z M1,0.9203 L0.9099,0.9203 L0.9099,0.9979 L1,0.9979 L1,0.9203 Z M1,0.9203 L0.9099,0.9203 L0.9099,0.9979 L1,0.9979 L1,0.9203 Z M0.0901,0.1158 L0.0901,0.1888 L0.0049,0.1888 L0.0049,0.1158 L0.0901,0.1158 Z M0.0925,0.1116 L0.0024,0.1116 Q0,0.1116,0,0.1137 L0,0.1909 Q0,0.193,0.0024,0.193 L0.0925,0.193 Q0.095,0.193,0.095,0.1909 L0.095,0.1137 Q0.095,0.1116,0.0925,0.1116 Z M0.2148,0.6086 L0.2148,0.682 L0.1296,0.682 L0.1296,0.6086 L0.2148,0.6086 Z M0.2172,0.6044 L0.1271,0.6044 Q0.1247,0.6044,0.1247,0.6065 L0.1247,0.6841 Q0.1247,0.6862,0.1271,0.6862 L0.2172,0.6862 Q0.2197,0.6862,0.2197,0.6841 L0.2197,0.6065 Q0.2197,0.6044,0.2172,0.6044 Z M0.34,0.1137 L0.2499,0.1137 L0.2499,0.1909 L0.34,0.1909 L0.34,0.1137 Z M0.3376,0.4102 L0.3376,0.4836 L0.2523,0.4836 L0.2523,0.4102 L0.3376,0.4102 Z M0.34,0.406 L0.2499,0.406 Q0.2474,0.406,0.2474,0.4081 L0.2474,0.4857 Q0.2474,0.4878,0.2499,0.4878 L0.34,0.4878 Q0.3424,0.4878,0.3424,0.4857 L0.3424,0.4081 Q0.3424,0.406,0.34,0.406 Z M0.3376,0.7131 L0.3376,0.7865 L0.2523,0.7865 L0.2523,0.7131 L0.3376,0.7131 Z M0.34,0.7089 L0.2499,0.7089 Q0.2474,0.7089,0.2474,0.711 L0.2474,0.7886 Q0.2474,0.7907,0.2499,0.7907 L0.34,0.7907 Q0.3424,0.7907,0.3424,0.7886 L0.3424,0.711 Q0.3424,0.7089,0.34,0.7089 Z M0.4593,0.2227 L0.4593,0.2836 L0.3887,0.2836 L0.3887,0.2227 L0.4593,0.2227 Z M0.4691,0.206 L0.379,0.206 Q0.3746,0.206,0.3719,0.2087 T0.3692,0.2143 L0.3692,0.2919 Q0.3692,0.2957,0.3724,0.298 T0.379,0.3003 L0.4691,0.3003 Q0.4735,0.3003,0.4761,0.2976 T0.4788,0.2919 L0.4788,0.2143 Q0.4788,0.211,0.4756,0.2085 T0.4691,0.206 Z M0.4593,0.3184 L0.4593,0.3792 L0.3887,0.3792 L0.3887,0.3184 L0.4593,0.3184 Z M0.4691,0.3016 L0.379,0.3016 Q0.3746,0.3016,0.3719,0.3043 T0.3692,0.31 L0.3692,0.3876 Q0.3692,0.3914,0.3724,0.3937 T0.379,0.396 L0.4691,0.396 Q0.4735,0.396,0.4761,0.3932 T0.4788,0.3876 L0.4788,0.31 Q0.4788,0.3062,0.4756,0.3039 T0.4691,0.3016 Z M0.4691,0.4081 L0.379,0.4081 L0.379,0.4857 L0.4691,0.4857 L0.4691,0.4081 Z M0.4691,0.5059 L0.379,0.5059 L0.379,0.5835 L0.4691,0.5835 L0.4691,0.5059 Z M0.4593,0.6149 L0.4593,0.6758 L0.3887,0.6758 L0.3887,0.6149 L0.4593,0.6149 Z M0.4691,0.5982 L0.379,0.5982 Q0.3746,0.5982,0.3719,0.6009 T0.3692,0.6065 L0.3692,0.6841 Q0.3692,0.6879,0.3724,0.6902 T0.379,0.6925 L0.4691,0.6925 Q0.4735,0.6925,0.4761,0.6898 T0.4788,0.6841 L0.4788,0.6065 Q0.4788,0.6028,0.4756,0.6005 T0.4691,0.5982 Z M0.5938,0.1137 L0.5037,0.1137 L0.5037,0.1909 L0.5938,0.1909 L0.5938,0.1137 Z M0.5938,0.2143 L0.5037,0.2143 L0.5037,0.2919 L0.5938,0.2919 L0.5938,0.2143 Z M0.5816,0.4186 L0.5816,0.4753 L0.5158,0.4753 L0.5158,0.4186 L0.5816,0.4186 Z M0.5938,0.3977 L0.5037,0.3977 Q0.4983,0.3977,0.4949,0.401 T0.4915,0.4081 L0.4915,0.4857 Q0.4915,0.4904,0.4954,0.4933 T0.5037,0.4962 L0.5938,0.4962 Q0.5991,0.4962,0.6025,0.4929 T0.6059,0.4857 L0.6059,0.4081 Q0.6059,0.4035,0.602,0.4006 T0.5938,0.3977 Z M0.5938,0.5059 L0.5037,0.5059 L0.5037,0.5835 L0.5938,0.5835 L0.5938,0.5059 Z M0.5913,0.7131 L0.5913,0.7865 L0.5061,0.7865 L0.5061,0.7131 L0.5913,0.7131 Z M0.5938,0.7089 L0.5037,0.7089 Q0.5012,0.7089,0.5012,0.711 L0.5012,0.7886 Q0.5012,0.7907,0.5037,0.7907 L0.5938,0.7907 Q0.5962,0.7907,0.5962,0.7886 L0.5962,0.711 Q0.5962,0.7089,0.5938,0.7089 Z M0.7311,0.1137 L0.641,0.1137 L0.641,0.1909 L0.7311,0.1909 L0.7311,0.1137 Z M0.7311,0.2143 L0.641,0.2143 L0.641,0.2919 L0.7311,0.2919 L0.7311,0.2143 Z M0.7311,0.8154 L0.641,0.8154 L0.641,0.8926 L0.7311,0.8926 L0.7311,0.8154 Z M0.7311,0.31 L0.641,0.31 L0.641,0.3876 L0.7311,0.3876 L0.7311,0.31 Z M0.7311,0.8154 L0.641,0.8154 L0.641,0.8926 L0.7311,0.8926 L0.7311,0.8154 Z M0.7311,0.4081 L0.641,0.4081 L0.641,0.4857 L0.7311,0.4857 L0.7311,0.4081 Z M0.7311,0.5059 L0.641,0.5059 L0.641,0.5835 L0.7311,0.5835 L0.7311,0.5059 Z M0.7311,0.711 L0.641,0.711 L0.641,0.7886 L0.7311,0.7886 L0.7311,0.711 Z M0.7311,0.8154 L0.641,0.8154 L0.641,0.8926 L0.7311,0.8926 L0.7311,0.8154 Z M0.8626,0.1137 L0.7725,0.1137 L0.7725,0.1909 L0.8626,0.1909 L0.8626,0.1137 Z M0.8626,0.2143 L0.7725,0.2143 L0.7725,0.2919 L0.8626,0.2919 L0.8626,0.2143 Z M0.8626,0.8154 L0.7725,0.8154 L0.7725,0.8926 L0.8626,0.8926 L0.8626,0.8154 Z M0.8626,0.31 L0.7725,0.31 L0.7725,0.3876 L0.8626,0.3876 L0.8626,0.31 Z M0.8626,0.8154 L0.7725,0.8154 L0.7725,0.8926 L0.8626,0.8926 L0.8626,0.8154 Z M0.8626,0.4081 L0.7725,0.4081 L0.7725,0.4857 L0.8626,0.4857 L0.8626,0.4081 Z M0.8626,0.5059 L0.7725,0.5059 L0.7725,0.5835 L0.8626,0.5835 L0.8626,0.5059 Z M0.8626,0.6065 L0.7725,0.6065 L0.7725,0.6841 L0.8626,0.6841 L0.8626,0.6065 Z M0.8626,0.711 L0.7725,0.711 L0.7725,0.7886 L0.8626,0.7886 L0.8626,0.711 Z M0.8626,0.8154 L0.7725,0.8154 L0.7725,0.8926 L0.8626,0.8926 L0.8626,0.8154 Z M1,0.1137 L0.9099,0.1137 L0.9099,0.1909 L1,0.1909 L1,0.1137 Z M1,0.2143 L0.9099,0.2143 L0.9099,0.2919 L1,0.2919 L1,0.2143 Z M1,0.8154 L0.9099,0.8154 L0.9099,0.8926 L1,0.8926 L1,0.8154 Z M1,0.31 L0.9099,0.31 L0.9099,0.3876 L1,0.3876 L1,0.31 Z M1,0.8154 L0.9099,0.8154 L0.9099,0.8926 L1,0.8926 L1,0.8154 Z M1,0.4081 L0.9099,0.4081 L0.9099,0.4857 L1,0.4857 L1,0.4081 Z M1,0.5059 L0.9099,0.5059 L0.9099,0.5835 L1,0.5835 L1,0.5059 Z M1,0.6065 L0.9099,0.6065 L0.9099,0.6841 L1,0.6841 L1,0.6065 Z M1,0.711 L0.9099,0.711 L0.9099,0.7886 L1,0.7886 L1,0.711 Z M1,0.8154 L0.9099,0.8154 L0.9099,0.8926 L1,0.8926 L1,0.8154 Z",type:0}},"\uE0C6":{type:5,data:{d:"M0.6529,0.018 L0.6529,0.112 L0.7724,0.112 L0.7724,0.018 L0.6529,0.018 Z M0.6411,0 L0.7837,0 Q0.7873,0,0.7902,0.0015 T0.7943,0.005 T0.7956,0.0092 L0.7956,0.1208 Q0.7956,0.125,0.7917,0.1275 T0.7837,0.13 L0.6411,0.13 Q0.6359,0.13,0.6326,0.1271 T0.6292,0.1208 L0.6292,0.0092 Q0.6292,0.005,0.6331,0.0025 T0.6411,0 Z M0.6529,0.018 L0.6529,0.112 L0.7724,0.112 L0.7724,0.018 L0.6529,0.018 Z M0.6411,0 L0.7837,0 Q0.7873,0,0.7902,0.0015 T0.7943,0.005 T0.7956,0.0092 L0.7956,0.1208 Q0.7956,0.125,0.7917,0.1275 T0.7837,0.13 L0.6411,0.13 Q0.6359,0.13,0.6326,0.1271 T0.6292,0.1208 L0.6292,0.0092 Q0.6292,0.005,0.6331,0.0025 T0.6411,0 Z M0.6529,0.018 L0.6529,0.112 L0.7724,0.112 L0.7724,0.018 L0.6529,0.018 Z M0.6411,0 L0.7837,0 Q0.7873,0,0.7902,0.0015 T0.7943,0.005 T0.7956,0.0092 L0.7956,0.1208 Q0.7956,0.125,0.7917,0.1275 T0.7837,0.13 L0.6411,0.13 Q0.6359,0.13,0.6326,0.1271 T0.6292,0.1208 L0.6292,0.0092 Q0.6292,0.005,0.6331,0.0025 T0.6411,0 Z M0.4325,0.0143 L0.4325,0.1158 L0.5628,0.1158 L0.5628,0.0143 L0.4325,0.0143 Z M0.4264,0.0042 L0.5695,0.0042 Q0.5721,0.0042,0.5739,0.0057 T0.5757,0.0092 L0.5757,0.1208 Q0.5757,0.1221,0.5752,0.1231 T0.5736,0.1248 T0.5716,0.1256 T0.5695,0.1258 L0.4264,0.1258 Q0.4238,0.1258,0.422,0.1244 T0.4202,0.1208 L0.4202,0.0092 Q0.4202,0.008,0.4207,0.0069 T0.4222,0.0052 T0.4243,0.0044 T0.4264,0.0042 Z M0.4325,0.0143 L0.4325,0.1158 L0.5628,0.1158 L0.5628,0.0143 L0.4325,0.0143 Z M0.4264,0.0042 L0.5695,0.0042 Q0.5721,0.0042,0.5739,0.0057 T0.5757,0.0092 L0.5757,0.1208 Q0.5757,0.1221,0.5752,0.1231 T0.5736,0.1248 T0.5716,0.1256 T0.5695,0.1258 L0.4264,0.1258 Q0.4238,0.1258,0.422,0.1244 T0.4202,0.1208 L0.4202,0.0092 Q0.4202,0.008,0.4207,0.0069 T0.4222,0.0052 T0.4243,0.0044 T0.4264,0.0042 Z M0.4325,0.0143 L0.4325,0.1158 L0.5628,0.1158 L0.5628,0.0143 L0.4325,0.0143 Z M0.4264,0.0042 L0.5695,0.0042 Q0.5721,0.0042,0.5739,0.0057 T0.5757,0.0092 L0.5757,0.1208 Q0.5757,0.1221,0.5752,0.1231 T0.5736,0.1248 T0.5716,0.1256 T0.5695,0.1258 L0.4264,0.1258 Q0.4238,0.1258,0.422,0.1244 T0.4202,0.1208 L0.4202,0.0092 Q0.4202,0.008,0.4207,0.0069 T0.4222,0.0052 T0.4243,0.0044 T0.4264,0.0042 Z M0.2178,0.0092 L0.3605,0.0092 L0.3605,0.1208 L0.2178,0.1208 L0.2178,0.0092 Z M0.2178,0.0092 L0.3605,0.0092 L0.3605,0.1208 L0.2178,0.1208 L0.2178,0.0092 Z M0.2178,0.0092 L0.3605,0.0092 L0.3605,0.1208 L0.2178,0.1208 L0.2178,0.0092 Z M0,0.0092 L0.1432,0.0092 L0.1432,0.1208 L0,0.1208 L0,0.0092 Z M0,0.0092 L0.1432,0.0092 L0.1432,0.1208 L0,0.1208 L0,0.0092 Z M0,0.0092 L0.1432,0.0092 L0.1432,0.1208 L0,0.1208 L0,0.0092 Z M0.8574,0.3217 L0.8574,0.4098 L0.9691,0.4098 L0.9691,0.3217 L0.8574,0.3217 Z M0.8419,0.2978 L0.9846,0.2978 Q0.9892,0.2978,0.9928,0.2997 T0.9982,0.3043 T1,0.31 L1,0.4216 Q1,0.427,0.9951,0.4304 T0.9846,0.4337 L0.8419,0.4337 Q0.8352,0.4337,0.8308,0.4299 T0.8265,0.4216 L0.8265,0.31 Q0.8265,0.3062,0.829,0.3035 T0.835,0.2993 T0.8419,0.2978 Z M0.8574,0.888 L0.8574,0.9757 L0.9691,0.9757 L0.9691,0.888 L0.8574,0.888 Z M0.8419,0.8637 L0.9846,0.8637 Q0.9892,0.8637,0.9928,0.8658 T0.9982,0.8706 T1,0.8758 L1,0.9878 Q1,0.9929,0.9951,0.9964 T0.9846,1 L0.8419,1 Q0.8352,1,0.8308,0.996 T0.8265,0.9878 L0.8265,0.8758 Q0.8265,0.8708,0.8314,0.8672 T0.8419,0.8637 Z M0.6509,0.169 L0.6509,0.2706 L0.7806,0.2706 L0.7806,0.169 L0.6509,0.169 Z M0.6442,0.159 L0.7868,0.159 Q0.7899,0.159,0.7917,0.1604 T0.7935,0.164 L0.7935,0.2756 Q0.7935,0.2781,0.7915,0.2796 T0.7868,0.281 L0.6442,0.281 Q0.6416,0.281,0.6395,0.2794 T0.6375,0.2756 L0.6375,0.164 Q0.6375,0.1619,0.6398,0.1604 T0.6442,0.159 Z M0.6632,0.6044 L0.6632,0.6858 L0.7678,0.6858 L0.7678,0.6044 L0.6632,0.6044 Z M0.6442,0.5742 L0.7868,0.5742 Q0.7951,0.5742,0.8007,0.5791 T0.8064,0.5893 L0.8064,0.7013 Q0.8064,0.7076,0.8002,0.712 T0.7868,0.7164 L0.6442,0.7164 Q0.6359,0.7164,0.6305,0.7114 T0.6251,0.7013 L0.6251,0.5893 Q0.6251,0.5847,0.6282,0.5812 T0.6357,0.5759 T0.6442,0.5742 Z M0.4264,0.164 L0.5695,0.164 L0.5695,0.2756 L0.4264,0.2756 L0.4264,0.164 Z M0.4325,0.315 L0.4325,0.4165 L0.5628,0.4165 L0.5628,0.315 L0.4325,0.315 Z M0.4264,0.3049 L0.5695,0.3049 Q0.5721,0.3049,0.5739,0.3064 T0.5757,0.31 L0.5757,0.4216 Q0.5757,0.4237,0.5736,0.4251 T0.5695,0.4266 L0.4264,0.4266 Q0.4238,0.4266,0.422,0.4251 T0.4202,0.4216 L0.4202,0.31 Q0.4202,0.3087,0.4207,0.3077 T0.4222,0.306 T0.4243,0.3052 T0.4264,0.3049 Z M0.4264,0.4476 L0.5695,0.4476 L0.5695,0.5596 L0.4264,0.5596 L0.4264,0.4476 Z M0.4392,0.7408 L0.4392,0.8326 L0.5561,0.8326 L0.5561,0.7408 L0.4392,0.7408 Z M0.4264,0.7206 L0.5695,0.7206 Q0.5747,0.7206,0.5783,0.724 T0.5819,0.7307 L0.5819,0.8427 Q0.5819,0.8469,0.5778,0.8498 T0.5695,0.8528 L0.4264,0.8528 Q0.4207,0.8528,0.4171,0.8496 T0.4135,0.8427 L0.4135,0.7307 Q0.4135,0.7265,0.4176,0.7236 T0.4264,0.7206 Z M0.2178,0.164 L0.3605,0.164 L0.3605,0.2756 L0.2178,0.2756 L0.2178,0.164 Z M0.2178,0.31 L0.3605,0.31 L0.3605,0.4216 L0.2178,0.4216 L0.2178,0.31 Z M0.2178,0.5893 L0.3605,0.5893 L0.3605,0.7013 L0.2178,0.7013 L0.2178,0.5893 Z M0.2178,0.7307 L0.3605,0.7307 L0.3605,0.8427 L0.2178,0.8427 L0.2178,0.7307 Z M0.2178,0.8758 L0.3605,0.8758 L0.3605,0.9878 L0.2178,0.9878 L0.2178,0.8758 Z M0,0.164 L0.1432,0.164 L0.1432,0.2756 L0,0.2756 L0,0.164 Z M0,0.31 L0.1432,0.31 L0.1432,0.4216 L0,0.4216 L0,0.31 Z M0,0.4476 L0.1432,0.4476 L0.1432,0.5596 L0,0.5596 L0,0.4476 Z M0,0.5893 L0.1432,0.5893 L0.1432,0.7013 L0,0.7013 L0,0.5893 Z M0,0.7307 L0.1432,0.7307 L0.1432,0.8427 L0,0.8427 L0,0.7307 Z M0,0.8758 L0.1432,0.8758 L0.1432,0.9878 L0,0.9878 L0,0.8758 Z",type:0}},"\uE0C7":{type:5,data:{d:"M0.3476,0.018 L0.3476,0.112 L0.2276,0.112 L0.2276,0.018 L0.3476,0.018 Z M0.3594,0 L0.2163,0 Q0.2132,0,0.2104,0.0015 T0.2062,0.005 T0.2049,0.0092 L0.2049,0.1208 Q0.2049,0.1237,0.2067,0.1258 T0.2111,0.129 T0.2163,0.13 L0.3594,0.13 Q0.3641,0.13,0.3674,0.1271 T0.3708,0.1208 L0.3708,0.0092 Q0.3708,0.005,0.3669,0.0025 T0.3594,0 Z M0.3476,0.018 L0.3476,0.112 L0.2276,0.112 L0.2276,0.018 L0.3476,0.018 Z M0.3594,0 L0.2163,0 Q0.2132,0,0.2104,0.0015 T0.2062,0.005 T0.2049,0.0092 L0.2049,0.1208 Q0.2049,0.1237,0.2067,0.1258 T0.2111,0.129 T0.2163,0.13 L0.3594,0.13 Q0.3641,0.13,0.3674,0.1271 T0.3708,0.1208 L0.3708,0.0092 Q0.3708,0.005,0.3669,0.0025 T0.3594,0 Z M0.3476,0.018 L0.3476,0.112 L0.2276,0.112 L0.2276,0.018 L0.3476,0.018 Z M0.3594,0 L0.2163,0 Q0.2132,0,0.2104,0.0015 T0.2062,0.005 T0.2049,0.0092 L0.2049,0.1208 Q0.2049,0.1237,0.2067,0.1258 T0.2111,0.129 T0.2163,0.13 L0.3594,0.13 Q0.3641,0.13,0.3674,0.1271 T0.3708,0.1208 L0.3708,0.0092 Q0.3708,0.005,0.3669,0.0025 T0.3594,0 Z M0.5675,0.0143 L0.5675,0.1158 L0.4377,0.1158 L0.4377,0.0143 L0.5675,0.0143 Z M0.5736,0.0042 L0.431,0.0042 Q0.4284,0.0042,0.4264,0.0057 T0.4243,0.0092 L0.4243,0.1208 Q0.4243,0.1221,0.4251,0.1231 T0.4269,0.1248 T0.4289,0.1256 T0.431,0.1258 L0.5736,0.1258 Q0.5767,0.1258,0.5785,0.1244 T0.5803,0.1208 L0.5803,0.0092 Q0.5803,0.008,0.5798,0.0069 T0.5783,0.0052 T0.5762,0.0044 T0.5736,0.0042 Z M0.5675,0.0143 L0.5675,0.1158 L0.4377,0.1158 L0.4377,0.0143 L0.5675,0.0143 Z M0.5736,0.0042 L0.431,0.0042 Q0.4284,0.0042,0.4264,0.0057 T0.4243,0.0092 L0.4243,0.1208 Q0.4243,0.1221,0.4251,0.1231 T0.4269,0.1248 T0.4289,0.1256 T0.431,0.1258 L0.5736,0.1258 Q0.5767,0.1258,0.5785,0.1244 T0.5803,0.1208 L0.5803,0.0092 Q0.5803,0.008,0.5798,0.0069 T0.5783,0.0052 T0.5762,0.0044 T0.5736,0.0042 Z M0.5675,0.0143 L0.5675,0.1158 L0.4377,0.1158 L0.4377,0.0143 L0.5675,0.0143 Z M0.5736,0.0042 L0.431,0.0042 Q0.4284,0.0042,0.4264,0.0057 T0.4243,0.0092 L0.4243,0.1208 Q0.4243,0.1221,0.4251,0.1231 T0.4269,0.1248 T0.4289,0.1256 T0.431,0.1258 L0.5736,0.1258 Q0.5767,0.1258,0.5785,0.1244 T0.5803,0.1208 L0.5803,0.0092 Q0.5803,0.008,0.5798,0.0069 T0.5783,0.0052 T0.5762,0.0044 T0.5736,0.0042 Z M0.7827,0.0092 L0.6395,0.0092 L0.6395,0.1208 L0.7827,0.1208 L0.7827,0.0092 Z M0.7827,0.0092 L0.6395,0.0092 L0.6395,0.1208 L0.7827,0.1208 L0.7827,0.0092 Z M0.7827,0.0092 L0.6395,0.0092 L0.6395,0.1208 L0.7827,0.1208 L0.7827,0.0092 Z M1,0.0092 L0.8574,0.0092 L0.8574,0.1208 L1,0.1208 L1,0.0092 Z M1,0.0092 L0.8574,0.0092 L0.8574,0.1208 L1,0.1208 L1,0.0092 Z M1,0.0092 L0.8574,0.0092 L0.8574,0.1208 L1,0.1208 L1,0.0092 Z M0.1432,0.3217 L0.1432,0.4098 L0.0309,0.4098 L0.0309,0.3217 L0.1432,0.3217 Z M0.1586,0.2978 L0.0154,0.2978 Q0.0088,0.2978,0.0044,0.3016 T0,0.31 L0,0.4216 Q0,0.427,0.0049,0.4304 T0.0154,0.4337 L0.1586,0.4337 Q0.1648,0.4337,0.1694,0.4299 T0.174,0.4216 L0.174,0.31 Q0.174,0.3062,0.1715,0.3035 T0.1653,0.2993 T0.1586,0.2978 Z M0.1432,0.888 L0.1432,0.9757 L0.0309,0.9757 L0.0309,0.888 L0.1432,0.888 Z M0.1586,0.8637 L0.0154,0.8637 Q0.0108,0.8637,0.0072,0.8658 T0.0018,0.8706 T0,0.8758 L0,0.9878 Q0,0.9929,0.0049,0.9964 T0.0154,1 L0.1586,1 Q0.1648,1,0.1694,0.996 T0.174,0.9878 L0.174,0.8758 Q0.174,0.8725,0.1715,0.8695 T0.1653,0.8651 T0.1586,0.8637 Z M0.3496,0.169 L0.3496,0.2706 L0.2194,0.2706 L0.2194,0.169 L0.3496,0.169 Z M0.3563,0.159 L0.2132,0.159 Q0.2106,0.159,0.2088,0.1604 T0.207,0.164 L0.207,0.2756 Q0.207,0.2781,0.2091,0.2796 T0.2132,0.281 L0.3563,0.281 Q0.3589,0.281,0.3607,0.2794 T0.3625,0.2756 L0.3625,0.164 Q0.3625,0.1619,0.3605,0.1604 T0.3563,0.159 Z M0.3368,0.6044 L0.3368,0.6858 L0.2327,0.6858 L0.2327,0.6044 L0.3368,0.6044 Z M0.3563,0.5742 L0.2132,0.5742 Q0.2049,0.5742,0.1993,0.5791 T0.1936,0.5893 L0.1936,0.7013 Q0.1936,0.7076,0.2001,0.712 T0.2132,0.7164 L0.3563,0.7164 Q0.3641,0.7164,0.3697,0.7114 T0.3754,0.7013 L0.3754,0.5893 Q0.3754,0.5847,0.3723,0.5812 T0.3648,0.5759 T0.3563,0.5742 Z M0.5736,0.164 L0.431,0.164 L0.431,0.2756 L0.5736,0.2756 L0.5736,0.164 Z M0.5675,0.315 L0.5675,0.4165 L0.4377,0.4165 L0.4377,0.315 L0.5675,0.315 Z M0.5736,0.3049 L0.431,0.3049 Q0.4284,0.3049,0.4264,0.3064 T0.4243,0.31 L0.4243,0.4216 Q0.4243,0.4237,0.4266,0.4251 T0.431,0.4266 L0.5736,0.4266 Q0.5767,0.4266,0.5785,0.4251 T0.5803,0.4216 L0.5803,0.31 Q0.5803,0.3087,0.5798,0.3077 T0.5783,0.306 T0.5762,0.3052 T0.5736,0.3049 Z M0.5736,0.4476 L0.431,0.4476 L0.431,0.5596 L0.5736,0.5596 L0.5736,0.4476 Z M0.5613,0.7408 L0.5613,0.8326 L0.4439,0.8326 L0.4439,0.7408 L0.5613,0.7408 Z M0.5736,0.7206 L0.431,0.7206 Q0.4253,0.7206,0.4217,0.724 T0.4181,0.7307 L0.4181,0.8427 Q0.4181,0.8469,0.4222,0.8498 T0.431,0.8528 L0.5736,0.8528 Q0.5778,0.8528,0.5808,0.8511 T0.5855,0.8471 T0.587,0.8427 L0.587,0.7307 Q0.587,0.7265,0.5826,0.7236 T0.5736,0.7206 Z M0.7827,0.164 L0.6395,0.164 L0.6395,0.2756 L0.7827,0.2756 L0.7827,0.164 Z M0.7827,0.31 L0.6395,0.31 L0.6395,0.4216 L0.7827,0.4216 L0.7827,0.31 Z M0.7827,0.5893 L0.6395,0.5893 L0.6395,0.7013 L0.7827,0.7013 L0.7827,0.5893 Z M0.7827,0.7307 L0.6395,0.7307 L0.6395,0.8427 L0.7827,0.8427 L0.7827,0.7307 Z M0.7827,0.8758 L0.6395,0.8758 L0.6395,0.9878 L0.7827,0.9878 L0.7827,0.8758 Z M1,0.164 L0.8574,0.164 L0.8574,0.2756 L1,0.2756 L1,0.164 Z M1,0.31 L0.8574,0.31 L0.8574,0.4216 L1,0.4216 L1,0.31 Z M1,0.4476 L0.8574,0.4476 L0.8574,0.5596 L1,0.5596 L1,0.4476 Z M1,0.5893 L0.8574,0.5893 L0.8574,0.7013 L1,0.7013 L1,0.5893 Z M1,0.7307 L0.8574,0.7307 L0.8574,0.8427 L1,0.8427 L1,0.7307 Z M1,0.8758 L0.8574,0.8758 L0.8574,0.9878 L1,0.9878 L1,0.8758 Z",type:0}},"\uE0C8":{type:5,data:{d:"M0.0008,0.9979 Q0.0008,1,0.0032,1 Q0.0482,1,0.0602,0.9975 Q0.0667,0.9963,0.0847,0.9917 T0.1084,0.9859 Q0.1124,0.9851,0.1221,0.9851 L0.1253,0.9851 L0.1285,0.9851 Q0.1518,0.9851,0.1655,0.9809 Q0.1695,0.9801,0.1731,0.9788 T0.1791,0.9766 T0.1839,0.9747 T0.1888,0.9732 L0.1936,0.972 T0.1988,0.9712 T0.2048,0.971 L0.2096,0.971 Q0.2201,0.9714,0.2233,0.9714 T0.2345,0.9693 Q0.2386,0.9685,0.2502,0.9674 T0.2715,0.966 L0.2811,0.9656 Q0.2916,0.9647,0.2916,0.9573 Q0.2916,0.954,0.2892,0.9515 T0.2819,0.9486 Q0.2755,0.9482,0.2683,0.9482 Q0.2643,0.9482,0.2582,0.9484 T0.2498,0.9486 Q0.2426,0.9486,0.2402,0.9473 Q0.2386,0.9469,0.2317,0.943 T0.2161,0.937 Q0.2104,0.9357,0.1992,0.933 T0.1831,0.9291 Q0.1807,0.9287,0.1783,0.9285 T0.1739,0.928 T0.1703,0.9276 T0.1675,0.9272 T0.1655,0.9266 T0.1639,0.9253 Q0.1598,0.9216,0.1462,0.9216 L0.143,0.9216 L0.1398,0.9216 L0.1365,0.9216 Q0.1197,0.9195,0.1197,0.9154 L0.1197,0.9154 Q0.1205,0.9121,0.1373,0.9088 T0.1703,0.9054 Q0.1791,0.9054,0.1863,0.904 T0.1984,0.9011 T0.2064,0.8992 Q0.2402,0.8967,0.2506,0.8934 Q0.2538,0.8926,0.2602,0.8899 T0.2707,0.8872 Q0.2715,0.8872,0.2731,0.8876 Q0.2795,0.8884,0.2884,0.8884 Q0.3068,0.8884,0.3092,0.8826 L0.3092,0.8797 Q0.3092,0.8772,0.3076,0.8747 T0.3044,0.8706 T0.3028,0.8681 Q0.3076,0.8656,0.3237,0.8656 Q0.3317,0.8656,0.3622,0.8623 Q0.3655,0.8623,0.3867,0.8617 T0.4217,0.8602 Q0.4241,0.8598,0.4289,0.8598 L0.4345,0.8598 L0.441,0.8598 Q0.4578,0.8598,0.4667,0.8586 Q0.4739,0.8573,0.4851,0.8567 T0.5004,0.8552 Q0.5012,0.8552,0.5108,0.855 T0.5269,0.853 T0.5333,0.8461 Q0.5333,0.8445,0.5325,0.8432 T0.5309,0.8409 T0.5289,0.8391 T0.5261,0.8378 L0.5229,0.837 T0.5185,0.8362 T0.5133,0.8355 T0.5076,0.8349 T0.502,0.8341 Q0.4932,0.8324,0.4859,0.8324 Q0.4827,0.8324,0.4787,0.8328 T0.4731,0.8333 Q0.4707,0.8333,0.4683,0.8324 Q0.4482,0.825,0.4394,0.825 Q0.4369,0.825,0.4345,0.8254 Q0.4225,0.827,0.4153,0.827 Q0.4088,0.827,0.4064,0.8258 Q0.3976,0.8204,0.3751,0.8204 Q0.3703,0.8204,0.3667,0.8214 T0.3622,0.8225 Q0.3598,0.8225,0.3558,0.82 Q0.351,0.8175,0.3402,0.8167 T0.3177,0.8156 T0.3028,0.8146 L0.2956,0.8121 T0.2871,0.8094 T0.2771,0.8073 T0.2635,0.8063 Q0.261,0.8063,0.2586,0.8067 Q0.2378,0.8071,0.2273,0.8071 Q0.2072,0.8071,0.2024,0.8051 Q0.1976,0.8034,0.1932,0.8026 T0.1867,0.8013 T0.1847,0.8001 L0.1847,0.7993 Q0.1855,0.7984,0.1855,0.7978 T0.1859,0.7966 T0.1867,0.7959 T0.188,0.7955 T0.1896,0.7951 T0.1916,0.7949 T0.1944,0.7945 T0.1984,0.7941 T0.2032,0.7939 Q0.212,0.7926,0.2325,0.7918 T0.2562,0.7905 Q0.2691,0.7893,0.2787,0.7847 Q0.2835,0.7827,0.2968,0.7791 T0.3149,0.7756 Q0.3189,0.7756,0.3305,0.7733 T0.3446,0.771 L0.3454,0.771 Q0.3462,0.771,0.3498,0.7721 T0.3574,0.774 T0.3655,0.7748 Q0.3703,0.7748,0.3743,0.7731 T0.3823,0.7708 T0.3916,0.77 T0.3992,0.7694 Q0.4008,0.7694,0.4076,0.7688 T0.4205,0.7675 L0.4305,0.7665 Q0.4345,0.7657,0.4402,0.7617 T0.4458,0.7545 Q0.4458,0.7503,0.4353,0.7487 Q0.4297,0.7478,0.4209,0.7462 T0.4044,0.7433 T0.3904,0.7416 L0.3847,0.7416 Q0.3807,0.7416,0.3747,0.7422 T0.3663,0.7428 T0.3606,0.7424 Q0.3582,0.742,0.3562,0.7418 T0.3522,0.7412 T0.3482,0.7406 T0.3446,0.7401 T0.341,0.7397 T0.3373,0.7393 T0.3341,0.7391 L0.3293,0.7391 Q0.3277,0.7395,0.3253,0.7395 Q0.3205,0.7395,0.3052,0.7387 T0.2835,0.7379 Q0.2755,0.7379,0.253,0.7325 Q0.2506,0.7316,0.2462,0.731 T0.2382,0.7296 T0.2305,0.7271 T0.2181,0.7234 T0.2048,0.7196 T0.2,0.7155 Q0.2,0.7142,0.2016,0.713 Q0.208,0.7059,0.2285,0.6978 T0.2618,0.6898 Q0.2739,0.6898,0.2831,0.6848 T0.2956,0.6794 Q0.3028,0.6786,0.3289,0.6765 T0.3566,0.674 Q0.3582,0.674,0.3855,0.6723 T0.4406,0.6688 T0.4755,0.6653 Q0.5004,0.6607,0.5124,0.6599 L0.5165,0.6599 Q0.5213,0.6599,0.5277,0.6605 T0.5373,0.6611 Q0.543,0.6611,0.5494,0.6599 Q0.592,0.6524,0.6,0.6524 Q0.6064,0.6524,0.61,0.6499 T0.6161,0.6474 Q0.6177,0.6474,0.6261,0.6495 T0.6418,0.6516 L0.6482,0.6516 Q0.653,0.6512,0.6651,0.6506 T0.6876,0.6493 T0.7084,0.6477 T0.7229,0.645 Q0.7269,0.6437,0.7333,0.6437 Q0.7373,0.6437,0.7458,0.6443 T0.7574,0.645 Q0.7614,0.645,0.7631,0.6445 T0.7655,0.6441 Q0.7671,0.6441,0.7723,0.6448 T0.7823,0.6454 Q0.788,0.6454,0.7952,0.6445 Q0.8233,0.6408,0.8723,0.6367 Q0.9012,0.6342,0.9092,0.6342 L0.9165,0.6342 L0.9205,0.6342 Q0.9277,0.6342,0.9382,0.6323 T0.9526,0.6304 Q0.9534,0.6304,0.9594,0.6307 T0.9695,0.6309 Q0.9976,0.6309,0.9992,0.6234 Q1,0.6201,1,0.618 Q1,0.6089,0.9775,0.608 Q0.9703,0.6076,0.9582,0.6076 Q0.9518,0.6076,0.9394,0.6078 T0.9213,0.608 Q0.9036,0.608,0.8972,0.6072 Q0.894,0.6068,0.8871,0.6058 T0.8751,0.6039 T0.8627,0.6024 T0.8506,0.6018 L0.8474,0.6018 Q0.841,0.6022,0.8281,0.6041 T0.8112,0.606 Q0.8096,0.606,0.808,0.6056 Q0.7783,0.6002,0.7647,0.6002 Q0.7598,0.6002,0.7454,0.6006 T0.7261,0.601 Q0.7221,0.601,0.7145,0.6022 T0.7036,0.6035 Q0.6996,0.6035,0.6964,0.6022 Q0.6723,0.5902,0.6442,0.5902 Q0.6426,0.5902,0.6386,0.5904 T0.6337,0.5906 Q0.6273,0.5906,0.6249,0.5894 L0.6177,0.5856 T0.6116,0.5838 T0.6016,0.5825 T0.5855,0.5823 Q0.5719,0.5823,0.5574,0.5798 T0.5373,0.5774 L0.5341,0.5774 Q0.5293,0.5778,0.5213,0.5786 T0.51,0.5794 Q0.5036,0.5794,0.4988,0.5769 Q0.4932,0.574,0.4771,0.574 Q0.4723,0.574,0.4631,0.5745 T0.4498,0.5749 Q0.4418,0.5749,0.4394,0.5736 Q0.4353,0.5724,0.4305,0.5715 T0.4225,0.5707 T0.4145,0.5705 T0.4056,0.5699 Q0.4008,0.5691,0.398,0.5689 T0.394,0.5689 T0.3908,0.5686 T0.3855,0.5674 Q0.3831,0.567,0.3811,0.5662 T0.3779,0.5645 T0.3743,0.563 T0.3695,0.5624 Q0.3655,0.5624,0.3594,0.5635 T0.3494,0.5645 T0.3406,0.5635 T0.3325,0.5624 Q0.3309,0.5624,0.3301,0.5628 L0.3141,0.5628 Q0.2635,0.5628,0.2506,0.5612 Q0.2474,0.5608,0.2434,0.5608 Q0.2402,0.5608,0.2349,0.561 T0.2273,0.5612 Q0.2209,0.5612,0.2177,0.5595 Q0.2145,0.557,0.2145,0.5558 Q0.2169,0.5537,0.2345,0.5521 Q0.2876,0.5475,0.3028,0.5425 Q0.3068,0.5413,0.3149,0.5396 T0.3309,0.5367 T0.3478,0.5338 T0.3606,0.5309 Q0.3631,0.5305,0.3755,0.5259 T0.3968,0.5214 Q0.3992,0.5214,0.4024,0.5218 T0.4096,0.5222 Q0.4161,0.5222,0.4213,0.5209 T0.4313,0.5185 T0.4402,0.5172 Q0.4418,0.5168,0.447,0.5168 L0.4558,0.5168 T0.4639,0.5162 T0.4707,0.5141 T0.4731,0.5102 Q0.4731,0.5089,0.4723,0.5081 Q0.4699,0.5019,0.4627,0.4994 T0.4369,0.4965 Q0.4273,0.4961,0.4193,0.4946 T0.4048,0.4915 T0.3952,0.4894 Q0.392,0.4886,0.388,0.4886 Q0.3847,0.4886,0.3775,0.4892 T0.3647,0.4898 Q0.3598,0.4898,0.355,0.4894 Q0.3165,0.4861,0.3141,0.4853 Q0.2779,0.4774,0.2715,0.4774 Q0.2699,0.4774,0.2683,0.4776 T0.2627,0.4778 Q0.2498,0.4778,0.2281,0.4762 Q0.2241,0.4749,0.2233,0.4737 Q0.2249,0.472,0.2321,0.4708 Q0.2402,0.4695,0.2554,0.4695 L0.2675,0.4695 L0.2795,0.4695 Q0.294,0.4695,0.2996,0.4679 Q0.3141,0.4629,0.3373,0.4616 Q0.3414,0.4612,0.3466,0.4606 T0.3582,0.4589 T0.3687,0.4562 T0.3727,0.4525 Q0.3727,0.4509,0.3703,0.4492 Q0.3639,0.4455,0.3474,0.4419 T0.3213,0.4384 L0.3157,0.4384 L0.31,0.4384 Q0.294,0.4384,0.2859,0.4363 Q0.2795,0.4347,0.2691,0.4347 L0.2618,0.4347 L0.2546,0.4347 Q0.2458,0.4347,0.2394,0.4338 Q0.2257,0.4318,0.2193,0.4303 T0.2092,0.4266 T0.2052,0.4224 T0.2024,0.4154 T0.196,0.4052 Q0.1912,0.3994,0.1867,0.3949 T0.1807,0.3884 T0.1791,0.3849 Q0.1791,0.3824,0.1839,0.3745 Q0.1871,0.3687,0.1944,0.3637 T0.2072,0.3561 T0.2221,0.3501 T0.2321,0.3463 Q0.2353,0.3455,0.241,0.3445 T0.2522,0.3428 T0.2639,0.3411 T0.2731,0.3384 T0.2956,0.3351 T0.3197,0.3326 Q0.3213,0.3322,0.3237,0.3322 T0.3325,0.3331 T0.3414,0.3339 Q0.3478,0.3339,0.3518,0.3314 Q0.3606,0.3264,0.3703,0.3264 Q0.3775,0.3264,0.3847,0.3293 Q0.3888,0.331,0.396,0.331 Q0.4024,0.331,0.4116,0.3295 T0.4265,0.3266 T0.4414,0.3227 T0.4514,0.3202 Q0.4578,0.3181,0.4839,0.3152 T0.5112,0.3123 T0.5177,0.3119 T0.5285,0.3111 T0.5398,0.3096 T0.549,0.3065 T0.5526,0.3015 Q0.5526,0.2999,0.5518,0.2982 Q0.547,0.2903,0.5386,0.2883 T0.5165,0.2862 Q0.5116,0.2862,0.5056,0.2866 T0.4972,0.287 Q0.4884,0.287,0.4787,0.2833 Q0.4594,0.2758,0.4265,0.2758 Q0.4169,0.2758,0.4129,0.2766 Q0.4112,0.2771,0.4104,0.2771 Q0.4088,0.2771,0.4056,0.2748 T0.3968,0.2721 Q0.3695,0.2696,0.359,0.2659 Q0.355,0.2646,0.351,0.2646 T0.3426,0.2661 T0.3365,0.2675 T0.3333,0.2659 Q0.3277,0.2605,0.3116,0.2553 T0.2859,0.2501 L0.2843,0.2501 L0.2819,0.2501 Q0.2779,0.2501,0.2723,0.2493 T0.2586,0.2472 T0.2474,0.2455 Q0.1799,0.236,0.1719,0.236 Q0.1663,0.236,0.1606,0.235 T0.1502,0.2316 T0.1454,0.2265 Q0.1454,0.2223,0.1558,0.2173 Q0.1679,0.2115,0.1859,0.2092 T0.2185,0.207 T0.2353,0.2057 Q0.2369,0.2049,0.2502,0.2041 T0.2799,0.2022 T0.302,0.2007 Q0.3084,0.1999,0.3245,0.1954 T0.347,0.1908 Q0.3502,0.1908,0.3534,0.1916 Q0.3663,0.197,0.388,0.197 Q0.404,0.197,0.4088,0.1929 Q0.4096,0.192,0.4108,0.192 T0.4177,0.1945 T0.4277,0.197 T0.4369,0.1949 Q0.4442,0.1908,0.4843,0.1875 Q0.4892,0.1871,0.4968,0.1839 T0.5084,0.1808 L0.5108,0.1808 Q0.5213,0.1825,0.5454,0.1825 Q0.5823,0.1825,0.5992,0.1779 Q0.6096,0.1746,0.6217,0.1746 Q0.6249,0.1746,0.6309,0.1748 T0.6394,0.175 Q0.6434,0.175,0.6466,0.1746 Q0.6474,0.1746,0.6594,0.1738 T0.6787,0.1713 T0.6892,0.1659 Q0.6924,0.1634,0.6924,0.1593 Q0.6924,0.1547,0.6888,0.151 T0.6795,0.1472 Q0.6779,0.1472,0.6763,0.1477 Q0.6699,0.1493,0.6667,0.1493 Q0.661,0.1493,0.6506,0.1456 Q0.6329,0.1394,0.6129,0.1394 Q0.6048,0.1394,0.5992,0.1377 T0.5888,0.136 T0.5803,0.1377 T0.5751,0.1394 Q0.5711,0.1394,0.5639,0.1369 Q0.5365,0.1273,0.5116,0.1273 Q0.4876,0.1273,0.4667,0.1253 Q0.4627,0.1248,0.4542,0.1211 T0.4337,0.1174 L0.4305,0.1174 Q0.4217,0.1178,0.4153,0.119 T0.408,0.1203 Q0.4056,0.1203,0.4,0.117 Q0.3976,0.1153,0.396,0.1136 T0.3932,0.1112 T0.39,0.1099 T0.3847,0.1095 Q0.3799,0.1095,0.3687,0.1103 Q0.355,0.1112,0.3434,0.1141 T0.3301,0.117 Q0.3261,0.117,0.3229,0.1145 Q0.3181,0.1116,0.3052,0.1087 T0.2787,0.1043 T0.2546,0.1029 Q0.2506,0.1029,0.2474,0.1033 Q0.2418,0.1037,0.2349,0.1054 T0.2249,0.107 Q0.2225,0.107,0.2193,0.1066 Q0.2024,0.1024,0.1863,0.1024 Q0.1823,0.1024,0.1562,0.0971 T0.1269,0.0917 T0.1161,0.0896 T0.106,0.0854 Q0.1076,0.0846,0.11,0.0842 Q0.1173,0.0821,0.1261,0.0821 Q0.1301,0.0821,0.1369,0.0825 T0.1462,0.083 Q0.1494,0.083,0.151,0.0825 T0.1831,0.0813 T0.2201,0.0784 T0.243,0.074 T0.2667,0.0718 Q0.2707,0.0718,0.2755,0.0724 T0.2843,0.073 T0.2936,0.0724 T0.3024,0.0705 T0.3072,0.068 T0.3092,0.0657 T0.3108,0.0647 Q0.3116,0.0643,0.3133,0.0643 L0.3157,0.0643 L0.3181,0.0643 Q0.3446,0.0643,0.3446,0.0556 T0.3205,0.0469 L0.3189,0.0469 Q0.3157,0.0469,0.3157,0.0465 Q0.3084,0.0427,0.2835,0.0386 Q0.2739,0.0369,0.2675,0.0342 T0.2538,0.0299 Q0.249,0.029,0.245,0.029 T0.2378,0.0303 T0.2321,0.0315 Q0.2305,0.0315,0.2273,0.0307 Q0.2201,0.0286,0.2004,0.0272 T0.1775,0.0245 Q0.1735,0.0228,0.1663,0.0226 T0.1522,0.0197 T0.1345,0.0145 T0.1124,0.0079 Q0.0996,0.0033,0.0671,0.0044 T0.0297,0.0041 Q0.0169,0,0.0024,0.0017 Q0,0.0021,0,0.0037 Z M0.0064,0.9954 L0.0056,0.0058 Q0.0177,0.005,0.0281,0.0083 Q0.0345,0.01,0.0675,0.0089 T0.1108,0.012 Q0.1221,0.0162,0.1329,0.0187 T0.1498,0.0236 T0.1606,0.0268 T0.1691,0.0274 T0.1759,0.0286 Q0.1807,0.0303,0.2,0.0315 T0.2257,0.0348 Q0.2305,0.0361,0.2337,0.0361 Q0.2361,0.0361,0.2398,0.0348 T0.2466,0.0336 Q0.249,0.0336,0.2522,0.034 Q0.2586,0.0353,0.2655,0.0382 T0.2827,0.0427 Q0.3068,0.0469,0.3124,0.0502 Q0.3157,0.0523,0.3237,0.0523 L0.3277,0.0523 L0.3309,0.0523 Q0.3398,0.0523,0.3398,0.0556 Q0.3398,0.0581,0.3341,0.0587 T0.3205,0.0593 T0.3092,0.0603 T0.3036,0.0643 T0.2932,0.068 Q0.2884,0.0684,0.2843,0.0684 Q0.2811,0.0684,0.2763,0.0678 T0.2667,0.0672 Q0.2586,0.0672,0.2422,0.0697 T0.2177,0.0747 Q0.2137,0.0759,0.1831,0.0767 T0.1494,0.0784 L0.1474,0.0784 T0.1398,0.078 T0.1285,0.0776 Q0.1173,0.0776,0.1084,0.08 Q0.1004,0.0817,0.1004,0.0846 Q0.1004,0.0867,0.104,0.0888 T0.1124,0.0923 T0.1213,0.0948 T0.1269,0.0958 T0.155,0.1014 T0.1863,0.107 Q0.2016,0.107,0.2185,0.1107 Q0.2217,0.1116,0.2257,0.1116 Q0.2289,0.1116,0.2365,0.1099 T0.2482,0.1074 L0.2538,0.1074 Q0.2683,0.1074,0.2912,0.1107 T0.3197,0.1182 Q0.3245,0.1215,0.3301,0.1215 Q0.3333,0.1215,0.3454,0.1184 T0.3687,0.1145 Q0.3735,0.1141,0.3779,0.1139 T0.3843,0.1134 T0.3871,0.1132 Q0.3896,0.1132,0.39,0.1136 T0.392,0.1161 T0.3968,0.1203 Q0.404,0.1248,0.408,0.1248 Q0.4096,0.1248,0.4165,0.1234 T0.4305,0.1215 L0.4337,0.1215 Q0.4442,0.1215,0.4518,0.1251 T0.4659,0.1294 Q0.4876,0.1319,0.5116,0.1319 T0.5622,0.141 Q0.5703,0.1439,0.5751,0.1439 Q0.5791,0.1439,0.5831,0.1423 T0.5888,0.1406 Q0.592,0.1406,0.5944,0.1412 T0.6012,0.1427 T0.6129,0.1435 Q0.6313,0.1435,0.649,0.1497 Q0.6602,0.1535,0.6667,0.1535 Q0.6715,0.1535,0.6779,0.1522 Q0.6787,0.1518,0.6795,0.1518 Q0.6827,0.1518,0.6847,0.1541 T0.6867,0.1593 Q0.6867,0.1618,0.6851,0.1634 Q0.6835,0.1655,0.6775,0.1667 T0.6598,0.169 T0.6458,0.1705 T0.6402,0.1709 Q0.6378,0.1709,0.6321,0.1705 T0.6225,0.1701 Q0.6096,0.1701,0.5968,0.1738 Q0.5823,0.1779,0.5446,0.1779 Q0.5205,0.1779,0.5116,0.1767 Q0.51,0.1763,0.5084,0.1763 Q0.5028,0.1763,0.4944,0.1796 T0.4835,0.1829 L0.4771,0.1833 T0.4659,0.1844 L0.4546,0.1858 T0.443,0.1881 T0.4337,0.1912 Q0.4321,0.1916,0.4305,0.1916 Q0.4273,0.1916,0.4209,0.1895 T0.412,0.1875 Q0.408,0.1875,0.4056,0.19 Q0.4024,0.1925,0.388,0.1925 Q0.3663,0.1925,0.3558,0.1879 Q0.3518,0.1862,0.3462,0.1862 Q0.3373,0.1862,0.3213,0.191 T0.3012,0.1962 Q0.2964,0.1966,0.2799,0.1976 T0.2494,0.1997 T0.2313,0.2024 L0.2313,0.2024 L0.2293,0.2024 L0.2261,0.2024 L0.2217,0.2024 Q0.204,0.2024,0.1851,0.2047 T0.1526,0.2136 Q0.1406,0.2198,0.1406,0.226 Q0.1406,0.2319,0.1518,0.2362 T0.1719,0.2406 Q0.1791,0.2406,0.2466,0.2497 Q0.2498,0.2501,0.2574,0.2516 T0.2711,0.2538 T0.2819,0.2547 L0.2851,0.2547 Q0.2851,0.2543,0.2859,0.2543 Q0.294,0.2543,0.3096,0.2592 T0.3285,0.2684 Q0.3317,0.2721,0.3373,0.2721 Q0.3406,0.2721,0.3462,0.2708 T0.3542,0.2696 Q0.3558,0.2696,0.3574,0.27 Q0.3679,0.2737,0.3968,0.2762 Q0.3984,0.2766,0.4004,0.2779 L0.4044,0.2804 T0.4096,0.2816 Q0.412,0.2816,0.4145,0.2808 Q0.4177,0.2804,0.4249,0.2804 Q0.4586,0.2804,0.4763,0.287 Q0.4867,0.2912,0.4972,0.2912 Q0.5004,0.2912,0.5064,0.2908 T0.5165,0.2903 Q0.5293,0.2903,0.5361,0.2922 T0.547,0.3003 L0.547,0.3011 Q0.547,0.3036,0.5414,0.3049 T0.5249,0.3067 T0.51,0.3082 Q0.5092,0.3082,0.4831,0.3111 T0.4498,0.3161 Q0.408,0.3264,0.3952,0.3264 Q0.3896,0.3264,0.3863,0.3252 Q0.3783,0.3223,0.3703,0.3223 Q0.359,0.3223,0.3494,0.3277 Q0.347,0.3293,0.343,0.3293 Q0.3414,0.3293,0.3353,0.3285 T0.3253,0.3277 T0.3181,0.3285 Q0.3149,0.3289,0.2952,0.3306 T0.2699,0.3347 Q0.2675,0.3364,0.2598,0.3372 T0.2434,0.3395 T0.2297,0.3426 L0.2205,0.3461 T0.2048,0.3523 T0.1908,0.3606 T0.1791,0.3725 Q0.1743,0.3803,0.1743,0.3845 Q0.1743,0.3874,0.1763,0.3901 T0.1827,0.3976 T0.1912,0.4073 T0.198,0.4189 T0.2032,0.4276 T0.2137,0.4326 T0.2386,0.438 Q0.2458,0.4392,0.2562,0.4392 L0.2639,0.4392 L0.2699,0.4392 Q0.2795,0.4392,0.2843,0.4405 Q0.2932,0.443,0.3092,0.443 L0.3157,0.443 L0.3213,0.443 Q0.3293,0.443,0.3458,0.4463 T0.3671,0.4525 Q0.3598,0.4558,0.3373,0.4571 Q0.3124,0.4587,0.298,0.4637 Q0.294,0.465,0.2811,0.465 L0.2703,0.465 L0.2586,0.465 Q0.241,0.465,0.2305,0.4666 Q0.2185,0.4687,0.2185,0.4728 Q0.2185,0.4745,0.2205,0.4764 T0.2241,0.4791 L0.2265,0.4803 L0.2273,0.4803 Q0.2506,0.4824,0.261,0.4824 Q0.2675,0.4824,0.2699,0.482 Q0.2699,0.4815,0.2707,0.4815 Q0.2771,0.4815,0.3124,0.4898 Q0.3173,0.4907,0.3229,0.4913 T0.3386,0.4925 T0.3542,0.4936 T0.3663,0.494 Q0.3711,0.494,0.3787,0.4936 T0.3888,0.4932 Q0.392,0.4932,0.3944,0.4936 T0.4032,0.4959 T0.4181,0.499 T0.4369,0.5006 Q0.449,0.501,0.455,0.5021 T0.4639,0.5048 T0.4675,0.5093 L0.4675,0.5097 Q0.4675,0.5106,0.4663,0.511 T0.4622,0.5116 L0.4566,0.512 T0.4486,0.5124 T0.4402,0.5127 Q0.4345,0.5131,0.4257,0.5153 T0.4096,0.5176 Q0.4064,0.5176,0.4024,0.5172 L0.3968,0.5172 Q0.3863,0.5172,0.3731,0.522 T0.359,0.5268 Q0.355,0.528,0.3317,0.5321 T0.3004,0.5388 Q0.2867,0.5433,0.2337,0.5479 Q0.2096,0.55,0.2096,0.5562 Q0.2096,0.5591,0.2145,0.5628 Q0.2193,0.5662,0.2289,0.5662 Q0.2313,0.5662,0.2373,0.5657 T0.2458,0.5653 L0.2498,0.5653 Q0.2635,0.5674,0.3124,0.5674 Q0.3261,0.5674,0.3301,0.567 L0.3317,0.567 Q0.3341,0.567,0.3394,0.568 T0.3486,0.5691 Q0.3542,0.5691,0.3606,0.5678 T0.3699,0.5666 T0.3759,0.5686 T0.3839,0.5718 T0.3908,0.573 T0.3964,0.5734 T0.4048,0.5742 T0.4165,0.5751 T0.4277,0.5757 T0.4369,0.5778 Q0.4426,0.5794,0.4538,0.5794 Q0.4586,0.5794,0.4679,0.5792 T0.4811,0.579 Q0.4924,0.579,0.4956,0.5807 Q0.502,0.584,0.5108,0.584 L0.5137,0.584 T0.5173,0.5838 L0.5205,0.5834 L0.5237,0.583 L0.5269,0.5825 T0.5305,0.5821 T0.5341,0.5819 L0.5365,0.5819 Q0.5414,0.5819,0.5562,0.5842 T0.5855,0.5865 Q0.5912,0.5865,0.5948,0.5867 T0.6012,0.5869 T0.6064,0.5873 T0.61,0.5877 T0.6133,0.5886 T0.6157,0.5896 L0.6185,0.591 T0.6217,0.5931 Q0.6265,0.5952,0.6369,0.5952 L0.6418,0.5952 L0.6466,0.5952 Q0.6723,0.5952,0.6932,0.606 Q0.698,0.608,0.7044,0.608 Q0.7084,0.608,0.7161,0.6068 T0.7261,0.6056 Q0.7309,0.6056,0.7454,0.6051 T0.7647,0.6047 Q0.7775,0.6047,0.8072,0.6101 Q0.8096,0.6105,0.812,0.6105 Q0.8177,0.6105,0.8297,0.6085 T0.8474,0.606 L0.8506,0.606 Q0.8562,0.606,0.8647,0.607 T0.8831,0.6095 T0.8964,0.6118 Q0.9036,0.6122,0.9221,0.6122 L0.9406,0.6122 L0.959,0.6122 L0.9767,0.6122 Q0.9952,0.613,0.9952,0.6172 Q0.9952,0.6176,0.9944,0.6221 Q0.9936,0.6259,0.9574,0.6259 L0.955,0.6259 L0.9526,0.6259 Q0.9478,0.6259,0.9373,0.628 T0.9205,0.63 L0.9173,0.63 Q0.9133,0.6296,0.9084,0.6296 Q0.9004,0.6296,0.8715,0.6321 Q0.8233,0.6367,0.7944,0.64 Q0.7888,0.6408,0.7839,0.6408 T0.7739,0.6402 T0.7671,0.6396 Q0.7639,0.6396,0.7622,0.64 Q0.7614,0.6404,0.759,0.6404 Q0.7574,0.6404,0.749,0.6398 T0.7357,0.6392 Q0.7261,0.6392,0.7213,0.6412 Q0.7173,0.6425,0.7032,0.6437 T0.6711,0.6458 T0.6474,0.647 Q0.645,0.6474,0.6418,0.6474 Q0.6353,0.6474,0.6277,0.6454 T0.6161,0.6433 Q0.6104,0.6433,0.6072,0.6456 T0.6,0.6479 Q0.5912,0.6479,0.5486,0.6557 Q0.5422,0.657,0.5373,0.657 Q0.5349,0.657,0.5289,0.6564 T0.5173,0.6557 L0.5124,0.6557 Q0.4996,0.6562,0.4739,0.6611 Q0.4675,0.6624,0.4398,0.6645 T0.3851,0.668 T0.3558,0.6698 Q0.355,0.6698,0.3285,0.6721 T0.2948,0.6752 Q0.2892,0.6761,0.2843,0.6786 T0.2747,0.6831 T0.2618,0.6852 Q0.253,0.6852,0.2394,0.6893 T0.2141,0.6993 T0.1968,0.7105 Q0.1944,0.713,0.1944,0.7155 Q0.1944,0.7196,0.2012,0.7229 T0.2177,0.7285 T0.2281,0.7308 Q0.2321,0.7329,0.2365,0.7339 T0.245,0.7356 T0.2514,0.7366 Q0.2522,0.737,0.2554,0.7379 T0.2614,0.7393 T0.2679,0.7406 T0.2755,0.7416 T0.2835,0.742 Q0.29,0.742,0.3048,0.7428 T0.3253,0.7437 L0.3301,0.7437 L0.3333,0.7437 Q0.3341,0.7437,0.3598,0.7466 Q0.3639,0.7474,0.3671,0.7474 T0.3759,0.7466 T0.3855,0.7457 Q0.388,0.7457,0.3904,0.7462 Q0.3984,0.7466,0.4137,0.7495 T0.4349,0.7532 T0.441,0.7557 Q0.441,0.7599,0.4289,0.7619 Q0.4257,0.7628,0.4197,0.7634 T0.4072,0.7644 T0.3984,0.7652 Q0.3968,0.7652,0.3912,0.7655 T0.3811,0.7665 T0.3719,0.7694 Q0.3695,0.7702,0.3655,0.7702 T0.3546,0.7686 T0.3462,0.7665 L0.3446,0.7665 Q0.3406,0.7665,0.3293,0.769 T0.3149,0.7715 Q0.3084,0.7715,0.2952,0.7748 T0.2763,0.781 Q0.2675,0.7847,0.2554,0.7864 Q0.253,0.7864,0.2325,0.7874 T0.2024,0.7893 Q0.2008,0.7893,0.1984,0.7897 T0.1948,0.7901 T0.1916,0.7903 T0.1888,0.7908 T0.1867,0.7914 T0.1847,0.7922 L0.1831,0.793 T0.1819,0.7943 L0.1811,0.7959 T0.1799,0.7976 Q0.1791,0.7993,0.1791,0.8001 Q0.1791,0.8034,0.1831,0.8051 T0.1932,0.8078 T0.2,0.8092 Q0.2072,0.8117,0.2257,0.8117 Q0.2337,0.8117,0.2586,0.8109 L0.2635,0.8109 Q0.2707,0.8109,0.2767,0.8117 T0.2859,0.8136 T0.2936,0.8163 T0.3012,0.8187 Q0.3068,0.82,0.3277,0.8206 T0.3526,0.8237 Q0.3582,0.8266,0.3622,0.8266 Q0.3647,0.8266,0.3687,0.8258 T0.3751,0.825 Q0.396,0.825,0.404,0.8291 Q0.408,0.8316,0.4161,0.8316 Q0.4217,0.8316,0.4353,0.8295 L0.4394,0.8295 Q0.4466,0.8295,0.4659,0.8366 Q0.4699,0.8378,0.4747,0.8378 Q0.4763,0.8378,0.4807,0.8374 T0.4884,0.837 Q0.494,0.837,0.5004,0.8382 Q0.5052,0.8391,0.5124,0.8399 Q0.5221,0.8411,0.5249,0.8422 T0.5277,0.8461 Q0.5277,0.8482,0.5233,0.849 T0.5112,0.8501 T0.4996,0.8511 Q0.4964,0.8519,0.4851,0.8523 T0.4659,0.8544 Q0.4578,0.8557,0.4402,0.8557 L0.4349,0.8557 L0.4297,0.8557 L0.4209,0.8557 Q0.408,0.8565,0.3867,0.8571 T0.3614,0.8582 Q0.3317,0.8611,0.3237,0.8611 L0.3225,0.8611 L0.3213,0.8611 Q0.2972,0.8611,0.2972,0.8681 Q0.2972,0.8714,0.3008,0.8747 T0.3044,0.8805 Q0.3044,0.881,0.3036,0.8814 Q0.3012,0.8839,0.2851,0.8839 Q0.2779,0.8839,0.2739,0.883 Q0.2723,0.8826,0.2699,0.8826 T0.2643,0.8835 T0.259,0.8853 T0.2542,0.8876 T0.249,0.8893 Q0.2386,0.8922,0.2056,0.8946 Q0.2016,0.8951,0.1916,0.898 T0.1703,0.9009 Q0.1518,0.9009,0.1337,0.9044 T0.1149,0.9146 L0.1149,0.9158 Q0.1149,0.9175,0.1157,0.9187 T0.1173,0.921 T0.1201,0.9226 T0.1233,0.9237 T0.1269,0.9245 L0.1305,0.9251 T0.1337,0.9255 L0.1357,0.9258 Q0.139,0.9262,0.1446,0.9262 T0.1542,0.9266 T0.1598,0.9282 Q0.1614,0.9303,0.1647,0.9314 T0.1739,0.9328 T0.1815,0.9336 Q0.1863,0.9345,0.1976,0.9372 T0.2145,0.9415 Q0.2225,0.9432,0.2285,0.9467 T0.2378,0.9515 Q0.2426,0.9531,0.2514,0.9531 Q0.2538,0.9531,0.2602,0.9529 T0.2707,0.9527 Q0.2763,0.9527,0.2815,0.9531 T0.2867,0.9569 T0.2811,0.9614 Q0.2434,0.9627,0.2329,0.9652 Q0.2249,0.9672,0.2241,0.9672 L0.2096,0.9668 L0.2056,0.9668 Q0.2,0.9668,0.196,0.9672 T0.1884,0.9687 T0.1815,0.9708 T0.1731,0.9737 T0.1639,0.9772 Q0.1518,0.9805,0.1285,0.9805 L0.1253,0.9805 Q0.1124,0.9805,0.1076,0.9818 Q0.1012,0.983,0.0831,0.9876 T0.0594,0.9934 Q0.049,0.9954,0.0064,0.9954 Z M0.0032,0.9979 L0.0024,0.0037 Q0.0161,0.0021,0.0289,0.0062 Q0.0337,0.0075,0.0671,0.0066 T0.1116,0.01 T0.1337,0.0166 T0.151,0.022 Q0.1582,0.0249,0.1659,0.0251 T0.1767,0.0265 Q0.1807,0.0278,0.2,0.0292 T0.2265,0.0328 Q0.2305,0.034,0.2329,0.034 T0.239,0.0326 T0.2458,0.0311 T0.253,0.0319 Q0.2578,0.0332,0.2614,0.0342 T0.2667,0.0363 T0.2723,0.0384 T0.2827,0.0406 Q0.3084,0.0448,0.3141,0.0485 Q0.3149,0.0494,0.3225,0.0494 T0.3361,0.0504 T0.3422,0.0554 T0.3357,0.0606 T0.3205,0.0618 T0.31,0.0626 Q0.3092,0.0626,0.3056,0.0659 T0.2932,0.0701 Q0.2884,0.0709,0.2843,0.0709 Q0.2811,0.0709,0.2759,0.0701 T0.2667,0.0693 Q0.2586,0.0693,0.2422,0.0718 T0.2193,0.0763 Q0.2137,0.0784,0.1831,0.079 T0.1502,0.0805 Q0.1494,0.0809,0.147,0.0809 Q0.1454,0.0809,0.1386,0.0803 T0.1277,0.0796 Q0.1173,0.0796,0.1092,0.0821 Q0.1036,0.0834,0.1036,0.0854 Q0.1036,0.0879,0.1129,0.0908 T0.1269,0.0937 Q0.1293,0.0937,0.1558,0.0991 T0.1863,0.1045 Q0.2016,0.1045,0.2193,0.1087 Q0.2225,0.1091,0.2249,0.1091 Q0.2289,0.1091,0.2357,0.1076 T0.2482,0.1054 Q0.2506,0.1049,0.2546,0.1049 Q0.2699,0.1049,0.2924,0.1085 T0.3213,0.1165 Q0.3253,0.119,0.3301,0.119 Q0.3325,0.119,0.3446,0.1161 T0.3687,0.1124 Q0.3831,0.1112,0.3863,0.1112 T0.3908,0.1118 T0.3936,0.1145 T0.3984,0.1186 Q0.4048,0.1228,0.408,0.1228 Q0.4096,0.1228,0.4161,0.1213 T0.4305,0.1195 L0.4337,0.1195 Q0.4426,0.1195,0.4474,0.1211 T0.4566,0.1246 T0.4667,0.1273 Q0.4876,0.1298,0.5116,0.1298 Q0.5365,0.1298,0.5631,0.1389 Q0.5703,0.1414,0.5751,0.1414 Q0.5783,0.1414,0.5819,0.1398 T0.5888,0.1381 Q0.5928,0.1381,0.5984,0.1398 T0.6129,0.1414 Q0.6321,0.1414,0.6498,0.1477 Q0.661,0.1514,0.6667,0.1514 Q0.6707,0.1514,0.6771,0.1501 Q0.6787,0.1497,0.6795,0.1497 Q0.6835,0.1497,0.6863,0.1526 T0.6892,0.1593 Q0.6892,0.1626,0.6876,0.1647 Q0.6851,0.1672,0.6799,0.1686 T0.6695,0.1705 T0.6566,0.1715 T0.6458,0.1725 Q0.6434,0.173,0.6402,0.173 Q0.6378,0.173,0.6317,0.1727 T0.6225,0.1725 Q0.6096,0.1725,0.5984,0.1759 Q0.5823,0.1804,0.5454,0.1804 Q0.5205,0.1804,0.5116,0.1788 L0.5084,0.1788 Q0.5036,0.1788,0.4956,0.1819 T0.4843,0.185 Q0.4434,0.1887,0.4353,0.1929 Q0.4321,0.1945,0.4289,0.1945 T0.4197,0.1922 T0.4112,0.19 T0.4072,0.1912 Q0.4032,0.1949,0.388,0.1949 Q0.3663,0.1949,0.3542,0.19 Q0.351,0.1883,0.3462,0.1883 Q0.3406,0.1883,0.3321,0.1906 T0.3153,0.1954 T0.302,0.1983 Q0.2972,0.1991,0.2859,0.1997 T0.2643,0.201 T0.245,0.2024 T0.2337,0.2041 Q0.2321,0.2045,0.2181,0.2045 T0.1851,0.207 T0.1542,0.2157 Q0.143,0.2211,0.143,0.226 Q0.143,0.2314,0.153,0.2348 T0.1719,0.2381 Q0.1791,0.2381,0.2474,0.2476 Q0.2506,0.248,0.2558,0.2489 T0.2651,0.2505 T0.2739,0.252 T0.2819,0.2526 Q0.2835,0.2526,0.2851,0.2522 L0.2859,0.2522 Q0.2948,0.2522,0.3104,0.2574 T0.3309,0.2671 Q0.3333,0.2696,0.3365,0.2696 Q0.339,0.2696,0.3442,0.2684 T0.3526,0.2671 T0.3582,0.2679 Q0.3687,0.2717,0.3968,0.2742 Q0.4,0.2746,0.4036,0.2771 T0.4096,0.2796 Q0.4112,0.2796,0.4137,0.2787 Q0.4169,0.2779,0.4257,0.2779 Q0.4594,0.2779,0.4779,0.2849 Q0.4876,0.2891,0.4972,0.2891 Q0.4996,0.2891,0.506,0.2887 T0.5165,0.2883 Q0.5253,0.2883,0.5305,0.2889 T0.541,0.292 T0.5494,0.299 L0.5494,0.3015 Q0.5494,0.3036,0.5466,0.3051 T0.5386,0.3073 T0.5285,0.3086 T0.5181,0.3094 T0.5108,0.3102 Q0.5068,0.3107,0.494,0.3121 L0.4683,0.315 T0.4506,0.3181 Q0.449,0.3181,0.4426,0.32 T0.4313,0.3229 T0.4197,0.3256 T0.4064,0.3281 T0.3952,0.3289 Q0.3888,0.3289,0.3855,0.3273 Q0.3783,0.3243,0.3703,0.3243 Q0.3598,0.3243,0.351,0.3297 Q0.3478,0.3314,0.3422,0.3314 Q0.3398,0.3314,0.3337,0.3306 T0.3245,0.3297 T0.3189,0.3306 Q0.3149,0.3314,0.2956,0.3328 T0.2715,0.3368 Q0.2691,0.338,0.2635,0.3391 T0.2522,0.3407 T0.2406,0.3424 T0.2313,0.3447 Q0.2305,0.3447,0.2213,0.348 T0.206,0.354 T0.1924,0.3621 T0.1815,0.3733 Q0.1767,0.3816,0.1767,0.3849 Q0.1767,0.387,0.1783,0.3893 T0.1847,0.3961 T0.1936,0.4063 T0.2004,0.4181 T0.2052,0.4264 T0.2145,0.4309 T0.2386,0.4359 Q0.2458,0.4372,0.2554,0.4372 Q0.2578,0.4372,0.2627,0.437 T0.2699,0.4367 Q0.2795,0.4367,0.2851,0.4384 Q0.294,0.4409,0.3092,0.4409 Q0.3116,0.4409,0.3157,0.4407 T0.3213,0.4405 Q0.3301,0.4405,0.3466,0.444 T0.3687,0.4509 Q0.3703,0.4517,0.3703,0.4525 Q0.3703,0.4575,0.3373,0.4596 Q0.3133,0.4608,0.2988,0.4658 Q0.294,0.4674,0.2803,0.4674 Q0.2763,0.4674,0.2687,0.4672 T0.257,0.467 Q0.2402,0.467,0.2313,0.4687 Q0.2209,0.4708,0.2209,0.4737 Q0.2209,0.4741,0.2213,0.4749 T0.2229,0.4764 L0.2249,0.4774 T0.2265,0.4778 L0.2273,0.4782 Q0.2498,0.4803,0.2618,0.4803 Q0.2667,0.4803,0.2691,0.4799 Q0.2699,0.4795,0.2715,0.4795 Q0.2779,0.4795,0.3133,0.4873 Q0.3181,0.4886,0.3233,0.4892 T0.3386,0.4905 T0.3542,0.4915 T0.3655,0.4919 Q0.3703,0.4919,0.3779,0.4915 T0.3888,0.4911 T0.3952,0.4915 Q0.3976,0.4919,0.4008,0.4927 L0.4072,0.4944 T0.4149,0.4961 T0.4249,0.4975 T0.4369,0.4985 Q0.449,0.499,0.4554,0.5 T0.4651,0.5031 T0.4699,0.5085 Q0.4707,0.5093,0.4707,0.5097 Q0.4707,0.5114,0.4687,0.5124 T0.4631,0.5139 T0.4562,0.5145 T0.4478,0.5147 L0.4402,0.5147 Q0.4353,0.5151,0.4265,0.5174 T0.4096,0.5197 L0.4024,0.5197 Q0.4,0.5193,0.3968,0.5193 Q0.3896,0.5193,0.3831,0.5209 T0.3703,0.5253 T0.3598,0.5288 Q0.3558,0.5301,0.3321,0.5342 T0.302,0.5409 Q0.2876,0.5454,0.2337,0.55 Q0.2112,0.5521,0.2112,0.5558 Q0.2112,0.5574,0.2161,0.5612 Q0.2201,0.5637,0.2281,0.5637 Q0.2305,0.5637,0.2361,0.5633 T0.2442,0.5628 Q0.2474,0.5628,0.2498,0.5633 Q0.2635,0.5653,0.3133,0.5653 Q0.3261,0.5653,0.3301,0.5649 L0.3325,0.5649 Q0.3349,0.5649,0.3402,0.5657 T0.3494,0.5666 Q0.3542,0.5666,0.3598,0.5657 Q0.3663,0.5645,0.3695,0.5645 Q0.3735,0.5645,0.3775,0.5666 T0.3847,0.5695 Q0.3888,0.5707,0.3912,0.5709 T0.3968,0.5711 T0.4048,0.572 Q0.4096,0.5724,0.4165,0.5726 T0.4281,0.5734 T0.4378,0.5757 Q0.4418,0.5774,0.4522,0.5774 Q0.4562,0.5774,0.4655,0.5769 T0.4795,0.5765 Q0.4932,0.5765,0.4972,0.579 Q0.5028,0.5819,0.51,0.5819 Q0.5141,0.5819,0.5221,0.5809 T0.5341,0.5798 L0.5373,0.5798 Q0.5422,0.5798,0.557,0.5821 T0.5855,0.5844 L0.5936,0.5844 L0.5992,0.5844 T0.604,0.5848 T0.6076,0.5852 T0.6108,0.5859 T0.6137,0.5865 T0.6161,0.5873 T0.6181,0.5881 T0.6209,0.5896 L0.6233,0.591 Q0.6265,0.5927,0.6353,0.5927 L0.6402,0.5927 L0.645,0.5927 Q0.6723,0.5927,0.6948,0.6039 Q0.6988,0.606,0.7036,0.606 Q0.7076,0.606,0.7153,0.6045 T0.7261,0.6031 Q0.7309,0.6031,0.7454,0.6027 T0.7647,0.6022 Q0.7783,0.6022,0.808,0.608 L0.812,0.608 Q0.8169,0.608,0.8293,0.6062 T0.8474,0.6039 L0.8506,0.6039 Q0.857,0.6039,0.8655,0.6049 T0.8835,0.6074 T0.8972,0.6093 Q0.9036,0.6101,0.9213,0.6101 L0.9398,0.6101 L0.959,0.6101 L0.9767,0.6101 Q0.9976,0.6109,0.9976,0.6176 Q0.9976,0.6188,0.9968,0.6226 Q0.9952,0.6284,0.9671,0.6284 L0.959,0.6284 L0.9526,0.6284 Q0.9486,0.6284,0.9382,0.6302 T0.9205,0.6321 L0.9173,0.6321 Q0.9133,0.6317,0.9092,0.6317 Q0.9012,0.6317,0.8715,0.6346 Q0.8233,0.6387,0.7944,0.6421 Q0.7888,0.6429,0.7831,0.6429 Q0.7783,0.6429,0.7731,0.6425 T0.7663,0.6421 L0.7622,0.6421 Q0.7614,0.6425,0.7586,0.6425 T0.7474,0.6421 T0.7349,0.6416 Q0.7261,0.6416,0.7221,0.6429 Q0.7173,0.645,0.7032,0.646 T0.6711,0.6479 T0.6482,0.6491 Q0.645,0.6495,0.6418,0.6495 Q0.6353,0.6495,0.6273,0.6474 T0.6161,0.6454 Q0.612,0.6454,0.6088,0.6477 T0.6,0.6499 Q0.5912,0.6499,0.5494,0.6578 Q0.5422,0.6591,0.5373,0.6591 Q0.5341,0.6591,0.5281,0.6584 T0.5173,0.6578 L0.5124,0.6578 Q0.5004,0.6582,0.4747,0.6632 Q0.4675,0.6649,0.4398,0.6667 T0.3851,0.6703 T0.3566,0.6719 Q0.3534,0.6723,0.3406,0.6734 T0.3141,0.6757 T0.2948,0.6773 Q0.2908,0.6781,0.2819,0.6829 T0.2618,0.6877 Q0.2482,0.6877,0.2273,0.6958 T0.1992,0.7117 Q0.1976,0.7138,0.1976,0.7155 Q0.1976,0.7188,0.2032,0.7213 T0.2181,0.7258 T0.2289,0.7292 Q0.2337,0.7308,0.2378,0.7316 L0.2458,0.7333 T0.2522,0.7345 Q0.2747,0.7399,0.2835,0.7399 Q0.29,0.7399,0.3048,0.7408 T0.3253,0.7416 Q0.3277,0.7416,0.3297,0.7414 T0.3341,0.7412 L0.3598,0.7445 Q0.3639,0.7449,0.3663,0.7449 Q0.3695,0.7449,0.3751,0.7443 T0.3855,0.7437 L0.3904,0.7437 Q0.3984,0.7441,0.4141,0.7472 T0.4353,0.7507 Q0.4434,0.752,0.4434,0.7553 Q0.4434,0.7578,0.439,0.7605 T0.4297,0.764 Q0.4257,0.7648,0.4201,0.7655 T0.4076,0.7665 T0.3984,0.7673 Q0.3968,0.7673,0.3912,0.7677 T0.3815,0.7688 T0.3735,0.771 T0.3655,0.7727 Q0.3606,0.7727,0.3534,0.7708 T0.3458,0.769 L0.3446,0.769 Q0.3414,0.769,0.3297,0.7713 T0.3149,0.7735 Q0.3092,0.7735,0.296,0.7771 T0.2771,0.7831 Q0.2683,0.7872,0.2554,0.7885 Q0.253,0.7889,0.2325,0.7897 T0.2032,0.7914 Q0.2008,0.7918,0.198,0.792 T0.194,0.7924 T0.1912,0.7928 T0.1888,0.793 T0.1867,0.7934 T0.1851,0.7943 T0.1843,0.7953 T0.1835,0.7968 T0.1823,0.7984 T0.1815,0.8001 Q0.1815,0.8022,0.1847,0.8032 T0.1932,0.8053 T0.2008,0.8071 Q0.2072,0.8092,0.2265,0.8092 Q0.2361,0.8092,0.2586,0.8088 L0.2635,0.8088 Q0.2731,0.8088,0.2795,0.8098 T0.2928,0.8134 T0.302,0.8167 Q0.306,0.8179,0.3277,0.8183 T0.3542,0.8221 Q0.359,0.8246,0.3622,0.8246 Q0.3639,0.8246,0.3675,0.8237 T0.3751,0.8229 Q0.3968,0.8229,0.4048,0.8275 Q0.4088,0.8291,0.4161,0.8291 Q0.4225,0.8291,0.4353,0.8275 Q0.4369,0.827,0.4394,0.827 Q0.4474,0.827,0.4675,0.8345 Q0.4707,0.8358,0.4739,0.8358 Q0.4755,0.8358,0.4799,0.8353 T0.4876,0.8349 Q0.494,0.8349,0.5012,0.8362 L0.5076,0.837 T0.5133,0.8376 T0.5181,0.8382 T0.5221,0.8391 L0.5253,0.8399 T0.5277,0.8409 L0.5293,0.8422 T0.5301,0.8438 L0.5301,0.8461 Q0.5301,0.8499,0.5249,0.8511 T0.5112,0.8526 T0.5004,0.8532 Q0.4964,0.854,0.4851,0.8546 T0.4667,0.8565 Q0.4578,0.8577,0.441,0.8577 L0.4345,0.8577 L0.4289,0.8577 L0.4209,0.8577 Q0.4153,0.8582,0.4088,0.8586 T0.3968,0.8592 T0.3855,0.8594 T0.3755,0.8596 T0.3675,0.86 T0.3622,0.8602 Q0.3317,0.8635,0.3237,0.8635 Q0.2996,0.8635,0.2996,0.8681 Q0.2996,0.8698,0.3032,0.8733 T0.3068,0.8797 L0.3068,0.8822 Q0.3052,0.8859,0.2859,0.8859 Q0.2787,0.8859,0.2739,0.8851 L0.2699,0.8851 Q0.2659,0.8851,0.259,0.888 T0.2498,0.8913 Q0.2394,0.8946,0.2056,0.8971 Q0.2032,0.8971,0.1928,0.9002 T0.1703,0.9034 Q0.1526,0.9034,0.1353,0.9067 T0.1173,0.915 L0.1173,0.9158 Q0.1173,0.9166,0.1177,0.9175 T0.1189,0.9191 T0.1205,0.9204 T0.1229,0.9212 T0.1257,0.922 T0.1285,0.9226 T0.1309,0.9231 T0.1337,0.9235 T0.1365,0.9237 L0.1446,0.9237 T0.1554,0.9243 T0.1614,0.9268 T0.1659,0.9293 T0.1743,0.9303 T0.1823,0.9316 Q0.1863,0.932,0.1912,0.9332 T0.2028,0.9361 T0.2153,0.939 Q0.2233,0.9411,0.2297,0.9448 T0.2386,0.9494 Q0.2426,0.9506,0.2506,0.9506 Q0.253,0.9506,0.2594,0.9504 T0.2699,0.9502 Q0.2763,0.9502,0.2819,0.9511 Q0.2851,0.9511,0.2871,0.9529 T0.2892,0.9573 Q0.2892,0.9627,0.2811,0.9635 Q0.2434,0.9652,0.2337,0.9672 Q0.2257,0.9693,0.2241,0.9693 Q0.2217,0.9693,0.2096,0.9689 L0.2056,0.9689 Q0.1976,0.9689,0.1924,0.9699 T0.1787,0.9741 L0.1647,0.9793 Q0.1518,0.983,0.1285,0.983 L0.1261,0.983 L0.1237,0.983 Q0.1124,0.983,0.1076,0.9838 Q0.102,0.9851,0.0839,0.9896 T0.0602,0.9954 T0.0406,0.9971 T0.0153,0.9979 L0.0032,0.9979 Z",type:0}},"\uE0CA":{type:5,data:{d:"M0.9992,0.998 Q0.9992,1,0.9968,1 Q0.9521,1,0.9392,0.998 Q0.9335,0.997,0.9152,0.9921 T0.8913,0.9862 Q0.8873,0.9852,0.8783,0.9852 L0.8747,0.9852 L0.871,0.9852 Q0.8483,0.9852,0.8345,0.9812 Q0.8289,0.9792,0.8228,0.9773 T0.8135,0.9743 T0.8054,0.9723 T0.7948,0.9713 L0.7908,0.9713 L0.777,0.9713 T0.7656,0.9693 Q0.7616,0.9683,0.7498,0.9674 T0.7283,0.9664 L0.7186,0.9654 Q0.708,0.9644,0.708,0.9575 Q0.708,0.9545,0.7109,0.952 T0.7178,0.9486 L0.7315,0.9486 L0.7417,0.9486 L0.7502,0.9486 Q0.7575,0.9486,0.7599,0.9476 Q0.7616,0.9466,0.7685,0.9426 T0.7843,0.9377 Q0.7899,0.9357,0.8009,0.9332 T0.8175,0.9298 Q0.82,0.9288,0.8228,0.9283 T0.8277,0.9278 T0.8313,0.9273 T0.8341,0.9263 T0.8362,0.9258 Q0.8402,0.9219,0.854,0.9219 L0.8569,0.9219 L0.8597,0.9219 L0.8637,0.9219 Q0.88,0.9199,0.88,0.9159 L0.88,0.9149 Q0.88,0.913,0.8719,0.911 T0.852,0.9075 T0.8297,0.906 Q0.8224,0.906,0.8167,0.905 T0.8074,0.9031 T0.8001,0.9006 T0.794,0.8991 Q0.7599,0.8971,0.7494,0.8942 Q0.7461,0.8932,0.7397,0.8902 T0.7299,0.8872 L0.7267,0.8872 Q0.721,0.8882,0.7121,0.8882 Q0.6926,0.8882,0.691,0.8833 L0.691,0.8803 Q0.691,0.8773,0.6922,0.8749 T0.6955,0.8704 L0.6975,0.8684 Q0.6926,0.8655,0.6764,0.8655 Q0.6683,0.8655,0.6375,0.8625 Q0.6358,0.8625,0.629,0.862 T0.6148,0.8615 T0.5973,0.861 T0.5791,0.8605 L0.5718,0.8605 L0.5661,0.8605 L0.5596,0.8605 Q0.5426,0.8605,0.5337,0.8586 Q0.5264,0.8576,0.515,0.8571 T0.4996,0.8556 Q0.498,0.8556,0.4947,0.8551 T0.4882,0.8546 T0.4809,0.8541 T0.474,0.8531 T0.4692,0.8506 T0.4672,0.8467 Q0.4672,0.8437,0.468,0.8422 T0.4704,0.8398 T0.4736,0.8383 T0.4781,0.8368 T0.4838,0.8358 T0.4911,0.8353 T0.4988,0.8338 Q0.5069,0.8328,0.5142,0.8328 Q0.5174,0.8328,0.5215,0.8333 T0.5264,0.8338 Q0.5296,0.8338,0.532,0.8328 Q0.5523,0.8249,0.5604,0.8249 L0.5653,0.8249 Q0.5775,0.8269,0.5848,0.8269 Q0.5912,0.8269,0.5937,0.8259 Q0.6026,0.821,0.6253,0.821 Q0.6294,0.821,0.6334,0.822 T0.6383,0.8229 Q0.6399,0.8229,0.644,0.82 Q0.6504,0.817,0.6723,0.816 T0.6975,0.814 Q0.6999,0.814,0.7048,0.8121 T0.7129,0.8091 T0.7226,0.8071 T0.7372,0.8061 L0.7421,0.8061 Q0.7624,0.8071,0.7721,0.8071 Q0.7924,0.8071,0.7981,0.8051 Q0.8021,0.8032,0.807,0.8027 T0.8135,0.8017 T0.8151,0.8002 L0.8151,0.7992 Q0.8143,0.7962,0.8127,0.7957 T0.8045,0.7943 Q0.7997,0.7943,0.7964,0.7933 Q0.7883,0.7933,0.7676,0.7923 T0.7437,0.7903 Q0.7307,0.7893,0.7218,0.7844 Q0.7194,0.7834,0.7141,0.7819 L0.7036,0.7789 T0.693,0.7765 T0.6853,0.7755 Q0.6805,0.7755,0.6691,0.773 T0.6553,0.7705 Q0.6545,0.7705,0.6545,0.7715 Q0.6537,0.7715,0.65,0.7725 T0.6423,0.774 T0.635,0.7745 Q0.6294,0.7745,0.6257,0.773 T0.618,0.771 T0.6083,0.77 T0.601,0.7695 Q0.5985,0.7695,0.5921,0.769 T0.5795,0.7676 T0.5702,0.7666 Q0.5653,0.7656,0.5596,0.7616 T0.5539,0.7547 Q0.5539,0.7498,0.5645,0.7488 Q0.5693,0.7478,0.5852,0.7448 T0.6099,0.7418 L0.6148,0.7418 Q0.6196,0.7418,0.6253,0.7423 T0.6338,0.7428 L0.6399,0.7428 Q0.6431,0.7418,0.6488,0.7409 T0.6586,0.7394 T0.6659,0.7389 L0.6707,0.7389 Q0.6723,0.7399,0.6748,0.7399 Q0.6796,0.7399,0.6946,0.7389 T0.7161,0.7379 Q0.7251,0.7379,0.7478,0.7319 Q0.7494,0.7319,0.7539,0.7315 T0.762,0.73 T0.7697,0.727 T0.7818,0.723 T0.7948,0.7196 T0.7997,0.7151 Q0.7997,0.7141,0.7989,0.7132 Q0.7924,0.7062,0.7717,0.6978 T0.738,0.6894 Q0.7267,0.6894,0.717,0.6845 T0.7048,0.6795 Q0.6975,0.6785,0.6711,0.6766 T0.6431,0.6746 Q0.6415,0.6736,0.6144,0.6721 T0.5596,0.6686 T0.5247,0.6657 Q0.4996,0.6607,0.4874,0.6597 L0.4834,0.6597 Q0.4785,0.6597,0.4724,0.6607 T0.4631,0.6617 Q0.4574,0.6617,0.4501,0.6597 Q0.4088,0.6518,0.4006,0.6518 Q0.3933,0.6518,0.3901,0.6499 T0.3844,0.6479 T0.3739,0.6499 T0.3585,0.6518 L0.352,0.6518 Q0.3471,0.6508,0.335,0.6503 T0.3122,0.6494 T0.2912,0.6479 T0.2766,0.6449 Q0.2733,0.6439,0.2668,0.6439 Q0.2628,0.6439,0.2543,0.6444 T0.2425,0.6449 T0.2368,0.6439 L0.2344,0.6439 Q0.2336,0.6439,0.2283,0.6444 T0.2182,0.6449 Q0.2117,0.6449,0.2052,0.6439 Q0.1768,0.6409,0.1281,0.637 Q0.0989,0.634,0.0916,0.634 L0.0835,0.634 L0.0795,0.634 Q0.0722,0.634,0.0616,0.632 T0.0479,0.6301 Q0.0462,0.6301,0.0406,0.6306 T0.0308,0.6311 Q0.0032,0.6311,0.0008,0.6231 Q0,0.6202,0,0.6182 Q0,0.6083,0.0227,0.6083 Q0.03,0.6073,0.0422,0.6073 Q0.0487,0.6073,0.0608,0.6078 T0.0787,0.6083 Q0.0973,0.6083,0.103,0.6073 Q0.1062,0.6063,0.1131,0.6053 T0.1253,0.6039 T0.1375,0.6024 T0.1492,0.6014 L0.1533,0.6014 Q0.1598,0.6024,0.1723,0.6044 T0.189,0.6063 Q0.1906,0.6063,0.1922,0.6053 Q0.2214,0.6004,0.236,0.6004 Q0.2409,0.6004,0.2551,0.6009 T0.2741,0.6014 Q0.2782,0.6014,0.2859,0.6024 T0.2968,0.6034 Q0.3009,0.6034,0.3041,0.6024 Q0.3277,0.5905,0.356,0.5905 L0.3613,0.5905 L0.3666,0.5905 Q0.3731,0.5905,0.3755,0.5895 Q0.382,0.5856,0.3844,0.5846 T0.3938,0.5831 T0.4144,0.5826 T0.4428,0.5801 T0.4631,0.5776 L0.4663,0.5776 Q0.4704,0.5776,0.4785,0.5786 T0.4899,0.5796 Q0.4964,0.5796,0.5012,0.5767 Q0.5069,0.5737,0.5223,0.5737 Q0.5272,0.5737,0.5369,0.5742 T0.5499,0.5747 Q0.558,0.5747,0.5612,0.5737 Q0.5661,0.5717,0.5714,0.5712 T0.5835,0.5702 T0.5945,0.5692 T0.6018,0.5687 L0.6062,0.5687 T0.6095,0.5682 T0.6148,0.5678 Q0.6172,0.5668,0.6196,0.5653 T0.6245,0.5633 T0.631,0.5628 T0.6407,0.5638 T0.6504,0.5648 T0.6594,0.5638 T0.6675,0.5628 L0.6707,0.5628 L0.6861,0.5628 Q0.7364,0.5628,0.7494,0.5608 L0.7567,0.5608 L0.7652,0.5608 L0.7729,0.5608 Q0.7794,0.5608,0.7818,0.5589 Q0.7851,0.5569,0.7859,0.5559 Q0.7835,0.5539,0.7656,0.5519 Q0.7121,0.547,0.6975,0.543 Q0.6934,0.541,0.6853,0.5396 L0.6691,0.5366 T0.6525,0.5336 T0.6391,0.5312 Q0.6367,0.5302,0.6245,0.5257 T0.6034,0.5213 L0.5977,0.5213 Q0.5945,0.5223,0.5904,0.5223 Q0.5839,0.5223,0.5787,0.5208 T0.5685,0.5183 T0.5596,0.5173 Q0.558,0.5173,0.5552,0.5168 T0.5495,0.5163 L0.5434,0.5163 T0.5373,0.5158 T0.532,0.5148 T0.5284,0.5129 T0.5272,0.5104 L0.5272,0.5074 Q0.5304,0.5015,0.5377,0.499 T0.5629,0.4965 Q0.5734,0.4955,0.5811,0.4946 T0.5953,0.4916 T0.605,0.4896 Q0.6083,0.4886,0.6123,0.4886 Q0.6148,0.4886,0.6225,0.4891 T0.635,0.4896 L0.6456,0.4896 Q0.6837,0.4857,0.6861,0.4857 Q0.7226,0.4768,0.7283,0.4768 Q0.7307,0.4768,0.732,0.4773 T0.7372,0.4777 Q0.7502,0.4777,0.7721,0.4758 Q0.7753,0.4748,0.777,0.4738 Q0.7745,0.4718,0.768,0.4708 Q0.7599,0.4688,0.7445,0.4688 Q0.7405,0.4688,0.7324,0.4693 T0.721,0.4698 Q0.7056,0.4698,0.6999,0.4679 Q0.6861,0.4629,0.6626,0.4619 Q0.6586,0.4609,0.6533,0.4604 T0.6419,0.459 T0.6314,0.456 T0.6269,0.452 Q0.6269,0.451,0.6302,0.4491 Q0.6358,0.4451,0.6525,0.4416 T0.6788,0.4382 L0.6845,0.4382 L0.6902,0.4382 Q0.7064,0.4382,0.7145,0.4362 Q0.7202,0.4342,0.7307,0.4342 L0.7356,0.4342 L0.7409,0.4342 L0.7453,0.4342 Q0.7543,0.4342,0.7607,0.4332 Q0.7778,0.4313,0.7847,0.4288 T0.7932,0.4248 T0.7972,0.4169 T0.8045,0.4045 L0.8094,0.3986 T0.8135,0.3942 L0.8167,0.3912 T0.8191,0.3887 T0.8204,0.3867 T0.8208,0.3848 Q0.8208,0.3818,0.8167,0.3739 Q0.8127,0.3689,0.8054,0.3635 T0.7924,0.3556 T0.7778,0.3497 T0.768,0.3462 Q0.764,0.3442,0.7478,0.3427 T0.7267,0.3383 Q0.7234,0.3363,0.7044,0.3348 T0.6805,0.3323 L0.6764,0.3323 T0.6679,0.3328 T0.6586,0.3333 Q0.6521,0.3333,0.648,0.3314 Q0.6391,0.3264,0.6294,0.3264 Q0.6229,0.3264,0.6156,0.3294 Q0.6115,0.3304,0.6042,0.3304 Q0.5977,0.3304,0.5888,0.3289 T0.5738,0.3264 T0.5584,0.3225 L0.5491,0.3195 Q0.5418,0.3185,0.5162,0.3155 T0.4895,0.3121 T0.4826,0.3116 T0.4716,0.3111 T0.4607,0.3096 T0.4513,0.3066 T0.4477,0.3017 Q0.4477,0.2997,0.4485,0.2977 Q0.4526,0.2898,0.4611,0.2878 T0.4842,0.2859 Q0.4882,0.2859,0.4943,0.2864 T0.5028,0.2868 Q0.5118,0.2868,0.5215,0.2829 Q0.5401,0.276,0.5734,0.276 Q0.5831,0.276,0.5872,0.277 L0.5904,0.277 T0.5949,0.2745 T0.6034,0.272 Q0.631,0.269,0.6407,0.2661 Q0.6448,0.2641,0.6488,0.2641 T0.6573,0.2656 T0.6634,0.2671 Q0.6659,0.2671,0.6675,0.2651 Q0.6723,0.2601,0.6886,0.2552 T0.7137,0.2502 L0.7153,0.2502 L0.7178,0.2502 Q0.7218,0.2502,0.7279,0.2493 T0.7417,0.2468 T0.7526,0.2453 Q0.8208,0.2354,0.8281,0.2354 Q0.8337,0.2354,0.8394,0.2344 T0.8496,0.2315 T0.854,0.2265 Q0.854,0.2216,0.8443,0.2176 Q0.8354,0.2127,0.8216,0.2102 T0.7972,0.2072 T0.777,0.2067 T0.7648,0.2057 Q0.7632,0.2047,0.7498,0.2038 T0.7202,0.2018 T0.6983,0.1998 Q0.6918,0.1998,0.676,0.1949 T0.6537,0.1899 Q0.6496,0.1899,0.6472,0.1919 Q0.6342,0.1968,0.6123,0.1968 Q0.5961,0.1968,0.5912,0.1929 Q0.5904,0.1919,0.5892,0.1919 T0.5823,0.1944 T0.5722,0.1968 T0.5637,0.1949 Q0.5556,0.1909,0.5158,0.1869 Q0.5109,0.1869,0.5032,0.1835 T0.4915,0.18 Q0.4907,0.18,0.4891,0.181 Q0.4793,0.182,0.455,0.182 Q0.4177,0.182,0.4015,0.1771 Q0.3901,0.1741,0.3779,0.1741 Q0.3747,0.1741,0.369,0.1746 T0.3609,0.1751 Q0.3569,0.1751,0.3536,0.1741 Q0.3512,0.1741,0.3459,0.1736 T0.3358,0.1726 T0.3256,0.1716 T0.3167,0.1696 T0.3106,0.1662 Q0.3082,0.1632,0.3082,0.1592 Q0.3082,0.1543,0.3114,0.1508 T0.3204,0.1474 L0.3236,0.1474 Q0.3301,0.1494,0.3333,0.1494 Q0.339,0.1494,0.3496,0.1454 Q0.3674,0.1385,0.3869,0.1385 Q0.395,0.1385,0.4011,0.137 T0.4116,0.1355 T0.4197,0.1375 T0.425,0.1395 Q0.429,0.1395,0.4355,0.1365 Q0.4631,0.1266,0.4882,0.1266 Q0.5126,0.1266,0.5337,0.1246 Q0.5369,0.1246,0.5458,0.1207 T0.5661,0.1167 L0.5693,0.1167 Q0.5791,0.1177,0.5852,0.1187 T0.5921,0.1197 Q0.5945,0.1197,0.6002,0.1167 Q0.6026,0.1147,0.605,0.1123 T0.6091,0.1093 T0.6156,0.1088 T0.6318,0.1098 Q0.6456,0.1108,0.6569,0.1137 T0.6707,0.1167 Q0.674,0.1167,0.6772,0.1147 Q0.6821,0.1108,0.6946,0.1083 T0.721,0.1044 T0.7453,0.1029 L0.7526,0.1029 Q0.7583,0.1039,0.7652,0.1053 T0.7753,0.1068 Q0.7778,0.1068,0.7802,0.1058 Q0.7981,0.1019,0.8143,0.1019 Q0.8175,0.1019,0.8439,0.0964 T0.8735,0.091 Q0.8759,0.091,0.8836,0.089 T0.8938,0.0851 Q0.8929,0.0841,0.8905,0.0841 Q0.8832,0.0821,0.8735,0.0821 Q0.8694,0.0821,0.8629,0.0826 T0.854,0.0831 Q0.8508,0.0831,0.8491,0.0821 Q0.8475,0.0821,0.8171,0.0811 T0.7802,0.0781 T0.7571,0.0737 T0.7332,0.0712 Q0.7299,0.0712,0.7247,0.0717 T0.7161,0.0722 L0.7064,0.0722 Q0.7032,0.0712,0.7003,0.0707 T0.6959,0.0692 T0.693,0.0678 T0.6914,0.0663 T0.6902,0.0648 T0.689,0.0643 L0.6869,0.0643 L0.6845,0.0643 L0.6821,0.0643 Q0.6553,0.0643,0.6553,0.0554 T0.6796,0.0465 L0.6813,0.0465 L0.6845,0.0465 Q0.6918,0.0425,0.717,0.0376 Q0.7226,0.0366,0.7267,0.0356 T0.7324,0.0336 T0.7376,0.0317 T0.747,0.0297 Q0.7518,0.0287,0.7551,0.0287 T0.762,0.0297 T0.768,0.0307 L0.7729,0.0307 Q0.7802,0.0277,0.7997,0.0267 T0.8224,0.0237 Q0.8273,0.0227,0.8341,0.0223 T0.8475,0.0193 T0.8654,0.0143 T0.8873,0.0079 Q0.9002,0.003,0.9331,0.004 T0.97,0.004 Q0.9838,0,0.9976,0.001 Q1,0.001,1,0.003 Z M0.9935,0.996 L0.9951,0.0049 Q0.9822,0.0049,0.9716,0.0079 Q0.9659,0.0099,0.9327,0.0089 T0.8897,0.0119 Q0.8775,0.0158,0.8666,0.0183 T0.85,0.0237 Q0.8418,0.0267,0.8341,0.0272 T0.8248,0.0277 Q0.82,0.0297,0.8001,0.0312 T0.7745,0.0346 Q0.7697,0.0356,0.7664,0.0356 Q0.764,0.0356,0.7603,0.0341 T0.7534,0.0326 Q0.751,0.0326,0.7478,0.0336 Q0.7429,0.0346,0.7397,0.0356 T0.7344,0.0376 T0.7283,0.0401 T0.7178,0.0425 Q0.6926,0.0465,0.6878,0.0495 Q0.6845,0.0514,0.6764,0.0514 L0.6723,0.0514 L0.6691,0.0514 Q0.6602,0.0514,0.6602,0.0554 Q0.6602,0.0574,0.6659,0.0579 T0.6796,0.0589 T0.691,0.0603 T0.6967,0.0643 T0.7072,0.0673 Q0.7121,0.0682,0.7153,0.0682 T0.7238,0.0678 T0.7332,0.0673 Q0.7413,0.0673,0.7579,0.0692 T0.7818,0.0742 Q0.7867,0.0752,0.8171,0.0762 T0.8508,0.0781 L0.852,0.0781 T0.8601,0.0776 T0.8719,0.0772 Q0.8832,0.0772,0.8921,0.0791 Q0.8994,0.0811,0.8994,0.0841 T0.8933,0.0895 T0.8812,0.094 T0.8735,0.0959 T0.8451,0.1014 T0.8143,0.1068 Q0.7989,0.1068,0.7818,0.1098 Q0.7778,0.1108,0.7745,0.1108 T0.768,0.1103 T0.7603,0.1088 T0.7518,0.1068 L0.7461,0.1068 Q0.7315,0.1068,0.7088,0.1103 T0.6805,0.1177 Q0.6756,0.1207,0.6699,0.1207 Q0.6667,0.1207,0.6545,0.1177 T0.631,0.1147 Q0.6269,0.1137,0.6225,0.1133 T0.616,0.1128 L0.6123,0.1128 T0.6103,0.1133 L0.6083,0.1157 T0.6034,0.1197 Q0.5961,0.1246,0.5921,0.1246 Q0.5904,0.1246,0.5835,0.1231 T0.5693,0.1217 L0.5661,0.1217 Q0.558,0.1217,0.5535,0.1231 T0.5446,0.1266 T0.5345,0.1296 Q0.5126,0.1316,0.4882,0.1316 T0.438,0.1405 Q0.4298,0.1434,0.425,0.1434 Q0.4209,0.1434,0.4173,0.1419 T0.4112,0.1405 T0.4027,0.1419 T0.3869,0.1434 Q0.3682,0.1434,0.3512,0.1494 Q0.3398,0.1533,0.3333,0.1533 Q0.3285,0.1533,0.3228,0.1513 L0.3212,0.1513 Q0.3179,0.1513,0.3155,0.1538 T0.3131,0.1592 Q0.3131,0.1612,0.3147,0.1632 Q0.3171,0.1652,0.3232,0.1667 T0.3406,0.1691 T0.3544,0.1701 L0.3601,0.1701 L0.3678,0.1701 L0.3771,0.1701 Q0.3909,0.1701,0.4031,0.1731 Q0.4177,0.178,0.4558,0.178 Q0.4793,0.178,0.4882,0.1761 L0.4915,0.1761 Q0.498,0.1761,0.5061,0.179 T0.5158,0.183 Q0.5166,0.183,0.5231,0.1835 T0.5341,0.1845 T0.545,0.1855 T0.5572,0.1874 T0.5661,0.1909 Q0.5677,0.1919,0.5702,0.1919 T0.5791,0.1894 T0.588,0.1869 Q0.5921,0.1869,0.5945,0.1899 Q0.5977,0.1919,0.6123,0.1919 Q0.6334,0.1919,0.6448,0.1879 Q0.6488,0.186,0.6537,0.186 Q0.6626,0.186,0.6784,0.1909 T0.6983,0.1958 Q0.704,0.1968,0.7206,0.1973 T0.7506,0.1993 T0.7689,0.2018 L0.7705,0.2018 L0.7741,0.2018 L0.7786,0.2018 Q0.8248,0.2018,0.8475,0.2136 Q0.8597,0.2196,0.8597,0.2255 T0.8483,0.2359 T0.8281,0.2404 Q0.8208,0.2404,0.7534,0.2493 Q0.7502,0.2502,0.7425,0.2512 T0.7287,0.2532 T0.7178,0.2542 L0.7153,0.2542 L0.7137,0.2542 Q0.7064,0.2542,0.6906,0.2591 T0.6715,0.2681 T0.6626,0.272 Q0.6594,0.272,0.6541,0.2705 T0.6464,0.269 T0.6431,0.27 Q0.6318,0.273,0.6034,0.276 Q0.6018,0.276,0.5998,0.2774 T0.5957,0.2799 T0.5904,0.2809 L0.5856,0.2809 Q0.5823,0.2799,0.575,0.2799 Q0.541,0.2799,0.5239,0.2868 Q0.5134,0.2908,0.5028,0.2908 Q0.4996,0.2908,0.4935,0.2903 T0.4842,0.2898 Q0.4704,0.2898,0.4635,0.2918 T0.4534,0.2997 L0.4534,0.3007 Q0.4534,0.3037,0.459,0.3046 T0.4753,0.3066 T0.4899,0.3076 Q0.4907,0.3076,0.5166,0.3106 T0.5507,0.3155 Q0.5921,0.3264,0.605,0.3264 Q0.6107,0.3264,0.6131,0.3254 Q0.6212,0.3225,0.6294,0.3225 Q0.6407,0.3225,0.6513,0.3274 Q0.6529,0.3294,0.6569,0.3294 Q0.6594,0.3294,0.665,0.3284 T0.6748,0.3274 T0.6821,0.3284 Q0.6861,0.3294,0.6967,0.3299 T0.7165,0.3314 T0.7299,0.3343 Q0.7324,0.3363,0.7486,0.3383 T0.7697,0.3422 Q0.7713,0.3432,0.7774,0.3452 T0.7887,0.3492 T0.8005,0.3546 T0.8127,0.3625 T0.8208,0.3719 Q0.8256,0.3798,0.8256,0.3848 Q0.8256,0.3877,0.8236,0.3902 T0.8171,0.3976 T0.809,0.4075 T0.8021,0.4189 T0.7968,0.4273 T0.7867,0.4322 T0.7616,0.4382 Q0.7543,0.4392,0.7437,0.4392 L0.7364,0.4392 L0.7299,0.4392 Q0.7202,0.4392,0.7161,0.4402 Q0.7072,0.4431,0.691,0.4431 L0.6845,0.4431 L0.6788,0.4431 Q0.6707,0.4431,0.6545,0.4461 T0.6334,0.452 Q0.6399,0.456,0.6626,0.457 Q0.6878,0.458,0.7024,0.4639 Q0.7064,0.4649,0.7186,0.4649 L0.7295,0.4649 L0.7413,0.4649 Q0.7591,0.4649,0.7697,0.4669 Q0.7818,0.4688,0.7818,0.4728 Q0.7818,0.4748,0.7798,0.4763 T0.7753,0.4787 L0.7737,0.4797 L0.7729,0.4797 Q0.7494,0.4827,0.7388,0.4827 Q0.7332,0.4827,0.7307,0.4817 L0.7291,0.4817 Q0.7226,0.4817,0.6878,0.4896 Q0.6829,0.4906,0.6772,0.4911 T0.6618,0.4921 T0.646,0.4931 T0.6342,0.4936 T0.6212,0.4931 T0.6115,0.4926 Q0.6083,0.4926,0.6058,0.4931 T0.5969,0.4955 T0.5819,0.499 T0.5629,0.5005 Q0.5572,0.5005,0.5527,0.501 T0.545,0.502 T0.5397,0.503 T0.5361,0.5045 T0.5337,0.5064 T0.5328,0.5094 L0.532,0.5094 Q0.532,0.5104,0.5337,0.5109 T0.5381,0.5114 T0.5438,0.5119 T0.5515,0.5124 L0.5604,0.5124 Q0.5661,0.5134,0.5746,0.5153 T0.5912,0.5173 L0.5977,0.5173 L0.6026,0.5173 Q0.6139,0.5173,0.6273,0.5218 T0.6407,0.5272 Q0.6448,0.5282,0.6683,0.5321 T0.6991,0.5381 Q0.7137,0.543,0.7664,0.548 Q0.7908,0.55,0.7908,0.5559 Q0.7908,0.5589,0.7859,0.5628 Q0.781,0.5658,0.7713,0.5658 Q0.7689,0.5658,0.7628,0.5653 T0.7543,0.5648 T0.7502,0.5658 Q0.7364,0.5678,0.6878,0.5678 Q0.674,0.5678,0.6699,0.5668 L0.6683,0.5668 Q0.6659,0.5668,0.6606,0.5678 T0.6513,0.5687 Q0.6464,0.5687,0.6391,0.5678 Q0.6334,0.5668,0.6302,0.5668 T0.6241,0.5687 T0.6164,0.5717 Q0.6123,0.5727,0.6099,0.5727 L0.6062,0.5727 T0.6022,0.5732 T0.5953,0.5737 Q0.5904,0.5747,0.5839,0.5747 T0.5726,0.5752 T0.5629,0.5776 T0.5458,0.5796 Q0.5418,0.5796,0.5324,0.5791 T0.5191,0.5786 Q0.5077,0.5786,0.5045,0.5806 Q0.498,0.5836,0.4899,0.5836 Q0.4858,0.5836,0.4777,0.5826 T0.4655,0.5816 L0.4631,0.5816 Q0.4582,0.5816,0.4436,0.5841 T0.4144,0.5865 L0.4051,0.5865 L0.3986,0.5865 T0.3933,0.587 T0.3897,0.5875 T0.3869,0.5885 T0.3844,0.5895 T0.3812,0.591 T0.3779,0.5925 Q0.3739,0.5955,0.3633,0.5955 L0.3585,0.5955 L0.3536,0.5955 Q0.3277,0.5955,0.3066,0.6053 Q0.3017,0.6083,0.296,0.6083 Q0.2912,0.6083,0.2839,0.6068 T0.2741,0.6053 Q0.2693,0.6053,0.2547,0.6048 T0.236,0.6044 Q0.2222,0.6044,0.193,0.6103 L0.1882,0.6103 Q0.1825,0.6103,0.1703,0.6083 T0.1525,0.6063 L0.15,0.6063 Q0.1436,0.6063,0.135,0.6073 T0.1168,0.6098 T0.1038,0.6113 Q0.0973,0.6123,0.0787,0.6123 L0.06,0.6123 L0.0414,0.6123 L0.0235,0.6123 Q0.0049,0.6133,0.0049,0.6172 L0.0057,0.6222 Q0.0073,0.6261,0.043,0.6261 L0.0454,0.6261 L0.0479,0.6261 Q0.0527,0.6261,0.0633,0.6281 T0.0803,0.6301 L0.0835,0.6301 L0.0916,0.6301 Q0.0998,0.6301,0.129,0.632 Q0.1768,0.636,0.206,0.64 Q0.2117,0.6409,0.2165,0.6409 Q0.2206,0.6409,0.2263,0.6405 T0.2336,0.64 L0.2384,0.64 L0.2409,0.64 T0.251,0.6395 T0.2644,0.639 Q0.2741,0.639,0.279,0.6409 Q0.283,0.6429,0.2972,0.6439 T0.3293,0.6459 T0.3528,0.6469 L0.3577,0.6469 Q0.3642,0.6469,0.3723,0.6449 T0.3844,0.6429 Q0.3901,0.6429,0.3933,0.6454 T0.4006,0.6479 Q0.4088,0.6479,0.4517,0.6558 Q0.4582,0.6568,0.4623,0.6568 Q0.4655,0.6568,0.4716,0.6563 T0.4826,0.6558 L0.4882,0.6558 Q0.5004,0.6558,0.5255,0.6607 Q0.5328,0.6627,0.5604,0.6647 T0.6152,0.6682 T0.644,0.6696 T0.6719,0.6721 T0.7056,0.6756 Q0.7113,0.6756,0.7198,0.6805 T0.738,0.6855 Q0.7453,0.6855,0.7551,0.6879 T0.7741,0.6939 T0.7912,0.7018 T0.8029,0.7102 Q0.8054,0.7132,0.8054,0.7151 Q0.8054,0.7191,0.8009,0.7216 T0.7908,0.7255 T0.779,0.7285 T0.7721,0.731 Q0.7672,0.7329,0.7632,0.7339 T0.7551,0.7354 T0.7486,0.7369 Q0.7478,0.7369,0.7445,0.7379 T0.7388,0.7394 T0.7324,0.7404 L0.7242,0.7413 T0.7161,0.7418 Q0.7105,0.7418,0.6955,0.7428 T0.6748,0.7438 L0.6699,0.7438 L0.6667,0.7438 Q0.6659,0.7438,0.6407,0.7468 Q0.6367,0.7478,0.6334,0.7478 T0.6245,0.7468 T0.6148,0.7458 L0.6099,0.7458 Q0.6018,0.7468,0.5864,0.7498 T0.5653,0.7532 T0.5596,0.7557 Q0.5596,0.7596,0.571,0.7616 Q0.5742,0.7626,0.5803,0.7631 T0.5929,0.7641 T0.6018,0.7656 L0.6087,0.7656 T0.6192,0.7666 T0.6277,0.7695 Q0.631,0.7705,0.6342,0.7705 Q0.6383,0.7705,0.6452,0.7685 T0.6537,0.7666 L0.6553,0.7666 Q0.6602,0.7666,0.6711,0.769 T0.6853,0.7715 Q0.691,0.7715,0.7048,0.775 T0.7242,0.7814 Q0.7324,0.7854,0.7445,0.7864 Q0.7478,0.7864,0.768,0.7873 T0.7972,0.7893 Q0.7989,0.7893,0.8017,0.7898 T0.8054,0.7903 L0.8082,0.7903 T0.811,0.7908 T0.8131,0.7913 T0.8151,0.7918 L0.8167,0.7928 T0.8179,0.7943 T0.8191,0.7957 T0.8204,0.7977 T0.8208,0.8002 Q0.8208,0.8032,0.8171,0.8051 T0.8074,0.8081 T0.7997,0.8091 Q0.7932,0.8121,0.7737,0.8121 Q0.7664,0.8121,0.7413,0.8111 L0.7364,0.8111 Q0.7275,0.8111,0.7214,0.8121 T0.7084,0.8155 T0.6991,0.819 Q0.6934,0.82,0.6723,0.8205 T0.6472,0.8239 Q0.6423,0.8269,0.6375,0.8269 Q0.6358,0.8269,0.6338,0.8264 T0.6294,0.8254 T0.6253,0.8249 Q0.6042,0.8249,0.5961,0.8289 Q0.5921,0.8318,0.5839,0.8318 Q0.5783,0.8318,0.5645,0.8299 L0.5604,0.8299 Q0.5531,0.8299,0.5337,0.8368 Q0.5296,0.8378,0.5255,0.8378 Q0.5239,0.8378,0.5195,0.8373 T0.5118,0.8368 Q0.5061,0.8368,0.4996,0.8388 Q0.4947,0.8388,0.4882,0.8398 Q0.4777,0.8417,0.4753,0.8427 T0.4728,0.8467 Q0.4728,0.8487,0.4769,0.8492 T0.4886,0.8501 T0.5004,0.8516 Q0.5036,0.8516,0.515,0.8521 T0.5345,0.8546 Q0.5426,0.8556,0.5596,0.8556 L0.5649,0.8556 L0.5702,0.8556 L0.5791,0.8556 Q0.5921,0.8566,0.6135,0.8571 T0.6383,0.8586 Q0.6683,0.8615,0.6764,0.8615 L0.6776,0.8615 L0.6788,0.8615 Q0.7032,0.8615,0.7032,0.8684 Q0.7032,0.8714,0.6995,0.8749 T0.6959,0.8803 L0.6959,0.8813 Q0.6991,0.8843,0.7153,0.8843 Q0.7218,0.8843,0.7259,0.8833 L0.7299,0.8833 Q0.7324,0.8833,0.7348,0.8838 T0.7393,0.8848 T0.7433,0.8863 L0.7474,0.8882 T0.751,0.8892 Q0.7616,0.8922,0.7948,0.8952 Q0.7981,0.8952,0.8082,0.8981 T0.8297,0.9011 T0.8528,0.9026 T0.8747,0.907 T0.8856,0.9149 L0.8856,0.9159 Q0.8856,0.9179,0.8848,0.9189 T0.8828,0.9209 T0.88,0.9228 T0.8767,0.9243 T0.8731,0.9248 T0.8698,0.9253 T0.8666,0.9258 L0.8646,0.9258 Q0.8621,0.9258,0.8577,0.9263 T0.85,0.9268 T0.8439,0.9273 T0.8402,0.9278 Q0.8386,0.9298,0.8358,0.9308 T0.8305,0.9322 T0.8244,0.9332 T0.8183,0.9337 Q0.8135,0.9347,0.8025,0.9372 T0.7859,0.9416 Q0.7778,0.9436,0.7717,0.9471 T0.7624,0.9515 Q0.7575,0.9535,0.7486,0.9535 Q0.7461,0.9535,0.7397,0.953 T0.7291,0.9525 Q0.7234,0.9525,0.7186,0.953 T0.7137,0.957 T0.7194,0.9614 Q0.7567,0.9634,0.7664,0.9654 Q0.7745,0.9674,0.7762,0.9674 L0.7908,0.9674 Q0.7924,0.9664,0.7948,0.9664 Q0.8029,0.9664,0.8082,0.9679 T0.8224,0.9723 T0.8362,0.9773 Q0.8483,0.9812,0.871,0.9812 L0.8751,0.9812 Q0.8873,0.9812,0.8929,0.9822 T0.9165,0.9876 T0.9408,0.9931 Q0.9513,0.996,0.9935,0.996 Z M0.9968,0.998 L0.9976,0.003 Q0.9838,0.002,0.9708,0.0059 Q0.9659,0.0069,0.9327,0.0059 T0.8881,0.0099 Q0.8767,0.0138,0.8658,0.0163 T0.8491,0.0218 Q0.8418,0.0247,0.8341,0.0247 T0.8232,0.0257 Q0.8191,0.0277,0.7997,0.0292 T0.7737,0.0326 Q0.7697,0.0336,0.7672,0.0336 T0.7612,0.0321 T0.7543,0.0307 T0.747,0.0317 Q0.7397,0.0326,0.7336,0.0356 T0.717,0.0406 Q0.6918,0.0445,0.6861,0.0475 Q0.6845,0.0485,0.6772,0.0485 T0.6638,0.05 T0.6577,0.0549 T0.6642,0.0598 T0.6796,0.0613 T0.6902,0.0623 Q0.691,0.0623,0.6946,0.0658 T0.7064,0.0702 L0.7161,0.0702 Q0.7194,0.0702,0.7242,0.0697 T0.7332,0.0692 Q0.7413,0.0692,0.7579,0.0717 T0.781,0.0762 Q0.7867,0.0781,0.8171,0.0786 T0.85,0.0801 L0.8532,0.0801 Q0.8548,0.0801,0.8613,0.0796 T0.8727,0.0791 Q0.8832,0.0791,0.8913,0.0811 Q0.897,0.0831,0.897,0.0851 Q0.897,0.088,0.8873,0.0905 T0.8735,0.093 Q0.871,0.093,0.8447,0.0984 T0.8143,0.1039 Q0.7981,0.1039,0.781,0.1078 Q0.7778,0.1088,0.7745,0.1088 T0.7644,0.1073 T0.7518,0.1048 L0.7453,0.1048 Q0.7299,0.1048,0.7076,0.1083 T0.6788,0.1157 Q0.6748,0.1187,0.6699,0.1187 Q0.6675,0.1187,0.6557,0.1157 T0.6318,0.1118 Q0.6164,0.1108,0.6135,0.1108 T0.6095,0.1113 T0.6067,0.1142 T0.6018,0.1187 Q0.5953,0.1227,0.5921,0.1227 Q0.5904,0.1227,0.5839,0.1212 T0.5693,0.1187 L0.5661,0.1187 Q0.558,0.1187,0.5527,0.1207 T0.5434,0.1246 T0.5337,0.1266 Q0.5126,0.1296,0.4882,0.1296 T0.4371,0.1385 Q0.4298,0.1414,0.425,0.1414 Q0.4225,0.1414,0.4189,0.1395 T0.4112,0.1375 Q0.4079,0.1375,0.4019,0.1395 T0.3869,0.1414 Q0.3682,0.1414,0.3504,0.1474 Q0.3398,0.1513,0.3333,0.1513 Q0.3293,0.1513,0.3228,0.1494 L0.3204,0.1494 Q0.3179,0.1494,0.3155,0.1508 T0.3118,0.1543 T0.3106,0.1592 T0.3127,0.1642 T0.32,0.1677 T0.3305,0.1701 T0.3435,0.1716 T0.3544,0.1721 Q0.3569,0.1731,0.3601,0.1731 Q0.3625,0.1731,0.3686,0.1726 T0.3779,0.1721 Q0.3901,0.1721,0.4023,0.1751 Q0.4177,0.18,0.455,0.18 Q0.4793,0.18,0.4882,0.178 L0.4915,0.178 Q0.4964,0.178,0.5045,0.1815 T0.5158,0.185 Q0.5564,0.1889,0.5645,0.1929 Q0.5677,0.1939,0.571,0.1939 Q0.575,0.1939,0.5807,0.1919 T0.5888,0.1899 T0.5929,0.1909 Q0.5969,0.1949,0.6123,0.1949 Q0.6334,0.1949,0.6456,0.1899 Q0.6488,0.1879,0.6537,0.1879 Q0.661,0.1879,0.6772,0.1929 T0.6983,0.1978 Q0.704,0.1988,0.7206,0.1998 T0.7502,0.2018 T0.7664,0.2038 Q0.7672,0.2038,0.7818,0.2043 T0.8151,0.2067 T0.8459,0.2156 Q0.8573,0.2206,0.8573,0.2255 Q0.8573,0.2295,0.8516,0.2324 T0.8394,0.2369 T0.8281,0.2384 Q0.8208,0.2384,0.7526,0.2473 Q0.7494,0.2483,0.7421,0.2493 T0.7287,0.2512 T0.7178,0.2522 L0.7153,0.2522 L0.7137,0.2522 Q0.7056,0.2522,0.6898,0.2572 T0.6691,0.2671 Q0.6667,0.269,0.6634,0.269 Q0.661,0.269,0.6557,0.2681 T0.6472,0.2671 Q0.6448,0.2671,0.6415,0.2681 Q0.631,0.271,0.6034,0.274 Q0.5994,0.274,0.5961,0.2765 T0.5904,0.2789 L0.5864,0.2789 Q0.5831,0.2779,0.5742,0.2779 Q0.541,0.2779,0.5223,0.2849 Q0.5126,0.2888,0.5028,0.2888 Q0.5004,0.2888,0.4943,0.2883 T0.4842,0.2878 Q0.4704,0.2878,0.4627,0.2898 T0.4509,0.2987 Q0.4501,0.2997,0.4501,0.3007 Q0.4501,0.3046,0.4574,0.3066 T0.4765,0.3091 T0.4899,0.3096 Q0.4931,0.3106,0.5061,0.3121 T0.5316,0.315 T0.5499,0.3175 Q0.5507,0.3175,0.5572,0.3195 T0.5685,0.3225 T0.5803,0.3254 T0.5933,0.3279 T0.605,0.3284 T0.6148,0.3274 Q0.6221,0.3244,0.6294,0.3244 Q0.6399,0.3244,0.6496,0.3294 Q0.6529,0.3314,0.6577,0.3314 Q0.6602,0.3314,0.6663,0.3304 T0.6756,0.3294 T0.6813,0.3304 Q0.6837,0.3304,0.6873,0.3309 L0.6946,0.3318 T0.7024,0.3323 T0.7105,0.3328 T0.7178,0.3338 T0.7238,0.3348 T0.7283,0.3363 Q0.7324,0.3383,0.7482,0.3403 T0.7689,0.3442 Q0.7697,0.3442,0.779,0.3477 T0.794,0.3536 T0.8074,0.3615 T0.8183,0.3729 Q0.8232,0.3808,0.8232,0.3848 Q0.8232,0.3867,0.8216,0.3892 T0.8151,0.3961 T0.8062,0.406 T0.7997,0.4179 T0.7952,0.4263 T0.7855,0.4308 T0.7616,0.4362 Q0.7543,0.4372,0.7445,0.4372 L0.7372,0.4372 L0.7307,0.4372 Q0.7202,0.4372,0.7153,0.4382 Q0.7064,0.4402,0.691,0.4402 L0.6845,0.4402 L0.6788,0.4402 Q0.6699,0.4402,0.6533,0.4436 T0.6318,0.451 Q0.6302,0.451,0.6302,0.452 Q0.6302,0.457,0.6626,0.459 Q0.6869,0.4609,0.7015,0.4659 Q0.7064,0.4669,0.7202,0.4669 L0.7311,0.4669 L0.7429,0.4669 Q0.7599,0.4669,0.7689,0.4688 Q0.7794,0.4708,0.7794,0.4738 L0.7786,0.4748 T0.777,0.4763 T0.7749,0.4773 L0.7737,0.4777 L0.7729,0.4777 Q0.7502,0.4797,0.738,0.4797 L0.7315,0.4797 L0.7291,0.4797 Q0.7226,0.4797,0.6869,0.4876 Q0.6821,0.4886,0.6768,0.4891 T0.6614,0.4901 T0.6456,0.4916 L0.6342,0.4916 Q0.6294,0.4916,0.6217,0.4911 T0.6115,0.4906 Q0.6083,0.4906,0.6054,0.4911 T0.5961,0.4936 T0.5815,0.4965 T0.5629,0.4985 Q0.5507,0.4985,0.5442,0.4995 T0.5345,0.5025 T0.5304,0.5084 L0.5296,0.5094 Q0.5296,0.5114,0.5316,0.5124 T0.5373,0.5138 T0.5442,0.5143 L0.5523,0.5143 L0.5604,0.5143 Q0.5637,0.5153,0.5677,0.5158 T0.5742,0.5173 T0.5811,0.5188 T0.5904,0.5193 L0.5977,0.5193 L0.6026,0.5193 Q0.6107,0.5193,0.6168,0.5208 T0.6294,0.5252 T0.6399,0.5292 Q0.6448,0.5302,0.6679,0.5341 T0.6983,0.541 Q0.7129,0.545,0.7664,0.55 Q0.7883,0.5519,0.7883,0.5559 Q0.7883,0.5579,0.7843,0.5608 T0.7721,0.5638 Q0.7697,0.5638,0.764,0.5633 T0.7559,0.5628 L0.7502,0.5628 Q0.7364,0.5648,0.6869,0.5648 L0.6699,0.5648 L0.6675,0.5648 Q0.665,0.5648,0.6602,0.5658 T0.6513,0.5668 Q0.6464,0.5668,0.6399,0.5658 Q0.6342,0.5648,0.6302,0.5648 T0.6225,0.5668 T0.6156,0.5697 Q0.6107,0.5707,0.6087,0.5707 T0.6034,0.5712 T0.5953,0.5717 Q0.5904,0.5727,0.5835,0.5727 T0.5718,0.5732 T0.562,0.5757 Q0.558,0.5767,0.5483,0.5767 L0.5349,0.5767 L0.5207,0.5767 Q0.5069,0.5767,0.5028,0.5786 Q0.4972,0.5816,0.4899,0.5816 Q0.4858,0.5816,0.4781,0.5806 T0.4663,0.5796 L0.4631,0.5796 Q0.4582,0.5796,0.4432,0.5821 T0.4144,0.5846 L0.4071,0.5846 L0.4011,0.5846 T0.3958,0.5851 T0.3921,0.5856 T0.3889,0.5861 T0.3865,0.5865 T0.384,0.5875 L0.382,0.5885 L0.3796,0.5895 L0.3771,0.5905 Q0.3731,0.5925,0.365,0.5925 L0.3601,0.5925 L0.3552,0.5925 Q0.3277,0.5925,0.3049,0.6044 Q0.3017,0.6053,0.296,0.6053 Q0.292,0.6053,0.2847,0.6044 T0.2741,0.6034 Q0.2693,0.6034,0.2547,0.6029 T0.236,0.6024 Q0.2222,0.6024,0.1922,0.6073 Q0.1906,0.6083,0.1882,0.6083 Q0.1833,0.6083,0.1711,0.6063 T0.1533,0.6044 Q0.1517,0.6034,0.1492,0.6034 Q0.1436,0.6034,0.135,0.6048 T0.1168,0.6078 T0.103,0.6093 Q0.0973,0.6103,0.0787,0.6103 L0.0604,0.6103 L0.0414,0.6103 L0.0235,0.6103 Q0.0024,0.6113,0.0024,0.6172 Q0.0024,0.6192,0.0032,0.6231 Q0.0049,0.6281,0.0333,0.6281 L0.0414,0.6281 L0.0479,0.6281 Q0.0519,0.6281,0.0624,0.6301 T0.0803,0.632 L0.0835,0.632 L0.0916,0.632 Q0.0989,0.632,0.1281,0.634 L0.206,0.6419 Q0.2117,0.6429,0.2174,0.6429 Q0.2214,0.6429,0.2271,0.6424 T0.2344,0.6419 L0.2376,0.6419 Q0.2393,0.6429,0.2417,0.6429 T0.2526,0.6419 T0.2652,0.6409 Q0.2741,0.6409,0.2782,0.6429 T0.2968,0.6459 T0.3293,0.6479 T0.352,0.6489 Q0.3552,0.6499,0.3585,0.6499 Q0.365,0.6499,0.3731,0.6474 T0.3844,0.6449 Q0.3885,0.6449,0.3917,0.6474 T0.4006,0.6499 Q0.4088,0.6499,0.4509,0.6578 Q0.4574,0.6588,0.4631,0.6588 Q0.4655,0.6588,0.4716,0.6583 T0.4834,0.6578 L0.4874,0.6578 Q0.5004,0.6588,0.5255,0.6637 Q0.532,0.6647,0.56,0.6667 T0.6148,0.6701 T0.644,0.6716 Q0.6464,0.6726,0.6594,0.6736 T0.6861,0.6756 T0.7048,0.6775 Q0.7097,0.6775,0.7186,0.6825 T0.738,0.6874 Q0.7518,0.6874,0.7729,0.6958 T0.8005,0.7122 Q0.8029,0.7141,0.8029,0.7151 Q0.8029,0.7191,0.7968,0.7216 T0.7818,0.726 T0.7705,0.729 L0.764,0.731 T0.7583,0.7324 T0.753,0.7334 T0.7486,0.7349 Q0.7251,0.7399,0.7161,0.7399 Q0.7097,0.7399,0.6951,0.7409 T0.6748,0.7418 L0.6703,0.7418 L0.6667,0.7418 Q0.6659,0.7418,0.6399,0.7448 L0.6334,0.7448 Q0.631,0.7448,0.6249,0.7443 T0.6148,0.7438 L0.6099,0.7438 Q0.601,0.7448,0.5856,0.7473 T0.5645,0.7507 Q0.5572,0.7517,0.5572,0.7547 T0.5616,0.7606 T0.5702,0.7646 Q0.5734,0.7646,0.5779,0.7651 T0.5864,0.7661 T0.5949,0.7671 T0.6018,0.7676 L0.6071,0.7676 T0.6135,0.7681 T0.62,0.769 T0.6269,0.771 T0.6342,0.7725 Q0.6391,0.7725,0.6464,0.7705 T0.6541,0.7685 L0.6553,0.7685 Q0.6586,0.7685,0.6699,0.771 T0.6853,0.7735 Q0.6902,0.7735,0.704,0.777 T0.7226,0.7834 Q0.7315,0.7873,0.7445,0.7883 Q0.747,0.7883,0.7676,0.7893 T0.7972,0.7913 Q0.7989,0.7913,0.8017,0.7918 T0.8058,0.7923 T0.809,0.7928 T0.8114,0.7933 T0.8131,0.7938 T0.8147,0.7943 T0.8159,0.7953 L0.8171,0.7967 T0.8175,0.7982 Q0.8183,0.7992,0.8183,0.8002 Q0.8183,0.8022,0.8155,0.8032 T0.807,0.8051 T0.7989,0.8071 Q0.7932,0.8091,0.7737,0.8091 L0.7413,0.8091 L0.7372,0.8091 Q0.7275,0.8091,0.721,0.8101 T0.7072,0.8136 T0.6983,0.817 Q0.6942,0.818,0.6825,0.818 T0.6602,0.819 T0.6456,0.822 Q0.6415,0.8249,0.6375,0.8249 Q0.6367,0.8249,0.6326,0.8239 T0.6253,0.8229 Q0.6034,0.8229,0.5953,0.8279 Q0.5912,0.8289,0.5848,0.8289 Q0.5775,0.8289,0.5645,0.8279 Q0.5629,0.8269,0.5604,0.8269 Q0.5523,0.8269,0.5328,0.8348 Q0.5296,0.8358,0.5264,0.8358 Q0.5247,0.8358,0.5203,0.8353 T0.5126,0.8348 Q0.5061,0.8348,0.4988,0.8358 Q0.4947,0.8368,0.4882,0.8378 Q0.4826,0.8388,0.4797,0.8388 T0.474,0.8398 T0.4704,0.8422 T0.4696,0.8462 T0.472,0.8501 T0.4781,0.8521 T0.4854,0.8526 L0.4935,0.8526 T0.4996,0.8536 Q0.5036,0.8536,0.515,0.8546 T0.5337,0.8566 Q0.5426,0.8576,0.5596,0.8576 L0.5657,0.8576 L0.571,0.8576 L0.5791,0.8576 Q0.5921,0.8586,0.6135,0.8591 T0.6383,0.8605 Q0.6683,0.8635,0.6764,0.8635 Q0.6999,0.8635,0.6999,0.8684 Q0.6999,0.8694,0.6967,0.8734 T0.6934,0.8803 L0.6934,0.8823 Q0.6951,0.8863,0.7137,0.8863 Q0.7218,0.8863,0.7267,0.8853 L0.7299,0.8853 Q0.734,0.8853,0.7409,0.8882 T0.7502,0.8912 Q0.7607,0.8942,0.794,0.8971 Q0.7972,0.8971,0.8078,0.9001 T0.8297,0.9031 Q0.8475,0.9031,0.8646,0.9065 T0.8824,0.9149 L0.8824,0.9159 Q0.8824,0.9169,0.882,0.9179 T0.8808,0.9194 L0.8792,0.9204 T0.8767,0.9214 T0.8743,0.9224 T0.8715,0.9228 L0.8686,0.9228 T0.8662,0.9233 T0.8637,0.9238 L0.8556,0.9238 T0.8447,0.9243 T0.8386,0.9268 Q0.837,0.9278,0.835,0.9288 T0.8305,0.9303 T0.8244,0.9308 T0.8175,0.9318 Q0.8127,0.9327,0.8017,0.9352 T0.7851,0.9397 Q0.777,0.9416,0.7705,0.9451 T0.7616,0.9496 Q0.7575,0.9505,0.7494,0.9505 L0.7405,0.9505 L0.7307,0.9505 Q0.7234,0.9505,0.7178,0.9515 Q0.7145,0.9515,0.7125,0.9535 T0.7105,0.9575 Q0.7105,0.9624,0.7194,0.9634 Q0.7567,0.9654,0.7664,0.9674 Q0.7745,0.9693,0.7762,0.9693 L0.7908,0.9693 L0.7948,0.9693 Q0.8005,0.9693,0.8058,0.9698 T0.8143,0.9718 T0.824,0.9753 T0.8354,0.9792 Q0.8483,0.9832,0.871,0.9832 L0.8739,0.9832 L0.8767,0.9832 Q0.8873,0.9832,0.8921,0.9842 Q0.8978,0.9852,0.9157,0.9896 T0.94,0.996 Q0.9424,0.996,0.9469,0.9965 T0.9566,0.997 T0.9676,0.9975 T0.9781,0.998 L0.9874,0.998 L0.9943,0.998 L0.9968,0.998 Z",type:0}},"\uE0CC":{type:5,data:{d:"M1,0.5071 L0.8623,0.7102 L0.5865,0.7102 L0.4489,0.5071 L0.5865,0.3045 L0.8623,0.3045 Z M0.4135,0.7974 L0.2758,1 L0,1 L0,0.5944 L0.2758,0.5944 Z M0.4135,0.2026 L0.2758,0.4056 L0,0.4056 L0,0 L0.2758,0 Z",type:0}},"\uE0CD":{type:5,data:{d:"M0.9726,0.5071 L0.8451,0.3184 L0.59,0.3184 L0.4624,0.5071 L0.59,0.6955 L0.8451,0.6955 Z M1,0.5071 L0.8588,0.716 L0.5768,0.716 L0.4355,0.5071 L0.5768,0.2982 L0.8588,0.2982 Z M0.401,0.7911 L0.2735,0.6028 L0.0236,0.6028 L0.0236,0.9799 L0.2735,0.9799 Z M0.4284,0.7911 L0.2872,1 L0,1 L0,0.5826 L0.2872,0.5826 Z M0.401,0.2089 L0.2735,0.0201 L0.0236,0.0201 L0.0236,0.3972 L0.2735,0.3972 Z M0.4284,0.2089 L0.2872,0.4174 L0,0.4174 L0,0 L0.2872,0 Z",type:0}},"\uE0CE":{type:5,data:{d:"M1,0.4996 Q1,0.5365,0.9924,0.568 T0.9718,0.6177 T0.9433,0.6363 L0.9425,0.6367 Q0.9355,0.6367,0.9285,0.6326 L0.9308,0.6326 Q0.9129,0.6195,0.9013,0.587 T0.8871,0.5164 T0.8943,0.4373 T0.9254,0.3687 L0.9254,0.3691 Q0.9339,0.3628,0.9425,0.3628 Q0.9584,0.3628,0.9716,0.3811 T0.9924,0.431 T1,0.4996 Z M0.9234,0.6321 L0.8776,0.7169 Q0.8791,0.732,0.8791,0.7441 Q0.8791,0.781,0.8716,0.8123 T0.8508,0.862 T0.8224,0.8809 L0.8216,0.8809 Q0.8154,0.8809,0.8092,0.8775 L0.8088,0.8775 Q0.7905,0.8633,0.7792,0.8297 T0.766,0.7571 T0.775,0.6768 T0.8088,0.6086 L0.7431,0.6065 Q0.719,0.6313,0.7072,0.6705 T0.6965,0.7487 T0.7097,0.823 T0.7423,0.8763 L0.7913,0.8775 L0.7314,0.9895 L0.4648,0.5038 L0.7299,0.0176 L0.7882,0.1229 L0.7404,0.1212 Q0.7244,0.138,0.7136,0.1617 T0.6982,0.2108 T0.6943,0.2632 T0.7003,0.3144 T0.7159,0.3586 T0.7396,0.3909 L0.8072,0.3926 Q0.7894,0.3796,0.7777,0.3479 T0.7635,0.2789 T0.7697,0.2011 T0.7987,0.1326 Q0.8092,0.1225,0.8204,0.1225 Q0.8321,0.1225,0.8426,0.1332 T0.8609,0.1623 T0.8731,0.2062 T0.8776,0.2592 Q0.8776,0.268,0.8768,0.2815 L0.9219,0.3628 L0.8644,0.3612 Q0.8403,0.3855,0.8284,0.4247 T0.8177,0.5029 T0.8309,0.5772 T0.8632,0.6305 Z M0.6759,0.6384 Q0.6576,0.625,0.6457,0.5914 T0.6319,0.5185 T0.6407,0.4375 T0.6751,0.3691 L0.609,0.367 Q0.5849,0.3918,0.5731,0.431 T0.5626,0.509 T0.5758,0.5833 T0.6082,0.6367 Z M0.6459,0.0025 L0.391,0.4551 L0.0035,0.4551 L0.246,0 Z M0.6553,1 L0.2421,1 L0,0.5449 L0.3875,0.5449 Z",type:0}},"\uE0CF":{type:5,data:{d:"M0.4996,0 Q0.5562,0,0.5961,0.0166 T0.6364,0.0566 L0.6368,0.0575 Q0.6368,0.0646,0.6324,0.0713 L0.6324,0.0692 Q0.6196,0.0868,0.5872,0.0986 T0.5166,0.1128 T0.4373,0.1057 T0.3689,0.0747 L0.3694,0.0747 Q0.3627,0.0663,0.3627,0.0575 Q0.3627,0.0336,0.4028,0.0168 T0.4996,0 Z M0.6324,0.0768 L0.717,0.1225 Q0.7321,0.1212,0.744,0.1212 Q0.8007,0.1212,0.8406,0.1378 T0.8809,0.1779 L0.8813,0.1783 Q0.8813,0.1846,0.8778,0.1909 Q0.8636,0.2093,0.8299,0.2208 T0.7573,0.2343 T0.6769,0.2253 T0.6089,0.1913 L0.6067,0.2567 Q0.6315,0.281,0.6707,0.2928 T0.7489,0.3035 T0.8231,0.2903 T0.8764,0.258 L0.8778,0.2085 L0.9898,0.2685 L0.504,0.5352 L0.0177,0.2701 L0.1227,0.2118 L0.1213,0.2596 Q0.1457,0.2836,0.1851,0.2955 T0.2635,0.3062 T0.3377,0.293 T0.3911,0.2605 L0.3928,0.193 Q0.38,0.2106,0.3483,0.2221 T0.2792,0.2364 T0.2013,0.2303 T0.1324,0.2013 Q0.1222,0.1909,0.1222,0.1795 Q0.1222,0.156,0.1623,0.1393 T0.2591,0.1225 Q0.2679,0.1225,0.2817,0.1233 L0.3627,0.078 L0.3609,0.1359 Q0.3857,0.1598,0.4249,0.1718 T0.5031,0.1825 T0.5773,0.1693 T0.6306,0.1367 Z M0.6386,0.3242 Q0.6253,0.3427,0.5917,0.3544 T0.5186,0.3683 T0.4376,0.3595 T0.3689,0.3251 L0.3671,0.3909 Q0.3915,0.4148,0.4309,0.4268 T0.5093,0.4375 T0.5835,0.4243 T0.6368,0.3918 Z M0.0027,0.354 L0.4553,0.6091 L0.4553,0.9962 L0,0.7542 Z M1,0.3448 L1,0.7576 L0.5447,1 L0.5447,0.6128 Z",type:0}},"\uE0D0":{type:5,data:{d:"M0,0.7699 Q0,0.7172,0.0277,0.6731 T0.103,0.6032 T0.2066,0.5774 Q0.2487,0.5774,0.2865,0.5922 T0.3526,0.6339 L0.3935,0.6715 Q0.4218,0.6984,0.438,0.7336 T0.4541,0.8075 Q0.4541,0.8602,0.4264,0.9046 T0.3511,0.9745 T0.2475,1 Q0.2054,1,0.1676,0.9852 T0.1016,0.9435 L0.0606,0.9059 Q0.0317,0.879,0.0159,0.8438 T0,0.7699 Z M0.5459,0.7699 Q0.5459,0.7172,0.5736,0.6731 T0.6489,0.6032 T0.7525,0.5774 Q0.7946,0.5774,0.8324,0.5922 T0.8984,0.6339 L0.9394,0.6715 Q0.9579,0.6892,0.9714,0.7105 T0.9925,0.7567 T1,0.8075 Q1,0.8602,0.9723,0.9046 T0.897,0.9745 T0.7934,1 Q0.7513,1,0.7135,0.9852 T0.6474,0.9435 L0.6065,0.9059 Q0.5776,0.879,0.5617,0.8438 T0.5459,0.7699 Z M0,0.1925 Q0,0.1398,0.0277,0.0957 T0.103,0.0258 T0.2066,0 Q0.2487,0,0.2865,0.0148 T0.3526,0.0565 L0.3935,0.0941 Q0.4218,0.121,0.438,0.1562 T0.4541,0.2301 Q0.4541,0.2828,0.4264,0.3269 T0.3511,0.3968 T0.2475,0.4226 Q0.2054,0.4226,0.1676,0.4078 T0.1016,0.3661 L0.0606,0.3285 Q0.0317,0.3016,0.0159,0.2664 T0,0.1925 Z M0.5459,0.1925 Q0.5459,0.1398,0.5736,0.0957 T0.6489,0.0258 T0.7525,0 Q0.7946,0,0.8324,0.0148 T0.8984,0.0565 L0.9394,0.0941 Q0.9677,0.121,0.9838,0.1562 T1,0.2301 Q1,0.2828,0.9723,0.3269 T0.897,0.3968 T0.7934,0.4226 Q0.7513,0.4226,0.7135,0.4078 T0.6474,0.3661 L0.6065,0.3285 Q0.5874,0.3108,0.5741,0.2895 T0.5534,0.2433 T0.5459,0.1925 Z M0.3301,0.6403 Q0.3306,0.6403,0.3312,0.6409 L0.3272,0.6371 L0.3272,0.6371 Z M0.8759,0.6403 Q0.8765,0.6403,0.8771,0.6409 Q0.8748,0.6387,0.8725,0.6371 L0.8731,0.6371 Z M0.3301,0.0629 Q0.3306,0.0629,0.3312,0.0634 L0.3272,0.0597 L0.3272,0.0602 Z M0.8759,0.0629 Q0.8765,0.0629,0.8771,0.0634 Q0.8748,0.0613,0.8725,0.0597 Q0.8731,0.0597,0.8731,0.0602 Z M0.779,0.4113 L0.7501,0.3849 Q0.7046,0.3844,0.6636,0.3661 Q0.7132,0.407,0.779,0.4113 Z M0.7957,0.4118 Q0.8477,0.4113,0.8918,0.3874 T0.9619,0.3223 T0.9885,0.2323 L0.9585,0.2048 Q0.9538,0.2769,0.8987,0.3282 T0.7657,0.3844 Z M0.9879,0.2167 Q0.9827,0.1554,0.9388,0.1097 Q0.9585,0.1478,0.959,0.1898 Z M0.2331,0.4113 L0.2043,0.3849 Q0.1587,0.3844,0.1177,0.3661 Q0.1673,0.407,0.2331,0.4113 Z M0.2499,0.4118 Q0.2885,0.4118,0.324,0.3973 T0.3852,0.3589 T0.4264,0.3019 T0.4426,0.2323 L0.4126,0.2048 Q0.408,0.2769,0.3529,0.3282 T0.2198,0.3844 Z M0.442,0.2167 Q0.4368,0.1554,0.393,0.1097 Q0.4126,0.1478,0.4132,0.1903 Z M0.779,0.9887 L0.7501,0.9624 Q0.7046,0.9618,0.6636,0.9435 Q0.7132,0.9844,0.779,0.9887 Z M0.7957,0.9892 Q0.8477,0.9887,0.8918,0.9648 T0.9619,0.8997 T0.9885,0.8097 L0.9585,0.7823 Q0.9538,0.8543,0.8987,0.9056 T0.7657,0.9618 Z M0.9879,0.7941 Q0.9827,0.7328,0.9388,0.6871 Q0.9585,0.7253,0.959,0.7672 Z M0.2331,0.9887 L0.2043,0.9624 Q0.1587,0.9618,0.1177,0.9435 Q0.1673,0.9844,0.2331,0.9887 Z M0.2499,0.9892 Q0.3018,0.9887,0.3459,0.9648 T0.416,0.8997 T0.4426,0.8102 L0.4126,0.7823 Q0.408,0.8543,0.3529,0.9056 T0.2198,0.9618 Z M0.442,0.7941 Q0.4368,0.7328,0.393,0.6871 Q0.4126,0.7253,0.4132,0.7672 Z M0.3191,0.6452 Q0.2943,0.622,0.2643,0.6091 T0.2066,0.5962 Q0.1795,0.5962,0.1497,0.6091 T0.0949,0.6441 T0.0537,0.6995 T0.0375,0.7699 Q0.0375,0.8419,0.0941,0.8941 Q0.1189,0.9172,0.1489,0.9304 T0.2066,0.9435 Q0.2337,0.9435,0.2634,0.9304 T0.3182,0.8952 T0.3595,0.8398 T0.3756,0.7699 Q0.3756,0.6978,0.3191,0.6452 Z M0.026,0.7731 L0.026,0.7699 Q0.026,0.6935,0.0854,0.6376 Q0.0658,0.6538,0.0514,0.6739 T0.0289,0.718 T0.0202,0.7677 Z M0.0271,0.7892 L0.0208,0.7833 Q0.0254,0.8419,0.0681,0.886 Q0.0664,0.8828,0.0646,0.879 Q0.0329,0.8382,0.0271,0.7892 Z M0.3866,0.7823 Q0.382,0.8511,0.3278,0.9016 Q0.3843,0.8565,0.3918,0.7871 Z M0.3872,0.7677 L0.393,0.7726 L0.393,0.7699 Q0.393,0.7349,0.3791,0.7038 Q0.3739,0.6973,0.3681,0.6914 Q0.3866,0.7274,0.3872,0.7677 Z M0.865,0.6452 Q0.8402,0.622,0.8102,0.6091 T0.7525,0.5962 Q0.7305,0.5962,0.7072,0.6043 T0.6619,0.628 T0.6223,0.6642 T0.5941,0.7124 T0.5834,0.7699 Q0.5834,0.8419,0.6399,0.8941 Q0.6561,0.9097,0.6757,0.9207 T0.7149,0.9376 T0.7525,0.9435 Q0.7796,0.9435,0.8093,0.9304 T0.8641,0.8952 T0.9054,0.8398 T0.9215,0.7699 Q0.9215,0.6978,0.865,0.6452 Z M0.5718,0.7731 L0.5718,0.7699 Q0.5718,0.6935,0.6313,0.6376 Q0.6013,0.6618,0.584,0.6954 T0.5661,0.7677 Z M0.573,0.7892 L0.5666,0.7833 Q0.5713,0.8419,0.614,0.886 Q0.6122,0.8828,0.6105,0.879 Q0.5788,0.8382,0.573,0.7892 Z M0.9325,0.7823 Q0.9279,0.8511,0.8736,0.9016 Q0.9308,0.8565,0.9377,0.7871 Z M0.9331,0.7672 L0.9388,0.7726 L0.9388,0.7699 Q0.9388,0.7349,0.925,0.7038 Q0.9198,0.6973,0.914,0.6914 Q0.9325,0.7274,0.9331,0.7672 Z M0.3191,0.0677 Q0.2943,0.0446,0.2643,0.0317 T0.2066,0.0188 Q0.1795,0.0188,0.1497,0.0317 T0.0949,0.0667 T0.0537,0.122 T0.0375,0.1925 Q0.0375,0.2645,0.0941,0.3167 Q0.1189,0.3398,0.1489,0.353 T0.2066,0.3661 Q0.2337,0.3661,0.2634,0.353 T0.3182,0.3177 T0.3595,0.2624 T0.3756,0.1925 Q0.3756,0.1204,0.3191,0.0677 Z M0.026,0.1957 L0.026,0.1925 Q0.026,0.1161,0.0854,0.0602 Q0.0554,0.0844,0.0381,0.118 T0.0202,0.1903 Z M0.0271,0.2118 L0.0208,0.2059 Q0.0254,0.2645,0.0681,0.3086 Q0.0664,0.3054,0.0646,0.3016 Q0.0329,0.2608,0.0271,0.2118 Z M0.3866,0.2048 Q0.382,0.2737,0.3278,0.3242 Q0.3849,0.279,0.3918,0.2097 Z M0.3872,0.1903 L0.393,0.1952 L0.393,0.1925 Q0.393,0.1581,0.3791,0.1263 Q0.3739,0.1199,0.3681,0.114 Q0.3866,0.15,0.3872,0.1903 Z M0.865,0.0677 Q0.8402,0.0446,0.8102,0.0317 T0.7525,0.0188 Q0.7253,0.0188,0.6956,0.0317 T0.6408,0.0667 T0.5995,0.122 T0.5834,0.1925 Q0.5834,0.2645,0.6399,0.3167 Q0.6647,0.3398,0.6947,0.353 T0.7525,0.3661 Q0.7796,0.3661,0.8093,0.353 T0.8641,0.3177 T0.9054,0.2624 T0.9215,0.1925 Q0.9215,0.1204,0.865,0.0677 Z M0.5718,0.1957 L0.5718,0.1925 Q0.5718,0.1161,0.6313,0.0602 Q0.6013,0.0844,0.584,0.118 T0.5661,0.1903 Z M0.573,0.2118 L0.5666,0.2059 Q0.5713,0.2645,0.614,0.3086 Q0.6122,0.3054,0.6105,0.3016 Q0.5788,0.2608,0.573,0.2118 Z M0.9325,0.2048 Q0.9279,0.2737,0.8736,0.3242 Q0.9308,0.279,0.9377,0.2097 Z M0.9331,0.1898 L0.9388,0.1952 L0.9388,0.1925 Q0.9388,0.1575,0.925,0.1263 Q0.9198,0.1199,0.914,0.114 Q0.9325,0.15,0.9331,0.1898 Z",type:0}},"\uE0D1":{type:5,data:{d:"M0.7639,0.1974 L0.9593,0.1974 L0.9593,0.1443 L0.7639,0.1443 L0.7639,0.1974 Z M0.7586,0.109 L0.9727,0.109 Q0.9839,0.109,0.992,0.1153 T1,0.1291 L1,0.382 Q1,0.3906,0.9912,0.3964 T0.9727,0.4021 L0.7586,0.4021 Q0.7468,0.4021,0.739,0.3956 T0.7313,0.382 L0.7313,0.1291 Q0.7313,0.1246,0.7339,0.1207 T0.7406,0.1143 T0.7495,0.1104 T0.7586,0.109 Z M0.7639,0.6575 L0.9593,0.6575 L0.9593,0.6044 L0.7639,0.6044 L0.7639,0.6575 Z M0.7313,0.8347 L0.7313,0.5843 Q0.7313,0.5757,0.7401,0.5699 T0.7586,0.5641 L0.9706,0.5641 Q0.9818,0.5641,0.9898,0.5705 T0.9979,0.5843 L0.9979,0.8347 Q0.9979,0.8433,0.989,0.8491 T0.9706,0.8549 L0.7586,0.8549 Q0.7468,0.8549,0.739,0.8483 T0.7313,0.8347 Z M0,1 L0,0 L0.7607,0 L0.7607,1 L0,1 Z",type:0}},"\uE0D2":{type:5,data:{d:"M0.9764,0.0029 L0.3179,0.4552 L0,0.4552 L0,0 Z M1,1 L0,1 L0,0.5448 L0.3091,0.5448 Z",type:0}},"\uE0D4":{type:5,data:{d:"M0.0236,0.0029 L0.6824,0.4552 L1,0.4552 L1,0 Z M0,1 L1,1 L1,0.5448 L0.6912,0.5448 Z",type:0}},"\uEE00":{type:3,data:"M0,0 L1,0 L1,.05 L.1,.05 L.1,.95 L1,.95 L1,1 L0,1 Z"},"\uEE01":{type:3,data:"M0,0 L1,0 L1,.05 L0,.05 Z M0,.95 L1,.95 L1,1 L0,1 Z"},"\uEE02":{type:3,data:"M1,0 L0,0 L0,.05 L.9,.05 L.9,.95 L0,.95 L0,1 L1,1 Z"},"\uEE03":{type:3,data:"M0,0 L1,0 L1,.05 L.1,.05 L.1,.95 L1,.95 L1,1 L0,1 Z M.25,.15 L1,.15 L1,.85 L.25,.85 Z"},"\uEE04":{type:3,data:"M0,0 L1,0 L1,.05 L0,.05 Z M0,.95 L1,.95 L1,1 L0,1 Z M0,.15 L1,.15 L1,.85 L0,.85 Z"},"\uEE05":{type:3,data:"M1,0 L0,0 L0,.05 L.9,.05 L.9,.95 L0,.95 L0,1 L1,1 Z M0,.15 L.75,.15 L.75,.85 L0,.85 Z"},"\uEE06":{type:5,data:{d:"M0.6574,0.303 Q0.623,0.291,0.5852,0.285 T0.5082,0.279 T0.4311,0.285 T0.359,0.303 L0.3082,0.248 Q0.3525,0.232,0.4041,0.2235 T0.5082,0.215 T0.6123,0.2235 T0.7082,0.248 Z",type:0},scaleType:1},"\uEE07":{type:5,data:{d:"M0.8557,0.582 L0.7656,0.551 Q0.7852,0.53,0.7951,0.507 T0.8049,0.46 Q0.8049,0.424,0.782,0.3905 T0.718,0.332 T0.6221,0.293 T0.5082,0.279 L0.5082,0.215 Q0.5607,0.215,0.6123,0.2235 T0.709,0.248 T0.7918,0.287 T0.8557,0.3375 T0.8959,0.3965 T0.9098,0.46 T0.8959,0.5235 T0.8557,0.582 Z",type:0},scaleType:1},"\uEE08":{type:5,data:{d:"M0.5082,0.705 Q0.482,0.705,0.4557,0.703 T0.4049,0.697 L0.4311,0.635 Q0.4508,0.638,0.4697,0.6395 T0.5082,0.641 Q0.5672,0.641,0.6221,0.627 T0.718,0.588 T0.782,0.5295 T0.8049,0.46 Q0.8049,0.436,0.7951,0.413 T0.7656,0.369 L0.8557,0.338 Q0.882,0.365,0.8959,0.3965 T0.9098,0.46 T0.8959,0.5235 T0.8557,0.5825 T0.7918,0.633 T0.709,0.672 T0.6123,0.6965 T0.5082,0.705 Z",type:0},scaleType:1},"\uEE09":{type:5,data:{d:"M0.5082,0.705 Q0.4557,0.705,0.4041,0.6965 T0.3074,0.672 T0.2246,0.633 T0.1607,0.5825 T0.1205,0.5235 T0.1066,0.46 L0.2115,0.46 Q0.2115,0.496,0.2344,0.5295 T0.2984,0.588 T0.3943,0.627 T0.5082,0.641 T0.6221,0.627 T0.718,0.588 T0.782,0.5295 T0.8049,0.46 L0.9098,0.46 Q0.9098,0.492,0.8959,0.5235 T0.8557,0.5825 T0.7918,0.633 T0.709,0.672 T0.6123,0.6965 T0.5082,0.705 Z",type:0},scaleType:1},"\uEE0A":{type:5,data:{d:"M0.5082,0.705 Q0.4557,0.705,0.4041,0.6965 T0.3074,0.672 T0.2246,0.633 T0.1607,0.5825 T0.1205,0.5235 T0.1066,0.46 T0.1205,0.3965 T0.1607,0.338 L0.2508,0.369 Q0.2311,0.39,0.2213,0.413 T0.2115,0.46 Q0.2115,0.496,0.2344,0.5295 T0.2984,0.588 T0.3943,0.627 T0.5082,0.641 Q0.5279,0.641,0.5467,0.6395 T0.5852,0.635 L0.6115,0.697 Q0.5869,0.701,0.5607,0.703 T0.5082,0.705 Z",type:0},scaleType:1},"\uEE0B":{type:5,data:{d:"M0.1607,0.582 Q0.1344,0.555,0.1205,0.5235 T0.1066,0.46 T0.1205,0.3965 T0.1607,0.3375 T0.2246,0.287 T0.3074,0.248 T0.4041,0.2235 T0.5082,0.215 L0.5082,0.279 Q0.4492,0.279,0.3943,0.293 T0.2984,0.332 T0.2344,0.3905 T0.2115,0.46 Q0.2115,0.484,0.2213,0.507 T0.2508,0.551 Z",type:0},scaleType:1},"\uF5D0":i2.LINE_H,"\uF5D1":i2.LINE_V,"\uF5D2":i2.FADE_RIGHT,"\uF5D3":i2.FADE_LEFT,"\uF5D4":i2.FADE_DOWN,"\uF5D5":i2.FADE_UP,"\uF5D6":i2.CURVE_DOWN_RIGHT,"\uF5D7":i2.CURVE_DOWN_LEFT,"\uF5D8":i2.CURVE_UP_RIGHT,"\uF5D9":i2.CURVE_UP_LEFT,"\uF5DA":[i2.LINE_V,i2.CURVE_UP_RIGHT],"\uF5DB":[i2.LINE_V,i2.CURVE_DOWN_RIGHT],"\uF5DC":[i2.CURVE_DOWN_RIGHT,i2.CURVE_UP_RIGHT],"\uF5DD":[i2.LINE_V,i2.CURVE_UP_LEFT],"\uF5DE":[i2.LINE_V,i2.CURVE_DOWN_LEFT],"\uF5DF":[i2.CURVE_DOWN_LEFT,i2.CURVE_UP_LEFT],"\uF5E0":[i2.LINE_H,i2.CURVE_DOWN_LEFT],"\uF5E1":[i2.LINE_H,i2.CURVE_DOWN_RIGHT],"\uF5E2":[i2.CURVE_DOWN_LEFT,i2.CURVE_DOWN_RIGHT],"\uF5E3":[i2.LINE_H,i2.CURVE_UP_LEFT],"\uF5E4":[i2.LINE_H,i2.CURVE_UP_RIGHT],"\uF5E5":[i2.CURVE_UP_LEFT,i2.CURVE_UP_RIGHT],"\uF5E6":[i2.LINE_V,i2.CURVE_UP_LEFT,i2.CURVE_UP_RIGHT],"\uF5E7":[i2.LINE_V,i2.CURVE_DOWN_LEFT,i2.CURVE_DOWN_RIGHT],"\uF5E8":[i2.LINE_H,i2.CURVE_DOWN_LEFT,i2.CURVE_UP_LEFT],"\uF5E9":[i2.LINE_H,i2.CURVE_DOWN_RIGHT,i2.CURVE_UP_RIGHT],"\uF5EA":[i2.LINE_V,i2.CURVE_DOWN_RIGHT,i2.CURVE_UP_LEFT],"\uF5EB":[i2.LINE_V,i2.CURVE_DOWN_LEFT,i2.CURVE_UP_RIGHT],"\uF5EC":[i2.LINE_H,i2.CURVE_DOWN_RIGHT,i2.CURVE_UP_LEFT],"\uF5ED":[i2.LINE_H,i2.CURVE_DOWN_LEFT,i2.CURVE_UP_RIGHT],"\uF5EE":[i2.NODE_FILL,i2.NODE_STROKE],"\uF5EF":i2.NODE_STROKE,"\uF5F0":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_RIGHT],"\uF5F1":[i2.NODE_STROKE,i2.NODE_LINE_RIGHT],"\uF5F2":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_LEFT],"\uF5F3":[i2.NODE_STROKE,i2.NODE_LINE_LEFT],"\uF5F4":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_LEFT,i2.NODE_LINE_RIGHT],"\uF5F5":[i2.NODE_STROKE,i2.NODE_LINE_LEFT,i2.NODE_LINE_RIGHT],"\uF5F6":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_DOWN],"\uF5F7":[i2.NODE_STROKE,i2.NODE_LINE_DOWN],"\uF5F8":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_UP],"\uF5F9":[i2.NODE_STROKE,i2.NODE_LINE_UP],"\uF5FA":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_DOWN,i2.NODE_LINE_UP],"\uF5FB":[i2.NODE_STROKE,i2.NODE_LINE_DOWN,i2.NODE_LINE_UP],"\uF5FC":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_DOWN,i2.NODE_LINE_RIGHT],"\uF5FD":[i2.NODE_STROKE,i2.NODE_LINE_DOWN,i2.NODE_LINE_RIGHT],"\uF5FE":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_DOWN,i2.NODE_LINE_LEFT],"\uF5FF":[i2.NODE_STROKE,i2.NODE_LINE_DOWN,i2.NODE_LINE_LEFT],"\uF600":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_RIGHT],"\uF601":[i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_RIGHT],"\uF602":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_LEFT],"\uF603":[i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_LEFT],"\uF604":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_DOWN,i2.NODE_LINE_RIGHT],"\uF605":[i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_DOWN,i2.NODE_LINE_RIGHT],"\uF606":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_DOWN,i2.NODE_LINE_LEFT],"\uF607":[i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_DOWN,i2.NODE_LINE_LEFT],"\uF608":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_DOWN,i2.NODE_LINE_LEFT,i2.NODE_LINE_RIGHT],"\uF609":[i2.NODE_STROKE,i2.NODE_LINE_DOWN,i2.NODE_LINE_LEFT,i2.NODE_LINE_RIGHT],"\uF60A":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_LEFT,i2.NODE_LINE_RIGHT],"\uF60B":[i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_LEFT,i2.NODE_LINE_RIGHT],"\uF60C":[i2.NODE_FILL,i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_DOWN,i2.NODE_LINE_LEFT,i2.NODE_LINE_RIGHT],"\uF60D":[i2.NODE_STROKE,i2.NODE_LINE_UP,i2.NODE_LINE_DOWN,i2.NODE_LINE_LEFT,i2.NODE_LINE_RIGHT],"\u{1FB00}":s2(1),"\u{1FB01}":s2(2),"\u{1FB02}":s2(3),"\u{1FB03}":s2(4),"\u{1FB04}":s2(5),"\u{1FB05}":s2(6),"\u{1FB06}":s2(7),"\u{1FB07}":s2(8),"\u{1FB08}":s2(9),"\u{1FB09}":s2(10),"\u{1FB0A}":s2(11),"\u{1FB0B}":s2(12),"\u{1FB0C}":s2(13),"\u{1FB0D}":s2(14),"\u{1FB0E}":s2(15),"\u{1FB0F}":s2(16),"\u{1FB10}":s2(17),"\u{1FB11}":s2(18),"\u{1FB12}":s2(19),"\u{1FB13}":s2(20),"\u{1FB14}":s2(22),"\u{1FB15}":s2(23),"\u{1FB16}":s2(24),"\u{1FB17}":s2(25),"\u{1FB18}":s2(26),"\u{1FB19}":s2(27),"\u{1FB1A}":s2(28),"\u{1FB1B}":s2(29),"\u{1FB1C}":s2(30),"\u{1FB1D}":s2(31),"\u{1FB1E}":s2(32),"\u{1FB1F}":s2(33),"\u{1FB20}":s2(34),"\u{1FB21}":s2(35),"\u{1FB22}":s2(36),"\u{1FB23}":s2(37),"\u{1FB24}":s2(38),"\u{1FB25}":s2(39),"\u{1FB26}":s2(40),"\u{1FB27}":s2(41),"\u{1FB28}":s2(43),"\u{1FB29}":s2(44),"\u{1FB2A}":s2(45),"\u{1FB2B}":s2(46),"\u{1FB2C}":s2(47),"\u{1FB2D}":s2(48),"\u{1FB2E}":s2(49),"\u{1FB2F}":s2(50),"\u{1FB30}":s2(51),"\u{1FB31}":s2(52),"\u{1FB32}":s2(53),"\u{1FB33}":s2(54),"\u{1FB34}":s2(55),"\u{1FB35}":s2(56),"\u{1FB36}":s2(57),"\u{1FB37}":s2(58),"\u{1FB38}":s2(59),"\u{1FB39}":s2(60),"\u{1FB3A}":s2(61),"\u{1FB3B}":s2(62),"\u{1FB3C}":{type:3,data:"M0,0.6667 L0,1 L0.5,1 Z"},"\u{1FB3D}":{type:3,data:"M0,0.6667 L0,1 L1,1 Z"},"\u{1FB3E}":{type:3,data:"M0,0.3333 L0,1 L0.5,1 Z"},"\u{1FB3F}":{type:3,data:"M0,0.3333 L0,1 L1,1 Z"},"\u{1FB40}":{type:3,data:"M0,0 L0,1 L0.5,1 Z"},"\u{1FB41}":{type:3,data:"M0,0.3333 L0.5,0 L1,0 L1,1 L0,1 Z"},"\u{1FB42}":{type:3,data:"M0,0.3333 L1,0 L1,1 L0,1 Z"},"\u{1FB43}":{type:3,data:"M0,0.6667 L0.5,0 L1,0 L1,1 L0,1 Z"},"\u{1FB44}":{type:3,data:"M0,0.6667 L1,0 L1,1 L0,1 Z"},"\u{1FB45}":{type:3,data:"M0,1 L0.5,0 L1,0 L1,1 Z"},"\u{1FB46}":{type:3,data:"M0,0.6667 L1,0.3333 L1,1 L0,1 Z"},"\u{1FB47}":{type:3,data:"M0.5,1 L1,0.6667 L1,1 Z"},"\u{1FB48}":{type:3,data:"M0,1 L1,0.6667 L1,1 Z"},"\u{1FB49}":{type:3,data:"M0.5,1 L1,0.3333 L1,1 Z"},"\u{1FB4A}":{type:3,data:"M0,1 L1,0.3333 L1,1 Z"},"\u{1FB4B}":{type:3,data:"M0.5,1 L1,0 L1,1 Z"},"\u{1FB4C}":{type:3,data:"M0.5,0 L0,0 L0,1 L1,1 L1,0.3333 Z"},"\u{1FB4D}":{type:3,data:"M0,0 L0,1 L1,1 L1,0.3333 Z"},"\u{1FB4E}":{type:3,data:"M0.5,0 L0,0 L0,1 L1,1 L1,0.6667 Z"},"\u{1FB4F}":{type:3,data:"M0,0 L0,1 L1,1 L1,0.6667 Z"},"\u{1FB50}":{type:3,data:"M0.5,0 L0,0 L0,1 L1,1 Z"},"\u{1FB51}":{type:3,data:"M0,0.3333 L0,1 L1,1 L1,0.6667 Z"},"\u{1FB52}":{type:3,data:"M0,0.6667 L0.5,1 L1,1 L1,0 L0,0 Z"},"\u{1FB53}":{type:3,data:"M0,0.6667 L1,1 L1,0 L0,0 Z"},"\u{1FB54}":{type:3,data:"M0,0.3333 L0.5,1 L1,1 L1,0 L0,0 Z"},"\u{1FB55}":{type:3,data:"M0,0.3333 L1,1 L1,0 L0,0 Z"},"\u{1FB56}":{type:3,data:"M0,0 L0.5,1 L1,1 L1,0 Z"},"\u{1FB57}":{type:3,data:"M0,0.3333 L0,0 L0.5,0 Z"},"\u{1FB58}":{type:3,data:"M0,0.3333 L0,0 L1,0 Z"},"\u{1FB59}":{type:3,data:"M0,0.6667 L0,0 L0.5,0 Z"},"\u{1FB5A}":{type:3,data:"M0,0.6667 L0,0 L1,0 Z"},"\u{1FB5B}":{type:3,data:"M0,1 L0,0 L0.5,0 Z"},"\u{1FB5C}":{type:3,data:"M0,0.6667 L0,0 L1,0 L1,0.3333 Z"},"\u{1FB5D}":{type:3,data:"M0.5,1 L0,1 L0,0 L1,0 L1,0.6667 Z"},"\u{1FB5E}":{type:3,data:"M0,1 L0,0 L1,0 L1,0.6667 Z"},"\u{1FB5F}":{type:3,data:"M0.5,1 L0,1 L0,0 L1,0 L1,0.3333 Z"},"\u{1FB60}":{type:3,data:"M0,1 L0,0 L1,0 L1,0.3333 Z"},"\u{1FB61}":{type:3,data:"M0.5,1 L0,1 L0,0 L1,0 Z"},"\u{1FB62}":{type:3,data:"M0.5,0 L1,0 L1,0.3333 Z"},"\u{1FB63}":{type:3,data:"M0,0 L1,0 L1,0.3333 Z"},"\u{1FB64}":{type:3,data:"M0.5,0 L1,0 L1,0.6667 Z"},"\u{1FB65}":{type:3,data:"M0,0 L1,0 L1,0.6667 Z"},"\u{1FB66}":{type:3,data:"M0.5,0 L1,0 L1,1 Z"},"\u{1FB67}":{type:3,data:"M0,0.3333 L1,0.6667 L1,0 L0,0 Z"},"\u{1FB68}":{type:3,data:"M0,0 L1,0 L1,1 L0,1 L0.5,0.5 Z"},"\u{1FB69}":{type:3,data:"M0,0 L0.5,0.5 L1,0 L1,1 L0,1 Z"},"\u{1FB6A}":{type:3,data:"M0,0 L1,0 L0.5,0.5 L1,1 L0,1 Z"},"\u{1FB6B}":{type:3,data:"M0,0 L1,0 L1,1 L0.5,0.5 L0,1 Z"},"\u{1FB6C}":{type:3,data:"M0,0 L0.5,0.5 L0,1 Z"},"\u{1FB6D}":{type:3,data:"M0,0 L1,0 L0.5,0.5 Z"},"\u{1FB6E}":{type:3,data:"M1,0 L1,1 L0.5,0.5 Z"},"\u{1FB6F}":{type:3,data:"M0,1 L1,1 L0.5,0.5 Z"},"\u{1FB70}":{type:0,data:[{x:1,y:0,w:1,h:8}]},"\u{1FB71}":{type:0,data:[{x:2,y:0,w:1,h:8}]},"\u{1FB72}":{type:0,data:[{x:3,y:0,w:1,h:8}]},"\u{1FB73}":{type:0,data:[{x:4,y:0,w:1,h:8}]},"\u{1FB74}":{type:0,data:[{x:5,y:0,w:1,h:8}]},"\u{1FB75}":{type:0,data:[{x:6,y:0,w:1,h:8}]},"\u{1FB76}":{type:0,data:[{x:0,y:1,w:8,h:1}]},"\u{1FB77}":{type:0,data:[{x:0,y:2,w:8,h:1}]},"\u{1FB78}":{type:0,data:[{x:0,y:3,w:8,h:1}]},"\u{1FB79}":{type:0,data:[{x:0,y:4,w:8,h:1}]},"\u{1FB7A}":{type:0,data:[{x:0,y:5,w:8,h:1}]},"\u{1FB7B}":{type:0,data:[{x:0,y:6,w:8,h:1}]},"\u{1FB7C}":{type:0,data:[{x:0,y:0,w:1,h:8},{x:0,y:7,w:8,h:1}]},"\u{1FB7D}":{type:0,data:[{x:0,y:0,w:1,h:8},{x:0,y:0,w:8,h:1}]},"\u{1FB7E}":{type:0,data:[{x:7,y:0,w:1,h:8},{x:0,y:0,w:8,h:1}]},"\u{1FB7F}":{type:0,data:[{x:7,y:0,w:1,h:8},{x:0,y:7,w:8,h:1}]},"\u{1FB80}":{type:0,data:[{x:0,y:0,w:8,h:1},{x:0,y:7,w:8,h:1}]},"\u{1FB81}":{type:0,data:[{x:0,y:0,w:8,h:1},{x:0,y:2,w:8,h:1},{x:0,y:4,w:8,h:1},{x:0,y:7,w:8,h:1}]},"\u{1FB82}":{type:0,data:[{x:0,y:0,w:8,h:2}]},"\u{1FB83}":{type:0,data:[{x:0,y:0,w:8,h:3}]},"\u{1FB84}":{type:0,data:[{x:0,y:0,w:8,h:5}]},"\u{1FB85}":{type:0,data:[{x:0,y:0,w:8,h:6}]},"\u{1FB86}":{type:0,data:[{x:0,y:0,w:8,h:7}]},"\u{1FB87}":{type:0,data:[{x:6,y:0,w:2,h:8}]},"\u{1FB88}":{type:0,data:[{x:5,y:0,w:3,h:8}]},"\u{1FB89}":{type:0,data:[{x:3,y:0,w:5,h:8}]},"\u{1FB8A}":{type:0,data:[{x:2,y:0,w:6,h:8}]},"\u{1FB8B}":{type:0,data:[{x:1,y:0,w:7,h:8}]},"\u{1FB8C}":{type:1,data:[[1,0],[0,1]],clipPath:"M0,0 L0.5,0 L0.5,1 L0,1 Z"},"\u{1FB8D}":{type:1,data:[[1,0],[0,1]],clipPath:"M0.5,0 L1,0 L1,1 L0.5,1 Z"},"\u{1FB8E}":{type:1,data:[[1,0],[0,1]],clipPath:"M0,0 L1,0 L1,0.5 L0,0.5 Z"},"\u{1FB8F}":{type:1,data:[[1,0],[0,1]],clipPath:"M0,0.5 L1,0.5 L1,1 L0,1 Z"},"\u{1FB90}":{type:1,data:[[0,1],[1,0]]},"\u{1FB91}":[{type:1,data:[[0,1],[1,0]],clipPath:"M0,0.5 L1,0.5 L1,1 L0,1 Z"},{type:0,data:[{x:0,y:0,w:8,h:4}]}],"\u{1FB92}":[{type:1,data:[[0,1],[1,0]],clipPath:"M0,0 L1,0 L1,0.5 L0,0.5 Z"},{type:0,data:[{x:0,y:4,w:8,h:4}]}],"\u{1FB94}":[{type:1,data:[[0,1],[1,0]],clipPath:"M0,0 L0.5,0 L0.5,1 L0,1 Z"},{type:0,data:[{x:4,y:0,w:4,h:8}]}],"\u{1FB95}":{type:0,data:[{x:0,y:0,w:2,h:2},{x:4,y:0,w:2,h:2},{x:2,y:2,w:2,h:2},{x:6,y:2,w:2,h:2},{x:0,y:4,w:2,h:2},{x:4,y:4,w:2,h:2},{x:2,y:6,w:2,h:2},{x:6,y:6,w:2,h:2}]},"\u{1FB96}":{type:0,data:[{x:2,y:0,w:2,h:2},{x:6,y:0,w:2,h:2},{x:0,y:2,w:2,h:2},{x:4,y:2,w:2,h:2},{x:2,y:4,w:2,h:2},{x:6,y:4,w:2,h:2},{x:0,y:6,w:2,h:2},{x:4,y:6,w:2,h:2}]},"\u{1FB97}":{type:0,data:[{x:0,y:2,w:8,h:2},{x:0,y:6,w:8,h:2}]},"\u{1FB98}":{type:2,data:"M-0.25,-0.25 L1.25,1.25 M-0.25,0 L1,1.25 M-0.25,0.25 L0.75,1.25 M-0.25,0.5 L0.5,1.25 M0,-0.25 L1.25,1 M0.25,-0.25 L1.25,0.75 M0.5,-0.25 L1.25,0.5 M-0.25,0.75 L0.25,1.25 M0.75,-0.25 L1.25,0.25",strokeWidth:1},"\u{1FB99}":{type:2,data:"M-0.25,0.5 L0.5,-0.25 M-0.25,0.75 L0.75,-0.25 M-0.25,1 L1,-0.25 M-0.25,1.25 L1.25,-0.25 M0,1.25 L1.25,0 M0.25,1.25 L1.25,0.25 M0.5,1.25 L1.25,0.5 M-0.25,0.25 L0.25,-0.25 M0.75,1.25 L1.25,0.75",strokeWidth:1},"\u{1FB9A}":{type:5,data:{d:"M0,0 L.5,.5 L0,1 L1,1 L.5,.5 L1,0",type:0}},"\u{1FB9B}":{type:5,data:{d:"M0,0 L.5,.5 L1,0 L1,1 L.5,.5 L0,1",type:0}},"\u{1FB9C}":{type:1,data:[[1,0],[0,1]],clipPath:"M0,0 L1,0 L0,1 Z"},"\u{1FB9D}":{type:1,data:[[1,0],[0,1]],clipPath:"M0,0 L1,0 L1,1 Z"},"\u{1FB9E}":{type:1,data:[[1,0],[0,1]],clipPath:"M1,0 L1,1 L0,1 Z"},"\u{1FB9F}":{type:1,data:[[1,0],[0,1]],clipPath:"M0,0 L1,1 L0,1 Z"},"\u{1FBA0}":{type:2,data:"M.5,0 L0,.5",strokeWidth:1},"\u{1FBA1}":{type:2,data:"M.5,0 L1,.5",strokeWidth:1},"\u{1FBA2}":{type:2,data:"M0,.5 L.5,1",strokeWidth:1},"\u{1FBA3}":{type:2,data:"M1,.5 L.5,1",strokeWidth:1},"\u{1FBA4}":{type:2,data:"M.5,0 L0,.5 L.5,1",strokeWidth:1},"\u{1FBA5}":{type:2,data:"M.5,0 L1,.5 L.5,1",strokeWidth:1},"\u{1FBA6}":{type:2,data:"M0,.5 L.5,1 L1,.5",strokeWidth:1},"\u{1FBA7}":{type:2,data:"M0,.5 L.5,0 L1,.5",strokeWidth:1},"\u{1FBA8}":{type:2,data:"M.5,0 L0,.5 M1,.5 L.5,1",strokeWidth:1},"\u{1FBA9}":{type:2,data:"M.5,0 L1,.5 M0,.5 L.5,1",strokeWidth:1},"\u{1FBAA}":{type:2,data:"M.5,0 L1,.5 L.5,1 L0,.5",strokeWidth:1},"\u{1FBAB}":{type:2,data:"M.5,0 L0,.5 L.5,1 L1,.5",strokeWidth:1},"\u{1FBAC}":{type:2,data:"M0,.5 L.5,0 L1,.5 L.5,1",strokeWidth:1},"\u{1FBAD}":{type:2,data:"M1,.5 L.5,0 L0,.5 L.5,1",strokeWidth:1},"\u{1FBAE}":{type:2,data:"M.5,0 L1,.5 L.5,1 L0,.5 Z",strokeWidth:1},"\u{1FBAF}":{type:2,data:"M0,.5 L1,.5 M.5,.35 L.5,.65",strokeWidth:1},"\u{1FBB0}":{type:3,data:"M0.1,0.2 L0.1,.8 L.4,.6 L.9,0.6 Z",scaleType:1},"\u{1FBB1}":{type:4,data:{d:"M.1,.525 L.35,.675 L.9,.35",type:1},scaleType:1},"\u{1FBB2}":{type:3,data:"M.29,.27 L0.13,.56 L.22,.59 L.35,0.35 L.67,.35 L.57,.57 L.71,.76 L.22,.76 L.42,1 L.53,.98 L.43,.86 L.9,.86 L.71,.6 L1,.6 L1,.52 L.83,.52 L.92,.36 L1,.36 L1,.27Z M.99,.13 A.12,.12,0,1,1,.75,.13 A.12,.12,0,1,1,.99,.13"},"\u{1FBB3}":{type:3,data:"M0,.27 L.3,.27 L.55,.12 L.63,.18 L.33,.36 L0,.36 M0,.52 L.33,.52 L.59,.89 L.73,.89 L.73,.98 L.53,.98 L.28,.6 L0,.6"},"\u{1FBB4}":{type:4,data:{d:"M.15,.55 L.5,.4 L.5,.45 L.65,.45 L.65,.35 L.85,.35 L.85,.625 L.5,.625 L.5,.7 Z",type:0},scaleType:1},"\u{1FBB5}":[{type:5,data:{d:"M0,0 L1,0 L1,.0625 L0,.0625 Z M0,.9375 L1,.9375 L1,1 L0,1 Z",type:0}},{type:5,data:{d:"M.15,.5 L.5,.35 L.5,.425 L.85,.425 L.85,.575 L.5,.575 L.5,.65 Z",type:0},scaleType:1}],"\u{1FBB6}":[{type:5,data:{d:"M0,0 L1,0 L1,.0625 L0,.0625 Z M0,.9375 L1,.9375 L1,1 L0,1 Z",type:0}},{type:5,data:{d:"M.85,.5 L.5,.35 L.5,.425 L.15,.425 L.15,.575 L.5,.575 L.5,.65 Z",type:0},scaleType:1}],"\u{1FBB7}":[{type:5,data:{d:"M.875,0 L1,0 L1,1 L.875,1 Z",type:0}},{type:5,data:{d:"M.5,.675 L.2,.5 L.35,.5 L.35,.325 L.65,.325 L.65,.5 L.8,.5 Z",type:0},scaleType:1}],"\u{1FBB8}":[{type:5,data:{d:"M.875,0 L1,0 L1,1 L.875,1 Z",type:0}},{type:5,data:{d:"M.5,.325 L.2,.5 L.35,.5 L.35,.675 L.65,.675 L.65,.5 L.8,.5 Z",type:0},scaleType:1}],"\u{1FBB9}":{type:5,data:{d:"M1,.89 L.11,.89 L.11,.37 L.36,.12 L.74,.12 L.96,.34 L1,.34 L1,.405 L.92,.405 L.69,.185 L.41,.185 L.21,.42 L.21,.825 L1,.825 Z",type:0}},"\u{1FBBA}":{type:5,data:{d:"M0,.89 L0,.825 L.78,.825 L.78,.53 L.7,.415 L0,.415 L0,.35 L.75,.35 L.88,.48 L.88,.89 Z",type:0}},"\u{1FBBB}":{type:5,data:{d:"M.31,.275 L.44,.275 L.44,.47 L.05,.47 L.05,.405 L.31,.405 Z M.56,.275 L.69,.275 L.69,.405 L.95,.405 L.95,.47 L.56,.47 Z M.05,.53 L.44,.53 L.44,.725 L.31,.725 L.31,.595 L.05,.595 Z M.56,.53 L.95,.53 L.95,.595 L.69,.595 L.69,.725 L.56,.725 Z",type:0},scaleType:1},"\u{1FBBC}":[{type:3,data:"M0,0 L1,0 L1,1 L0,1 L0,.935 L.885,.935 L.885,.065 L0,.065 Z"},{type:3,data:"M.67,.5 A.17,.085,0,1,1,.33,.5 A.17,.085,0,1,1,.67,.5",scaleType:1}],"\u{1FBBD}":{type:4,data:{d:"M0,0 L.5,.5 L1,0 L1,1 L.5,.5 L0,1 Z",type:1}},"\u{1FBBE}":{type:4,data:{d:"M1,.5 L.5,1",type:1}},"\u{1FBBF}":{type:4,data:{d:"M.5,0 L1,.5 L.5,1 L0,.5 Z",type:1}},"\u{1FBC0}":{type:3,data:"M.16,.445 A.02,.01,0,0,1,.39,.33 L.5,.375 L.61,.33 A.02,.01,0,0,1,.84,.445 L.75,.5 L.84,.555 A.02,.01,0,0,1,.61,.67 L.5,.625 L.39,.67 A.02,.01,0,0,1,.16,.555 L.25,.5 Z M.24,.41 L.39,.5 L.24,.59 L.32,.63 L.5,.555 L.68,.63 L.76,.59 L.61,.5 L.76,.41 L.68,.37 L.5,.445 L.32,.37 Z",scaleType:1},"\u{1FBC1}":{type:3,data:"M0.142308,0.924 Q0.130770,0.924,0.120193,0.922 T0.101924,0.916 T0.089424,0.9065 T0.084616,0.895 V0.025 Q0.084616,0.019,0.089424,0.0135 T0.101924,0.004 T0.120193,-0.002 T0.142308,-0.004 Q0.150000,-0.004,0.157693,-0.003 T0.173078,0.000 L0.430770,0.085 Q0.451924,0.059,0.494232,0.041 T0.587501,0.013 T0.692309,-0.0005 T0.800001,-0.004 H1 V0.055 H0.800001 Q0.771155,0.055,0.742309,0.056 T0.685578,0.060 T0.630770,0.0685 T0.580770,0.0825 T0.543270,0.1045 T0.528847,0.133 V0.647 H0.530770 Q0.596155,0.645,0.659616,0.638 T0.782693,0.6165 T0.893270,0.5805 T1,0.531 V0.623 Q0.934616,0.644,0.880770,0.6595 T0.769232,0.685 T0.650963,0.700 T0.528847,0.707 V0.787 Q0.528847,0.802,0.543270,0.8155 T0.580770,0.8375 T0.630770,0.8515 T0.685578,0.860 T0.742309,0.864 T0.800001,0.865 H1 V0.924 H0.800001 Q0.746155,0.924,0.692309,0.9205 T0.587501,0.907 T0.494232,0.879 T0.430770,0.835 L0.173078,0.920 Q0.165386,0.922,0.157693,0.923 T0.142308,0.924 Z M0.2,0.841 L0.415385,0.770 V0.150 L0.2,0.079 V0.841 Z M1,0.698 Q0.973077,0.694,0.969231,0.6885 T0.965385,0.677 Q0.965385,0.672,0.969231,0.6665 T1,0.657 V0.698 Z"},"\u{1FBC2}":{type:3,data:"M0,0.623 V0.531 Q0.044231,0.512,0.0625,0.4905 T0.092308,0.4465 T0.108654,0.4005 T0.113462,0.354 V0.351 Q0.113462,0.345,0.117308,0.3395 T0.129808,0.3305 T0.148846,0.3245 T0.171154,0.323 H0.501923 Q0.532692,0.323,0.563462,0.3185 T0.617308,0.303 T0.651923,0.2765 T0.663462,0.2435 V0.241 Q0.663462,0.219,0.657692,0.197 T0.639423,0.1545 T0.604808,0.115 T0.549615,0.082 T0.475577,0.0615 T0.392308,0.055 H0 V-0.004 H1 V0.055 H0.667308 Q0.694231,0.071,0.713462,0.09 T0.746154,0.129 T0.766346,0.1705 T0.775,0.213 H1 V0.316 Q0.971154,0.305,0.955769,0.2965 T0.920192,0.2825 T0.876923,0.2745 T0.830769,0.272 H0.773077 Q0.767308,0.297,0.743269,0.319 T0.680769,0.355 T0.595192,0.375 T0.501923,0.381 H0.227308 Q0.223462,0.415,0.211,0.4485 T0.172692,0.5135 T0.108269,0.573 T0,0.623 Z M0.611538,0.924 H0 V0.865 H0.611538 Q0.642308,0.865,0.673077,0.8605 T0.727885,0.845 T0.762308,0.8185 T0.773077,0.787 V0.785 Q0.773077,0.769,0.762308,0.7535 T0.727885,0.727 T0.673077,0.7115 T0.611538,0.707 H0.059615 Q0.048077,0.707,0.037115,0.7045 T0,0.694 V0.653 Q0.026923,0.648,0.037115,0.646 T0.059615,0.644 H0.722692 Q0.753462,0.644,0.784231,0.6395 T0.839038,0.624 T0.873846,0.5975 T0.884615,0.566 V0.564 Q0.884615,0.548,0.873846,0.532 T0.839038,0.5055 T0.784231,0.49 T0.722692,0.4855 H0.392308 Q0.380769,0.4855,0.370192,0.483 T0.351923,0.4765 T0.339423,0.467 T0.334615,0.4555 T0.339423,0.444 T0.351923,0.4345 T0.370192,0.428 T0.392308,0.4255 H0.832692 Q0.855769,0.4255,0.878846,0.4235 T0.922115,0.416 T0.957692,0.4015 T1,0.3815 V0.4685 Q0.975,0.4705,0.966346,0.4725 T0.95,0.4765 Q0.961538,0.4835,0.969231,0.4915 T1,0.5075 V0.6205 Q0.965385,0.6455,0.926923,0.665 T0.84,0.6935 Q0.866923,0.7115,0.877596,0.7345 T0.894231,0.7855 V0.787 Q0.894231,0.815,0.876923,0.8425 T0.820192,0.8895 T0.726923,0.916 T0.611538,0.924 Z"},"\u{1FBC3}":{type:3,data:"M0,0.473 V0.386 Q0.025490,0.378,0.028431,0.3695 T0.031373,0.352 V0.35 Q0.031373,0.342,0.028431,0.333 T0,0.316 V0.213 H0.652941 Q0.684314,0.213,0.715686,0.2085 T0.770588,0.193 T0.804902,0.1665 T0.815686,0.135 V0.133 Q0.815686,0.117,0.804902,0.101 T0.770588,0.0745 T0.715686,0.0595 T0.652941,0.055 H0 V-0.004 H0.652941 Q0.707843,-0.004,0.761765,0.004 T0.856863,0.031 T0.915686,0.0775 T0.933333,0.133 V0.135 Q0.933333,0.163,0.915686,0.1905 T0.856863,0.237 T0.761765,0.264 T0.652941,0.272 H0.109804 Q0.131373,0.29,0.140196,0.31 T0.149020,0.35 V0.352 Q0.149020,0.37,0.142157,0.388 T0.118627,0.4225 T0.076471,0.452 T0,0.473 Z M0,0.625 V0.513 Q0.029412,0.526,0.032353,0.54 T0.035294,0.568 V0.57 Q0.035294,0.584,0.032353,0.598 T0,0.625 Z"},"\u{1FBC4}":{type:3,data:"M0.019231,1.085 V-0.165 H0.980769 V1.085 H0.019231 Z M0.446154,0.527 H0.553846 Q0.553846,0.511,0.5625,0.495 T0.591346,0.466 T0.631731,0.4405 T0.666346,0.4135 T0.688462,0.3825 T0.696154,0.35 Q0.696154,0.33,0.682692,0.311 T0.641346,0.2785 T0.575,0.259 T0.498077,0.253 T0.421154,0.259 T0.356731,0.279 T0.315385,0.3125 T0.301923,0.352 V0.357 H0.409615 V0.354 Q0.409615,0.345,0.414423,0.335 T0.431731,0.318 T0.4625,0.3075 T0.498077,0.304 Q0.517308,0.304,0.534615,0.307 T0.564423,0.3165 T0.582692,0.332 T0.588462,0.35 Q0.588462,0.366,0.572115,0.3805 T0.535577,0.4075 T0.496154,0.4335 T0.465385,0.4625 T0.45,0.4945 T0.446154,0.527 Z M0.5,0.667 Q0.519231,0.667,0.5375,0.664 T0.569231,0.654 T0.588462,0.637 T0.594231,0.617 T0.588462,0.5975 T0.569231,0.581 T0.5375,0.571 T0.5,0.568 T0.4625,0.571 T0.430769,0.581 T0.411538,0.5975 T0.405769,0.617 T0.411538,0.637 T0.430769,0.654 T0.4625,0.664 T0.5,0.667 Z",scaleType:1},"\u{1FBC5}":{type:5,data:{d:"M.44,.425 L.44,.51 L.44,.56 L.19,.685 L.26,.72 L.5,.605 L.74,.72 L.81,.685 L.56,.56 L.56,.51 L.56,.425 Z M.17,.46 L.17,.51 L.83,.51 L.83,.46 Z M.67,.35 C.67,.303,.594,.265,.5,.265 C.406,.265,.33,.303,.33,.35 C.33,.397,.406,.435,.5,.435 C.594,.435,.67,.397,.67,.35 Z M.56,.35 C.56,.3665,.533,.38,.5,.38 C.467,.38,.44,.3665,.44,.35 C.44,.3335,.467,.32,.5,.32 C.533,.32,.56,.3335,.56,.35 Z",type:0},scaleType:1},"\u{1FBC6}":{type:5,data:{d:"M.44,.425 L.44,.46 L.23,.385 L.19,.43 L.44,.51 L.44,.56 L.29,.71 L.38,.735 L.5,.605 L.61,.735 L.7,.71 L.56,.56 L.56,.51 L.81,.43 L.77,.385 L.56,.46 L.56,.425 Z M.67,.35 C.67,.303,.594,.265,.5,.265 C.406,.265,.33,.303,.33,.35 C.33,.397,.406,.435,.5,.435 C.594,.435,.67,.397,.67,.35 Z M.56,.35 C.56,.3665,.533,.38,.5,.38 C.467,.38,.44,.3665,.44,.35 C.44,.3335,.467,.32,.5,.32 C.533,.32,.56,.3335,.56,.35 Z",type:0},scaleType:1},"\u{1FBC7}":{type:5,data:{d:"M.44,.425 L.44,.56 L.29,.71 L.38,.735 L.5,.605 L.74,.72 L.81,.685 L.56,.56 L.56,.425 Z M.18,.53 L.23,.575 L.81,.43 L.77,.385 Z M.67,.35 C.67,.303,.594,.265,.5,.265 C.406,.265,.33,.303,.33,.35 C.33,.397,.406,.435,.5,.435 C.594,.435,.67,.397,.67,.35 Z M.56,.35 C.56,.3665,.533,.38,.5,.38 C.467,.38,.44,.3665,.44,.35 C.44,.3335,.467,.32,.5,.32 C.533,.32,.56,.3335,.56,.35 Z",type:0},scaleType:1},"\u{1FBC8}":{type:5,data:{d:"M.44,.425 L.44,.56 L.19,.685 L.26,.72 L.5,.605 L.62,.735 L.71,.71 L.56,.56 L.56,.425 Z M.23,.385 L.18,.43 L.77,.575 L.81,.53 Z M.67,.35 C.67,.303,.594,.265,.5,.265 C.406,.265,.33,.303,.33,.35 C.33,.397,.406,.435,.5,.435 C.594,.435,.67,.397,.67,.35 Z M.56,.35 C.56,.3665,.533,.38,.5,.38 C.467,.38,.44,.3665,.44,.35 C.44,.3335,.467,.32,.5,.32 C.533,.32,.56,.3335,.56,.35 Z",type:0},scaleType:1},"\u{1FBC9}":{type:5,data:{d:"M.44,.425 L.45,.495 L.15,.645 L.34,.645 L.34,.7 L.44,.7 L.44,.645 L.56,.645 L.56,.7 L.66,.7 L.66,.645 L.84,.645 L.54,.495 L.56,.425 Z M.39,.6 L.5,.55 L.60,.6 Z M.17,.46 L.17,.51 L.83,.51 L.83,.46 Z M.67,.35 C.67,.303,.594,.265,.5,.265 C.406,.265,.33,.303,.33,.35 C.33,.397,.406,.435,.5,.435 C.594,.435,.67,.397,.67,.35 Z M.56,.35 C.56,.3665,.533,.38,.5,.38 C.467,.38,.44,.3665,.44,.35 C.44,.3335,.467,.32,.5,.32 C.533,.32,.56,.3335,.56,.35 Z",type:0},scaleType:1},"\u{1FBCA}":{type:5,data:{d:"M.26,.375 L.5,.255 L.74,.375 L.74,.665 L.5,.55 L.26,.665 Z M.37,.395 L.37,.54 L.5,.475 L.63,.54 L.63,.395 L.5,.33 Z",type:0},scaleType:1},"\u{1FBCB}":{type:5,data:{d:"M.09,.41 L.32,.295 L.5,.375 L.68,.295 L.91,.41 L.75,.5 L.91,.59 L.68,.705 L.5,.625 L.32,.705 L.09,.59 L.25,.5 Z M.24,.41 L.39,.5 L.24,.59 L.32,.63 L.5,.555 L.68,.63 L.76,.59 L.61,.5 L.76,.41 L.68,.37 L.5,.445 L.32,.37 Z",type:0},scaleType:1},"\u{1FBCC}":{type:5,data:{d:"M.55,.305 L.88,.305 L.88,.355 L.65,.355 L.65,.47 L.88,.47 L.88,.52 L.55,.52 Z",type:0},scaleType:1},"\u{1FBCD}":{type:5,data:{d:"M.38,.39 L.5,.33 L.62,.39 L.62,.53 L.5,.47 L.38,.53 Z",type:0},scaleType:1},"\u{1FBCE}":{type:3,data:"M0,0 L0.6667,0 L0.6667,1 L0,1 Z"},"\u{1FBCF}":{type:3,data:"M0,0 L0.3333,0 L0.3333,1 L0,1 Z"},"\u{1FBD0}":{type:2,data:"M1,.5 L0,1",strokeWidth:1},"\u{1FBD1}":{type:2,data:"M1,0 L0,.5",strokeWidth:1},"\u{1FBD2}":{type:2,data:"M0,0 L1,.5",strokeWidth:1},"\u{1FBD3}":{type:2,data:"M0,.5 L1,1",strokeWidth:1},"\u{1FBD4}":{type:2,data:"M0,0 L.5,1",strokeWidth:1},"\u{1FBD5}":{type:2,data:"M.5,0 L1,1",strokeWidth:1},"\u{1FBD6}":{type:2,data:"M1,0 L.5,1",strokeWidth:1},"\u{1FBD7}":{type:2,data:"M.5,0 L0,1",strokeWidth:1},"\u{1FBD8}":{type:2,data:"M0,0 L.5,.5 L1,0",strokeWidth:1},"\u{1FBD9}":{type:2,data:"M1,0 L.5,.5 L1,1",strokeWidth:1},"\u{1FBDA}":{type:2,data:"M0,1 L.5,.5 L1,1",strokeWidth:1},"\u{1FBDB}":{type:2,data:"M0,0 L.5,.5 L0,1",strokeWidth:1},"\u{1FBDC}":{type:2,data:"M0,0 L.5,1 L1,0",strokeWidth:1},"\u{1FBDD}":{type:2,data:"M1,0 L0,.5 L1,1",strokeWidth:1},"\u{1FBDE}":{type:2,data:"M0,1 L.5,0 L1,1",strokeWidth:1},"\u{1FBDF}":{type:2,data:"M0,0 L1,.5 L0,1",strokeWidth:1},"\u{1FBE0}":{type:5,data:{d:"M0,0 C0,.276,.224,.5,.5,.5 C.776,.5,1,.276,1,0",type:1}},"\u{1FBE1}":{type:5,data:{d:"M1,0 C.724,0,.5,.224,.5,.5 C.5,.776,.724,1,1,1",type:1}},"\u{1FBE2}":{type:5,data:{d:"M0,1 C0,.724,.224,.5,.5,.5 C.776,.5,1,.724,1,1",type:1}},"\u{1FBE3}":{type:5,data:{d:"M0,0 C.276,0,.5,.224,.5,.5 C.5,.776,.276,1,0,1",type:1}},"\u{1FBE4}":{type:0,data:[{x:2,y:0,w:4,h:4}]},"\u{1FBE5}":{type:0,data:[{x:2,y:4,w:4,h:4}]},"\u{1FBE6}":{type:0,data:[{x:0,y:2,w:4,h:4}]},"\u{1FBE7}":{type:0,data:[{x:4,y:2,w:4,h:4}]},"\u{1FBE8}":{type:5,data:{d:"M0,0 C0,.276,.224,.5,.5,.5 C.776,.5,1,.276,1,0 Z",type:0}},"\u{1FBE9}":{type:5,data:{d:"M1,0 C.724,0,.5,.224,.5,.5 C.5,.776,.724,1,1,1 Z",type:0}},"\u{1FBEA}":{type:5,data:{d:"M0,1 C0,.724,.224,.5,.5,.5 C.776,.5,1,.724,1,1 Z",type:0}},"\u{1FBEB}":{type:5,data:{d:"M0,0 C.276,0,.5,.224,.5,.5 C.5,.776,.276,1,0,1 Z",type:0}},"\u{1FBEC}":{type:5,data:{d:"M1,0 L.5,0 C.5,.276,.724,.5,1,.5 Z",type:0}},"\u{1FBED}":{type:5,data:{d:"M0,1 L.5,1 C.5,.724,.276,.5,0,.5 Z",type:0}},"\u{1FBEE}":{type:5,data:{d:"M1,1 L1,.5 C.724,.5,.5,.724,.5,1 Z",type:0}},"\u{1FBEF}":{type:5,data:{d:"M0,0 L0,.5 C.276,.5,.5,.276,.5,0 Z",type:0}},"\u{1FBF0}":{type:5,data:{d:r(126),type:0},scaleType:1},"\u{1FBF1}":{type:5,data:{d:r(48),type:0},scaleType:1},"\u{1FBF2}":{type:5,data:{d:r(109),type:0},scaleType:1},"\u{1FBF3}":{type:5,data:{d:r(121),type:0},scaleType:1},"\u{1FBF4}":{type:5,data:{d:r(51),type:0},scaleType:1},"\u{1FBF5}":{type:5,data:{d:r(91),type:0},scaleType:1},"\u{1FBF6}":{type:5,data:{d:r(95),type:0},scaleType:1},"\u{1FBF7}":{type:5,data:{d:r(114),type:0},scaleType:1},"\u{1FBF8}":{type:5,data:{d:r(127),type:0},scaleType:1},"\u{1FBF9}":{type:5,data:{d:r(123),type:0},scaleType:1},"\u{1FBFA}":{type:5,data:{d:"M.5,.175 C.2,.175,.15,.305,.15,.435 L.05,.63 L.35,.63 C.35,.682,.42,.76,.5,.76 C.58,.76,.65,.682,.65,.63 L.95,.63 L.85,.435 C.85,.305,.8,.175,.5,.175 Z",type:0},scaleType:1},...Object.fromEntries(Array.from({length:256},(t3,e3)=>[String.fromCodePoint(10240+e3),{type:6,data:e3}]))},e2.blockPatternCodepoints=new Set([9617,9618,9619,129932,129933,129934,129935,129936,129937,129938,129940,129948,129949,129950,129951])},183(t2,e2,i2){;Object.defineProperty(e2,"__esModule",{value:true}),e2.tryDrawCustomGlyph=function(t3,e3,i3,s3,o2,n2,L2,h2,T2,l2,d,c,_=0){const Q=r.customGlyphDefinitions[e3];if(Q){const e4=Array.isArray(Q)?Q:[Q];for(const r2 of e4)a(t3,r2,i3,s3,o2,n2,L2,h2,T2,l2,d,c,_);return true}return false};const s2=i2(959),r=i2(915);function a(t3,e3,i3,r2,a2,h2,d,c,_,Q,u,p,g=0){let M=a2,y=h2,v=i3,f=r2;switch(1===e3.scaleType&&(M=d,y=c,v=i3+(a2-d)/2,f=r2+(h2-c)/2),e3.clipPath&&(t3.save(),(function(t4,e4,i4,s3,r3,a3){t4.beginPath();for(const o2 of e4.split(" ")){const e5=o2[0];if("Z"===e5){t4.closePath();continue}const n2=o2.substring(1).split(",");if(!n2[0]||!n2[1])continue;const L2=i4+parseFloat(n2[0])*r3,h3=s3+parseFloat(n2[1])*a3;"M"===e5?t4.moveTo(L2,h3):"L"===e5&&t4.lineTo(L2,h3)}t4.clip()})(t3,e3.clipPath,v,f,M,y)),e3.type){case 0:!(function(t4,e4,i4,s3,r3,a3){for(let o2=0;o27&&parseInt(T2.slice(7,9),16)||1;else{if(!T2.startsWith("rgba"))throw new Error(`Unexpected fillStyle color format "${T2}" when drawing pattern glyph`);[L2,d3,c3,_2]=T2.substring(5,T2.length-1).split(",").map(t5=>parseFloat(t5))}for(let t5=0;t5>1&1,c2=1&n2;0!==d2||0!==c2?l2.setTransform(new DOMMatrix().translateSelf(-d2,-c2)):l2.setTransform(new DOMMatrix),t4.fillStyle=l2,t4.fillRect(i4,r3,a3,o2)})(t3,e3.data,v,f,M,y,g);break;case 2:!(function(t4,e4,i4,s3,r3,a3,o2,n2,L2){let h3;t4.save(),t4.beginPath(),t4.rect(i4,s3,r3,a3),t4.clip(),t4.beginPath(),h3="function"==typeof e4?e4(.15,.15/a3*r3):e4;const d2={currentX:0,currentY:0,lastControlX:0,lastControlY:0,lastCommand:""};for(const e5 of h3.split(" ")){const L3=e5[0];if("Z"===L3){;t4.closePath(),d2.lastCommand=L3;continue}const h4=T[L3];if(!h4){n2.error(`Could not find drawing instructions for "${L3}"`);continue}const c2=e5.substring(1).split(",");c2[0]&&c2[1]&&(h4(t4,l(c2,r3,a3,i4,s3,true,o2,0,0,false),d2),d2.lastCommand=L3)};void 0!==L2?(t4.strokeStyle=t4.fillStyle,t4.lineWidth=o2*L2,t4.stroke()):t4.fill(),t4.closePath(),t4.restore()})(t3,e3.data,v,f,M,y,Q,u,e3.strokeWidth);break;case 3:!(function(t4,e4,i4,s3,r3,a3,o2,L2){const h3="string"==typeof e4?e4:e4(0,0);t4.beginPath();let T2=0,l2=0,d2=0,c2=0,_2="";for(const e5 of h3.split(" ")){const o3=e5[0],L3=e5.substring(1).split(",");if("Z"===o3){;t4.closePath(),_2=o3;continue}if("V"===o3){const e6=s3+parseFloat(L3[0])*a3;t4.lineTo(T2,e6),l2=e6,d2=T2,c2=l2,_2=o3;continue}if("H"===o3){const e6=i4+parseFloat(L3[0])*r3;t4.lineTo(e6,l2),T2=e6,d2=T2,c2=l2,_2=o3;continue}if(!L3[0]||!L3[1])continue;if("A"===o3){const e6=parseFloat(L3[0])*r3,o4=parseFloat(L3[1])*a3,h5=parseFloat(L3[2])*Math.PI/180,d3=parseInt(L3[3],10),c3=parseInt(L3[4],10),_3=i4+parseFloat(L3[5])*r3,Q2=s3+parseFloat(L3[6])*a3;n(t4,T2,l2,e6,o4,h5,d3,c3,_3,Q2),T2=_3,l2=Q2;continue}const h4=L3.map((t5,e6)=>{const o4=parseFloat(t5);return e6%2==0?i4+o4*r3:s3+o4*a3});if("M"===o3)t4.moveTo(h4[0],h4[1]),T2=h4[0],l2=h4[1],d2=T2,c2=l2;else if("L"===o3)t4.lineTo(h4[0],h4[1]),T2=h4[0],l2=h4[1],d2=T2,c2=l2;else if("Q"===o3)t4.quadraticCurveTo(h4[0],h4[1],h4[2],h4[3]),d2=h4[0],c2=h4[1],T2=h4[2],l2=h4[3];else if("T"===o3){let e6,i5;"Q"===_2||"T"===_2?(e6=2*T2-d2,i5=2*l2-c2):(e6=T2,i5=l2),t4.quadraticCurveTo(e6,i5,h4[0],h4[1]),d2=e6,c2=i5,T2=h4[0],l2=h4[1]}else"C"===o3&&(t4.bezierCurveTo(h4[0],h4[1],h4[2],h4[3],h4[4],h4[5]),d2=h4[2],c2=h4[3],T2=h4[4],l2=h4[5]);_2=o3}void 0!==L2?(t4.strokeStyle=t4.fillStyle,t4.lineWidth=o2*L2,t4.stroke()):t4.fill()})(t3,e3.data,v,f,M,y,Q,e3.strokeWidth);break;case 4:!(function(t4,e4,i4,s3,r3,a3,o2,n2){;t4.save(),t4.fillRect(i4,s3,r3,a3),n2&&(t4.fillStyle=n2,t4.strokeStyle=n2),t4.lineWidth=o2,t4.lineCap="square",t4.beginPath();for(const o3 of e4.d.split(" ")){const e5=o3[0],n3=o3.substring(1).split(",");if(!n3[0]||!n3[1]){"Z"===e5&&t4.closePath();continue}const L2=n3.map((t5,e6)=>{const o4=parseFloat(t5);return e6%2==0?i4+o4*r3:s3+o4*a3});"M"===e5?t4.moveTo(L2[0],L2[1]):"L"===e5&&t4.lineTo(L2[0],L2[1])};1===e4.type?t4.stroke():t4.fill(),t4.restore()})(t3,e3.data,v,f,M,y,Q,p);break;case 5:!(function(t4,e4,i4,s3,r3,a3,o2,n2,L2){const h3=new Path2D;h3.rect(i4,s3,r3,a3),t4.clip(h3),t4.beginPath();const d2=o2/12;t4.lineWidth=n2*d2;const c2={currentX:0,currentY:0,lastControlX:0,lastControlY:0,lastCommand:""};for(const o3 of e4.d.split(" ")){const h4=o3[0];if("Z"===h4){;t4.closePath(),c2.lastCommand=h4;continue}const _2=T[h4];if(!_2){L2.error(`Could not find drawing instructions for "${h4}"`);continue}const Q2=o3.substring(1).split(",");Q2[0]&&Q2[1]&&(_2(t4,l(Q2,r3,a3,i4,s3,false,n2,(e4.leftPadding??0)*(d2/2),(e4.rightPadding??0)*(d2/2)),c2),c2.lastCommand=h4)};1===e4.type?(t4.strokeStyle=t4.fillStyle,t4.stroke()):t4.fill(),t4.closePath()})(t3,e3.data,v,f,M,y,_,Q,u);break;case 6:!(function(t4,e4,i4,s3,r3,a3){const n2=r3/8,L2=.1*a3,h3=.8*a3/8,T2=Math.min(n2,h3);for(let r4=0;r4<8;r4++)if(e4&1<1){const t4=Math.sqrt(y);u=(s3*=t4)*s3,p=(r2*=t4)*r2}let v=(u*p-u*M-p*g)/(u*M+p*g);v<0&&(v=0);const f=(o2===n2?-1:1)*Math.sqrt(v),C=f*(s3*Q/r2),m=f*(-r2*_/s3),w=T2*C-l2*m+(e3+L2)/2,x=l2*C+T2*m+(i3+h2)/2,b=(_-C)/s3,E=(Q-m)/r2,R=(-_-C)/s3,k=(-Q-m)/r2,A=Math.atan2(E,b);let Z=Math.atan2(k,R)-A;0===n2&&Z>0?Z-=2*Math.PI:1===n2&&Z<0&&(Z+=2*Math.PI);const D=A+Z;t3.ellipse(w,x,s3,r2,a2,A,D,0===n2)}const L=new Map;function h(t3,e3,i3=0){return Math.max(Math.min(t3,e3),i3)}const T={C:(t3,e3,i3)=>{;t3.bezierCurveTo(e3[0],e3[1],e3[2],e3[3],e3[4],e3[5]),i3.lastControlX=e3[2],i3.lastControlY=e3[3],i3.currentX=e3[4],i3.currentY=e3[5]},L:(t3,e3,i3)=>{;t3.lineTo(e3[0],e3[1]),i3.lastControlX=i3.currentX=e3[0],i3.lastControlY=i3.currentY=e3[1]},M:(t3,e3,i3)=>{;t3.moveTo(e3[0],e3[1]),i3.lastControlX=i3.currentX=e3[0],i3.lastControlY=i3.currentY=e3[1]},Q:(t3,e3,i3)=>{;t3.quadraticCurveTo(e3[0],e3[1],e3[2],e3[3]),i3.lastControlX=e3[0],i3.lastControlY=e3[1],i3.currentX=e3[2],i3.currentY=e3[3]},T:(t3,e3,i3)=>{let s3,r2;"Q"===i3.lastCommand||"T"===i3.lastCommand?(s3=2*i3.currentX-i3.lastControlX,r2=2*i3.currentY-i3.lastControlY):(s3=i3.currentX,r2=i3.currentY),t3.quadraticCurveTo(s3,r2,e3[0],e3[1]),i3.lastControlX=s3,i3.lastControlY=r2,i3.currentX=e3[0],i3.currentY=e3[1]}};function l(t3,e3,i3,s3,r2,a2,o2,n2=0,L2=0,T2=true){const l2=t3.map(t4=>parseFloat(t4)||parseInt(t4));if(l2.length<2)throw new Error("Too few arguments for instruction");for(let t4=0;t4{;this._refreshCharAtlas(t3,e4),this.reset(t3)})),this._register((0,r.toDisposable)(()=>{this._canvas.remove()}))}_initCanvas(){;this._ctx=(0,a.throwIfFalsy)(this._canvas.getContext("2d",{alpha:this._alpha})),this._alpha||this._clearAll()}handleBlur(t3){}handleFocus(t3){}handleCursorMove(t3){}handleGridChanged(t3,e3,i3){}handleSelectionChanged(t3,e3,i3,s3=false){}_setTransparency(t3,e3){if(e3===this._alpha)return;const i3=this._canvas;this._alpha=e3,this._canvas=this._canvas.cloneNode(),this._initCanvas(),this._container.replaceChild(this._canvas,i3),this._refreshCharAtlas(t3,this._themeService.colors),this.handleGridChanged(t3,0,t3.rows-1)}_refreshCharAtlas(t3,e3){;this._deviceCharWidth<=0&&this._deviceCharHeight<=0||(this._charAtlas=(0,s2.acquireTextureAtlas)(t3,this._optionsService.rawOptions,e3,this._deviceCellWidth,this._deviceCellHeight,this._deviceCharWidth,this._deviceCharHeight,this._coreBrowserService.dpr,2048),this._charAtlas.warmUp())}resize(t3,e3){;this._deviceCellWidth=e3.device.cell.width,this._deviceCellHeight=e3.device.cell.height,this._deviceCharWidth=e3.device.char.width,this._deviceCharHeight=e3.device.char.height,this._deviceCharLeft=e3.device.char.left,this._deviceCharTop=e3.device.char.top,this._canvas.width=e3.device.canvas.width,this._canvas.height=e3.device.canvas.height,this._canvas.style.width=`${e3.css.canvas.width}px`,this._canvas.style.height=`${e3.css.canvas.height}px`,this._alpha||this._clearAll(),this._refreshCharAtlas(t3,this._themeService.colors)}_fillBottomLineAtCells(t3,e3,i3=1){this._ctx.fillRect(t3*this._deviceCellWidth,(e3+1)*this._deviceCellHeight-this._coreBrowserService.dpr-1,i3*this._deviceCellWidth,this._coreBrowserService.dpr)}_clearAll(){this._alpha?this._ctx.clearRect(0,0,this._canvas.width,this._canvas.height):(this._ctx.fillStyle=this._themeService.colors.background.css,this._ctx.fillRect(0,0,this._canvas.width,this._canvas.height))}_clearCells(t3,e3,i3,s3){this._alpha?this._ctx.clearRect(t3*this._deviceCellWidth,e3*this._deviceCellHeight,i3*this._deviceCellWidth,s3*this._deviceCellHeight):(this._ctx.fillStyle=this._themeService.colors.background.css,this._ctx.fillRect(t3*this._deviceCellWidth,e3*this._deviceCellHeight,i3*this._deviceCellWidth,s3*this._deviceCellHeight))}_fillCharTrueColor(t3,e3,i3,s3){;this._ctx.font=this._getFont(t3,false,false),this._ctx.textBaseline=o.TEXT_BASELINE,this._clipCell(i3,s3,e3.getWidth()),this._ctx.fillText(e3.getChars(),i3*this._deviceCellWidth+this._deviceCharLeft,s3*this._deviceCellHeight+this._deviceCharTop+this._deviceCharHeight)}_clipCell(t3,e3,i3){;this._ctx.beginPath(),this._ctx.rect(t3*this._deviceCellWidth,e3*this._deviceCellHeight,i3*this._deviceCellWidth,this._deviceCellHeight),this._ctx.clip()}_getFont(t3,e3,i3){return`${i3?"italic":""} ${e3?t3.options.fontWeightBold:t3.options.fontWeight} ${t3.options.fontSize*this._coreBrowserService.dpr}px ${t3.options.fontFamily}`}}e2.BaseRenderLayer=n},306(t2,e2,i2){;Object.defineProperty(e2,"__esModule",{value:true}),e2.LinkRenderLayer=void 0;const s2=i2(657),r=i2(814),a=i2(133);class o extends a.BaseRenderLayer{constructor(t3,e3,i3,s3,r2,a2,o2){;super(i3,t3,"link",e3,true,r2,a2,o2),this._register(s3.onShowLinkUnderline(t4=>this._handleShowLinkUnderline(t4))),this._register(s3.onHideLinkUnderline(t4=>this._handleHideLinkUnderline(t4)))}resize(t3,e3){;super.resize(t3,e3),this._state=void 0}reset(t3){this._clearCurrentLink()}_clearCurrentLink(){if(this._state){this._clearCells(this._state.x1,this._state.y1,this._state.cols-this._state.x1,1);const t3=this._state.y2-this._state.y1-1;t3>0&&this._clearCells(0,this._state.y1+1,this._state.cols,t3),this._clearCells(0,this._state.y2,this._state.x2,1),this._state=void 0}}_handleShowLinkUnderline(t3){if(t3.fg===r.INVERTED_DEFAULT_COLOR?this._ctx.fillStyle=this._themeService.colors.background.css:void 0!==t3.fg&&(0,s2.is256Color)(t3.fg)?this._ctx.fillStyle=this._themeService.colors.ansi[t3.fg].css:this._ctx.fillStyle=this._themeService.colors.foreground.css,t3.y1===t3.y2)this._fillBottomLineAtCells(t3.x1,t3.y1,t3.x2-t3.x1);else{this._fillBottomLineAtCells(t3.x1,t3.y1,t3.cols-t3.x1);for(let e3=t3.y1+1;e3{var t2=s;Object.defineProperty(t2,"__esModule",{value:true}),t2.WebglAddon=void 0;const e2=i(426),r=i(95),a=i(399),o=i(414);class n extends e2.Disposable{constructor(t3){if(r.isSafari&&(0,r.getSafariVersion)()<16){const t4={antialias:false,depth:false,preserveDrawingBuffer:true};if(!document.createElement("canvas").getContext("webgl2",t4))throw new Error("WebGL2 is only supported on Safari 16 and above")};super(),this._onChangeTextureAtlas=this._register(new o.Emitter),this.onChangeTextureAtlas=this._onChangeTextureAtlas.event,this._onAddTextureAtlasCanvas=this._register(new o.Emitter),this.onAddTextureAtlasCanvas=this._onAddTextureAtlasCanvas.event,this._onRemoveTextureAtlasCanvas=this._register(new o.Emitter),this.onRemoveTextureAtlasCanvas=this._onRemoveTextureAtlasCanvas.event,this._onContextLoss=this._register(new o.Emitter),this.onContextLoss=this._onContextLoss.event,this._customGlyphs=t3?.customGlyphs??true,this._preserveDrawingBuffer=t3?.preserveDrawingBuffer}activate(t3){const i2=t3._core;if(!t3.element)return void this._register(i2.onWillOpen(()=>this.activate(t3)));this._terminal=t3;const s2=i2.coreService,r2=i2.optionsService,n2=i2,L=n2._renderService,h=n2._characterJoinerService,T=n2._charSizeService,l=n2._coreBrowserService,d=n2._decorationService,c=n2._logService,_=n2._themeService;this._renderer=this._register(new a.WebglRenderer(t3,h,T,l,s2,d,c,r2,_,this._customGlyphs,this._preserveDrawingBuffer)),this._register(o.EventUtils.forward(this._renderer.onContextLoss,this._onContextLoss)),this._register(o.EventUtils.forward(this._renderer.onChangeTextureAtlas,this._onChangeTextureAtlas)),this._register(o.EventUtils.forward(this._renderer.onAddTextureAtlasCanvas,this._onAddTextureAtlasCanvas)),this._register(o.EventUtils.forward(this._renderer.onRemoveTextureAtlasCanvas,this._onRemoveTextureAtlasCanvas)),L.setRenderer(this._renderer),this._register((0,e2.toDisposable)(()=>{if(this._terminal._core._store._isDisposed)return;const e3=this._terminal._core._renderService;e3.setRenderer(this._terminal._core._createRenderer()),e3.handleResize(t3.cols,t3.rows)}))}get textureAtlas(){return this._renderer?.textureAtlas}clearTextureAtlas(){this._renderer?.clearTextureAtlas()}}t2.WebglAddon=n})(),s})()); -diff -ruN a/lib/addon-webgl.mjs b/lib/addon-webgl.mjs +diff --git a/lib/addon-webgl.mjs b/lib/addon-webgl.mjs --- a/lib/addon-webgl.mjs +++ b/lib/addon-webgl.mjs @@ -9,12 +9,7 @@ @@ -70,7 +70,7 @@ diff -ruN a/lib/addon-webgl.mjs b/lib/addon-webgl.mjs -}`,l0=8,Me=l0*Float32Array.BYTES_PER_ELEMENT,e1=20*l0,ne=class{constructor(){this.attributes=new Float32Array(e1),this.count=0}},p0=0,Lt=0,ht=0,dt=0,_t=0,ct=0,ut=0,ae=class extends W{constructor(t,r,s,a,o){super();this._terminal=t;this._gl=r;this._dimensions=s;this._themeService=a;this._logService=o;this._vertices=new ne;this._verticesCursor=new ne;let n=this._gl;this._program=g(ie(n,jt,Jt,this._logService)),this._register(M(()=>n.deleteProgram(this._program))),this._projectionLocation=g(n.getUniformLocation(this._program,"u_projection"));let L=this._vertexArrayObject=n.createVertexArray();this._register(M(()=>n.deleteVertexArray(L))),n.bindVertexArray(L);let T=new Float32Array([0,0,1,0,0,1,1,1]),h=n.createBuffer();this._register(M(()=>n.deleteBuffer(h))),n.bindBuffer(n.ARRAY_BUFFER,h),n.bufferData(n.ARRAY_BUFFER,T,n.STATIC_DRAW),n.enableVertexAttribArray(3),n.vertexAttribPointer(3,2,this._gl.FLOAT,!1,0,0);let d=new Uint8Array([0,1,2,3]),_=n.createBuffer();this._register(M(()=>n.deleteBuffer(_))),n.bindBuffer(n.ELEMENT_ARRAY_BUFFER,_),n.bufferData(n.ELEMENT_ARRAY_BUFFER,d,n.STATIC_DRAW),this._attributesBuffer=g(n.createBuffer()),this._register(M(()=>n.deleteBuffer(this._attributesBuffer))),n.bindBuffer(n.ARRAY_BUFFER,this._attributesBuffer),n.enableVertexAttribArray(0),n.vertexAttribPointer(0,2,n.FLOAT,!1,Me,0),n.vertexAttribDivisor(0,1),n.enableVertexAttribArray(1),n.vertexAttribPointer(1,2,n.FLOAT,!1,Me,2*Float32Array.BYTES_PER_ELEMENT),n.vertexAttribDivisor(1,1),n.enableVertexAttribArray(2),n.vertexAttribPointer(2,4,n.FLOAT,!1,Me,4*Float32Array.BYTES_PER_ELEMENT),n.vertexAttribDivisor(2,1),this._updateCachedColors(a.colors),this._register(this._themeService.onChangeColors(p=>{this._updateCachedColors(p),this._updateViewportRectangle()}))}renderBackgrounds(){this._renderVertices(this._vertices)}renderCursor(){this._renderVertices(this._verticesCursor)}_renderVertices(t){let r=this._gl;r.useProgram(this._program),r.bindVertexArray(this._vertexArrayObject),r.uniformMatrix4fv(this._projectionLocation,!1,te),r.bindBuffer(r.ARRAY_BUFFER,this._attributesBuffer),r.bufferData(r.ARRAY_BUFFER,t.attributes,r.DYNAMIC_DRAW),r.drawElementsInstanced(this._gl.TRIANGLE_STRIP,4,r.UNSIGNED_BYTE,0,t.count)}handleResize(){this._updateViewportRectangle()}setDimensions(t){this._dimensions=t}_updateCachedColors(t){this._bgFloat=this._colorToFloat32Array(t.background),this._cursorFloat=this._colorToFloat32Array(t.cursor)}_updateViewportRectangle(){this._addRectangleFloat(this._vertices.attributes,0,0,0,this._terminal.cols*this._dimensions.device.cell.width,this._terminal.rows*this._dimensions.device.cell.height,this._bgFloat)}updateBackgrounds(t){let r=this._terminal,s=this._vertices,a=1,o,n,L,T,h,d,_,p,c,u,C;for(o=0;o>24&255)/255,_t=(p0>>16&255)/255,ct=(p0>>8&255)/255,ut=1,this._addRectangle(t.attributes,r,Lt,ht,(n-o)*this._dimensions.device.cell.width,this._dimensions.device.cell.height,dt,_t,ct,ut)}_addRectangle(t,r,s,a,o,n,L,T,h,d){t[r]=s/this._dimensions.device.canvas.width,t[r+1]=a/this._dimensions.device.canvas.height,t[r+2]=o/this._dimensions.device.canvas.width,t[r+3]=n/this._dimensions.device.canvas.height,t[r+4]=L,t[r+5]=T,t[r+6]=h,t[r+7]=d}_addRectangleFloat(t,r,s,a,o,n,L){t[r]=s/this._dimensions.device.canvas.width,t[r+1]=a/this._dimensions.device.canvas.height,t[r+2]=o/this._dimensions.device.canvas.width,t[r+3]=n/this._dimensions.device.canvas.height,t[r+4]=L[0],t[r+5]=L[1],t[r+6]=L[2],t[r+7]=L[3]}_colorToFloat32Array(t){return new Float32Array([(t.rgba>>24&255)/255,(t.rgba>>16&255)/255,(t.rgba>>8&255)/255,(t.rgba&255)/255])}};var oe=class extends W{constructor(t,r,s,a,o,n,L,T){super();this._container=r;this._alpha=o;this._coreBrowserService=n;this._optionsService=L;this._themeService=T;this._deviceCharWidth=0;this._deviceCharHeight=0;this._deviceCellWidth=0;this._deviceCellHeight=0;this._deviceCharLeft=0;this._deviceCharTop=0;this._canvas=this._coreBrowserService.mainDocument.createElement("canvas"),this._canvas.classList.add(`xterm-${s}-layer`),this._canvas.style.zIndex=a.toString(),this._initCanvas(),this._container.appendChild(this._canvas),this._register(this._themeService.onChangeColors(h=>{this._refreshCharAtlas(t,h),this.reset(t)})),this._register(M(()=>{this._canvas.remove()}))}_initCanvas(){this._ctx=g(this._canvas.getContext("2d",{alpha:this._alpha})),this._alpha||this._clearAll()}handleBlur(t){}handleFocus(t){}handleCursorMove(t){}handleGridChanged(t,r,s){}handleSelectionChanged(t,r,s,a=!1){}_setTransparency(t,r){if(r===this._alpha)return;let s=this._canvas;this._alpha=r,this._canvas=this._canvas.cloneNode(),this._initCanvas(),this._container.replaceChild(this._canvas,s),this._refreshCharAtlas(t,this._themeService.colors),this.handleGridChanged(t,0,t.rows-1)}_refreshCharAtlas(t,r){this._deviceCharWidth<=0&&this._deviceCharHeight<=0||(this._charAtlas=Y0(t,this._optionsService.rawOptions,r,this._deviceCellWidth,this._deviceCellHeight,this._deviceCharWidth,this._deviceCharHeight,this._coreBrowserService.dpr,2048),this._charAtlas.warmUp())}resize(t,r){this._deviceCellWidth=r.device.cell.width,this._deviceCellHeight=r.device.cell.height,this._deviceCharWidth=r.device.char.width,this._deviceCharHeight=r.device.char.height,this._deviceCharLeft=r.device.char.left,this._deviceCharTop=r.device.char.top,this._canvas.width=r.device.canvas.width,this._canvas.height=r.device.canvas.height,this._canvas.style.width=`${r.css.canvas.width}px`,this._canvas.style.height=`${r.css.canvas.height}px`,this._alpha||this._clearAll(),this._refreshCharAtlas(t,this._themeService.colors)}_fillBottomLineAtCells(t,r,s=1){this._ctx.fillRect(t*this._deviceCellWidth,(r+1)*this._deviceCellHeight-this._coreBrowserService.dpr-1,s*this._deviceCellWidth,this._coreBrowserService.dpr)}_clearAll(){this._alpha?this._ctx.clearRect(0,0,this._canvas.width,this._canvas.height):(this._ctx.fillStyle=this._themeService.colors.background.css,this._ctx.fillRect(0,0,this._canvas.width,this._canvas.height))}_clearCells(t,r,s,a){this._alpha?this._ctx.clearRect(t*this._deviceCellWidth,r*this._deviceCellHeight,s*this._deviceCellWidth,a*this._deviceCellHeight):(this._ctx.fillStyle=this._themeService.colors.background.css,this._ctx.fillRect(t*this._deviceCellWidth,r*this._deviceCellHeight,s*this._deviceCellWidth,a*this._deviceCellHeight))}_fillCharTrueColor(t,r,s,a){this._ctx.font=this._getFont(t,!1,!1),this._ctx.textBaseline=K0,this._clipCell(s,a,r.getWidth()),this._ctx.fillText(r.getChars(),s*this._deviceCellWidth+this._deviceCharLeft,a*this._deviceCellHeight+this._deviceCharTop+this._deviceCharHeight)}_clipCell(t,r,s){this._ctx.beginPath(),this._ctx.rect(t*this._deviceCellWidth,r*this._deviceCellHeight,s*this._deviceCellWidth,this._deviceCellHeight),this._ctx.clip()}_getFont(t,r,s){let a=r?t.options.fontWeightBold:t.options.fontWeight;return`${s?"italic":""} ${a} ${t.options.fontSize*this._coreBrowserService.dpr}px ${t.options.fontFamily}`}};var Te=class extends oe{constructor(e,t,r,s,a,o,n){super(r,e,"link",t,!0,a,o,n),this._register(s.onShowLinkUnderline(L=>this._handleShowLinkUnderline(L))),this._register(s.onHideLinkUnderline(L=>this._handleHideLinkUnderline(L)))}resize(e,t){super.resize(e,t),this._state=void 0}reset(e){this._clearCurrentLink()}_clearCurrentLink(){if(this._state){this._clearCells(this._state.x1,this._state.y1,this._state.cols-this._state.x1,1);let e=this._state.y2-this._state.y1-1;e>0&&this._clearCells(0,this._state.y1+1,this._state.cols,e),this._clearCells(0,this._state.y2,this._state.x2,1),this._state=void 0}}_handleShowLinkUnderline(e){if(e.fg===257?this._ctx.fillStyle=this._themeService.colors.background.css:e.fg!==void 0&&st(e.fg)?this._ctx.fillStyle=this._themeService.colors.ansi[e.fg].css:this._ctx.fillStyle=this._themeService.colors.foreground.css,e.y1===e.y2)this._fillBottomLineAtCells(e.x1,e.y1,e.x2-e.x1);else{this._fillBottomLineAtCells(e.x1,e.y1,e.cols-e.x1);for(let t=e.y1+1;tthis._handleColorChange())),this._cellColorResolver=new G0(this._terminal,this._optionsService,this._model.selection,this._decorationService,this._coreBrowserService,this._themeService),this._core=this._terminal._core,this._renderLayers=[new Te(this._core.screenElement,2,this._terminal,this._core.linkifier,this._coreBrowserService,T,this._themeService)],this.dimensions=Ue(),this._devicePixelRatio=this._coreBrowserService.dpr,this._updateDimensions(),this._updateCursorBlink(),this._register(T.onOptionChange(()=>this._handleOptionsChanged())),this._textBlinkStateManager=this._register(new J0(()=>this._requestRedrawViewport(),this._coreBrowserService,this._optionsService)),this._resetBlinkingRowState(),this._deviceMaxTextureSize=this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE),this._register(le(this._canvas,"webglcontextlost",c=>{this._logService.debug("webglcontextlost event received"),c.preventDefault(),this._contextRestorationTimeout=setTimeout(()=>{this._contextRestorationTimeout=void 0,this._logService.warn("webgl context not restored; firing onContextLoss"),this._onContextLoss.fire(c)},3e3)})),this._register(le(this._canvas,"webglcontextrestored",c=>{this._logService.warn("webglcontextrestored event received"),clearTimeout(this._contextRestorationTimeout),this._contextRestorationTimeout=void 0,be(this._terminal),this._initializeWebGLState(),this._requestRedrawViewport()})),this._observerDisposable.value=ve(this._canvas,this._coreBrowserService.window,(c,u)=>this._setCanvasDevicePixelDimensions(c,u)),this._register(this._coreBrowserService.onWindowChange(c=>{this._observerDisposable.value=ve(this._canvas,c,(u,C)=>this._setCanvasDevicePixelDimensions(u,C))})),this._register(le(this._coreBrowserService.mainDocument,"mousedown",()=>this._cursorBlinkStateManager.value?.restartBlinkAnimation())),this._core.screenElement.appendChild(this._canvas),[this._rectangleRenderer.value,this._glyphRenderer.value]=this._initializeWebGLState(),this._isAttached=this._core.screenElement.isConnected,this._register(M(()=>{clearTimeout(this._contextRestorationTimeout),this._contextRestorationTimeout=void 0;for(let c of this._renderLayers)c.dispose();this._canvas.parentElement?.removeChild(this._canvas),be(this._terminal)}))}get textureAtlas(){return this._charAtlas?.pages[0].canvas}_handleColorChange(){this._refreshCharAtlas(),this._clearModel(!0)}handleDevicePixelRatioChange(){this._devicePixelRatio!==this._coreBrowserService.dpr&&(this._devicePixelRatio=this._coreBrowserService.dpr,this.handleResize(this._terminal.cols,this._terminal.rows))}handleResize(t,r){this._updateDimensions(),this._model.resize(this._terminal.cols,this._terminal.rows),this._resetBlinkingRowState();for(let s of this._renderLayers)s.resize(this._terminal,this.dimensions);this._canvas.width=this.dimensions.device.canvas.width,this._canvas.height=this.dimensions.device.canvas.height,this._canvas.style.width=`${this.dimensions.css.canvas.width}px`,this._canvas.style.height=`${this.dimensions.css.canvas.height}px`,this._core.screenElement.style.width=`${this.dimensions.css.canvas.width}px`,this._core.screenElement.style.height=`${this.dimensions.css.canvas.height}px`,this._rectangleRenderer.value?.setDimensions(this.dimensions),this._rectangleRenderer.value?.handleResize(),this._glyphRenderer.value?.setDimensions(this.dimensions),this._glyphRenderer.value?.handleResize(),this._refreshCharAtlas(),this._clearModel(!1),this._onRequestRedraw.fire({start:0,end:this._terminal.rows-1,sync:!0})}handleCharSizeChanged(){this.handleResize(this._terminal.cols,this._terminal.rows)}handleBlur(){for(let t of this._renderLayers)t.handleBlur(this._terminal);this._cursorBlinkStateManager.value?.pause(),this._requestRedrawViewport()}handleFocus(){for(let t of this._renderLayers)t.handleFocus(this._terminal);this._cursorBlinkStateManager.value?.resume(),this._requestRedrawViewport()}handleViewportVisibilityChange(t){this._textBlinkStateManager.setViewportVisible(t)}handleSelectionChanged(t,r,s){for(let a of this._renderLayers)a.handleSelectionChanged(this._terminal,t,r,s);this._model.selection.update(this._core,t,r,s),this._requestRedrawViewport()}handleCursorMove(){for(let t of this._renderLayers)t.handleCursorMove(this._terminal);this._cursorBlinkStateManager.value?.restartBlinkAnimation()}_handleOptionsChanged(){this._updateDimensions(),this._refreshCharAtlas(),this._updateCursorBlink()}_initializeWebGLState(){return this._rectangleRenderer.value=new ae(this._terminal,this._gl,this.dimensions,this._themeService,this._logService),this._glyphRenderer.value=new re(this._terminal,this._gl,this.dimensions,this._optionsService,this._logService),this.handleCharSizeChanged(),[this._rectangleRenderer.value,this._glyphRenderer.value]}_refreshCharAtlas(){if(this.dimensions.device.char.width<=0&&this.dimensions.device.char.height<=0){this._isAttached=!1;return}let t=Y0(this._terminal,this._optionsService.rawOptions,this._themeService.colors,this.dimensions.device.cell.width,this.dimensions.device.cell.height,this.dimensions.device.char.width,this.dimensions.device.char.height,this._coreBrowserService.dpr,this._deviceMaxTextureSize,this._customGlyphs);this._charAtlas!==t&&(this._onChangeTextureAtlas.fire(t.pages[0].canvas),this._charAtlasDisposable.value=De(c0.forward(t.onAddTextureAtlasCanvas,this._onAddTextureAtlasCanvas),c0.forward(t.onRemoveTextureAtlasCanvas,this._onRemoveTextureAtlasCanvas))),this._charAtlas=t,this._charAtlas.warmUp(),this._glyphRenderer.value?.setAtlas(this._charAtlas)}_clearModel(t){this._model.clear(),t&&this._glyphRenderer.value?.clear()}clearTextureAtlas(){this._charAtlas?.clearTexture(),this._clearModel(!0),this._requestRedrawViewport()}clear(){this._clearModel(!0);for(let t of this._renderLayers)t.reset(this._terminal);this._resetBlinkingRowState(),this._textBlinkStateManager.setNeedsBlinkInViewport(!1),this._cursorBlinkStateManager.value?.restartBlinkAnimation(),this._updateCursorBlink()}renderRows(t,r){if(!this._isAttached)if(this._core.screenElement?.isConnected&&this._charSizeService.width&&this._charSizeService.height)this._updateDimensions(),this._refreshCharAtlas(),this._isAttached=!0;else return;for(let o of this._renderLayers)o.handleGridChanged(this._terminal,t,r);if(!this._glyphRenderer.value||!this._rectangleRenderer.value)return;this._glyphRenderer.value.beginFrame()?(this._clearModel(!0),this._updateModel(0,this._terminal.rows-1)):this._updateModel(t,r);let s=!1,a=0;for(;this._charAtlas&&this._glyphRenderer.value.beginFrame()&&a++<32;)s=!0,this._clearModel(!0),this._updateModel(0,this._terminal.rows-1);s&&this._glyphRenderer.value.invalidateAtlasTextures(),this._rectangleRenderer.value.renderBackgrounds(),this._glyphRenderer.value.render(this._model),(!this._cursorBlinkStateManager.value||this._cursorBlinkStateManager.value.isCursorVisible)&&this._rectangleRenderer.value.renderCursor()}_updateCursorBlink(){this._coreService.decPrivateModes.cursorBlink??this._terminal.options.cursorBlink?this._cursorBlinkStateManager.value=new q0(()=>{this._requestRedrawCursor()},this._coreBrowserService):this._cursorBlinkStateManager.clear(),this._requestRedrawCursor()}_updateModel(t,r){let s=this._core,a=this._workCell,o,n,L,T,h,d,_,p,c,u,C,Q,m,R,F,E,y;t=pt(t,s.rows-1,0),r=pt(r,s.rows-1,0);let Y=this._coreService.decPrivateModes.cursorStyle??s.options.cursorStyle??"block",X=this._terminal.buffer.active.baseY+this._terminal.buffer.active.cursorY,A0=X-s.buffer.ydisp,t0=Math.min(this._terminal.buffer.active.cursorX,s.cols-1),L0=-1,A=this._coreService.isCursorInitialized&&!this._coreService.isCursorHidden&&(!this._cursorBlinkStateManager.value||this._cursorBlinkStateManager.value.isCursorVisible);this._model.cursor=void 0;let n0=!1;for(n=t;n<=r;n++){L=n+s.buffer.ydisp;let a0=s.buffer.lines.get(L);if(!a0){for(this._model.lineLengths[n]=0,E=0;E=_,c=E,h.length>0&&E===h[0][0]&&p){u=h.shift();let q=this._model.selection.isCellSelected(this._terminal,u[0],L);for(F=u[0]+1;F=u[1],p?(d=!0,a=new ge(a,T.translateToString(!0,u[0],u[1]),u[1]-u[0]),c=u[1]-1):_=u[1]}if(Q=a.getChars(),m=a.getCode(),F=(n*s.cols+E)*4,!$&&a.isBlink()&&($=!0),this._cellColorResolver.resolve(a,E,L,this.dimensions.device.cell.width,this.dimensions.device.cell.height),A&&L===X&&(E===t0&&(this._model.cursor={x:t0,y:A0,width:a.getWidth(),style:this._coreBrowserService.isFocused?Y:s.options.cursorInactiveStyle,cursorWidth:s.options.cursorWidth,dpr:this._devicePixelRatio},L0=t0+a.getWidth()-1),E>=t0&&E<=L0&&(this._coreBrowserService.isFocused&&Y==="block"||this._coreBrowserService.isFocused===!1&&s.options.cursorInactiveStyle==="block")&&(this._cellColorResolver.result.fg=50331648|this._themeService.colors.cursorAccent.rgba>>8&16777215,this._cellColorResolver.result.bg=50331648|this._themeService.colors.cursor.rgba>>8&16777215)),this._textBlinkStateManager.isEnabled&&!this._textBlinkStateManager.isBlinkOn&&a.isBlink()&&(this._cellColorResolver.result.fg|=1073741824),m!==0&&(this._model.lineLengths[n]=E+1),!(this._model.cells[F]===m&&this._model.cells[F+1]===this._cellColorResolver.result.bg&&this._model.cells[F+2]===this._cellColorResolver.result.fg&&this._model.cells[F+3]===this._cellColorResolver.result.ext)&&(n0=!0,Q.length>1&&(m|=lt),this._model.cells[F]=m,this._model.cells[F+1]=this._cellColorResolver.result.bg,this._model.cells[F+2]=this._cellColorResolver.result.fg,this._model.cells[F+3]=this._cellColorResolver.result.ext,R=a.getWidth(),this._glyphRenderer.value.updateCell(E,n,m,this._cellColorResolver.result.bg,this._cellColorResolver.result.fg,this._cellColorResolver.result.ext,Q,R,o),d)){for(a=this._workCell,E++;E<=c;E++)y=(n*s.cols+E)*4,this._nullModelCell(E,n,y,this._cellColorResolver.result.bg,this._cellColorResolver.result.fg,this._cellColorResolver.result.ext);E--}}this._setRowBlinkState(n,$)}n0&&this._rectangleRenderer.value.updateBackgrounds(this._model),this._rectangleRenderer.value.updateCursor(this._model),this._updateTextBlinkState()}_nullModelCell(t,r,s,a,o,n){this._glyphRenderer.value.updateCell(t,r,0,a,o,n,"",0,0),this._model.cells[s]=0,this._model.cells[s+1]=a,this._model.cells[s+2]=o,this._model.cells[s+3]=n}_resetBlinkingRowState(){this._rowHasBlinkingCells=new Array(this._terminal.rows).fill(!1),this._rowHasBlinkingCellsCount=0}_setRowBlinkState(t,r){this._rowHasBlinkingCells[t]!==r&&(this._rowHasBlinkingCells[t]=r,this._rowHasBlinkingCellsCount+=r?1:-1)}_updateTextBlinkState(){this._textBlinkStateManager.setNeedsBlinkInViewport(this._rowHasBlinkingCellsCount>0)}_updateDimensions(){!this._charSizeService.width||!this._charSizeService.height||(this.dimensions.device.char.width=Math.floor(this._charSizeService.width*this._devicePixelRatio),this.dimensions.device.char.height=Math.ceil(this._charSizeService.height*this._devicePixelRatio),this.dimensions.device.cell.height=Math.floor(this.dimensions.device.char.height*this._optionsService.rawOptions.lineHeight),this.dimensions.device.char.top=this._optionsService.rawOptions.lineHeight===1?0:Math.round((this.dimensions.device.cell.height-this.dimensions.device.char.height)/2),this.dimensions.device.cell.width=this.dimensions.device.char.width+Math.round(this._optionsService.rawOptions.letterSpacing),this.dimensions.device.char.left=Math.floor(this._optionsService.rawOptions.letterSpacing/2),this.dimensions.device.canvas.height=this._terminal.rows*this.dimensions.device.cell.height,this.dimensions.device.canvas.width=this._terminal.cols*this.dimensions.device.cell.width,this.dimensions.css.canvas.height=Math.round(this.dimensions.device.canvas.height/this._devicePixelRatio),this.dimensions.css.canvas.width=Math.round(this.dimensions.device.canvas.width/this._devicePixelRatio),this.dimensions.css.cell.height=this.dimensions.device.cell.height/this._devicePixelRatio,this.dimensions.css.cell.width=this.dimensions.device.cell.width/this._devicePixelRatio)}_setCanvasDevicePixelDimensions(t,r){this._canvas.width===t&&this._canvas.height===r||(this._canvas.width=t,this._canvas.height=r,this._onRequestRedraw.fire({start:0,end:this._terminal.rows-1,sync:!0}))}_requestRedrawViewport(){this._onRequestRedraw.fire({start:0,end:this._terminal.rows-1})}_requestRedrawCursor(){let t=this._terminal.buffer.active.cursorY;this._onRequestRedraw.fire({start:t,end:t})}},ge=class extends s0{constructor(t,r,s){super();this.content=0;this.combinedData="";this.fg=t.fg,this.bg=t.bg,this.combinedData=r,this._width=s}isCombined(){return 2097152}getWidth(){return this._width}getChars(){return this.combinedData}getCode(){return 2097151}setFromCharData(t){throw new Error("not implemented")}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}};function pt(i,e,t=0){return Math.max(Math.min(i,e),t)}var Ct=class extends W{constructor(t){if(ue&&He()<16){let r={antialias:!1,depth:!1,preserveDrawingBuffer:!0};if(!document.createElement("canvas").getContext("webgl2",r))throw new Error("WebGL2 is only supported on Safari 16 and above")}super();this._onChangeTextureAtlas=this._register(new U);this.onChangeTextureAtlas=this._onChangeTextureAtlas.event;this._onAddTextureAtlasCanvas=this._register(new U);this.onAddTextureAtlasCanvas=this._onAddTextureAtlasCanvas.event;this._onRemoveTextureAtlasCanvas=this._register(new U);this.onRemoveTextureAtlasCanvas=this._onRemoveTextureAtlasCanvas.event;this._onContextLoss=this._register(new U);this.onContextLoss=this._onContextLoss.event;this._customGlyphs=t?.customGlyphs??!0,this._preserveDrawingBuffer=t?.preserveDrawingBuffer}activate(t){let r=t._core;if(!t.element){this._register(r.onWillOpen(()=>this.activate(t)));return}this._terminal=t;let s=r.coreService,a=r.optionsService,o=r,n=o._renderService,L=o._characterJoinerService,T=o._charSizeService,h=o._coreBrowserService,d=o._decorationService,_=o._logService,p=o._themeService;this._renderer=this._register(new Le(t,L,T,h,s,d,_,a,p,this._customGlyphs,this._preserveDrawingBuffer)),this._register(c0.forward(this._renderer.onContextLoss,this._onContextLoss)),this._register(c0.forward(this._renderer.onChangeTextureAtlas,this._onChangeTextureAtlas)),this._register(c0.forward(this._renderer.onAddTextureAtlasCanvas,this._onAddTextureAtlasCanvas)),this._register(c0.forward(this._renderer.onRemoveTextureAtlasCanvas,this._onRemoveTextureAtlasCanvas)),n.setRenderer(this._renderer),this._register(M(()=>{if(this._terminal._core._store._isDisposed)return;let c=this._terminal._core._renderService;c.setRenderer(this._terminal._core._createRenderer()),c.handleResize(t.cols,t.rows)}))}get textureAtlas(){return this._renderer?.textureAtlas}clearTextureAtlas(){this._renderer?.clearTextureAtlas()}};export{Ct as WebglAddon}; -//# sourceMappingURL=addon-webgl.mjs.map +}`,l0=8,Me=l0*Float32Array.BYTES_PER_ELEMENT,e1=20*l0,ne=class{constructor(){;this.attributes=new Float32Array(e1),this.count=0}},p0=0,Lt=0,ht=0,dt=0,_t=0,ct=0,ut=0,ae=class extends W{constructor(t,r,s,a,o){super();this._terminal=t;this._gl=r;this._dimensions=s;this._themeService=a;this._logService=o;this._vertices=new ne;this._verticesCursor=new ne;let n=this._gl;this._program=g(ie(n,jt,Jt,this._logService)),this._register(M(()=>n.deleteProgram(this._program))),this._projectionLocation=g(n.getUniformLocation(this._program,"u_projection"));let L=this._vertexArrayObject=n.createVertexArray();this._register(M(()=>n.deleteVertexArray(L))),n.bindVertexArray(L);let T=new Float32Array([0,0,1,0,0,1,1,1]),h=n.createBuffer();this._register(M(()=>n.deleteBuffer(h))),n.bindBuffer(n.ARRAY_BUFFER,h),n.bufferData(n.ARRAY_BUFFER,T,n.STATIC_DRAW),n.enableVertexAttribArray(3),n.vertexAttribPointer(3,2,this._gl.FLOAT,false,0,0);let d=new Uint8Array([0,1,2,3]),_=n.createBuffer();this._register(M(()=>n.deleteBuffer(_))),n.bindBuffer(n.ELEMENT_ARRAY_BUFFER,_),n.bufferData(n.ELEMENT_ARRAY_BUFFER,d,n.STATIC_DRAW),this._attributesBuffer=g(n.createBuffer()),this._register(M(()=>n.deleteBuffer(this._attributesBuffer))),n.bindBuffer(n.ARRAY_BUFFER,this._attributesBuffer),n.enableVertexAttribArray(0),n.vertexAttribPointer(0,2,n.FLOAT,false,Me,0),n.vertexAttribDivisor(0,1),n.enableVertexAttribArray(1),n.vertexAttribPointer(1,2,n.FLOAT,false,Me,2*Float32Array.BYTES_PER_ELEMENT),n.vertexAttribDivisor(1,1),n.enableVertexAttribArray(2),n.vertexAttribPointer(2,4,n.FLOAT,false,Me,4*Float32Array.BYTES_PER_ELEMENT),n.vertexAttribDivisor(2,1),this._updateCachedColors(a.colors),this._register(this._themeService.onChangeColors(p=>{;this._updateCachedColors(p),this._updateViewportRectangle()}))}renderBackgrounds(){this._renderVertices(this._vertices)}renderCursor(){this._renderVertices(this._verticesCursor)}_renderVertices(t){let r=this._gl;r.useProgram(this._program),r.bindVertexArray(this._vertexArrayObject),r.uniformMatrix4fv(this._projectionLocation,false,te),r.bindBuffer(r.ARRAY_BUFFER,this._attributesBuffer),r.bufferData(r.ARRAY_BUFFER,t.attributes,r.DYNAMIC_DRAW),r.drawElementsInstanced(this._gl.TRIANGLE_STRIP,4,r.UNSIGNED_BYTE,0,t.count)}handleResize(){this._updateViewportRectangle()}setDimensions(t){this._dimensions=t}_updateCachedColors(t){;this._bgFloat=this._colorToFloat32Array(t.background),this._cursorFloat=this._colorToFloat32Array(t.cursor)}_updateViewportRectangle(){this._addRectangleFloat(this._vertices.attributes,0,0,0,this._terminal.cols*this._dimensions.device.cell.width,this._terminal.rows*this._dimensions.device.cell.height,this._bgFloat)}updateBackgrounds(t){let r=this._terminal,s=this._vertices,a=1,o,n,L,T,h,d,_,p,c,u,C;for(o=0;o>24&255)/255,_t=(p0>>16&255)/255,ct=(p0>>8&255)/255,ut=1,this._addRectangle(t.attributes,r,Lt,ht,(n-o)*this._dimensions.device.cell.width,this._dimensions.device.cell.height,dt,_t,ct,ut)}_addRectangle(t,r,s,a,o,n,L,T,h,d){;t[r]=s/this._dimensions.device.canvas.width,t[r+1]=a/this._dimensions.device.canvas.height,t[r+2]=o/this._dimensions.device.canvas.width,t[r+3]=n/this._dimensions.device.canvas.height,t[r+4]=L,t[r+5]=T,t[r+6]=h,t[r+7]=d}_addRectangleFloat(t,r,s,a,o,n,L){;t[r]=s/this._dimensions.device.canvas.width,t[r+1]=a/this._dimensions.device.canvas.height,t[r+2]=o/this._dimensions.device.canvas.width,t[r+3]=n/this._dimensions.device.canvas.height,t[r+4]=L[0],t[r+5]=L[1],t[r+6]=L[2],t[r+7]=L[3]}_colorToFloat32Array(t){return new Float32Array([(t.rgba>>24&255)/255,(t.rgba>>16&255)/255,(t.rgba>>8&255)/255,(t.rgba&255)/255])}};var oe=class extends W{constructor(t,r,s,a,o,n,L,T){super();this._container=r;this._alpha=o;this._coreBrowserService=n;this._optionsService=L;this._themeService=T;this._deviceCharWidth=0;this._deviceCharHeight=0;this._deviceCellWidth=0;this._deviceCellHeight=0;this._deviceCharLeft=0;this._deviceCharTop=0;this._canvas=this._coreBrowserService.mainDocument.createElement("canvas"),this._canvas.classList.add(`xterm-${s}-layer`),this._canvas.style.zIndex=a.toString(),this._initCanvas(),this._container.appendChild(this._canvas),this._register(this._themeService.onChangeColors(h=>{;this._refreshCharAtlas(t,h),this.reset(t)})),this._register(M(()=>{this._canvas.remove()}))}_initCanvas(){;this._ctx=g(this._canvas.getContext("2d",{alpha:this._alpha})),this._alpha||this._clearAll()}handleBlur(t){}handleFocus(t){}handleCursorMove(t){}handleGridChanged(t,r,s){}handleSelectionChanged(t,r,s,a=false){}_setTransparency(t,r){if(r===this._alpha)return;let s=this._canvas;this._alpha=r,this._canvas=this._canvas.cloneNode(),this._initCanvas(),this._container.replaceChild(this._canvas,s),this._refreshCharAtlas(t,this._themeService.colors),this.handleGridChanged(t,0,t.rows-1)}_refreshCharAtlas(t,r){;this._deviceCharWidth<=0&&this._deviceCharHeight<=0||(this._charAtlas=Y0(t,this._optionsService.rawOptions,r,this._deviceCellWidth,this._deviceCellHeight,this._deviceCharWidth,this._deviceCharHeight,this._coreBrowserService.dpr,2048),this._charAtlas.warmUp())}resize(t,r){;this._deviceCellWidth=r.device.cell.width,this._deviceCellHeight=r.device.cell.height,this._deviceCharWidth=r.device.char.width,this._deviceCharHeight=r.device.char.height,this._deviceCharLeft=r.device.char.left,this._deviceCharTop=r.device.char.top,this._canvas.width=r.device.canvas.width,this._canvas.height=r.device.canvas.height,this._canvas.style.width=`${r.css.canvas.width}px`,this._canvas.style.height=`${r.css.canvas.height}px`,this._alpha||this._clearAll(),this._refreshCharAtlas(t,this._themeService.colors)}_fillBottomLineAtCells(t,r,s=1){this._ctx.fillRect(t*this._deviceCellWidth,(r+1)*this._deviceCellHeight-this._coreBrowserService.dpr-1,s*this._deviceCellWidth,this._coreBrowserService.dpr)}_clearAll(){this._alpha?this._ctx.clearRect(0,0,this._canvas.width,this._canvas.height):(this._ctx.fillStyle=this._themeService.colors.background.css,this._ctx.fillRect(0,0,this._canvas.width,this._canvas.height))}_clearCells(t,r,s,a){this._alpha?this._ctx.clearRect(t*this._deviceCellWidth,r*this._deviceCellHeight,s*this._deviceCellWidth,a*this._deviceCellHeight):(this._ctx.fillStyle=this._themeService.colors.background.css,this._ctx.fillRect(t*this._deviceCellWidth,r*this._deviceCellHeight,s*this._deviceCellWidth,a*this._deviceCellHeight))}_fillCharTrueColor(t,r,s,a){;this._ctx.font=this._getFont(t,false,false),this._ctx.textBaseline=K0,this._clipCell(s,a,r.getWidth()),this._ctx.fillText(r.getChars(),s*this._deviceCellWidth+this._deviceCharLeft,a*this._deviceCellHeight+this._deviceCharTop+this._deviceCharHeight)}_clipCell(t,r,s){;this._ctx.beginPath(),this._ctx.rect(t*this._deviceCellWidth,r*this._deviceCellHeight,s*this._deviceCellWidth,this._deviceCellHeight),this._ctx.clip()}_getFont(t,r,s){let a=r?t.options.fontWeightBold:t.options.fontWeight;return`${s?"italic":""} ${a} ${t.options.fontSize*this._coreBrowserService.dpr}px ${t.options.fontFamily}`}};var Te=class extends oe{constructor(e,t,r,s,a,o,n){;super(r,e,"link",t,true,a,o,n),this._register(s.onShowLinkUnderline(L=>this._handleShowLinkUnderline(L))),this._register(s.onHideLinkUnderline(L=>this._handleHideLinkUnderline(L)))}resize(e,t){;super.resize(e,t),this._state=void 0}reset(e){this._clearCurrentLink()}_clearCurrentLink(){if(this._state){this._clearCells(this._state.x1,this._state.y1,this._state.cols-this._state.x1,1);let e=this._state.y2-this._state.y1-1;e>0&&this._clearCells(0,this._state.y1+1,this._state.cols,e),this._clearCells(0,this._state.y2,this._state.x2,1),this._state=void 0}}_handleShowLinkUnderline(e){if(e.fg===257?this._ctx.fillStyle=this._themeService.colors.background.css:e.fg!==void 0&&st(e.fg)?this._ctx.fillStyle=this._themeService.colors.ansi[e.fg].css:this._ctx.fillStyle=this._themeService.colors.foreground.css,e.y1===e.y2)this._fillBottomLineAtCells(e.x1,e.y1,e.x2-e.x1);else{this._fillBottomLineAtCells(e.x1,e.y1,e.cols-e.x1);for(let t=e.y1+1;tthis._handleColorChange())),this._cellColorResolver=new G0(this._terminal,this._optionsService,this._model.selection,this._decorationService,this._coreBrowserService,this._themeService),this._core=this._terminal._core,this._renderLayers=[new Te(this._core.screenElement,2,this._terminal,this._core.linkifier,this._coreBrowserService,T,this._themeService)],this.dimensions=Ue(),this._devicePixelRatio=this._coreBrowserService.dpr,this._updateDimensions(),this._updateCursorBlink(),this._register(T.onOptionChange(()=>this._handleOptionsChanged())),this._textBlinkStateManager=this._register(new J0(()=>this._requestRedrawViewport(),this._coreBrowserService,this._optionsService)),this._resetBlinkingRowState(),this._deviceMaxTextureSize=this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE),this._register(le(this._canvas,"webglcontextlost",c=>{;this._logService.debug("webglcontextlost event received"),c.preventDefault(),this._contextRestorationTimeout=setTimeout(()=>{;this._contextRestorationTimeout=void 0,this._logService.warn("webgl context not restored; firing onContextLoss"),this._onContextLoss.fire(c)},3e3)})),this._register(le(this._canvas,"webglcontextrestored",c=>{;this._logService.warn("webglcontextrestored event received"),clearTimeout(this._contextRestorationTimeout),this._contextRestorationTimeout=void 0,be(this._terminal),this._initializeWebGLState(),this._requestRedrawViewport()})),this._observerDisposable.value=ve(this._canvas,this._coreBrowserService.window,(c,u)=>this._setCanvasDevicePixelDimensions(c,u)),this._register(this._coreBrowserService.onWindowChange(c=>{this._observerDisposable.value=ve(this._canvas,c,(u,C)=>this._setCanvasDevicePixelDimensions(u,C))})),this._register(le(this._coreBrowserService.mainDocument,"mousedown",()=>this._cursorBlinkStateManager.value?.restartBlinkAnimation())),this._core.screenElement.appendChild(this._canvas),[this._rectangleRenderer.value,this._glyphRenderer.value]=this._initializeWebGLState(),this._isAttached=this._core.screenElement.isConnected,this._register(M(()=>{;clearTimeout(this._contextRestorationTimeout),this._contextRestorationTimeout=void 0;for(let c of this._renderLayers)c.dispose();this._canvas.parentElement?.removeChild(this._canvas),be(this._terminal)}))}get textureAtlas(){return this._charAtlas?.pages[0].canvas}_handleColorChange(){;this._refreshCharAtlas(),this._clearModel(true)}handleDevicePixelRatioChange(){this._devicePixelRatio!==this._coreBrowserService.dpr&&(this._devicePixelRatio=this._coreBrowserService.dpr,this.handleResize(this._terminal.cols,this._terminal.rows))}handleResize(t,r){;this._updateDimensions(),this._model.resize(this._terminal.cols,this._terminal.rows),this._resetBlinkingRowState();for(let s of this._renderLayers)s.resize(this._terminal,this.dimensions);this._canvas.width=this.dimensions.device.canvas.width,this._canvas.height=this.dimensions.device.canvas.height,this._canvas.style.width=`${this.dimensions.css.canvas.width}px`,this._canvas.style.height=`${this.dimensions.css.canvas.height}px`,this._core.screenElement.style.width=`${this.dimensions.css.canvas.width}px`,this._core.screenElement.style.height=`${this.dimensions.css.canvas.height}px`,this._rectangleRenderer.value?.setDimensions(this.dimensions),this._rectangleRenderer.value?.handleResize(),this._glyphRenderer.value?.setDimensions(this.dimensions),this._glyphRenderer.value?.handleResize(),this._refreshCharAtlas(),this._clearModel(false),this._onRequestRedraw.fire({start:0,end:this._terminal.rows-1,sync:true})}handleCharSizeChanged(){this.handleResize(this._terminal.cols,this._terminal.rows)}handleBlur(){for(let t of this._renderLayers)t.handleBlur(this._terminal);this._cursorBlinkStateManager.value?.pause(),this._requestRedrawViewport()}handleFocus(){for(let t of this._renderLayers)t.handleFocus(this._terminal);this._cursorBlinkStateManager.value?.resume(),this._requestRedrawViewport()}handleViewportVisibilityChange(t){this._textBlinkStateManager.setViewportVisible(t)}handleSelectionChanged(t,r,s){for(let a of this._renderLayers)a.handleSelectionChanged(this._terminal,t,r,s);this._model.selection.update(this._core,t,r,s),this._requestRedrawViewport()}handleCursorMove(){for(let t of this._renderLayers)t.handleCursorMove(this._terminal);this._cursorBlinkStateManager.value?.restartBlinkAnimation()}_handleOptionsChanged(){;this._updateDimensions(),this._refreshCharAtlas(),this._updateCursorBlink()}_initializeWebGLState(){return this._rectangleRenderer.value=new ae(this._terminal,this._gl,this.dimensions,this._themeService,this._logService),this._glyphRenderer.value=new re(this._terminal,this._gl,this.dimensions,this._optionsService,this._logService),this.handleCharSizeChanged(),[this._rectangleRenderer.value,this._glyphRenderer.value]}_refreshCharAtlas(){if(this.dimensions.device.char.width<=0&&this.dimensions.device.char.height<=0){this._isAttached=false;return}let t=Y0(this._terminal,this._optionsService.rawOptions,this._themeService.colors,this.dimensions.device.cell.width,this.dimensions.device.cell.height,this.dimensions.device.char.width,this.dimensions.device.char.height,this._coreBrowserService.dpr,this._deviceMaxTextureSize,this._customGlyphs);this._charAtlas!==t&&(this._onChangeTextureAtlas.fire(t.pages[0].canvas),this._charAtlasDisposable.value=De(c0.forward(t.onAddTextureAtlasCanvas,this._onAddTextureAtlasCanvas),c0.forward(t.onRemoveTextureAtlasCanvas,this._onRemoveTextureAtlasCanvas))),this._charAtlas=t,this._charAtlas.warmUp(),this._glyphRenderer.value?.setAtlas(this._charAtlas)}_clearModel(t){;this._model.clear(),t&&this._glyphRenderer.value?.clear()}clearTextureAtlas(){;this._charAtlas?.clearTexture(),this._clearModel(true),this._requestRedrawViewport()}clear(){this._clearModel(true);for(let t of this._renderLayers)t.reset(this._terminal);this._resetBlinkingRowState(),this._textBlinkStateManager.setNeedsBlinkInViewport(false),this._cursorBlinkStateManager.value?.restartBlinkAnimation(),this._updateCursorBlink()}renderRows(t,r){if(!this._isAttached)if(this._core.screenElement?.isConnected&&this._charSizeService.width&&this._charSizeService.height)this._updateDimensions(),this._refreshCharAtlas(),this._isAttached=true;else return;for(let o of this._renderLayers)o.handleGridChanged(this._terminal,t,r);if(!this._glyphRenderer.value||!this._rectangleRenderer.value)return;this._glyphRenderer.value.beginFrame()?(this._clearModel(true),this._updateModel(0,this._terminal.rows-1)):this._updateModel(t,r);let s=false,a=0;for(;this._charAtlas&&a++<32&&this._glyphRenderer.value.beginFrame();)s=true,this._clearModel(true),this._updateModel(0,this._terminal.rows-1);s&&this._glyphRenderer.value.invalidateAtlasTextures(),this._rectangleRenderer.value.renderBackgrounds(),this._glyphRenderer.value.render(this._model),(!this._cursorBlinkStateManager.value||this._cursorBlinkStateManager.value.isCursorVisible)&&this._rectangleRenderer.value.renderCursor()}_updateCursorBlink(){;this._coreService.decPrivateModes.cursorBlink??this._terminal.options.cursorBlink?this._cursorBlinkStateManager.value=new q0(()=>{this._requestRedrawCursor()},this._coreBrowserService):this._cursorBlinkStateManager.clear(),this._requestRedrawCursor()}_updateModel(t,r){let s=this._core,a=this._workCell,o,n,L,T,h,d,_,p,c,u,C,Q,m,R,F,E,y;t=pt(t,s.rows-1,0),r=pt(r,s.rows-1,0);let Y=this._coreService.decPrivateModes.cursorStyle??s.options.cursorStyle??"block",X=this._terminal.buffer.active.baseY+this._terminal.buffer.active.cursorY,A0=X-s.buffer.ydisp,t0=Math.min(this._terminal.buffer.active.cursorX,s.cols-1),L0=-1,A=this._coreService.isCursorInitialized&&!this._coreService.isCursorHidden&&(!this._cursorBlinkStateManager.value||this._cursorBlinkStateManager.value.isCursorVisible);this._model.cursor=void 0;let n0=false;for(n=t;n<=r;n++){L=n+s.buffer.ydisp;let a0=s.buffer.lines.get(L);if(!a0){for(this._model.lineLengths[n]=0,E=0;E=_,c=E,h.length>0&&E===h[0][0]&&p){u=h.shift();let q=this._model.selection.isCellSelected(this._terminal,u[0],L);for(F=u[0]+1;F=u[1],p?(d=true,a=new ge(a,T.translateToString(true,u[0],u[1]),u[1]-u[0]),c=u[1]-1):_=u[1]}if(Q=a.getChars(),m=a.getCode(),F=(n*s.cols+E)*4,!$&&a.isBlink()&&($=true),this._cellColorResolver.resolve(a,E,L,this.dimensions.device.cell.width,this.dimensions.device.cell.height),A&&L===X&&(E===t0&&(this._model.cursor={x:t0,y:A0,width:a.getWidth(),style:this._coreBrowserService.isFocused?Y:s.options.cursorInactiveStyle,cursorWidth:s.options.cursorWidth,dpr:this._devicePixelRatio},L0=t0+a.getWidth()-1),E>=t0&&E<=L0&&(this._coreBrowserService.isFocused&&Y==="block"||this._coreBrowserService.isFocused===false&&s.options.cursorInactiveStyle==="block")&&(this._cellColorResolver.result.fg=50331648|this._themeService.colors.cursorAccent.rgba>>8&16777215,this._cellColorResolver.result.bg=50331648|this._themeService.colors.cursor.rgba>>8&16777215)),this._textBlinkStateManager.isEnabled&&!this._textBlinkStateManager.isBlinkOn&&a.isBlink()&&(this._cellColorResolver.result.fg|=1073741824),m!==0&&(this._model.lineLengths[n]=E+1),!(this._model.cells[F]===m&&this._model.cells[F+1]===this._cellColorResolver.result.bg&&this._model.cells[F+2]===this._cellColorResolver.result.fg&&this._model.cells[F+3]===this._cellColorResolver.result.ext)&&(n0=true,Q.length>1&&(m|=lt),this._model.cells[F]=m,this._model.cells[F+1]=this._cellColorResolver.result.bg,this._model.cells[F+2]=this._cellColorResolver.result.fg,this._model.cells[F+3]=this._cellColorResolver.result.ext,R=a.getWidth(),this._glyphRenderer.value.updateCell(E,n,m,this._cellColorResolver.result.bg,this._cellColorResolver.result.fg,this._cellColorResolver.result.ext,Q,R,o),d)){for(a=this._workCell,E++;E<=c;E++)y=(n*s.cols+E)*4,this._nullModelCell(E,n,y,this._cellColorResolver.result.bg,this._cellColorResolver.result.fg,this._cellColorResolver.result.ext);E--}}this._setRowBlinkState(n,$)};n0&&this._rectangleRenderer.value.updateBackgrounds(this._model),this._rectangleRenderer.value.updateCursor(this._model),this._updateTextBlinkState()}_nullModelCell(t,r,s,a,o,n){;this._glyphRenderer.value.updateCell(t,r,0,a,o,n,"",0,0),this._model.cells[s]=0,this._model.cells[s+1]=a,this._model.cells[s+2]=o,this._model.cells[s+3]=n}_resetBlinkingRowState(){;this._rowHasBlinkingCells=new Array(this._terminal.rows).fill(false),this._rowHasBlinkingCellsCount=0}_setRowBlinkState(t,r){this._rowHasBlinkingCells[t]!==r&&(this._rowHasBlinkingCells[t]=r,this._rowHasBlinkingCellsCount+=r?1:-1)}_updateTextBlinkState(){this._textBlinkStateManager.setNeedsBlinkInViewport(this._rowHasBlinkingCellsCount>0)}_updateDimensions(){!this._charSizeService.width||!this._charSizeService.height||(this.dimensions.device.char.width=Math.floor(this._charSizeService.width*this._devicePixelRatio),this.dimensions.device.char.height=Math.ceil(this._charSizeService.height*this._devicePixelRatio),this.dimensions.device.cell.height=Math.floor(this.dimensions.device.char.height*this._optionsService.rawOptions.lineHeight),this.dimensions.device.char.top=this._optionsService.rawOptions.lineHeight===1?0:Math.round((this.dimensions.device.cell.height-this.dimensions.device.char.height)/2),this.dimensions.device.cell.width=this.dimensions.device.char.width+Math.round(this._optionsService.rawOptions.letterSpacing),this.dimensions.device.char.left=Math.floor(this._optionsService.rawOptions.letterSpacing/2),this.dimensions.device.canvas.height=this._terminal.rows*this.dimensions.device.cell.height,this.dimensions.device.canvas.width=this._terminal.cols*this.dimensions.device.cell.width,this.dimensions.css.canvas.height=Math.round(this.dimensions.device.canvas.height/this._devicePixelRatio),this.dimensions.css.canvas.width=Math.round(this.dimensions.device.canvas.width/this._devicePixelRatio),this.dimensions.css.cell.height=this.dimensions.device.cell.height/this._devicePixelRatio,this.dimensions.css.cell.width=this.dimensions.device.cell.width/this._devicePixelRatio)}_setCanvasDevicePixelDimensions(t,r){;this._canvas.width===t&&this._canvas.height===r||(this._canvas.width=t,this._canvas.height=r,this._onRequestRedraw.fire({start:0,end:this._terminal.rows-1,sync:true}))}_requestRedrawViewport(){this._onRequestRedraw.fire({start:0,end:this._terminal.rows-1})}_requestRedrawCursor(){let t=this._terminal.buffer.active.cursorY;this._onRequestRedraw.fire({start:t,end:t})}},ge=class extends s0{constructor(t,r,s){super();this.content=0;this.combinedData="";this.fg=t.fg,this.bg=t.bg,this.combinedData=r,this._width=s}isCombined(){return 2097152}getWidth(){return this._width}getChars(){return this.combinedData}getCode(){return 2097151}setFromCharData(t){throw new Error("not implemented")}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}};function pt(i5,e,t=0){return Math.max(Math.min(i5,e),t)}var Ct=class extends W{constructor(t){if(ue&&He()<16){let r={antialias:false,depth:false,preserveDrawingBuffer:true};if(!document.createElement("canvas").getContext("webgl2",r))throw new Error("WebGL2 is only supported on Safari 16 and above")}super();this._onChangeTextureAtlas=this._register(new U);this.onChangeTextureAtlas=this._onChangeTextureAtlas.event;this._onAddTextureAtlasCanvas=this._register(new U);this.onAddTextureAtlasCanvas=this._onAddTextureAtlasCanvas.event;this._onRemoveTextureAtlasCanvas=this._register(new U);this.onRemoveTextureAtlasCanvas=this._onRemoveTextureAtlasCanvas.event;this._onContextLoss=this._register(new U);this.onContextLoss=this._onContextLoss.event;this._customGlyphs=t?.customGlyphs??true,this._preserveDrawingBuffer=t?.preserveDrawingBuffer}activate(t){let r=t._core;if(!t.element){this._register(r.onWillOpen(()=>this.activate(t)));return}this._terminal=t;let s=r.coreService,a=r.optionsService,o=r,n=o._renderService,L=o._characterJoinerService,T=o._charSizeService,h=o._coreBrowserService,d=o._decorationService,_=o._logService,p=o._themeService;this._renderer=this._register(new Le(t,L,T,h,s,d,_,a,p,this._customGlyphs,this._preserveDrawingBuffer)),this._register(c0.forward(this._renderer.onContextLoss,this._onContextLoss)),this._register(c0.forward(this._renderer.onChangeTextureAtlas,this._onChangeTextureAtlas)),this._register(c0.forward(this._renderer.onAddTextureAtlasCanvas,this._onAddTextureAtlasCanvas)),this._register(c0.forward(this._renderer.onRemoveTextureAtlasCanvas,this._onRemoveTextureAtlasCanvas)),n.setRenderer(this._renderer),this._register(M(()=>{if(this._terminal._core._store._isDisposed)return;let c=this._terminal._core._renderService;c.setRenderer(this._terminal._core._createRenderer()),c.handleResize(t.cols,t.rows)}))}get textureAtlas(){return this._renderer?.textureAtlas}clearTextureAtlas(){this._renderer?.clearTextureAtlas()}};export{Ct as WebglAddon}; -diff -ruN a/src/GlyphRenderer.ts b/src/GlyphRenderer.ts +diff --git a/src/GlyphRenderer.ts b/src/GlyphRenderer.ts --- a/src/GlyphRenderer.ts +++ b/src/GlyphRenderer.ts @@ -74,7 +74,11 @@ @@ -142,7 +142,7 @@ diff -ruN a/src/GlyphRenderer.ts b/src/GlyphRenderer.ts this.invalidateAtlasTextures(); } -diff -ruN a/src/TextureAtlas.ts b/src/TextureAtlas.ts +diff --git a/src/TextureAtlas.ts b/src/TextureAtlas.ts --- a/src/TextureAtlas.ts +++ b/src/TextureAtlas.ts @@ -133,6 +133,7 @@ @@ -299,7 +299,7 @@ diff -ruN a/src/TextureAtlas.ts b/src/TextureAtlas.ts this._onAddTextureAtlasCanvas.fire(this._overflowSizePage.canvas); } activePage = this._overflowSizePage; -diff -ruN a/src/Types.ts b/src/Types.ts +diff --git a/src/Types.ts b/src/Types.ts --- a/src/Types.ts +++ b/src/Types.ts @@ -59,6 +59,9 @@ @@ -312,7 +312,7 @@ diff -ruN a/src/Types.ts b/src/Types.ts onAddTextureAtlasCanvas: IEvent; onRemoveTextureAtlasCanvas: IEvent; -diff -ruN a/src/WebglRenderer.ts b/src/WebglRenderer.ts +diff --git a/src/WebglRenderer.ts b/src/WebglRenderer.ts --- a/src/WebglRenderer.ts +++ b/src/WebglRenderer.ts @@ -386,7 +386,10 @@ diff --git a/config/scripts/computer-e2e-workflow.test.mjs b/config/scripts/computer-e2e-workflow.test.mjs index 36a5d701f5e..8e7d9a70197 100644 --- a/config/scripts/computer-e2e-workflow.test.mjs +++ b/config/scripts/computer-e2e-workflow.test.mjs @@ -171,7 +171,7 @@ describe('computer-use e2e workflow', () => { expect(checkout.with['persist-credentials'], jobName).toBe(false) expect(install.with['native-runtime'], jobName).toBe('electron') expect( - job.steps.some((step) => step.uses === 'pnpm/action-setup@v6'), + job.steps.some((step) => step.uses === 'pnpm/setup@v2'), jobName ).toBe(false) expect( diff --git a/config/scripts/ensure-native-runtime.mjs b/config/scripts/ensure-native-runtime.mjs index 424b671b11e..a4cc6db8843 100644 --- a/config/scripts/ensure-native-runtime.mjs +++ b/config/scripts/ensure-native-runtime.mjs @@ -377,7 +377,8 @@ function rebuildNodeRuntimeModules(moduleNames) { } function runPnpm(args, { cwd = projectDir } = {}) { - const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' + // cmd.exe resolves both Corepack's pnpm.cmd and pnpm 12's native pnpm.exe. + const command = 'pnpm' const env = process.platform === 'linux' && args.includes('node-gyp') ? { ...process.env, CXXFLAGS: `${process.env.CXXFLAGS ?? ''} -std=gnu++2a`.trim() } diff --git a/config/scripts/install-node-dependencies-action.test.mjs b/config/scripts/install-node-dependencies-action.test.mjs index a90747513a1..3b20a0eb07e 100644 --- a/config/scripts/install-node-dependencies-action.test.mjs +++ b/config/scripts/install-node-dependencies-action.test.mjs @@ -24,10 +24,15 @@ function createFixture() { for (const directory of [workspace, detachedCwd]) { writeFileSync(join(directory, 'package.json'), '{"name":"fixture"}\n') writeFileSync(join(directory, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n') + writeFileSync(join(directory, 'pnpm-workspace.yaml'), 'packages: []\n') } expect(run('git', ['init', '-q'], { cwd: workspace }).status).toBe(0) - expect(run('git', ['add', 'package.json', 'pnpm-lock.yaml'], { cwd: workspace }).status).toBe(0) + expect( + run('git', ['add', 'package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'], { + cwd: workspace + }).status + ).toBe(0) const pnpm = join(bin, 'pnpm') writeFileSync(pnpm, '#!/bin/sh\nexit 0\n') @@ -76,7 +81,8 @@ describe('install-node-dependencies action', () => { it.each([ ['package.json', '{"name":"changed"}\n'], - ['pnpm-lock.yaml', 'lockfileVersion: 9\nchanged: true\n'] + ['pnpm-lock.yaml', 'lockfileVersion: 9\nchanged: true\n'], + ['pnpm-workspace.yaml', 'packages: []\nchanged: true\n'] ])('rejects a changed %s when the composite step cwd is detached', (file, contents) => { const fixture = createFixture() try { diff --git a/config/scripts/package-electron-runtime-contract.test.mjs b/config/scripts/package-electron-runtime-contract.test.mjs index 64b2f96505a..05ad5bd47d7 100644 --- a/config/scripts/package-electron-runtime-contract.test.mjs +++ b/config/scripts/package-electron-runtime-contract.test.mjs @@ -8,14 +8,13 @@ import { relayArtifactFilenames } from '../../src/shared/relay-artifacts.ts' const projectDir = resolve(import.meta.dirname, '../..') const require = createRequire(import.meta.url) const { createPackagedRuntimeNodeModuleResources } = require('../packaged-runtime-node-modules.cjs') -const packageJson = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8')) +const readProject = (file) => readFileSync(join(projectDir, file), 'utf8') +const packageJson = JSON.parse(readProject('package.json')) +const pnpmWorkspace = parse(readProject('pnpm-workspace.yaml')) describe('Electron runtime package contract', () => { it('keeps shared WebGL atlas invalidation reproducible from vendored source', () => { - const patch = readFileSync( - join(projectDir, 'config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch'), - 'utf8' - ) + const patch = readProject('config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch') expect(patch).toContain('readonly clearModelGeneration: number') expect(patch).toContain('const generation = this._atlas.clearModelGeneration') @@ -26,7 +25,7 @@ describe('Electron runtime package contract', () => { it('keeps root postinstall as the single Electron binary install owner', () => { expect(packageJson.scripts.postinstall).toBe('node config/scripts/rebuild-native-deps.mjs') - expect(packageJson.pnpm.onlyBuiltDependencies).not.toContain('electron') + expect(pnpmWorkspace.allowBuilds).not.toHaveProperty('electron') }) it('keeps the native Windows registry addon optional and platform-gated', () => { @@ -41,7 +40,7 @@ describe('Electron runtime package contract', () => { expect(packageJson.optionalDependencies['windows-native-registry']).toBe('3.2.2') // Why: pnpm installs optional target architectures on every host; the root // Windows-only rebuild owns this addon so macOS/Linux never run node-gyp for it. - expect(packageJson.pnpm.onlyBuiltDependencies).not.toContain('windows-native-registry') + expect(pnpmWorkspace.allowBuilds['windows-native-registry']).toBe(false) // Why assert the guard and the member separately: the list now carries more // than one addon, so pinning the whole literal only tested its formatting. expect(rebuildScript).toContain("rebuildPlatform === 'win32'") @@ -80,14 +79,14 @@ describe('Electron runtime package contract', () => { expect(packageJson.optionalDependencies['@vscode/windows-process-tree']).toBe('0.8.0') // Why: same rule as the registry addon -- pnpm installs optional deps on // every host, so macOS/Linux must never run node-gyp for a Windows addon. - expect(packageJson.pnpm.onlyBuiltDependencies).not.toContain('@vscode/windows-process-tree') + expect(pnpmWorkspace.allowBuilds['@vscode/windows-process-tree']).toBe(false) expect(rebuildScript).toContain("'@vscode/windows-process-tree'") expect(ensureScript).toContain("'@vscode/windows-process-tree'") // Why pin the patch: the upstream binding.gyp requires Spectre-mitigated // libraries our build agents do not carry, and the enumeration stops after // 1024 processes -- on a busy host that silently hides the very descendants // teardown is looking for. - expect(packageJson.pnpm.patchedDependencies['@vscode/windows-process-tree@0.8.0']).toBe( + expect(pnpmWorkspace.patchedDependencies['@vscode/windows-process-tree@0.8.0']).toBe( 'config/patches/@vscode__windows-process-tree@0.8.0.patch' ) const packageTargets = { diff --git a/config/scripts/pr-workflow-parallelism.test.mjs b/config/scripts/pr-workflow-parallelism.test.mjs index f67abe5ee6d..40c110fc8d9 100644 --- a/config/scripts/pr-workflow-parallelism.test.mjs +++ b/config/scripts/pr-workflow-parallelism.test.mjs @@ -9,6 +9,7 @@ const dependencyAction = parse( readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8') ) const packageJson = JSON.parse(readFileSync('package.json', 'utf8')) +const pnpmWorkspace = parse(readFileSync('pnpm-workspace.yaml', 'utf8')) const shellContractFiles = [ 'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts', 'src/main/daemon/shell-ready.test.ts', @@ -224,7 +225,10 @@ describe('PR workflow parallelism', () => { expect(pnpmIndex).toBeLessThan(nodeIndex) expect(pnpmIndex).toBeLessThan(requestedNodeIndex) const packageManagerVersion = /^pnpm@([^+]+)/.exec(packageJson.packageManager)?.[1] - expect(steps[pnpmIndex].with.version).toBe(packageManagerVersion) + expect(packageManagerVersion).toBe('12.0.0') + expect(steps[pnpmIndex].uses).toBe('pnpm/setup@v2') + expect(steps[pnpmIndex].with.version).toBeUndefined() + expect(steps[pnpmIndex].with.install).toBe(false) expect(steps[nodeIndex].with.cache).toBe('pnpm') expect(steps[nodeIndex].if).toBe("inputs.node-version == ''") expect(steps[requestedNodeIndex].if).toBe("inputs.node-version != ''") @@ -232,20 +236,20 @@ describe('PR workflow parallelism', () => { expect(steps[requestedNodeIndex].with.cache).toBe('pnpm') }) - it('pins every direct pnpm setup to the repository package-manager version', () => { - const packageManagerVersion = /^pnpm@([^+]+)/.exec(packageJson.packageManager)?.[1] + it('uses the repository package-manager version for every direct pnpm setup', () => { const directSetups = globSync('.github/workflows/*.yml').flatMap((workflowPath) => { const parsed = parse(readFileSync(workflowPath, 'utf8')) return Object.values(parsed.jobs ?? {}).flatMap((job) => (job.steps ?? []) - .filter((step) => step.uses === 'pnpm/action-setup@v6') + .filter((step) => step.uses === 'pnpm/setup@v2') .map((step) => ({ workflowPath, step })) ) }) expect(directSetups.length).toBeGreaterThan(0) for (const { workflowPath, step } of directSetups) { - expect(step.with?.version, workflowPath).toBe(packageManagerVersion) + expect(step.with?.version, workflowPath).toBeUndefined() + expect(step.with?.install, workflowPath).toBe(false) } }) @@ -293,15 +297,15 @@ describe('PR workflow parallelism', () => { expect(dependencyInstall.run).toContain('--frozen-lockfile') expect(dependencyInstall.run).not.toContain('--no-frozen-lockfile') expect(dependencyInstall.run).toContain( - 'git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml' + 'git -C "$GITHUB_WORKSPACE" diff --exit-code -- package.json pnpm-lock.yaml pnpm-workspace.yaml' ) expect(dependencyInstall.run).toContain('--ignore-scripts') expect(dependencyInstall.run).not.toContain('--os=') expect(dependencyInstall.run).not.toContain('--cpu=') - expect(packageJson.pnpm.supportedArchitectures.os).toEqual( + expect(pnpmWorkspace.supportedArchitectures.os).toEqual( expect.arrayContaining(['current', 'win32']) ) - expect(packageJson.pnpm.supportedArchitectures.cpu).toContain('current') + expect(pnpmWorkspace.supportedArchitectures.cpu).toContain('current') const prepareRuntime = dependencyAction.runs.steps.find( (step) => step.name === 'Prepare native runtime' ) diff --git a/config/scripts/regenerate-xterm-patches.mjs b/config/scripts/regenerate-xterm-patches.mjs index 160d075aee2..a2da6d9da2d 100644 --- a/config/scripts/regenerate-xterm-patches.mjs +++ b/config/scripts/regenerate-xterm-patches.mjs @@ -22,13 +22,15 @@ const DEFAULT_REPO_ROOT = path.resolve(import.meta.dirname, '..', '..') const MANIFEST_RELATIVE_PATH = path.join('config', 'patches', 'xterm-upstream.json') /** - * Flags pnpm@10 passes to `git diff` in its own `diffFolders()`. A patch built + * Flags pnpm@12 passes to `git diff` in its own `diff_folders()`. A patch built * with anything else is a patch pnpm may re-diff differently on the next * `pnpm patch-commit`, so the byte-comparison gate would never settle. */ export const PNPM_DIFF_FLAGS = [ '-c', 'core.safecrlf=false', + '-c', + 'core.quotePath=false', 'diff', '--src-prefix=a/', '--dst-prefix=b/', @@ -38,7 +40,8 @@ export const PNPM_DIFF_FLAGS = [ '--no-index', '--text', '--no-ext-diff', - '--no-color' + '--no-color', + '--' ] /** @@ -47,14 +50,12 @@ export const PNPM_DIFF_FLAGS = [ */ export const CHECKOUT_DIFF_FLAGS = PNPM_DIFF_FLAGS.filter((flag) => flag !== '--no-index') -/** Blanks the vars pnpm blanks so user and system git config cannot reach the diff. */ +/** Applies pnpm's git config isolation so local machine settings cannot change the patch. */ export function pnpmDiffEnvironment(baseEnvironment = process.env) { return { ...baseEnvironment, GIT_CONFIG_NOSYSTEM: '1', - HOME: '', - XDG_CONFIG_HOME: '', - USERPROFILE: '' + GIT_CONFIG_GLOBAL: '/dev/null' } } @@ -216,7 +217,7 @@ export function patchHash(patchText) { function lockfilePatchHashPattern(packageKey) { // Unscoped keys such as `node-pty@1.1.0` are emitted unquoted. - return new RegExp(`(^ '?${escapeRegExp(packageKey)}'?:\\n hash: )([0-9a-f]{64})$`, 'm') + return new RegExp(`(^ '?${escapeRegExp(packageKey)}'?: )([0-9a-f]{64})$`, 'm') } export function readLockfilePatchHash(lockfileText, packageKey) { @@ -507,7 +508,7 @@ function diffFolders(folderA, folderB) { /** The source of truth for the hand-written half: what the checkout itself holds. */ function diffCheckoutSource(packageRoot) { - return run('git', [...CHECKOUT_DIFF_FLAGS, '--', 'src/'], { + return run('git', [...CHECKOUT_DIFF_FLAGS, 'src/'], { cwd: packageRoot, env: pnpmDiffEnvironment(), maxBuffer: 64 * 1024 * 1024 diff --git a/config/scripts/regenerate-xterm-patches.test.mjs b/config/scripts/regenerate-xterm-patches.test.mjs index ac9f9a7ca52..2c4a34dc501 100644 --- a/config/scripts/regenerate-xterm-patches.test.mjs +++ b/config/scripts/regenerate-xterm-patches.test.mjs @@ -98,6 +98,8 @@ describe('pnpm diff format', () => { expect(PNPM_DIFF_FLAGS).toEqual([ '-c', 'core.safecrlf=false', + '-c', + 'core.quotePath=false', 'diff', '--src-prefix=a/', '--dst-prefix=b/', @@ -107,18 +109,18 @@ describe('pnpm diff format', () => { '--no-index', '--text', '--no-ext-diff', - '--no-color' + '--no-color', + '--' ]) }) - it('blanks the config-bearing environment variables', () => { + it('matches pnpm git config isolation', () => { const environment = pnpmDiffEnvironment({ PATH: '/usr/bin', HOME: '/Users/someone' }) expect(environment).toMatchObject({ PATH: '/usr/bin', GIT_CONFIG_NOSYSTEM: '1', - HOME: '', - XDG_CONFIG_HOME: '', - USERPROFILE: '' + GIT_CONFIG_GLOBAL: '/dev/null', + HOME: '/Users/someone' }) }) @@ -350,12 +352,8 @@ describe('manifest guards', () => { describe('lockfile coupling', () => { const lockfile = [ 'patchedDependencies:', - " '@xterm/xterm@6.1.0-beta.287':", - ` hash: ${'0'.repeat(64)}`, - ' path: config/patches/@xterm__xterm@6.1.0-beta.287.patch', - ' node-pty@1.1.0:', - ` hash: ${'1'.repeat(64)}`, - ' path: config/patches/node-pty@1.1.0.patch', + ` '@xterm/xterm@6.1.0-beta.287': ${'0'.repeat(64)}`, + ` node-pty@1.1.0: ${'1'.repeat(64)}`, 'snapshots:', ` '@xterm/addon-fit@0.12.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=${'0'.repeat(64)}))':`, ` '@xterm/xterm': 6.1.0-beta.287(patch_hash=${'0'.repeat(64)})`, @@ -396,7 +394,10 @@ describe('lockfile coupling', () => { it('reports a lockfile stale in its resolution keys alone', () => { const key = '@xterm/xterm@6.1.0-beta.287' - const halfUpdated = lockfile.replace(`hash: ${'0'.repeat(64)}`, `hash: ${'a'.repeat(64)}`) + const halfUpdated = lockfile.replace( + `'@xterm/xterm@6.1.0-beta.287': ${'0'.repeat(64)}`, + `'@xterm/xterm@6.1.0-beta.287': ${'a'.repeat(64)}` + ) expect(readLockfilePatchHash(halfUpdated, key)).toBe('a'.repeat(64)) expect(lockfilePatchHashIsStale(halfUpdated, key, 'a'.repeat(64))).toBe(true) diff --git a/config/scripts/verify-telemetry-constants.mjs b/config/scripts/verify-telemetry-constants.mjs index 6b90c89279c..3bdcd8b3ec6 100644 --- a/config/scripts/verify-telemetry-constants.mjs +++ b/config/scripts/verify-telemetry-constants.mjs @@ -34,7 +34,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs' import { join, resolve } from 'node:path' // Why @electron/asar: canonical replacement for the deprecated `asar` package. // It's transitively available via electron-builder (and pnpm's -// `shamefully-hoist=true` in `.npmrc` flattens it into the root +// `shamefullyHoist: true` in `pnpm-workspace.yaml` flattens it into the root // `node_modules`). If electron-builder ever drops it, promote this to a // direct devDependency in package.json. import { extractFile, listPackage } from '@electron/asar' diff --git a/config/scripts/windows-process-tree-patch-contract.test.mjs b/config/scripts/windows-process-tree-patch-contract.test.mjs index f9c1c90c91a..03b66a9c976 100644 --- a/config/scripts/windows-process-tree-patch-contract.test.mjs +++ b/config/scripts/windows-process-tree-patch-contract.test.mjs @@ -1,6 +1,7 @@ import { createHash } from 'node:crypto' import { readFileSync } from 'node:fs' import { join, resolve } from 'node:path' +import { parseAllDocuments } from 'yaml' import { describe, expect, it } from 'vitest' const projectDir = resolve(import.meta.dirname, '../..') @@ -16,7 +17,9 @@ describe('Windows process-tree patch contract', () => { const patchHash = createHash('sha256') .update(patchBytes.toString('utf8').replaceAll('\r\n', '\n')) .digest('hex') - const lockfile = readFileSync(join(projectDir, 'pnpm-lock.yaml'), 'utf8') - expect(lockfile).toContain(`hash: ${patchHash}`) + const lockfile = parseAllDocuments(readFileSync(join(projectDir, 'pnpm-lock.yaml'), 'utf8')) + .map((document) => document.toJS()) + .find((document) => document.patchedDependencies) + expect(lockfile.patchedDependencies['@vscode/windows-process-tree@0.8.0']).toBe(patchHash) }) }) diff --git a/docs/reference/windows-process-enumeration.md b/docs/reference/windows-process-enumeration.md index 4b8acd87aae..ef1faf5237c 100644 --- a/docs/reference/windows-process-enumeration.md +++ b/docs/reference/windows-process-enumeration.md @@ -206,7 +206,7 @@ The addon is Windows-only, so it follows the same contract as `config/scripts/package-electron-runtime-contract.test.mjs`): - an `optionalDependency`, so a macOS/Linux install tolerates its absence; -- **not** in `pnpm.onlyBuiltDependencies` — pnpm installs optional dependencies +- **not** enabled in `allowBuilds` in `pnpm-workspace.yaml` — pnpm installs optional dependencies on every host, and macOS/Linux must never run `node-gyp` for it; - listed in the win32 branch of `rebuild-native-deps.mjs` and `ensure-native-runtime.mjs`; diff --git a/mobile/.npmrc b/mobile/.npmrc deleted file mode 100644 index d7913f44df7..00000000000 --- a/mobile/.npmrc +++ /dev/null @@ -1 +0,0 @@ -minimum-release-age=4320 diff --git a/mobile/package.json b/mobile/package.json index 75db0998903..6c419e232fb 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -81,13 +81,5 @@ "typescript": "6.0.3", "vite": "^8.0.16", "vitest": "^4.1.9" - }, - "pnpm": { - "overrides": { - "xcode>uuid": "11.1.1" - }, - "patchedDependencies": { - "react-native@0.83.9": "patches/react-native@0.83.9.patch" - } } } diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml index 01092f2acc9..eb2090dd867 100644 --- a/mobile/pnpm-lock.yaml +++ b/mobile/pnpm-lock.yaml @@ -8,9 +8,7 @@ overrides: xcode>uuid: 11.1.1 patchedDependencies: - react-native@0.83.9: - hash: 44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d - path: patches/react-native@0.83.9.patch + react-native@0.83.9: 44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d importers: diff --git a/mobile/pnpm-workspace.yaml b/mobile/pnpm-workspace.yaml index 35f983a345a..22c9be6445e 100644 --- a/mobile/pnpm-workspace.yaml +++ b/mobile/pnpm-workspace.yaml @@ -1,5 +1,10 @@ allowBuilds: esbuild: true +minimumReleaseAge: 4320 + overrides: xcode>uuid: 11.1.1 + +patchedDependencies: + react-native@0.83.9: patches/react-native@0.83.9.patch diff --git a/package.json b/package.json index 59b7dcef7fd..0e84852442c 100644 --- a/package.json +++ b/package.json @@ -298,41 +298,7 @@ "engines": { "node": "24" }, - "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a", - "pnpm": { - "overrides": { - "monaco-editor>dompurify": "3.4.13" - }, - "supportedArchitectures": { - "os": [ - "current", - "darwin", - "linux", - "win32" - ], - "cpu": [ - "current", - "x64", - "arm64" - ] - }, - "onlyBuiltDependencies": [ - "@parcel/watcher", - "cpu-features", - "esbuild", - "node-pty", - "sherpa-onnx" - ], - "patchedDependencies": { - "node-pty@1.1.0": "config/patches/node-pty@1.1.0.patch", - "@xterm/addon-ligatures@0.11.0-beta.287": "config/patches/@xterm__addon-ligatures@0.11.0-beta.287.patch", - "@xterm/addon-webgl@0.20.0-beta.286": "config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch", - "@xterm/addon-serialize@0.15.0-beta.287": "config/patches/@xterm__addon-serialize@0.15.0-beta.287.patch", - "@xterm/xterm@6.1.0-beta.287": "config/patches/@xterm__xterm@6.1.0-beta.287.patch", - "lint-staged@16.4.0": "config/patches/lint-staged@16.4.0.patch", - "@vscode/windows-process-tree@0.8.0": "config/patches/@vscode__windows-process-tree@0.8.0.patch" - } - }, + "packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67", "reactDoctor": { "rules": { "react-doctor/js-combine-iterations": "off" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d063a91690a..3c4f7e82f7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,3 +1,104 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.0.0 + version: 12.0.0 + +packages: + + '@pnpm/exe.darwin-arm64@12.0.0': + resolution: {integrity: sha512-sqeoPfVMIfQhbwzDrKraXY2ynyuWClFqzvfImzAS/yczEru1m5SGvQ9kgFPDvQzJZ9AetedgJeDZC6qYvH8/tQ==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.0.0': + resolution: {integrity: sha512-Quc3J6c9cGTy+LDgz1cLVgCNOU9IERuyAlDoEj0DCilKqvo50Jx1GV8k74iwn4J9fFSKkm8JrwNvtTDj3uWnUA==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.linux-arm64-musl@12.0.0': + resolution: {integrity: sha512-EVWd3OTmgsMFhXx69b5JxIzoabG9Ma7m4OeTaf0ZKBzMnfYi8u21NDQo92ToMrdYL5dYDDCHsyYIjXzk+d0HhA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.0.0': + resolution: {integrity: sha512-cXHHW8M4rAPsYNkKZO9WVcpLLK55i9EaIsZPfIqUuY2eopd5LqnFyBge54HCh1GC0yCX8ySn0hYIi+4OyAEoDg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.0.0': + resolution: {integrity: sha512-UcXwMdFjly0mpddkGigHKTxe27IMv2fUK4IWW/MHmJ3yMguxXmkwNlEI4aE+G1HO2TLo20uNEUWD4ymLe/DaCQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.0.0': + resolution: {integrity: sha512-6Rsl+zEWMOmus7v7/9J3OE8EMvHyNAfxYmDfmhQG4J0985OuT3G3Ho9NSGHjkBn4aU4bgklWifRhe1HX8dUSyw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.0.0': + resolution: {integrity: sha512-O5F76A4oVFrpDGdFxEszRIThOSBfjHdH5c006gR+7UTCfiXrukr1XfqPungUI1DXcSR5gb9jBsPQqQZOAoOOxw==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.0.0': + resolution: {integrity: sha512-5dKFajIEWJ1ai+KHXFJvskY6vchbunmLwSUV2ywbLymcmJjfY5XJVpgzPCyIoVCMVG0zHorr66+hM8h8b3aRfQ==} + cpu: [x64] + os: [win32] + + pnpm@12.0.0: + resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.darwin-arm64@12.0.0': + optional: true + + '@pnpm/exe.darwin-x64@12.0.0': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.0.0': + optional: true + + '@pnpm/exe.linux-arm64@12.0.0': + optional: true + + '@pnpm/exe.linux-x64-musl@12.0.0': + optional: true + + '@pnpm/exe.linux-x64@12.0.0': + optional: true + + '@pnpm/exe.win32-arm64@12.0.0': + optional: true + + '@pnpm/exe.win32-x64@12.0.0': + optional: true + + pnpm@12.0.0: + optionalDependencies: + '@pnpm/exe.darwin-arm64': 12.0.0 + '@pnpm/exe.darwin-x64': 12.0.0 + '@pnpm/exe.linux-arm64': 12.0.0 + '@pnpm/exe.linux-arm64-musl': 12.0.0 + '@pnpm/exe.linux-x64': 12.0.0 + '@pnpm/exe.linux-x64-musl': 12.0.0 + '@pnpm/exe.win32-arm64': 12.0.0 + '@pnpm/exe.win32-x64': 12.0.0 + +--- lockfileVersion: '9.0' settings: @@ -8,27 +109,13 @@ overrides: monaco-editor>dompurify: 3.4.13 patchedDependencies: - '@vscode/windows-process-tree@0.8.0': - hash: 946ffb2b1026bcb554d9920681fb00f546c28fc5de6d95a877d198b49bf73988 - path: config/patches/@vscode__windows-process-tree@0.8.0.patch - '@xterm/addon-ligatures@0.11.0-beta.287': - hash: 47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920 - path: config/patches/@xterm__addon-ligatures@0.11.0-beta.287.patch - '@xterm/addon-serialize@0.15.0-beta.287': - hash: af3b156143ed2ee9903b753145b63dd4a2ee72cadd7ef333ab0f4d5d3ebfc32c - path: config/patches/@xterm__addon-serialize@0.15.0-beta.287.patch - '@xterm/addon-webgl@0.20.0-beta.286': - hash: 118f649459ba56a51627d46603e2d5d81d837ac9c4dc393f5cc20263d0ac1b91 - path: config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch - '@xterm/xterm@6.1.0-beta.287': - hash: 46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7 - path: config/patches/@xterm__xterm@6.1.0-beta.287.patch - lint-staged@16.4.0: - hash: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673 - path: config/patches/lint-staged@16.4.0.patch - node-pty@1.1.0: - hash: 9a2eedbf2448b8ff1387a8a740ee5e4e968bf8484d7d80e12a3f6a2dc26b0e17 - path: config/patches/node-pty@1.1.0.patch + '@vscode/windows-process-tree@0.8.0': 9217ef36c01ed74127fef5512b0c92089cdbf820fd6c109dd671137eebdc7585 + '@xterm/addon-ligatures@0.11.0-beta.287': 47405b9994b5acf1b4e90b49250358c1ca03649854d59560e7732b72fe336920 + '@xterm/addon-serialize@0.15.0-beta.287': af3b156143ed2ee9903b753145b63dd4a2ee72cadd7ef333ab0f4d5d3ebfc32c + '@xterm/addon-webgl@0.20.0-beta.286': 2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91 + '@xterm/xterm@6.1.0-beta.287': 46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7 + lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673 + node-pty@1.1.0: 9a2eedbf2448b8ff1387a8a740ee5e4e968bf8484d7d80e12a3f6a2dc26b0e17 importers: @@ -241,7 +328,7 @@ importers: version: 0.13.0-beta.287(@xterm/xterm@6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7)) '@xterm/addon-webgl': specifier: 0.20.0-beta.286 - version: 0.20.0-beta.286(patch_hash=118f649459ba56a51627d46603e2d5d81d837ac9c4dc393f5cc20263d0ac1b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7)) + version: 0.20.0-beta.286(patch_hash=2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7)) '@xterm/xterm': specifier: 6.1.0-beta.287 version: 6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7) @@ -419,7 +506,7 @@ importers: optionalDependencies: '@vscode/windows-process-tree': specifier: 0.8.0 - version: 0.8.0(patch_hash=946ffb2b1026bcb554d9920681fb00f546c28fc5de6d95a877d198b49bf73988) + version: 0.8.0(patch_hash=9217ef36c01ed74127fef5512b0c92089cdbf820fd6c109dd671137eebdc7585) sherpa-onnx-darwin-arm64: specifier: 1.12.37 version: 1.12.37 @@ -3405,6 +3492,7 @@ packages: '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} + '@xterm/addon-fit@0.12.0-beta.287': resolution: {integrity: sha512-2MDj+J4x67bjOS/SuBPxSYEWH38NbX6ENV18RbKVOhfRYCX3yERnuHBOrgH9hYdY8rCtsLQmuVgF6cmvCJiV2w==} peerDependencies: @@ -9623,7 +9711,7 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@vscode/windows-process-tree@0.8.0(patch_hash=946ffb2b1026bcb554d9920681fb00f546c28fc5de6d95a877d198b49bf73988)': + '@vscode/windows-process-tree@0.8.0(patch_hash=9217ef36c01ed74127fef5512b0c92089cdbf820fd6c109dd671137eebdc7585)': dependencies: node-addon-api: 7.1.0 optional: true @@ -9656,7 +9744,7 @@ snapshots: dependencies: '@xterm/xterm': 6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7) - '@xterm/addon-webgl@0.20.0-beta.286(patch_hash=118f649459ba56a51627d46603e2d5d81d837ac9c4dc393f5cc20263d0ac1b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7))': + '@xterm/addon-webgl@0.20.0-beta.286(patch_hash=2c301a06ad9caa635d746147dcb2b1c69aa026904f65e1183564f08a0e601b91)(@xterm/xterm@6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7))': dependencies: '@xterm/xterm': 6.1.0-beta.287(patch_hash=46796c152f3b73e28238f44499eaf5a867a863809bc7b470b159526a41e354f7) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 52304c4dedb..a03bf7fbd2d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,3 +3,43 @@ # package graph. Without this file `pnpm -r` auto-discovers mobile/ and its # nested package, and the root's patchedDependencies fail as ERR_PNPM_UNUSED_PATCH. packages: [] + +minimumReleaseAge: 4320 +shamefullyHoist: true + +supportedArchitectures: + os: + - current + - darwin + - linux + - win32 + cpu: + - current + - x64 + - arm64 + +allowBuilds: + '@parcel/watcher': true + '@swc/core': false + '@vscode/windows-process-tree': false + agent-browser: false + cpu-features: true + electron-winstaller: false + esbuild: true + msw: false + node-pty: true + sherpa-onnx: true + ssh2: false + windows-native-registry: false + +overrides: + monaco-editor>dompurify: 3.4.13 + +patchedDependencies: + node-pty@1.1.0: config/patches/node-pty@1.1.0.patch + '@xterm/addon-ligatures@0.11.0-beta.287': config/patches/@xterm__addon-ligatures@0.11.0-beta.287.patch + '@xterm/addon-webgl@0.20.0-beta.286': config/patches/@xterm__addon-webgl@0.20.0-beta.286.patch + '@xterm/addon-serialize@0.15.0-beta.287': config/patches/@xterm__addon-serialize@0.15.0-beta.287.patch + '@xterm/xterm@6.1.0-beta.287': config/patches/@xterm__xterm@6.1.0-beta.287.patch + lint-staged@16.4.0: config/patches/lint-staged@16.4.0.patch + '@vscode/windows-process-tree@0.8.0': config/patches/@vscode__windows-process-tree@0.8.0.patch diff --git a/tests/e2e/terminal-windows-codex-multiline-paste.spec.ts b/tests/e2e/terminal-windows-codex-multiline-paste.spec.ts index c5ec42520f3..543f6072247 100644 --- a/tests/e2e/terminal-windows-codex-multiline-paste.spec.ts +++ b/tests/e2e/terminal-windows-codex-multiline-paste.spec.ts @@ -31,7 +31,7 @@ function pastePayload(repeats = 4): string { '', '- If the worktree is dirty before starting, stop and report it.', '', - '- Use native Windows PowerShell, Node 24, and pnpm 10.', + '- Use native Windows PowerShell, Node 24, and pnpm 12.', '', '- Confirm the checked-out full SHA before testing.', '', From 2dfaa676d87ec4051e1cc9884fc3a96f6080d5cd Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:13:35 -0700 Subject: [PATCH 09/80] chore: update oxlint and oxfmt (#17150) --- .oxlintrc.json | 12 + mobile/package.json | 4 +- mobile/pnpm-lock.yaml | 797 +++++++++--------- mobile/src/files/file-list-fallback.ts | 11 +- .../mobile-session-tabs-stream-health.test.ts | 15 +- ...mobile-session-tabs-reconciliation.test.ts | 10 +- .../mobile-direct-endpoint-probe.test.ts | 14 +- .../mobile-relay-runtime-failover.test.ts | 14 +- package.json | 4 +- pnpm-lock.yaml | 324 +++---- src/main/ai-vault/session-scanner.ts | 40 +- src/main/browser/cdp-bridge.ts | 2 +- .../claude-usage/claude-model-pricing.test.ts | 4 +- .../codex-accounts/service-test-harness.ts | 7 +- .../codex-state-db-backfill-recovery.test.ts | 47 +- .../emulator-start-lease-registry.test.ts | 16 +- .../github/client/fetch/get-pr-comments.ts | 68 +- src/main/github/issue-work-item-details.ts | 18 +- .../filesystem-watcher-event-batch.test.ts | 8 +- .../filesystem-watcher-large-batch.test.ts | 19 +- .../filesystem-watcher-remote-batch.test.ts | 11 +- src/main/ipc/orca-profiles.ts | 16 +- src/main/ipc/pet.ts | 5 +- .../ipc/remote-watcher-event-batch.test.ts | 11 +- .../ipc/ssh-pty-model-admission-pressure.ts | 2 +- .../ipc/workspace-cleanup-broad-scan.test.ts | 17 +- src/main/ipc/workspace-cleanup.test.ts | 21 +- src/main/ipc/workspace-cleanup.ts | 6 +- src/main/ipc/workspace-space.ts | 6 +- src/main/jira/adf-media-destination.ts | 2 +- .../native-chat/transcript-read-cache.test.ts | 4 +- src/main/opencode-usage/snapshot-rollups.ts | 40 +- .../pi/agent-status-extension-source.test.ts | 4 +- src/main/rate-limits/service.ts | 54 +- src/main/runtime/orca-runtime.ts | 24 +- .../rpc/terminal-output-frame-chunks.ts | 2 +- src/main/skills/skill-install-filesystem.ts | 18 +- .../pty-handler-source-publication.test.ts | 5 +- .../src/app-shell/use-persisted-ui-writer.ts | 54 +- src/renderer/src/components/TaskPage.tsx | 4 +- .../src/components/WorktreeJumpPalette.tsx | 24 +- ...on-run-output-snapshot-equivalence.test.ts | 2 +- .../annotate/browser-annotation-output.ts | 2 +- .../useDashboardPopoutBridge.test.tsx | 7 +- .../issue-conversation-entries.ts | 32 +- ...ctured-agent-session-message-projection.ts | 16 +- ...ovider-check-classification-parity.test.ts | 18 +- .../file-explorer-watch-reconcile.test.ts | 18 +- .../DeveloperPermissionsPane.test.tsx | 8 +- .../components/settings/McpConfigSection.tsx | 10 +- .../settings/appearance-sidebar-search.ts | 136 ++- .../settings/ephemeral-vms-search.ts | 59 +- .../settings/mobile-settings-search.ts | 95 +-- .../components/settings/settings-search.ts | 12 +- .../task-page-localized-options.tsx | 16 +- .../use-task-page-github-source-banners.ts | 4 +- .../use-terminal-tab-cold-parking.ts | 5 +- .../hooks/runtime-client-events-sync.test.ts | 20 +- ...ents-agent-status-batch-projection.test.ts | 22 +- ...ts-agent-status-snapshot-hydration.test.ts | 20 +- .../remote-server-update-coordinator.test.ts | 14 +- src/renderer/src/store/slices/browser.ts | 20 +- .../metadata/worktree-lineage-refresh.ts | 4 +- ...eb-preload-api-runtime-environment.test.ts | 15 +- .../map-settled-with-concurrency.test.ts | 9 +- src/shared/raster-image-dimensions.test.ts | 2 +- tests/e2e/helpers/store.ts | 34 +- .../terminal-ime-recorded-dom-trace-replay.ts | 2 +- 68 files changed, 1110 insertions(+), 1256 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 97a2b68701a..b93f1afd619 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -40,6 +40,18 @@ "react/no-string-refs": "error", "react/no-unescaped-entities": "error", "react/require-render-return": "error", + "react/error-boundaries": "off", + "react/globals": "off", + "react/immutability": "off", + "react/incompatible-library": "off", + "react/preserve-manual-memoization": "off", + "react/purity": "off", + "react/refs": "off", + "react/set-state-in-effect": "off", + "react/set-state-in-render": "off", + "react/static-components": "off", + "react/use-memo": "off", + "react/void-use-memo": "off", "react/jsx-curly-brace-presence": [ "error", { "props": "never", "children": "never", "propElementValues": "always" } diff --git a/mobile/package.json b/mobile/package.json index 6c419e232fb..2f6170748aa 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -74,8 +74,8 @@ "esbuild": "0.25.4", "expo-module-scripts": "^55.0.2", "happy-dom": "^20.9.0", - "oxfmt": "^0.52.0", - "oxlint": "^1.71.0", + "oxfmt": "^0.65.0", + "oxlint": "^1.80.0", "react-test-renderer": "19.2.6", "tsx": "^4.22.4", "typescript": "6.0.3", diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml index eb2090dd867..0fd91ca7e6c 100644 --- a/mobile/pnpm-lock.yaml +++ b/mobile/pnpm-lock.yaml @@ -19,10 +19,10 @@ importers: version: 1.8.0 '@orca/expo-two-way-audio': specifier: file:./packages/expo-two-way-audio - version: file:packages/expo-two-way-audio(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: file:packages/expo-two-way-audio(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) '@react-native-async-storage/async-storage': specifier: ^2.2.0 - version: 2.2.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) + version: 2.2.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) '@xterm/addon-unicode11': specifier: 0.10.0-beta.285 version: 0.10.0-beta.285(@xterm/xterm@6.1.0-beta.285) @@ -37,19 +37,19 @@ importers: version: 6.0.3 expo: specifier: ^55.0.27 - version: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + version: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-build-properties: specifier: ^55.0.13 version: 55.0.13(expo@55.0.27) expo-camera: specifier: ^55.0.18 - version: 55.0.18(@types/emscripten@1.41.5)(expo@55.0.27)(react-native-web@0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 55.0.18(@types/emscripten@1.41.5)(expo@55.0.27)(react-native-web@0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) expo-clipboard: specifier: ^55.0.13 - version: 55.0.13(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 55.0.13(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) expo-constants: specifier: ^55.0.16 - version: 55.0.16(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) + version: 55.0.16(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) expo-crypto: specifier: ^55.0.14 version: 55.0.14(expo@55.0.27) @@ -61,7 +61,7 @@ importers: version: 55.0.13(expo@55.0.27) expo-file-system: specifier: 55.0.19 - version: 55.0.19(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) + version: 55.0.19(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) expo-haptics: specifier: ^55.0.14 version: 55.0.14(expo@55.0.27) @@ -76,19 +76,19 @@ importers: version: 55.0.8(expo@55.0.27)(react@19.2.6) expo-linking: specifier: ^55.0.15 - version: 55.0.15(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 55.0.15(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) expo-modules-core: specifier: ~55.0.25 - version: 55.0.25(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 55.0.25(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) expo-network: specifier: ~55.0.14 version: 55.0.14(expo@55.0.27)(react@19.2.6) expo-notifications: specifier: ^55.0.22 - version: 55.0.22(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + version: 55.0.22(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-router: specifier: ^55.0.14 - version: 55.0.14(50649306d987d6f4e510b7a8e94ea6ba) + version: 55.0.14(eadb226b6771bbf124ad73e49beee0de) expo-secure-store: specifier: ^55.0.13 version: 55.0.13(expo@55.0.27) @@ -97,13 +97,13 @@ importers: version: 55.0.20(expo@55.0.27)(typescript@6.0.3) expo-status-bar: specifier: ^55.0.6 - version: 55.0.6(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 55.0.6(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) lowlight: specifier: ^3.3.0 version: 3.3.0 lucide-react-native: specifier: ^1.14.0 - version: 1.14.0(react-native-svg@15.15.4(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 1.14.0(react-native-svg@15.15.4(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) mermaid: specifier: 11.16.1 version: 11.16.1 @@ -115,31 +115,31 @@ importers: version: 19.2.6(react@19.2.6) react-native: specifier: ^0.83.9 - version: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + version: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) react-native-gesture-handler: specifier: ^2.31.2 - version: 2.31.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 2.31.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) react-native-reanimated: specifier: ^4.3.0 - version: 4.3.0(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 4.3.0(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) react-native-safe-area-context: specifier: ^5.7.0 - version: 5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) react-native-screens: specifier: ^4.24.0 - version: 4.24.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 4.24.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) react-native-svg: specifier: ^15.15.4 - version: 15.15.4(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 15.15.4(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) react-native-web: specifier: ^0.21.2 version: 0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react-native-webview: specifier: ^13.16.1 - version: 13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) react-native-worklets: specifier: ^0.8.3 - version: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + version: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) tweetnacl: specifier: ^1.0.3 version: 1.0.3 @@ -173,16 +173,16 @@ importers: version: 0.25.4 expo-module-scripts: specifier: ^55.0.2 - version: 55.0.2(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.25.4)(eslint@9.39.4)(expo@55.0.27)(jest@29.7.0(@types/node@26.1.2))(prettier@2.8.8)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-refresh@0.14.2)(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6) + version: 55.0.2(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.25.4)(eslint@9.39.4)(expo@55.0.27)(jest@29.7.0(@types/node@26.1.2))(prettier@2.8.8)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-refresh@0.14.2)(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6) happy-dom: specifier: ^20.9.0 version: 20.11.1 oxfmt: - specifier: ^0.52.0 - version: 0.52.0 + specifier: ^0.65.0 + version: 0.65.0 oxlint: - specifier: ^1.71.0 - version: 1.71.0 + specifier: ^1.80.0 + version: 1.80.0 react-test-renderer: specifier: 19.2.6 version: 19.2.6(react@19.2.6) @@ -1945,246 +1945,246 @@ packages: '@oxc-project/types@0.137.0': resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} - '@oxfmt/binding-android-arm-eabi@0.52.0': - resolution: {integrity: sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ==} + '@oxfmt/binding-android-arm-eabi@0.65.0': + resolution: {integrity: sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.52.0': - resolution: {integrity: sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ==} + '@oxfmt/binding-android-arm64@0.65.0': + resolution: {integrity: sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.52.0': - resolution: {integrity: sha512-f9+bLvOYxy7NttCLFTvQ7afmqDOWY4wIP9xdvfj5trQ1qj6f2UFAGwZESlfsMjvJNTyRpXfIlOanCI9FOvoeQA==} + '@oxfmt/binding-darwin-arm64@0.65.0': + resolution: {integrity: sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.52.0': - resolution: {integrity: sha512-YSTB9sJ5nnQd/Q0ddHkgof0ZCHPAnWZT1IW2SJ8omz7CP7KluJhO1fNHrpqdxCtpztJwSs4hY1uAee35wKxxaw==} + '@oxfmt/binding-darwin-x64@0.65.0': + resolution: {integrity: sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.52.0': - resolution: {integrity: sha512-NIrRNTTPCs4UbmVs0bxLSCDlLCtIRMJIXklNKaXa5Oj2/K1UIMBvgE8+uPVo01Io3N9HF0+GAX+aAHjUgZS7vA==} + '@oxfmt/binding-freebsd-x64@0.65.0': + resolution: {integrity: sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.52.0': - resolution: {integrity: sha512-JXUCde8mn3GpgQouz2PXUokgy/uT1QrRJBL2s983VWcSQp62wTFYiNXgTKdeo1Jgbr0IgUnKKvzIk/YBlj/nVQ==} + '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': + resolution: {integrity: sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.52.0': - resolution: {integrity: sha512-psbUXaRZ+V8DaXz10Qf7LSHtdtdKAmC8fxXgeU608jjzrmWK4quamZMOpl6sf+dikoFHA85uE93Q0BqxrCdQrQ==} + '@oxfmt/binding-linux-arm-musleabihf@0.65.0': + resolution: {integrity: sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.52.0': - resolution: {integrity: sha512-Jw7MgWUU9lcLCcy82updISP3EthTlfvAwR6gWNxPzqly7+fLvOi2gHQE9xXQjpqaVLm/8P+gOzlv9ODuoVlaaw==} + '@oxfmt/binding-linux-arm64-gnu@0.65.0': + resolution: {integrity: sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.52.0': - resolution: {integrity: sha512-wZg6bLjDvh2KibyI3QFUYo8GTXneIFsd0JvehtvJiUmQ8WRPERgxd/VM4ctWb86U5FT1FkqgS8/wZKVB+AZScg==} + '@oxfmt/binding-linux-arm64-musl@0.65.0': + resolution: {integrity: sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.52.0': - resolution: {integrity: sha512-IngE8uxhNvxcMrLjZNDo9xNLY7rEK33AKnaMd2B46he1e/mz2CfcW6If/U1wUjdRZddm1QzQaciqZkuMkdh1FA==} + '@oxfmt/binding-linux-ppc64-gnu@0.65.0': + resolution: {integrity: sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.52.0': - resolution: {integrity: sha512-H3+DdFMv/efN3Efmhsv18jDrpiWWqKG7wsfAlQBqAt6z/E2Bx+TwEj2Nowe51CPOWB8/mFBC2dAMSgVFLvvowA==} + '@oxfmt/binding-linux-riscv64-gnu@0.65.0': + resolution: {integrity: sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.52.0': - resolution: {integrity: sha512-zji+1kb7lJKohSDjzC1IsS+K/cKRs1hdVf0ZH0VbdbiakmtLvN9twBoXo/k8VdjFax7kfo+DyPxS7vv52br1aw==} + '@oxfmt/binding-linux-riscv64-musl@0.65.0': + resolution: {integrity: sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.52.0': - resolution: {integrity: sha512-hcLBYedpCy7ToUvvBidWk7+11Yhg1oAZ4+6hKPic/mQI6NaqXJSXMps5nFlwUuX2ewhtLZZDPg63TI042qGKBg==} + '@oxfmt/binding-linux-s390x-gnu@0.65.0': + resolution: {integrity: sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.52.0': - resolution: {integrity: sha512-IDO2loXK2OtTOhSPchU9MW25mWL2QCDGdJbjN8MXKZVS80qXe5gMTwQWu/gMJ3juoBHbkuUZNB2N1LHzNT7DoA==} + '@oxfmt/binding-linux-x64-gnu@0.65.0': + resolution: {integrity: sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.52.0': - resolution: {integrity: sha512-mAV2Hjn0SatJ+KoAzKUC3eJhdJ8wv+3m1KyuS0dTsbF0c5weq+QrCt/DRZZM+uj/XiKzCDEUKYsBF30e2qkcyw==} + '@oxfmt/binding-linux-x64-musl@0.65.0': + resolution: {integrity: sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.52.0': - resolution: {integrity: sha512-vd4npaUIwChxp7XzkqmepBWTT9YMcSe/NBApVGPC30/lLyOVaV3dvma1SKo03t8O73BPRAG7EyJzGlN5cJM5hQ==} + '@oxfmt/binding-openharmony-arm64@0.65.0': + resolution: {integrity: sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.52.0': - resolution: {integrity: sha512-k2sz6gWQdMfh5HPpIS+Bw/0UEV/kaK2xuqJRrWL233sEHx9WLlsmvlPFM4HUNThkYbSN0U0vPW7LVKZWDS8hPQ==} + '@oxfmt/binding-win32-arm64-msvc@0.65.0': + resolution: {integrity: sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.52.0': - resolution: {integrity: sha512-rhke69GTcArodLHpjMTfNnvjTEBryDeZcUCKK/VjXDMtfTULl6QRh0ymX5/hbCUv2WjYm9h/QbW++q2vE15gWQ==} + '@oxfmt/binding-win32-ia32-msvc@0.65.0': + resolution: {integrity: sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.52.0': - resolution: {integrity: sha512-q5xL7oeXkZdEtNZWBdvehJcmt+GRu9l2bK40yJs1jJXlqq+r0Hygb1rTjq+FM2o/2xyt4cufH6KRplHp3Jjsvw==} + '@oxfmt/binding-win32-x64-msvc@0.65.0': + resolution: {integrity: sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.71.0': - resolution: {integrity: sha512-ImGmd1njEg4FEJH03jhRnveEegtO3czCtfptvaHivKAZQIYATbVFBrrzbaYMYv0oJioTnxZAZVSyV+oL7W8S2g==} + '@oxlint/binding-android-arm-eabi@1.80.0': + resolution: {integrity: sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.71.0': - resolution: {integrity: sha512-4A5BEexBrwY1YFF8Kiq/lp/wQPRG79G3BWIE1FuWaM5MvmpYSd+7ZySVcKkHdwo0UDzdQGddp6pD9mpctMqLnw==} + '@oxlint/binding-android-arm64@1.80.0': + resolution: {integrity: sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.71.0': - resolution: {integrity: sha512-9wJA9GJulLwS2usU3CEisI/ESDO1n1z9eyTCvApMDrAkbJ1ve0mORgTMjcWWsKxkzkeZ2N/Gpra5IQE7x8tYgQ==} + '@oxlint/binding-darwin-arm64@1.80.0': + resolution: {integrity: sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.71.0': - resolution: {integrity: sha512-PlLCjS06V0PeJMAJwzjrExw1sYNW9Gch3JtNlcwwZDXGlTYDuwHNN89zYH8LTXFfgkVtsYvs2nv0FqrzyuFDzg==} + '@oxlint/binding-darwin-x64@1.80.0': + resolution: {integrity: sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.71.0': - resolution: {integrity: sha512-Lhil7bWre0ncxbUoDoxfS0JzpTz17BRQKW7iwoAUY8GJ66+WwJEfYPCFJ1P0WgVZR5/O/b3Q2pENlHOjeXLOGQ==} + '@oxlint/binding-freebsd-x64@1.80.0': + resolution: {integrity: sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.71.0': - resolution: {integrity: sha512-Oo9/L58PYD3RC0x05d2upAPLllHytTjHQGsnC06P6Ynn7jKkp5mdImQxXdJ3+FnBaKspNpGogzgVsi6g872LiA==} + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': + resolution: {integrity: sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.71.0': - resolution: {integrity: sha512-mSHfyfgJrEbyIR29ejaeS50BdPk+GoNPlC1dckpDiUZbJAIel68sjSMdOt4WY0/gva+ECC7FNITQkxMJU+vSBw==} + '@oxlint/binding-linux-arm-musleabihf@1.80.0': + resolution: {integrity: sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.71.0': - resolution: {integrity: sha512-n9yY4M2tiy3aij4AqtlnspzpfdpeT5JQfK2/w2d8oyp5W0FRwOb1dIeX99nORNcxGr08iD9bH8N5XFz3I2iy8w==} + '@oxlint/binding-linux-arm64-gnu@1.80.0': + resolution: {integrity: sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.71.0': - resolution: {integrity: sha512-fJZrs5sDZtTaPIOiemRQQmo82Ezy+vOGXemPc4Ok7iVVsYsFa7SlW6Z5XN819VfsqBHRm3NJ3rTdnR8+bJYJdQ==} + '@oxlint/binding-linux-arm64-musl@1.80.0': + resolution: {integrity: sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.71.0': - resolution: {integrity: sha512-cwl7VKGERIy9p+G+AvZdfy/06q0aHXaTt/mMRReC751iuNYJgqKjB7NydXSS30nBT9vtr2tunciOtrR4fD6FUA==} + '@oxlint/binding-linux-ppc64-gnu@1.80.0': + resolution: {integrity: sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.71.0': - resolution: {integrity: sha512-eZ8ieVXvzGi8jr7+ybQGPK2STw3mldfxZlgA2738iflfB/rzA69sE6m5rDRpQaxC7dpm745Enlh1Tod0QAk9Gg==} + '@oxlint/binding-linux-riscv64-gnu@1.80.0': + resolution: {integrity: sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.71.0': - resolution: {integrity: sha512-puMDbQYe6+NXwfMusojoA7CXGn2b3utukmd23PQqc1E3XhVCwyZ+FueSMzDYeNgDV2dUfIVXAAKZBcFDeCL6sA==} + '@oxlint/binding-linux-riscv64-musl@1.80.0': + resolution: {integrity: sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.71.0': - resolution: {integrity: sha512-4NJLxBs1ujISCt3L/1FcywLs73PWtJuw+piD6feK2V6h6OS6P7xu9/sWt1DTRLibe6QCzmfZzmM/2HPORoV/Lg==} + '@oxlint/binding-linux-s390x-gnu@1.80.0': + resolution: {integrity: sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.71.0': - resolution: {integrity: sha512-cFDaiR8L3430qp88tfZnvFlt3KotFhR/DlbIL0nHOMMYiG/9Wy4l+6f7t8G8pTa9bd8Lt8+M0y/qjRQ/xcB74g==} + '@oxlint/binding-linux-x64-gnu@1.80.0': + resolution: {integrity: sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.71.0': - resolution: {integrity: sha512-orfixdt76KlpNly9z0PkWBBNfwjKz+JFVLP/7wnVchlKNU9Dpt9InU/ZggeSej6fC7qwHmHNOGlhLnQXcYoGuA==} + '@oxlint/binding-linux-x64-musl@1.80.0': + resolution: {integrity: sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.71.0': - resolution: {integrity: sha512-9emQu2lAp6yhPB3XuI+++vR+l/o6JR1X+EpxwcumPdQXBWXEPAsquPGL7l158EqU8SebQMXTUa/S5zN98juyHw==} + '@oxlint/binding-openharmony-arm64@1.80.0': + resolution: {integrity: sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.71.0': - resolution: {integrity: sha512-bd5kI8spYwTm3BILDtGhi73zoup5dw8MlPQNT8YB3BD5UIsjNe3K9/4ctrzQMX4SZMoK5HgzVLkLJzacEXB7fA==} + '@oxlint/binding-win32-arm64-msvc@1.80.0': + resolution: {integrity: sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.71.0': - resolution: {integrity: sha512-W4HvOHGzVLHcrmFu+bMrJlho+/yrlX5ZNdJZqGe8MEldkQG+RHYhxxad9P4jvWAYFmIqUA5i9DQ8QsJqSU9GIw==} + '@oxlint/binding-win32-ia32-msvc@1.80.0': + resolution: {integrity: sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.71.0': - resolution: {integrity: sha512-D2kyEIPHk/G/wiZLnwTVC/sVst+T/lKldVOjAFpgTIBUAOlry72e5OiapDbDBF4LfJLkN5ypJb/8Eu6yJzkveQ==} + '@oxlint/binding-win32-x64-msvc@1.80.0': + resolution: {integrity: sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4123,6 +4123,7 @@ packages: eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -5759,8 +5760,8 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - oxfmt@0.52.0: - resolution: {integrity: sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug==} + oxfmt@0.65.0: + resolution: {integrity: sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5772,12 +5773,12 @@ packages: vite-plus: optional: true - oxlint@1.71.0: - resolution: {integrity: sha512-U1m1X+C0vDj7DC1e13IoZULzEcPczE7UOMTs8VlZGHUEIUaSTZKo5qkPsQEfzpgnQ29Pea/w3Xntk62UCecxZw==} + oxlint@1.80.0: + resolution: {integrity: sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.22.1' + oxlint-tsgolint: '>=7.0.2001' vite-plus: '*' peerDependenciesMeta: oxlint-tsgolint: @@ -8614,7 +8615,7 @@ snapshots: '@expo-google-fonts/material-symbols@0.4.34': {} - '@expo/cli@55.0.33(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-constants@55.0.16)(expo-font@55.0.8)(expo-router@55.0.14)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3)': + '@expo/cli@55.0.33(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-constants@55.0.16)(expo-font@55.0.8)(expo-router@55.0.14)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3)': dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/config': 55.0.18(typescript@6.0.3) @@ -8623,7 +8624,7 @@ snapshots: '@expo/env': 2.1.2 '@expo/image-utils': 0.8.14(typescript@6.0.3) '@expo/json-file': 10.2.0 - '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) '@expo/metro': 55.1.1 '@expo/metro-config': 55.0.24(expo@55.0.27)(typescript@6.0.3) '@expo/osascript': 2.7.0 @@ -8648,7 +8649,7 @@ snapshots: connect: 3.7.0 debug: 4.4.3 dnssd-advertise: 1.1.6 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-server: 55.0.11 fetch-nodeshim: 0.4.10 getenv: 2.0.0 @@ -8675,8 +8676,8 @@ snapshots: ws: 8.21.0 zod: 3.25.76 optionalDependencies: - expo-router: 55.0.14(50649306d987d6f4e510b7a8e94ea6ba) - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + expo-router: 55.0.14(eadb226b6771bbf124ad73e49beee0de) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) transitivePeerDependencies: - '@expo/dom-webview' - '@expo/metro-runtime' @@ -8787,18 +8788,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/devtools@55.0.3(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': + '@expo/devtools@55.0.3(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': dependencies: chalk: 4.1.2 optionalDependencies: react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - '@expo/dom-webview@55.0.5(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': + '@expo/dom-webview@55.0.5(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) '@expo/env@2.1.2': dependencies: @@ -8865,22 +8866,22 @@ snapshots: - supports-color - typescript - '@expo/log-box@55.0.11(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': + '@expo/log-box@55.0.11(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': dependencies: - '@expo/dom-webview': 55.0.5(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@expo/dom-webview': 55.0.5(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) anser: 1.4.10 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) stacktrace-parser: 0.1.11 - '@expo/log-box@55.0.12(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': + '@expo/log-box@55.0.12(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': dependencies: - '@expo/dom-webview': 55.0.5(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@expo/dom-webview': 55.0.5(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) anser: 1.4.10 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) stacktrace-parser: 0.1.11 '@expo/metro-config@55.0.24(expo@55.0.27)(typescript@6.0.3)': @@ -8905,21 +8906,21 @@ snapshots: postcss: 8.5.25 resolve-from: 5.0.0 optionalDependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color - typescript - utf-8-validate - '@expo/metro-runtime@55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': + '@expo/metro-runtime@55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': dependencies: - '@expo/log-box': 55.0.11(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@expo/log-box': 55.0.11(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) anser: 1.4.10 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) pretty-format: 29.7.0 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) stacktrace-parser: 0.1.11 whatwg-fetch: 3.6.20 optionalDependencies: @@ -8986,7 +8987,7 @@ snapshots: '@expo/json-file': 10.0.14 '@react-native/normalize-colors': 0.83.6 debug: 4.4.3 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) resolve-from: 5.0.0 semver: 7.7.4 xml2js: 0.6.0 @@ -9003,7 +9004,7 @@ snapshots: '@expo/json-file': 10.2.0 '@react-native/normalize-colors': 0.83.6 debug: 4.4.3 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) resolve-from: 5.0.0 semver: 7.8.4 xml2js: 0.6.0 @@ -9034,14 +9035,14 @@ snapshots: '@expo/router-server@55.0.18(@expo/metro-runtime@55.0.10)(expo-constants@55.0.16)(expo-font@55.0.8)(expo-router@55.0.14)(expo-server@55.0.11)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: debug: 4.4.3 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) - expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) - expo-font: 55.0.8(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) + expo-font: 55.0.8(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) expo-server: 55.0.11 react: 19.2.6 optionalDependencies: - '@expo/metro-runtime': 55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - expo-router: 55.0.14(50649306d987d6f4e510b7a8e94ea6ba) + '@expo/metro-runtime': 55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + expo-router: 55.0.14(eadb226b6771bbf124ad73e49beee0de) react-dom: 19.2.6(react@19.2.6) transitivePeerDependencies: - supports-color @@ -9060,11 +9061,11 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/vector-icons@15.1.1(expo-font@55.0.8)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': + '@expo/vector-icons@15.1.1(expo-font@55.0.8)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': dependencies: - expo-font: 55.0.8(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + expo-font: 55.0.8(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) '@expo/ws-tunnel@1.0.6': {} @@ -9324,126 +9325,126 @@ snapshots: '@noble/hashes@1.8.0': {} - '@orca/expo-two-way-audio@file:packages/expo-two-way-audio(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': + '@orca/expo-two-way-audio@file:packages/expo-two-way-audio(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) '@oxc-project/types@0.137.0': {} - '@oxfmt/binding-android-arm-eabi@0.52.0': + '@oxfmt/binding-android-arm-eabi@0.65.0': optional: true - '@oxfmt/binding-android-arm64@0.52.0': + '@oxfmt/binding-android-arm64@0.65.0': optional: true - '@oxfmt/binding-darwin-arm64@0.52.0': + '@oxfmt/binding-darwin-arm64@0.65.0': optional: true - '@oxfmt/binding-darwin-x64@0.52.0': + '@oxfmt/binding-darwin-x64@0.65.0': optional: true - '@oxfmt/binding-freebsd-x64@0.52.0': + '@oxfmt/binding-freebsd-x64@0.65.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.52.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.52.0': + '@oxfmt/binding-linux-arm-musleabihf@0.65.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.52.0': + '@oxfmt/binding-linux-arm64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.52.0': + '@oxfmt/binding-linux-arm64-musl@0.65.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.52.0': + '@oxfmt/binding-linux-ppc64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.52.0': + '@oxfmt/binding-linux-riscv64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.52.0': + '@oxfmt/binding-linux-riscv64-musl@0.65.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.52.0': + '@oxfmt/binding-linux-s390x-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.52.0': + '@oxfmt/binding-linux-x64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.52.0': + '@oxfmt/binding-linux-x64-musl@0.65.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.52.0': + '@oxfmt/binding-openharmony-arm64@0.65.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.52.0': + '@oxfmt/binding-win32-arm64-msvc@0.65.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.52.0': + '@oxfmt/binding-win32-ia32-msvc@0.65.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.52.0': + '@oxfmt/binding-win32-x64-msvc@0.65.0': optional: true - '@oxlint/binding-android-arm-eabi@1.71.0': + '@oxlint/binding-android-arm-eabi@1.80.0': optional: true - '@oxlint/binding-android-arm64@1.71.0': + '@oxlint/binding-android-arm64@1.80.0': optional: true - '@oxlint/binding-darwin-arm64@1.71.0': + '@oxlint/binding-darwin-arm64@1.80.0': optional: true - '@oxlint/binding-darwin-x64@1.71.0': + '@oxlint/binding-darwin-x64@1.80.0': optional: true - '@oxlint/binding-freebsd-x64@1.71.0': + '@oxlint/binding-freebsd-x64@1.80.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.71.0': + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.71.0': + '@oxlint/binding-linux-arm-musleabihf@1.80.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.71.0': + '@oxlint/binding-linux-arm64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.71.0': + '@oxlint/binding-linux-arm64-musl@1.80.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.71.0': + '@oxlint/binding-linux-ppc64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.71.0': + '@oxlint/binding-linux-riscv64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.71.0': + '@oxlint/binding-linux-riscv64-musl@1.80.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.71.0': + '@oxlint/binding-linux-s390x-gnu@1.80.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.71.0': + '@oxlint/binding-linux-x64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-x64-musl@1.71.0': + '@oxlint/binding-linux-x64-musl@1.80.0': optional: true - '@oxlint/binding-openharmony-arm64@1.71.0': + '@oxlint/binding-openharmony-arm64@1.80.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.71.0': + '@oxlint/binding-win32-arm64-msvc@1.80.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.71.0': + '@oxlint/binding-win32-ia32-msvc@1.80.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.71.0': + '@oxlint/binding-win32-x64-msvc@1.80.0': optional: true '@pkgr/core@0.2.9': {} @@ -9640,10 +9641,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - '@react-native-async-storage/async-storage@2.2.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))': + '@react-native-async-storage/async-storage@2.2.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))': dependencies: merge-options: 3.0.4 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) '@react-native/assets-registry@0.83.9': {} @@ -9882,24 +9883,24 @@ snapshots: '@react-native/normalize-colors@0.83.9': {} - '@react-native/virtualized-lists@0.83.9(@types/react@19.2.14)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': + '@react-native/virtualized-lists@0.83.9(@types/react@19.2.14)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) optionalDependencies: '@types/react': 19.2.14 - '@react-navigation/bottom-tabs@7.15.11(@react-navigation/native@7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-screens@4.24.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': - dependencies: - '@react-navigation/elements': 2.9.15(@react-navigation/native@7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - '@react-navigation/native': 7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + ? '@react-navigation/bottom-tabs@7.15.11(@react-navigation/native@7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-screens@4.24.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)' + : dependencies: + '@react-navigation/elements': 2.9.15(@react-navigation/native@7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@react-navigation/native': 7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) color: 4.2.3 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - react-native-safe-area-context: 5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - react-native-screens: 4.24.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native-safe-area-context: 5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native-screens: 4.24.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) sf-symbols-typescript: 2.2.0 transitivePeerDependencies: - '@react-native-masked-view/masked-view' @@ -9916,38 +9917,38 @@ snapshots: use-latest-callback: 0.2.6(react@19.2.6) use-sync-external-store: 1.6.0(react@19.2.6) - '@react-navigation/elements@2.9.15(@react-navigation/native@7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': + '@react-navigation/elements@2.9.15(@react-navigation/native@7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': dependencies: - '@react-navigation/native': 7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@react-navigation/native': 7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) color: 4.2.3 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - react-native-safe-area-context: 5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native-safe-area-context: 5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) use-latest-callback: 0.2.6(react@19.2.6) use-sync-external-store: 1.6.0(react@19.2.6) - '@react-navigation/native-stack@7.14.12(@react-navigation/native@7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-screens@4.24.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': - dependencies: - '@react-navigation/elements': 2.9.15(@react-navigation/native@7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - '@react-navigation/native': 7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + ? '@react-navigation/native-stack@7.14.12(@react-navigation/native@7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-screens@4.24.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)' + : dependencies: + '@react-navigation/elements': 2.9.15(@react-navigation/native@7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@react-navigation/native': 7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) color: 4.2.3 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - react-native-safe-area-context: 5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - react-native-screens: 4.24.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native-safe-area-context: 5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native-screens: 4.24.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) sf-symbols-typescript: 2.2.0 warn-once: 0.1.1 transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@react-navigation/native@7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': + '@react-navigation/native@7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)': dependencies: '@react-navigation/core': 7.17.2(react@19.2.6) escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 nanoid: 3.3.18 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) use-latest-callback: 0.2.6(react@19.2.6) '@react-navigation/routers@7.5.3': @@ -10021,13 +10022,13 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@26.1.2))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6)': + '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@26.1.2))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: jest-matcher-utils: 30.3.0 picocolors: 1.1.1 pretty-format: 30.3.0 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) react-test-renderer: 19.2.6(react@19.2.6) redent: 3.0.0 optionalDependencies: @@ -10242,7 +10243,7 @@ snapshots: '@types/react-native@0.73.0(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)': dependencies: - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) transitivePeerDependencies: - '@babel/core' - '@react-native-community/cli' @@ -10282,7 +10283,7 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 '@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@6.0.3) @@ -10302,7 +10303,7 @@ snapshots: dependencies: '@typescript-eslint/scope-manager': 8.59.2 '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 eslint: 9.39.4 @@ -10319,15 +10320,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.2(typescript@6.0.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) - '@typescript-eslint/types': 8.59.2 - debug: 4.4.3 - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/scope-manager@8.59.2': dependencies: '@typescript-eslint/types': 8.59.2 @@ -10337,10 +10329,6 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.59.2(typescript@6.0.3)': - dependencies: - typescript: 6.0.3 - '@typescript-eslint/type-utils@8.59.2(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.59.2 @@ -10370,21 +10358,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3)': - dependencies: - '@typescript-eslint/project-service': 8.59.2(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) - '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/visitor-keys': 8.59.2 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.7.4 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.59.2(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) @@ -10766,7 +10739,7 @@ snapshots: resolve-from: 5.0.0 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -10799,7 +10772,7 @@ snapshots: resolve-from: 5.0.0 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -11664,11 +11637,11 @@ snapshots: eslint-config-universe@15.0.4(eslint@9.39.4)(prettier@2.8.8)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@6.0.3) eslint: 9.39.4 eslint-config-prettier: 9.1.2(eslint@9.39.4) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4) eslint-plugin-n: 17.24.0(eslint@9.39.4)(typescript@5.9.3) eslint-plugin-node: 11.1.0(eslint@9.39.4) eslint-plugin-prettier: 5.5.5(eslint-config-prettier@9.1.2(eslint@9.39.4))(eslint@9.39.4)(prettier@2.8.8) @@ -11692,7 +11665,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4): dependencies: debug: 3.2.7 optionalDependencies: @@ -11715,7 +11688,7 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11726,7 +11699,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4 eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4) hasown: 2.0.3 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -11914,15 +11887,15 @@ snapshots: expo-application@55.0.14(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) - expo-asset@55.0.17(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3): + expo-asset@55.0.17(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.8.14(typescript@6.0.3) - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) - expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) transitivePeerDependencies: - supports-color - typescript @@ -11930,42 +11903,42 @@ snapshots: expo-build-properties@55.0.13(expo@55.0.27): dependencies: '@expo/schema-utils': 55.0.4 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) resolve-from: 5.0.0 semver: 7.7.4 - expo-camera@55.0.18(@types/emscripten@1.41.5)(expo@55.0.27)(react-native-web@0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + expo-camera@55.0.18(@types/emscripten@1.41.5)(expo@55.0.27)(react-native-web@0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: barcode-detector: 3.1.3(@types/emscripten@1.41.5) - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) optionalDependencies: react-native-web: 0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) transitivePeerDependencies: - '@types/emscripten' - expo-clipboard@55.0.13(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + expo-clipboard@55.0.13(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - expo-constants@55.0.16(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)): + expo-constants@55.0.16(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)): dependencies: '@expo/env': 2.1.2 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) transitivePeerDependencies: - supports-color expo-crypto@55.0.14(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-dev-client@55.0.35(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-dev-launcher: 55.0.36(expo@55.0.27) expo-dev-menu: 55.0.30(expo@55.0.27) expo-dev-menu-interface: 55.0.2(expo@55.0.27) @@ -11975,69 +11948,69 @@ snapshots: expo-dev-launcher@55.0.36(expo@55.0.27): dependencies: '@expo/schema-utils': 55.0.4 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-dev-menu: 55.0.30(expo@55.0.27) expo-manifests: 55.0.17(expo@55.0.27) expo-dev-menu-interface@55.0.2(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-dev-menu@55.0.30(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-dev-menu-interface: 55.0.2(expo@55.0.27) expo-document-picker@55.0.13(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) - expo-file-system@55.0.19(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)): + expo-file-system@55.0.19(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - expo-file-system@55.0.23(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)): + expo-file-system@55.0.23(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - expo-font@55.0.8(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + expo-font@55.0.8(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) fontfaceobserver: 2.3.0 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - expo-glass-effect@55.0.11(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + expo-glass-effect@55.0.11(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) expo-haptics@55.0.14(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-image-loader@55.0.1(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-image-manipulator@55.0.17(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-image-loader: 55.0.1(expo@55.0.27) expo-image-picker@55.0.20(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-image-loader: 55.0.1(expo@55.0.27) - expo-image@55.0.10(expo@55.0.27)(react-native-web@0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + expo-image@55.0.10(expo@55.0.27)(react-native-web@0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) sf-symbols-typescript: 2.2.0 optionalDependencies: react-native-web: 0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -12046,25 +12019,25 @@ snapshots: expo-keep-awake@55.0.8(expo@55.0.27)(react@19.2.6): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 - expo-linking@55.0.15(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + expo-linking@55.0.15(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: - expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) + expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) invariant: 2.2.4 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) transitivePeerDependencies: - expo - supports-color expo-manifests@55.0.17(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-json-utils: 55.0.2 - expo-module-scripts@55.0.2(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.25.4)(eslint@9.39.4)(expo@55.0.27)(jest@29.7.0(@types/node@26.1.2))(prettier@2.8.8)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-refresh@0.14.2)(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6): + expo-module-scripts@55.0.2(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.25.4)(eslint@9.39.4)(expo@55.0.27)(jest@29.7.0(@types/node@26.1.2))(prettier@2.8.8)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-refresh@0.14.2)(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@babel/cli': 7.28.6(@babel/core@7.29.7) '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.7) @@ -12072,7 +12045,7 @@ snapshots: '@babel/preset-typescript': 7.28.5(@babel/core@7.29.7) '@expo/npm-proofread': 1.0.1 '@expo/spawn-async': 1.7.2 - '@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@26.1.2))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6) + '@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@26.1.2))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6) '@tsconfig/node18': 18.2.6 '@types/jest': 29.5.14 babel-plugin-dynamic-import-node: 2.3.3 @@ -12080,7 +12053,7 @@ snapshots: commander: 12.1.0 eslint-config-universe: 15.0.4(eslint@9.39.4)(prettier@2.8.8)(typescript@5.9.3) glob: 13.0.6 - jest-expo: 55.0.17(@babel/core@7.29.7)(expo@55.0.27)(jest@29.7.0(@types/node@26.1.2))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) + jest-expo: 55.0.17(@babel/core@7.29.7)(expo@55.0.27)(jest@29.7.0(@types/node@26.1.2))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) jest-snapshot-prettier: prettier@2.8.8 jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@26.1.2)) resolve-workspace-root: 2.0.1 @@ -12120,63 +12093,63 @@ snapshots: - supports-color - typescript - expo-modules-core@55.0.25(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + expo-modules-core@55.0.25(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: invariant: 2.2.4 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) optionalDependencies: - react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) expo-network@55.0.14(expo@55.0.27)(react@19.2.6): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) react: 19.2.6 - expo-notifications@55.0.22(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3): + expo-notifications@55.0.22(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.8.14(typescript@6.0.3) abort-controller: 3.0.0 badgin: 1.2.3 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-application: 55.0.14(expo@55.0.27) - expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) + expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) transitivePeerDependencies: - supports-color - typescript - expo-router@55.0.14(50649306d987d6f4e510b7a8e94ea6ba): + expo-router@55.0.14(eadb226b6771bbf124ad73e49beee0de): dependencies: - '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - '@expo/metro-runtime': 55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@expo/metro-runtime': 55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) '@expo/schema-utils': 55.0.4 '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.6) '@radix-ui/react-tabs': 1.1.13(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@react-navigation/bottom-tabs': 7.15.11(@react-navigation/native@7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-screens@4.24.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - '@react-navigation/native': 7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - '@react-navigation/native-stack': 7.14.12(@react-navigation/native@7.2.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-screens@4.24.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@react-navigation/bottom-tabs': 7.15.11(@react-navigation/native@7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-screens@4.24.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@react-navigation/native': 7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@react-navigation/native-stack': 7.14.12(@react-navigation/native@7.2.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-safe-area-context@5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-screens@4.24.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) client-only: 0.0.1 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) - expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) - expo-glass-effect: 55.0.11(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - expo-image: 55.0.10(expo@55.0.27)(react-native-web@0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - expo-linking: 55.0.15(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) + expo-glass-effect: 55.0.11(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + expo-image: 55.0.10(expo@55.0.27)(react-native-web@0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + expo-linking: 55.0.15(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) expo-server: 55.0.9 - expo-symbols: 55.0.8(expo-font@55.0.8)(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + expo-symbols: 55.0.8(expo-font@55.0.8)(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) fast-deep-equal: 3.1.3 invariant: 2.2.4 nanoid: 3.3.18 query-string: 7.1.3 react: 19.2.6 react-fast-compare: 3.2.2 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - react-native-is-edge-to-edge: 1.3.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - react-native-safe-area-context: 5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - react-native-screens: 4.24.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native-is-edge-to-edge: 1.3.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native-safe-area-context: 5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native-screens: 4.24.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) semver: 7.6.3 server-only: 0.0.1 sf-symbols-typescript: 2.2.0 @@ -12184,10 +12157,10 @@ snapshots: use-latest-callback: 0.2.6(react@19.2.6) vaul: 1.1.2(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) optionalDependencies: - '@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@26.1.2))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6) + '@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@26.1.2))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react-test-renderer@19.2.6(react@19.2.6))(react@19.2.6) react-dom: 19.2.6(react@19.2.6) - react-native-gesture-handler: 2.31.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - react-native-reanimated: 4.3.0(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native-gesture-handler: 2.31.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native-reanimated: 4.3.0(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) react-native-web: 0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) transitivePeerDependencies: - '@react-native-masked-view/masked-view' @@ -12198,7 +12171,7 @@ snapshots: expo-secure-store@55.0.13(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) expo-server@55.0.11: {} @@ -12207,61 +12180,61 @@ snapshots: expo-splash-screen@55.0.20(expo@55.0.27)(typescript@6.0.3): dependencies: '@expo/prebuild-config': 55.0.17(expo@55.0.27)(typescript@6.0.3) - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) transitivePeerDependencies: - supports-color - typescript - expo-status-bar@55.0.6(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + expo-status-bar@55.0.6(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - react-native-is-edge-to-edge: 1.3.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native-is-edge-to-edge: 1.3.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - expo-symbols@55.0.8(expo-font@55.0.8)(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + expo-symbols@55.0.8(expo-font@55.0.8)(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: '@expo-google-fonts/material-symbols': 0.4.34 - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) - expo-font: 55.0.8(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo-font: 55.0.8(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) sf-symbols-typescript: 2.2.0 expo-updates-interface@55.1.6(expo@55.0.27): dependencies: - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) - expo@55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3): + expo@55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.7 - '@expo/cli': 55.0.33(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-constants@55.0.16)(expo-font@55.0.8)(expo-router@55.0.14)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + '@expo/cli': 55.0.33(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-constants@55.0.16)(expo-font@55.0.8)(expo-router@55.0.14)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) '@expo/config': 55.0.18(typescript@6.0.3) '@expo/config-plugins': 55.0.10 - '@expo/devtools': 55.0.3(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@expo/devtools': 55.0.3(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) '@expo/fingerprint': 0.16.7 '@expo/local-build-cache-provider': 55.0.14(typescript@6.0.3) - '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) '@expo/metro': 55.1.1 '@expo/metro-config': 55.0.24(expo@55.0.27)(typescript@6.0.3) - '@expo/vector-icons': 15.1.1(expo-font@55.0.8)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@expo/vector-icons': 15.1.1(expo-font@55.0.8)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) '@ungap/structured-clone': 1.3.1 babel-preset-expo: 55.0.23(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@55.0.27)(react-refresh@0.14.2) - expo-asset: 55.0.17(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) - expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) - expo-file-system: 55.0.23(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) - expo-font: 55.0.8(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + expo-asset: 55.0.17(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo-constants: 55.0.16(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) + expo-file-system: 55.0.23(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6)) + expo-font: 55.0.8(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) expo-keep-awake: 55.0.8(expo@55.0.27)(react@19.2.6) expo-modules-autolinking: 55.0.24(typescript@6.0.3) - expo-modules-core: 55.0.25(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + expo-modules-core: 55.0.25(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) pretty-format: 29.7.0 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) react-refresh: 0.14.2 whatwg-url-minimum: 0.1.2 optionalDependencies: - '@expo/dom-webview': 55.0.5(expo@55.0.27)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - '@expo/metro-runtime': 55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - react-native-webview: 13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@expo/dom-webview': 55.0.5(expo@55.0.27)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@expo/metro-runtime': 55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.27)(react-dom@19.2.6(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native-webview: 13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) transitivePeerDependencies: - '@babel/core' - bufferutil @@ -12962,21 +12935,21 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - jest-expo@55.0.17(@babel/core@7.29.7)(expo@55.0.27)(jest@29.7.0(@types/node@26.1.2))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@5.9.3): + jest-expo@55.0.17(@babel/core@7.29.7)(expo@55.0.27)(jest@29.7.0(@types/node@26.1.2))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@5.9.3): dependencies: '@expo/config': 55.0.16(typescript@5.9.3) '@expo/json-file': 10.0.14 '@jest/create-cache-key-function': 29.7.0 '@jest/globals': 29.7.0 babel-jest: 29.7.0(@babel/core@7.29.7) - expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) + expo: 55.0.27(@babel/core@7.29.7)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.14)(react-dom@19.2.6(react@19.2.6))(react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6)(typescript@6.0.3) jest-environment-jsdom: 29.7.0 jest-snapshot: 29.7.0 jest-watch-select-projects: 2.0.0 jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@26.1.2)) json5: 2.2.3 lodash: 4.18.1 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) react-test-renderer: 19.2.0(react@19.2.6) server-only: 0.0.1 stacktrace-js: 2.0.2 @@ -13409,11 +13382,11 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react-native@1.14.0(react-native-svg@15.15.4(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + lucide-react-native@1.14.0(react-native-svg@15.15.4(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - react-native-svg: 15.15.4(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native-svg: 15.15.4(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) magic-string@0.30.21: dependencies: @@ -14026,51 +13999,51 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - oxfmt@0.52.0: + oxfmt@0.65.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.52.0 - '@oxfmt/binding-android-arm64': 0.52.0 - '@oxfmt/binding-darwin-arm64': 0.52.0 - '@oxfmt/binding-darwin-x64': 0.52.0 - '@oxfmt/binding-freebsd-x64': 0.52.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.52.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.52.0 - '@oxfmt/binding-linux-arm64-gnu': 0.52.0 - '@oxfmt/binding-linux-arm64-musl': 0.52.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.52.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.52.0 - '@oxfmt/binding-linux-riscv64-musl': 0.52.0 - '@oxfmt/binding-linux-s390x-gnu': 0.52.0 - '@oxfmt/binding-linux-x64-gnu': 0.52.0 - '@oxfmt/binding-linux-x64-musl': 0.52.0 - '@oxfmt/binding-openharmony-arm64': 0.52.0 - '@oxfmt/binding-win32-arm64-msvc': 0.52.0 - '@oxfmt/binding-win32-ia32-msvc': 0.52.0 - '@oxfmt/binding-win32-x64-msvc': 0.52.0 + '@oxfmt/binding-android-arm-eabi': 0.65.0 + '@oxfmt/binding-android-arm64': 0.65.0 + '@oxfmt/binding-darwin-arm64': 0.65.0 + '@oxfmt/binding-darwin-x64': 0.65.0 + '@oxfmt/binding-freebsd-x64': 0.65.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.65.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.65.0 + '@oxfmt/binding-linux-arm64-gnu': 0.65.0 + '@oxfmt/binding-linux-arm64-musl': 0.65.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.65.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.65.0 + '@oxfmt/binding-linux-riscv64-musl': 0.65.0 + '@oxfmt/binding-linux-s390x-gnu': 0.65.0 + '@oxfmt/binding-linux-x64-gnu': 0.65.0 + '@oxfmt/binding-linux-x64-musl': 0.65.0 + '@oxfmt/binding-openharmony-arm64': 0.65.0 + '@oxfmt/binding-win32-arm64-msvc': 0.65.0 + '@oxfmt/binding-win32-ia32-msvc': 0.65.0 + '@oxfmt/binding-win32-x64-msvc': 0.65.0 - oxlint@1.71.0: + oxlint@1.80.0: optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.71.0 - '@oxlint/binding-android-arm64': 1.71.0 - '@oxlint/binding-darwin-arm64': 1.71.0 - '@oxlint/binding-darwin-x64': 1.71.0 - '@oxlint/binding-freebsd-x64': 1.71.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.71.0 - '@oxlint/binding-linux-arm-musleabihf': 1.71.0 - '@oxlint/binding-linux-arm64-gnu': 1.71.0 - '@oxlint/binding-linux-arm64-musl': 1.71.0 - '@oxlint/binding-linux-ppc64-gnu': 1.71.0 - '@oxlint/binding-linux-riscv64-gnu': 1.71.0 - '@oxlint/binding-linux-riscv64-musl': 1.71.0 - '@oxlint/binding-linux-s390x-gnu': 1.71.0 - '@oxlint/binding-linux-x64-gnu': 1.71.0 - '@oxlint/binding-linux-x64-musl': 1.71.0 - '@oxlint/binding-openharmony-arm64': 1.71.0 - '@oxlint/binding-win32-arm64-msvc': 1.71.0 - '@oxlint/binding-win32-ia32-msvc': 1.71.0 - '@oxlint/binding-win32-x64-msvc': 1.71.0 + '@oxlint/binding-android-arm-eabi': 1.80.0 + '@oxlint/binding-android-arm64': 1.80.0 + '@oxlint/binding-darwin-arm64': 1.80.0 + '@oxlint/binding-darwin-x64': 1.80.0 + '@oxlint/binding-freebsd-x64': 1.80.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.80.0 + '@oxlint/binding-linux-arm-musleabihf': 1.80.0 + '@oxlint/binding-linux-arm64-gnu': 1.80.0 + '@oxlint/binding-linux-arm64-musl': 1.80.0 + '@oxlint/binding-linux-ppc64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-musl': 1.80.0 + '@oxlint/binding-linux-s390x-gnu': 1.80.0 + '@oxlint/binding-linux-x64-gnu': 1.80.0 + '@oxlint/binding-linux-x64-musl': 1.80.0 + '@oxlint/binding-openharmony-arm64': 1.80.0 + '@oxlint/binding-win32-arm64-msvc': 1.80.0 + '@oxlint/binding-win32-ia32-msvc': 1.80.0 + '@oxlint/binding-win32-x64-msvc': 1.80.0 p-limit@2.3.0: dependencies: @@ -14260,46 +14233,46 @@ snapshots: react-is@19.2.6: {} - react-native-gesture-handler@2.31.2(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + react-native-gesture-handler@2.31.2(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: '@egjs/hammerjs': 2.0.17 '@types/react-test-renderer': 19.1.0 hoist-non-react-statics: 3.3.2 invariant: 2.2.4 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - react-native-is-edge-to-edge@1.3.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + react-native-is-edge-to-edge@1.3.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - react-native-reanimated@4.3.0(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + react-native-reanimated@4.3.0(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - react-native-is-edge-to-edge: 1.3.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) - react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native-is-edge-to-edge: 1.3.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) semver: 7.7.4 - react-native-safe-area-context@5.7.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + react-native-safe-area-context@5.7.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - react-native-screens@4.24.0(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + react-native-screens@4.24.0(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 react-freeze: 1.0.4(react@19.2.6) - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) warn-once: 0.1.1 - react-native-svg@15.15.4(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + react-native-svg@15.15.4(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: css-select: 5.2.2 css-tree: 1.1.3 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) warn-once: 0.1.1 react-native-web@0.21.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6): @@ -14317,14 +14290,14 @@ snapshots: transitivePeerDependencies: - encoding - react-native-webview@13.16.1(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + react-native-webview@13.16.1(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: escape-string-regexp: 4.0.0 invariant: 2.2.4 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) - react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): + react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6): dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.7) @@ -14339,12 +14312,12 @@ snapshots: '@react-native/metro-config': 0.85.2(@babel/core@7.29.7) convert-source-map: 2.0.0 react: 19.2.6 - react-native: 0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) + react-native: 0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6) semver: 7.7.4 transitivePeerDependencies: - supports-color - react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6): + react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6): dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native/assets-registry': 0.83.9 @@ -14353,7 +14326,7 @@ snapshots: '@react-native/gradle-plugin': 0.83.9 '@react-native/js-polyfills': 0.83.9 '@react-native/normalize-colors': 0.83.9 - '@react-native/virtualized-lists': 0.83.9(@types/react@19.2.14)(react-native@0.83.9(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) + '@react-native/virtualized-lists': 0.83.9(@types/react@19.2.14)(react-native@0.83.9(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.6))(react@19.2.6) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 @@ -14979,10 +14952,6 @@ snapshots: dependencies: typescript: 5.9.3 - ts-api-utils@2.5.0(typescript@6.0.3): - dependencies: - typescript: 6.0.3 - ts-declaration-location@1.0.7(typescript@5.9.3): dependencies: picomatch: 4.0.4 diff --git a/mobile/src/files/file-list-fallback.ts b/mobile/src/files/file-list-fallback.ts index ef3041029af..aa7323e13b5 100644 --- a/mobile/src/files/file-list-fallback.ts +++ b/mobile/src/files/file-list-fallback.ts @@ -60,13 +60,10 @@ export function directoryCacheFromFileList(files: LegacyMobileFileEntry[]): Dire Array.from(childrenByDir, ([path, children]) => [ path, { - entries: Array.from( - children, - ([name, isDirectory]): MobileDirEntry => ({ - name, - isDirectory - }) - ) + entries: Array.from(children, ([name, isDirectory]): MobileDirEntry => ({ + name, + isDirectory + })) } ]) ) diff --git a/mobile/src/session/mobile-session-tabs-stream-health.test.ts b/mobile/src/session/mobile-session-tabs-stream-health.test.ts index 399128bb511..b86e5061fa0 100644 --- a/mobile/src/session/mobile-session-tabs-stream-health.test.ts +++ b/mobile/src/session/mobile-session-tabs-stream-health.test.ts @@ -72,12 +72,10 @@ function makeHarness(options?: { } as unknown as RpcClient const apply = options?.apply ?? - vi.fn( - (value: TestResult): SessionTabsApplyOutcome => ({ - accepted: true, - effectiveTabs: value.tabs - }) - ) + vi.fn((value: TestResult): SessionTabsApplyOutcome => ({ + accepted: true, + effectiveTabs: value.tabs + })) const consumeAccepted = vi.fn() let recoveryNeeded = false const controller = new MobileSessionTabsStreamHealth({ @@ -306,9 +304,8 @@ describe('MobileSessionTabsStreamHealth', () => { }) it('does not consume a rejected stream snapshot or update', () => { - const apply = vi.fn( - (value: TestResult): SessionTabsApplyOutcome => - value.type ? { accepted: false } : { accepted: true, effectiveTabs: value.tabs } + const apply = vi.fn((value: TestResult): SessionTabsApplyOutcome => + value.type ? { accepted: false } : { accepted: true, effectiveTabs: value.tabs } ) const harness = makeHarness({ apply }) const subscription = harness.controller.beginSubscription() diff --git a/mobile/src/session/use-mobile-session-tabs-reconciliation.test.ts b/mobile/src/session/use-mobile-session-tabs-reconciliation.test.ts index 33c507f2712..fe979521874 100644 --- a/mobile/src/session/use-mobile-session-tabs-reconciliation.test.ts +++ b/mobile/src/session/use-mobile-session-tabs-reconciliation.test.ts @@ -39,12 +39,10 @@ type TestResult = { } const fetchTerminals = vi.fn(async () => {}) -const applySessionTabs = vi.fn( - (value: TestResult): SessionTabsApplyOutcome => ({ - accepted: true, - effectiveTabs: value.tabs - }) -) +const applySessionTabs = vi.fn((value: TestResult): SessionTabsApplyOutcome => ({ + accepted: true, + effectiveTabs: value.tabs +})) const consumeAcceptedSessionTabs = vi.fn() let recoveryNeeded = false let clearRecoveryAt = Number.POSITIVE_INFINITY diff --git a/mobile/src/transport/mobile-direct-endpoint-probe.test.ts b/mobile/src/transport/mobile-direct-endpoint-probe.test.ts index 3afa8c08aac..69fe8a2ab8a 100644 --- a/mobile/src/transport/mobile-direct-endpoint-probe.test.ts +++ b/mobile/src/transport/mobile-direct-endpoint-probe.test.ts @@ -4,14 +4,12 @@ import { openAuthenticatedDirectEndpoint } from './mobile-direct-endpoint-probe' import type { ConnectionState, HostProfile, RpcResponse } from './types' class FakeClient implements RpcClient { - readonly sendRequest = vi.fn( - async (): Promise => ({ - id: 'rpc-1', - ok: true, - result: {}, - _meta: { runtimeId: 'runtime-1' } - }) - ) + readonly sendRequest = vi.fn(async (): Promise => ({ + id: 'rpc-1', + ok: true, + result: {}, + _meta: { runtimeId: 'runtime-1' } + })) readonly subscribe = vi.fn(() => () => {}) readonly updateTerminalSubscriptionViewport = vi.fn() readonly notifyForeground = vi.fn() diff --git a/mobile/src/transport/mobile-relay-runtime-failover.test.ts b/mobile/src/transport/mobile-relay-runtime-failover.test.ts index 0187ac251d8..795f2618dfb 100644 --- a/mobile/src/transport/mobile-relay-runtime-failover.test.ts +++ b/mobile/src/transport/mobile-relay-runtime-failover.test.ts @@ -41,14 +41,12 @@ vi.mock('./e2ee', () => ({ })) class FakeSession implements RpcClient { - readonly sendRequest = vi.fn( - async (): Promise => ({ - id: 'rpc-1', - ok: true, - result: {}, - _meta: { runtimeId: 'runtime-1' } - }) - ) + readonly sendRequest = vi.fn(async (): Promise => ({ + id: 'rpc-1', + ok: true, + result: {}, + _meta: { runtimeId: 'runtime-1' } + })) readonly subscribe = vi.fn(() => () => {}) readonly updateTerminalSubscriptionViewport = vi.fn() readonly notifyForeground = vi.fn() diff --git a/package.json b/package.json index 0e84852442c..52ce04d7583 100644 --- a/package.json +++ b/package.json @@ -239,8 +239,8 @@ "marked": "^17.0.1", "mermaid": "^11.16.1", "monaco-editor": "^0.55.1", - "oxfmt": "^0.52.0", - "oxlint": "^1.77.0", + "oxfmt": "^0.65.0", + "oxlint": "^1.80.0", "oxlint-plugin-react-doctor": "0.9.1", "oxlint-tsgolint": "7.0.2001", "pdfjs-dist": "^6.2.108", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c4f7e82f7f..3c425b79c2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -396,11 +396,11 @@ importers: specifier: ^0.55.1 version: 0.55.1 oxfmt: - specifier: ^0.52.0 - version: 0.52.0 + specifier: ^0.65.0 + version: 0.65.0 oxlint: - specifier: ^1.77.0 - version: 1.77.0(oxlint-tsgolint@7.0.2001) + specifier: ^1.80.0 + version: 1.80.0(oxlint-tsgolint@7.0.2001) oxlint-plugin-react-doctor: specifier: 0.9.1 version: 0.9.1 @@ -1444,124 +1444,124 @@ packages: '@oxc-project/types@0.141.0': resolution: {integrity: sha512-S4as7z0j0xQkXcJlyY5ehntwK8/wRkQb9Cyqw+J/N2rkWGQGK0SxD6X6DhQTc7qsxVTBxXbxZtBJh3mr3PtIzQ==} - '@oxfmt/binding-android-arm-eabi@0.52.0': - resolution: {integrity: sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ==} + '@oxfmt/binding-android-arm-eabi@0.65.0': + resolution: {integrity: sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.52.0': - resolution: {integrity: sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ==} + '@oxfmt/binding-android-arm64@0.65.0': + resolution: {integrity: sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.52.0': - resolution: {integrity: sha512-f9+bLvOYxy7NttCLFTvQ7afmqDOWY4wIP9xdvfj5trQ1qj6f2UFAGwZESlfsMjvJNTyRpXfIlOanCI9FOvoeQA==} + '@oxfmt/binding-darwin-arm64@0.65.0': + resolution: {integrity: sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.52.0': - resolution: {integrity: sha512-YSTB9sJ5nnQd/Q0ddHkgof0ZCHPAnWZT1IW2SJ8omz7CP7KluJhO1fNHrpqdxCtpztJwSs4hY1uAee35wKxxaw==} + '@oxfmt/binding-darwin-x64@0.65.0': + resolution: {integrity: sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.52.0': - resolution: {integrity: sha512-NIrRNTTPCs4UbmVs0bxLSCDlLCtIRMJIXklNKaXa5Oj2/K1UIMBvgE8+uPVo01Io3N9HF0+GAX+aAHjUgZS7vA==} + '@oxfmt/binding-freebsd-x64@0.65.0': + resolution: {integrity: sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.52.0': - resolution: {integrity: sha512-JXUCde8mn3GpgQouz2PXUokgy/uT1QrRJBL2s983VWcSQp62wTFYiNXgTKdeo1Jgbr0IgUnKKvzIk/YBlj/nVQ==} + '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': + resolution: {integrity: sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.52.0': - resolution: {integrity: sha512-psbUXaRZ+V8DaXz10Qf7LSHtdtdKAmC8fxXgeU608jjzrmWK4quamZMOpl6sf+dikoFHA85uE93Q0BqxrCdQrQ==} + '@oxfmt/binding-linux-arm-musleabihf@0.65.0': + resolution: {integrity: sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.52.0': - resolution: {integrity: sha512-Jw7MgWUU9lcLCcy82updISP3EthTlfvAwR6gWNxPzqly7+fLvOi2gHQE9xXQjpqaVLm/8P+gOzlv9ODuoVlaaw==} + '@oxfmt/binding-linux-arm64-gnu@0.65.0': + resolution: {integrity: sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.52.0': - resolution: {integrity: sha512-wZg6bLjDvh2KibyI3QFUYo8GTXneIFsd0JvehtvJiUmQ8WRPERgxd/VM4ctWb86U5FT1FkqgS8/wZKVB+AZScg==} + '@oxfmt/binding-linux-arm64-musl@0.65.0': + resolution: {integrity: sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.52.0': - resolution: {integrity: sha512-IngE8uxhNvxcMrLjZNDo9xNLY7rEK33AKnaMd2B46he1e/mz2CfcW6If/U1wUjdRZddm1QzQaciqZkuMkdh1FA==} + '@oxfmt/binding-linux-ppc64-gnu@0.65.0': + resolution: {integrity: sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.52.0': - resolution: {integrity: sha512-H3+DdFMv/efN3Efmhsv18jDrpiWWqKG7wsfAlQBqAt6z/E2Bx+TwEj2Nowe51CPOWB8/mFBC2dAMSgVFLvvowA==} + '@oxfmt/binding-linux-riscv64-gnu@0.65.0': + resolution: {integrity: sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.52.0': - resolution: {integrity: sha512-zji+1kb7lJKohSDjzC1IsS+K/cKRs1hdVf0ZH0VbdbiakmtLvN9twBoXo/k8VdjFax7kfo+DyPxS7vv52br1aw==} + '@oxfmt/binding-linux-riscv64-musl@0.65.0': + resolution: {integrity: sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.52.0': - resolution: {integrity: sha512-hcLBYedpCy7ToUvvBidWk7+11Yhg1oAZ4+6hKPic/mQI6NaqXJSXMps5nFlwUuX2ewhtLZZDPg63TI042qGKBg==} + '@oxfmt/binding-linux-s390x-gnu@0.65.0': + resolution: {integrity: sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.52.0': - resolution: {integrity: sha512-IDO2loXK2OtTOhSPchU9MW25mWL2QCDGdJbjN8MXKZVS80qXe5gMTwQWu/gMJ3juoBHbkuUZNB2N1LHzNT7DoA==} + '@oxfmt/binding-linux-x64-gnu@0.65.0': + resolution: {integrity: sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.52.0': - resolution: {integrity: sha512-mAV2Hjn0SatJ+KoAzKUC3eJhdJ8wv+3m1KyuS0dTsbF0c5weq+QrCt/DRZZM+uj/XiKzCDEUKYsBF30e2qkcyw==} + '@oxfmt/binding-linux-x64-musl@0.65.0': + resolution: {integrity: sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.52.0': - resolution: {integrity: sha512-vd4npaUIwChxp7XzkqmepBWTT9YMcSe/NBApVGPC30/lLyOVaV3dvma1SKo03t8O73BPRAG7EyJzGlN5cJM5hQ==} + '@oxfmt/binding-openharmony-arm64@0.65.0': + resolution: {integrity: sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.52.0': - resolution: {integrity: sha512-k2sz6gWQdMfh5HPpIS+Bw/0UEV/kaK2xuqJRrWL233sEHx9WLlsmvlPFM4HUNThkYbSN0U0vPW7LVKZWDS8hPQ==} + '@oxfmt/binding-win32-arm64-msvc@0.65.0': + resolution: {integrity: sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.52.0': - resolution: {integrity: sha512-rhke69GTcArodLHpjMTfNnvjTEBryDeZcUCKK/VjXDMtfTULl6QRh0ymX5/hbCUv2WjYm9h/QbW++q2vE15gWQ==} + '@oxfmt/binding-win32-ia32-msvc@0.65.0': + resolution: {integrity: sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.52.0': - resolution: {integrity: sha512-q5xL7oeXkZdEtNZWBdvehJcmt+GRu9l2bK40yJs1jJXlqq+r0Hygb1rTjq+FM2o/2xyt4cufH6KRplHp3Jjsvw==} + '@oxfmt/binding-win32-x64-msvc@0.65.0': + resolution: {integrity: sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1596,124 +1596,124 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.77.0': - resolution: {integrity: sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ==} + '@oxlint/binding-android-arm-eabi@1.80.0': + resolution: {integrity: sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.77.0': - resolution: {integrity: sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA==} + '@oxlint/binding-android-arm64@1.80.0': + resolution: {integrity: sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.77.0': - resolution: {integrity: sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA==} + '@oxlint/binding-darwin-arm64@1.80.0': + resolution: {integrity: sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.77.0': - resolution: {integrity: sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA==} + '@oxlint/binding-darwin-x64@1.80.0': + resolution: {integrity: sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.77.0': - resolution: {integrity: sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg==} + '@oxlint/binding-freebsd-x64@1.80.0': + resolution: {integrity: sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.77.0': - resolution: {integrity: sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA==} + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': + resolution: {integrity: sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.77.0': - resolution: {integrity: sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ==} + '@oxlint/binding-linux-arm-musleabihf@1.80.0': + resolution: {integrity: sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.77.0': - resolution: {integrity: sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw==} + '@oxlint/binding-linux-arm64-gnu@1.80.0': + resolution: {integrity: sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.77.0': - resolution: {integrity: sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g==} + '@oxlint/binding-linux-arm64-musl@1.80.0': + resolution: {integrity: sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.77.0': - resolution: {integrity: sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg==} + '@oxlint/binding-linux-ppc64-gnu@1.80.0': + resolution: {integrity: sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.77.0': - resolution: {integrity: sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q==} + '@oxlint/binding-linux-riscv64-gnu@1.80.0': + resolution: {integrity: sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.77.0': - resolution: {integrity: sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA==} + '@oxlint/binding-linux-riscv64-musl@1.80.0': + resolution: {integrity: sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.77.0': - resolution: {integrity: sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw==} + '@oxlint/binding-linux-s390x-gnu@1.80.0': + resolution: {integrity: sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.77.0': - resolution: {integrity: sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw==} + '@oxlint/binding-linux-x64-gnu@1.80.0': + resolution: {integrity: sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.77.0': - resolution: {integrity: sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ==} + '@oxlint/binding-linux-x64-musl@1.80.0': + resolution: {integrity: sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.77.0': - resolution: {integrity: sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw==} + '@oxlint/binding-openharmony-arm64@1.80.0': + resolution: {integrity: sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.77.0': - resolution: {integrity: sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA==} + '@oxlint/binding-win32-arm64-msvc@1.80.0': + resolution: {integrity: sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.77.0': - resolution: {integrity: sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA==} + '@oxlint/binding-win32-ia32-msvc@1.80.0': + resolution: {integrity: sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.77.0': - resolution: {integrity: sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw==} + '@oxlint/binding-win32-x64-msvc@1.80.0': + resolution: {integrity: sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -5638,8 +5638,8 @@ packages: resolution: {integrity: sha512-uFkGGr1KMWd6aWv9UAqooYrN78trw8MWWmoPvgWokfBEUq1+eiIQ+qfj3wokhy0fxtZWZk+0dHoS7/yRTJtd6w==} engines: {node: ^20.19.0 || >=22.12.0} - oxfmt@0.52.0: - resolution: {integrity: sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug==} + oxfmt@0.65.0: + resolution: {integrity: sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5659,8 +5659,8 @@ packages: resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==} hasBin: true - oxlint@1.77.0: - resolution: {integrity: sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg==} + oxlint@1.80.0: + resolution: {integrity: sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -7852,61 +7852,61 @@ snapshots: '@oxc-project/types@0.141.0': {} - '@oxfmt/binding-android-arm-eabi@0.52.0': + '@oxfmt/binding-android-arm-eabi@0.65.0': optional: true - '@oxfmt/binding-android-arm64@0.52.0': + '@oxfmt/binding-android-arm64@0.65.0': optional: true - '@oxfmt/binding-darwin-arm64@0.52.0': + '@oxfmt/binding-darwin-arm64@0.65.0': optional: true - '@oxfmt/binding-darwin-x64@0.52.0': + '@oxfmt/binding-darwin-x64@0.65.0': optional: true - '@oxfmt/binding-freebsd-x64@0.52.0': + '@oxfmt/binding-freebsd-x64@0.65.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.52.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.52.0': + '@oxfmt/binding-linux-arm-musleabihf@0.65.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.52.0': + '@oxfmt/binding-linux-arm64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.52.0': + '@oxfmt/binding-linux-arm64-musl@0.65.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.52.0': + '@oxfmt/binding-linux-ppc64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.52.0': + '@oxfmt/binding-linux-riscv64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.52.0': + '@oxfmt/binding-linux-riscv64-musl@0.65.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.52.0': + '@oxfmt/binding-linux-s390x-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.52.0': + '@oxfmt/binding-linux-x64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.52.0': + '@oxfmt/binding-linux-x64-musl@0.65.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.52.0': + '@oxfmt/binding-openharmony-arm64@0.65.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.52.0': + '@oxfmt/binding-win32-arm64-msvc@0.65.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.52.0': + '@oxfmt/binding-win32-ia32-msvc@0.65.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.52.0': + '@oxfmt/binding-win32-x64-msvc@0.65.0': optional: true '@oxlint-tsgolint/darwin-arm64@7.0.2001': @@ -7927,61 +7927,61 @@ snapshots: '@oxlint-tsgolint/win32-x64@7.0.2001': optional: true - '@oxlint/binding-android-arm-eabi@1.77.0': + '@oxlint/binding-android-arm-eabi@1.80.0': optional: true - '@oxlint/binding-android-arm64@1.77.0': + '@oxlint/binding-android-arm64@1.80.0': optional: true - '@oxlint/binding-darwin-arm64@1.77.0': + '@oxlint/binding-darwin-arm64@1.80.0': optional: true - '@oxlint/binding-darwin-x64@1.77.0': + '@oxlint/binding-darwin-x64@1.80.0': optional: true - '@oxlint/binding-freebsd-x64@1.77.0': + '@oxlint/binding-freebsd-x64@1.80.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.77.0': + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.77.0': + '@oxlint/binding-linux-arm-musleabihf@1.80.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.77.0': + '@oxlint/binding-linux-arm64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.77.0': + '@oxlint/binding-linux-arm64-musl@1.80.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.77.0': + '@oxlint/binding-linux-ppc64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.77.0': + '@oxlint/binding-linux-riscv64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.77.0': + '@oxlint/binding-linux-riscv64-musl@1.80.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.77.0': + '@oxlint/binding-linux-s390x-gnu@1.80.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.77.0': + '@oxlint/binding-linux-x64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-x64-musl@1.77.0': + '@oxlint/binding-linux-x64-musl@1.80.0': optional: true - '@oxlint/binding-openharmony-arm64@1.77.0': + '@oxlint/binding-openharmony-arm64@1.80.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.77.0': + '@oxlint/binding-win32-arm64-msvc@1.80.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.77.0': + '@oxlint/binding-win32-ia32-msvc@1.80.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.77.0': + '@oxlint/binding-win32-x64-msvc@1.80.0': optional: true '@parcel/watcher-android-arm64@2.5.6': @@ -12264,29 +12264,29 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc': 0.141.0 '@oxc-parser/binding-win32-x64-msvc': 0.141.0 - oxfmt@0.52.0: + oxfmt@0.65.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.52.0 - '@oxfmt/binding-android-arm64': 0.52.0 - '@oxfmt/binding-darwin-arm64': 0.52.0 - '@oxfmt/binding-darwin-x64': 0.52.0 - '@oxfmt/binding-freebsd-x64': 0.52.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.52.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.52.0 - '@oxfmt/binding-linux-arm64-gnu': 0.52.0 - '@oxfmt/binding-linux-arm64-musl': 0.52.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.52.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.52.0 - '@oxfmt/binding-linux-riscv64-musl': 0.52.0 - '@oxfmt/binding-linux-s390x-gnu': 0.52.0 - '@oxfmt/binding-linux-x64-gnu': 0.52.0 - '@oxfmt/binding-linux-x64-musl': 0.52.0 - '@oxfmt/binding-openharmony-arm64': 0.52.0 - '@oxfmt/binding-win32-arm64-msvc': 0.52.0 - '@oxfmt/binding-win32-ia32-msvc': 0.52.0 - '@oxfmt/binding-win32-x64-msvc': 0.52.0 + '@oxfmt/binding-android-arm-eabi': 0.65.0 + '@oxfmt/binding-android-arm64': 0.65.0 + '@oxfmt/binding-darwin-arm64': 0.65.0 + '@oxfmt/binding-darwin-x64': 0.65.0 + '@oxfmt/binding-freebsd-x64': 0.65.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.65.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.65.0 + '@oxfmt/binding-linux-arm64-gnu': 0.65.0 + '@oxfmt/binding-linux-arm64-musl': 0.65.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.65.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.65.0 + '@oxfmt/binding-linux-riscv64-musl': 0.65.0 + '@oxfmt/binding-linux-s390x-gnu': 0.65.0 + '@oxfmt/binding-linux-x64-gnu': 0.65.0 + '@oxfmt/binding-linux-x64-musl': 0.65.0 + '@oxfmt/binding-openharmony-arm64': 0.65.0 + '@oxfmt/binding-win32-arm64-msvc': 0.65.0 + '@oxfmt/binding-win32-ia32-msvc': 0.65.0 + '@oxfmt/binding-win32-x64-msvc': 0.65.0 oxlint-plugin-react-doctor@0.9.1: dependencies: @@ -12304,27 +12304,27 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 7.0.2001 '@oxlint-tsgolint/win32-x64': 7.0.2001 - oxlint@1.77.0(oxlint-tsgolint@7.0.2001): + oxlint@1.80.0(oxlint-tsgolint@7.0.2001): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.77.0 - '@oxlint/binding-android-arm64': 1.77.0 - '@oxlint/binding-darwin-arm64': 1.77.0 - '@oxlint/binding-darwin-x64': 1.77.0 - '@oxlint/binding-freebsd-x64': 1.77.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.77.0 - '@oxlint/binding-linux-arm-musleabihf': 1.77.0 - '@oxlint/binding-linux-arm64-gnu': 1.77.0 - '@oxlint/binding-linux-arm64-musl': 1.77.0 - '@oxlint/binding-linux-ppc64-gnu': 1.77.0 - '@oxlint/binding-linux-riscv64-gnu': 1.77.0 - '@oxlint/binding-linux-riscv64-musl': 1.77.0 - '@oxlint/binding-linux-s390x-gnu': 1.77.0 - '@oxlint/binding-linux-x64-gnu': 1.77.0 - '@oxlint/binding-linux-x64-musl': 1.77.0 - '@oxlint/binding-openharmony-arm64': 1.77.0 - '@oxlint/binding-win32-arm64-msvc': 1.77.0 - '@oxlint/binding-win32-ia32-msvc': 1.77.0 - '@oxlint/binding-win32-x64-msvc': 1.77.0 + '@oxlint/binding-android-arm-eabi': 1.80.0 + '@oxlint/binding-android-arm64': 1.80.0 + '@oxlint/binding-darwin-arm64': 1.80.0 + '@oxlint/binding-darwin-x64': 1.80.0 + '@oxlint/binding-freebsd-x64': 1.80.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.80.0 + '@oxlint/binding-linux-arm-musleabihf': 1.80.0 + '@oxlint/binding-linux-arm64-gnu': 1.80.0 + '@oxlint/binding-linux-arm64-musl': 1.80.0 + '@oxlint/binding-linux-ppc64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-musl': 1.80.0 + '@oxlint/binding-linux-s390x-gnu': 1.80.0 + '@oxlint/binding-linux-x64-gnu': 1.80.0 + '@oxlint/binding-linux-x64-musl': 1.80.0 + '@oxlint/binding-openharmony-arm64': 1.80.0 + '@oxlint/binding-win32-arm64-msvc': 1.80.0 + '@oxlint/binding-win32-ia32-msvc': 1.80.0 + '@oxlint/binding-win32-x64-msvc': 1.80.0 oxlint-tsgolint: 7.0.2001 p-cancelable@2.1.1: {} diff --git a/src/main/ai-vault/session-scanner.ts b/src/main/ai-vault/session-scanner.ts index b4ef6f06ce7..4b0deb4c68f 100644 --- a/src/main/ai-vault/session-scanner.ts +++ b/src/main/ai-vault/session-scanner.ts @@ -85,23 +85,21 @@ export async function scanAiVaultSessions( const candidates = dedupeCodexRolloutFileAliases( discoveries .flatMap((discovery) => - discovery.files.map( - (file): SessionFileCandidate => ({ - agent: discovery.agent, - file, - codexHome: - discovery.agent === 'codex' - ? codexHomeForSessionsDir( - discovery.rootDir, - options.defaultCodexHomeDir ?? DEFAULT_CODEX_HOME_DIR - ) - : null, - antigravityHistoryPath: - discovery.agent === 'antigravity' - ? antigravityHistoryPathForBrainDir(discovery.rootDir) - : undefined - }) - ) + discovery.files.map((file): SessionFileCandidate => ({ + agent: discovery.agent, + file, + codexHome: + discovery.agent === 'codex' + ? codexHomeForSessionsDir( + discovery.rootDir, + options.defaultCodexHomeDir ?? DEFAULT_CODEX_HOME_DIR + ) + : null, + antigravityHistoryPath: + discovery.agent === 'antigravity' + ? antigravityHistoryPathForBrainDir(discovery.rootDir) + : undefined + })) ) .sort((left, right) => right.file.mtimeMs - left.file.mtimeMs), { @@ -203,9 +201,11 @@ async function scanInScopeSessions(args: { excludedFilePaths: args.alreadyParsedFilePaths, issues: args.issues }) - const candidates = files.map( - (file): SessionFileCandidate => ({ agent: 'claude', file, codexHome: null }) - ) + const candidates = files.map((file): SessionFileCandidate => ({ + agent: 'claude', + file, + codexHome: null + })) if (candidates.length === 0) { return [] } diff --git a/src/main/browser/cdp-bridge.ts b/src/main/browser/cdp-bridge.ts index 73dec07f2d2..918683a5135 100644 --- a/src/main/browser/cdp-bridge.ts +++ b/src/main/browser/cdp-bridge.ts @@ -822,7 +822,7 @@ export class CdpBridge { async tabSwitch(index: number): Promise { // Why: filter to live tabs so indices match tabList(), skipping destroyed-but-uncleaned entries. - const liveEntries = [...this.getRegisteredTabs()].filter(([_, wcId]) => { + const liveEntries = [...this.getRegisteredTabs()].filter(([_tabId, wcId]) => { const guest = webContents.fromId(wcId) return guest && !guest.isDestroyed() }) diff --git a/src/main/claude-usage/claude-model-pricing.test.ts b/src/main/claude-usage/claude-model-pricing.test.ts index 8c5f317d875..9eb3c2987c7 100644 --- a/src/main/claude-usage/claude-model-pricing.test.ts +++ b/src/main/claude-usage/claude-model-pricing.test.ts @@ -33,8 +33,8 @@ describe('estimateCostUsd cache-write TTL rates', () => { }) it('never lowers a long-context estimate as writes shift from 5-minute to 1-hour', () => { - const costs = [0, 50_000, 100_000, 200_000, 300_000, 400_000].map( - (write1h) => estimateCostUsd('claude-sonnet-4-6', 0, 0, 0, 400_000, write1h)! + const costs = [0, 50_000, 100_000, 200_000, 300_000, 400_000].map((write1h) => + estimateCostUsd('claude-sonnet-4-6', 0, 0, 0, 400_000, write1h)! ) for (let index = 1; index < costs.length; index++) { expect(costs[index]).toBeGreaterThan(costs[index - 1]) diff --git a/src/main/codex-accounts/service-test-harness.ts b/src/main/codex-accounts/service-test-harness.ts index adc1f4304dd..9afd6a8ba5e 100644 --- a/src/main/codex-accounts/service-test-harness.ts +++ b/src/main/codex-accounts/service-test-harness.ts @@ -205,9 +205,10 @@ export function createRuntimeHome(): RuntimeHomeStub { return { syncForCurrentSelection: vi.fn(), clearLastWrittenAuthJson: vi.fn(), - prepareForRateLimitFetch: vi.fn( - (): CodexRateLimitHomeResolution => ({ kind: 'ready', codexHomePath: null }) - ) + prepareForRateLimitFetch: vi.fn((): CodexRateLimitHomeResolution => ({ + kind: 'ready', + codexHomePath: null + })) } } diff --git a/src/main/codex/codex-state-db-backfill-recovery.test.ts b/src/main/codex/codex-state-db-backfill-recovery.test.ts index 981fa74c1bc..1986ccbdae9 100644 --- a/src/main/codex/codex-state-db-backfill-recovery.test.ts +++ b/src/main/codex/codex-state-db-backfill-recovery.test.ts @@ -191,13 +191,11 @@ describe('Codex state DB backfill recovery', () => { const summary = await runCodexStateDbBackfillRecovery('/managed-home', controller.signal, { spawnProcess: spawnProcess as never, - readStatus: vi.fn( - (): CodexStateDbBackfillStatus => ({ - kind: 'incomplete', - stateDbPath: '/state.sqlite', - status: 'running' - }) - ), + readStatus: vi.fn((): CodexStateDbBackfillStatus => ({ + kind: 'incomplete', + stateDbPath: '/state.sqlite', + status: 'running' + })), terminate, sleep, now: () => now @@ -270,15 +268,14 @@ describe('Codex state DB backfill recovery', () => { new AbortController().signal, { spawnProcess: spawnProcess as never, - readStatus: vi.fn( - (): CodexStateDbBackfillStatus => - spawnProcess.mock.calls.length >= 3 - ? { kind: 'complete', stateDbPath: '/state.sqlite' } - : { - kind: 'incomplete', - stateDbPath: '/state.sqlite', - status: 'running' - } + readStatus: vi.fn((): CodexStateDbBackfillStatus => + spawnProcess.mock.calls.length >= 3 + ? { kind: 'complete', stateDbPath: '/state.sqlite' } + : { + kind: 'incomplete', + stateDbPath: '/state.sqlite', + status: 'running' + } ), terminate: terminate as never, sleep, @@ -328,11 +325,10 @@ describe('Codex state DB backfill recovery', () => { await expect( runCodexStateDbBackfillRecovery('/managed-home', new AbortController().signal, { spawnProcess: spawnProcess as never, - readStatus: vi.fn( - (): CodexStateDbBackfillStatus => - spawnCount >= 2 - ? { kind: 'complete', stateDbPath: '/state.sqlite' } - : { kind: 'incomplete', stateDbPath: '/state.sqlite', status: 'running' } + readStatus: vi.fn((): CodexStateDbBackfillStatus => + spawnCount >= 2 + ? { kind: 'complete', stateDbPath: '/state.sqlite' } + : { kind: 'incomplete', stateDbPath: '/state.sqlite', status: 'running' } ), terminate, sleep: vi.fn(async (ms: number) => { @@ -427,11 +423,10 @@ describe('Codex state DB backfill recovery', () => { } return child }) - const readStatus = vi.fn( - (): CodexStateDbBackfillStatus => - spawnCount >= 2 - ? { kind: 'complete', stateDbPath: '/state.sqlite' } - : { kind: 'incomplete', stateDbPath: '/state.sqlite', status: 'running' } + const readStatus = vi.fn((): CodexStateDbBackfillStatus => + spawnCount >= 2 + ? { kind: 'complete', stateDbPath: '/state.sqlite' } + : { kind: 'incomplete', stateDbPath: '/state.sqlite', status: 'running' } ) await expect( diff --git a/src/main/emulator/emulator-start-lease-registry.test.ts b/src/main/emulator/emulator-start-lease-registry.test.ts index f0b0e927569..e08490932c6 100644 --- a/src/main/emulator/emulator-start-lease-registry.test.ts +++ b/src/main/emulator/emulator-start-lease-registry.test.ts @@ -13,15 +13,13 @@ function makeBackend(): { const resolveDeviceId = vi.fn(async () => { throw new Error('shutdown AVD cannot resolve before boot') }) - const startSession = vi.fn( - async (): Promise => ({ - deviceUdid: 'emulator-5554', - streamUrl: 'scrcpy://emulator-5554', - wsUrl: '', - streamCodec: 'h264', - backend: 'android' - }) - ) + const startSession = vi.fn(async (): Promise => ({ + deviceUdid: 'emulator-5554', + streamUrl: 'scrcpy://emulator-5554', + wsUrl: '', + streamCodec: 'h264', + backend: 'android' + })) const stopHelperForDevice = vi.fn(async () => {}) const shutdownDevice = vi.fn(async () => {}) const backend: EmulatorBackend = { diff --git a/src/main/github/client/fetch/get-pr-comments.ts b/src/main/github/client/fetch/get-pr-comments.ts index 17ba9175a00..06df95e29fb 100644 --- a/src/main/github/client/fetch/get-pr-comments.ts +++ b/src/main/github/client/fetch/get-pr-comments.ts @@ -149,19 +149,17 @@ export async function getPRComments( if (!pullRequest) { console.warn('Review threads response missing pullRequest; keeping REST results') } - const graphQLIssueComments = (pullRequest?.comments?.nodes ?? []).map( - (c): PRComment => ({ - id: c.databaseId, - author: c.author?.login ?? 'ghost', - authorAvatarUrl: c.author?.avatarUrl ?? '', - body: c.body ?? '', - createdAt: c.createdAt, - url: c.url, - isBot: c.author?.__typename === 'Bot', - reactionSubjectId: c.id, - reactions: mapGraphQLReactionGroups(c.reactionGroups) - }) - ) + const graphQLIssueComments = (pullRequest?.comments?.nodes ?? []).map((c): PRComment => ({ + id: c.databaseId, + author: c.author?.login ?? 'ghost', + authorAvatarUrl: c.author?.avatarUrl ?? '', + body: c.body ?? '', + createdAt: c.createdAt, + url: c.url, + isBot: c.author?.__typename === 'Bot', + reactionSubjectId: c.id, + reactions: mapGraphQLReactionGroups(c.reactionGroups) + })) if (graphQLIssueComments.length > 0) { issueComments = graphQLIssueComments } @@ -169,19 +167,17 @@ export async function getPRComments( graphQLReviewSummaries = pullRequest ? (pullRequest.reviews?.nodes ?? []) .filter((review) => review.body?.trim()) - .map( - (review): PRComment => ({ - id: review.databaseId, - author: review.author?.login ?? 'ghost', - authorAvatarUrl: review.author?.avatarUrl ?? '', - body: review.body, - createdAt: review.createdAt, - url: review.url, - isBot: review.author?.__typename === 'Bot', - reactionSubjectId: review.id, - reactions: mapGraphQLReactionGroups(review.reactionGroups) - }) - ) + .map((review): PRComment => ({ + id: review.databaseId, + author: review.author?.login ?? 'ghost', + authorAvatarUrl: review.author?.avatarUrl ?? '', + body: review.body, + createdAt: review.createdAt, + url: review.url, + isBot: review.author?.__typename === 'Bot', + reactionSubjectId: review.id, + reactions: mapGraphQLReactionGroups(review.reactionGroups) + })) : undefined const threads = pullRequest?.reviewThreads?.nodes ?? [] @@ -228,17 +224,15 @@ export async function getPRComments( } else if (reviewsResult.status === 'fulfilled') { reviewSummaries = (JSON.parse(reviewsResult.value.stdout) as RESTReview[]) .filter((r) => r.body?.trim()) - .map( - (r): PRComment => ({ - id: r.id, - author: r.user?.login ?? 'ghost', - authorAvatarUrl: r.user?.avatar_url ?? '', - body: r.body, - createdAt: r.submitted_at, - url: r.html_url, - isBot: r.user?.type === 'Bot' - }) - ) + .map((r): PRComment => ({ + id: r.id, + author: r.user?.login ?? 'ghost', + authorAvatarUrl: r.user?.avatar_url ?? '', + body: r.body, + createdAt: r.submitted_at, + url: r.html_url, + isBot: r.user?.type === 'Bot' + })) } else { console.warn('Failed to fetch review summaries:', reviewsResult.reason) } diff --git a/src/main/github/issue-work-item-details.ts b/src/main/github/issue-work-item-details.ts index d58491abd87..5cb95f50d48 100644 --- a/src/main/github/issue-work-item-details.ts +++ b/src/main/github/issue-work-item-details.ts @@ -238,16 +238,14 @@ export async function getIssueBodyAndComments( }[] assignees?: { login: string }[] } - const comments = (data.comments ?? []).map( - (comment, index): PRComment => ({ - id: index, - author: comment.author?.login ?? 'ghost', - authorAvatarUrl: '', - body: comment.body ?? '', - createdAt: comment.createdAt, - url: comment.url ?? '' - }) - ) + const comments = (data.comments ?? []).map((comment, index): PRComment => ({ + id: index, + author: comment.author?.login ?? 'ghost', + authorAvatarUrl: '', + body: comment.body ?? '', + createdAt: comment.createdAt, + url: comment.url ?? '' + })) return { body: data.body ?? '', comments, diff --git a/src/main/ipc/filesystem-watcher-event-batch.test.ts b/src/main/ipc/filesystem-watcher-event-batch.test.ts index ff166b7b880..31f983b96ce 100644 --- a/src/main/ipc/filesystem-watcher-event-batch.test.ts +++ b/src/main/ipc/filesystem-watcher-event-batch.test.ts @@ -8,10 +8,10 @@ import { describe('queueWatcherEvents', () => { function events(count: number): WatcherEvent[] { - return Array.from( - { length: count }, - (_, index): WatcherEvent => ({ type: 'update', path: `/repo/file-${index}.ts` }) - ) + return Array.from({ length: count }, (_, index): WatcherEvent => ({ + type: 'update', + path: `/repo/file-${index}.ts` + })) } it('keeps precise events while the batch remains under the overflow limit', () => { diff --git a/src/main/ipc/filesystem-watcher-large-batch.test.ts b/src/main/ipc/filesystem-watcher-large-batch.test.ts index cfb99c217c1..3d71581595e 100644 --- a/src/main/ipc/filesystem-watcher-large-batch.test.ts +++ b/src/main/ipc/filesystem-watcher-large-batch.test.ts @@ -73,10 +73,10 @@ describe('local filesystem watcher large batches', () => { { worktreePath } ) - const events = Array.from( - { length: 200_000 }, - (_, index): WatcherEvent => ({ type: 'delete', path: join(worktreePath, `file-${index}`) }) - ) + const events = Array.from({ length: 200_000 }, (_, index): WatcherEvent => ({ + type: 'delete', + path: join(worktreePath, `file-${index}`) + })) expect(() => watcherCallback?.(null, events)).not.toThrow() await closeAllWatchers() @@ -97,13 +97,10 @@ describe('local filesystem watcher large batches', () => { await handlers['fs:watchWorktree']({ sender }, { worktreePath }) watcherCallback?.( null, - Array.from( - { length: 5_001 }, - (_, index): WatcherEvent => ({ - type: 'update', - path: join(worktreePath, `file-${index}.txt`) - }) - ) + Array.from({ length: 5_001 }, (_, index): WatcherEvent => ({ + type: 'update', + path: join(worktreePath, `file-${index}.txt`) + })) ) await vi.advanceTimersByTimeAsync(150) diff --git a/src/main/ipc/filesystem-watcher-remote-batch.test.ts b/src/main/ipc/filesystem-watcher-remote-batch.test.ts index befc9a194e2..de1f4fb3d37 100644 --- a/src/main/ipc/filesystem-watcher-remote-batch.test.ts +++ b/src/main/ipc/filesystem-watcher-remote-batch.test.ts @@ -107,13 +107,10 @@ describe('remote filesystem watcher batching', () => { await handlers['fs:watchWorktree']({ sender }, WATCH_ARGS) watchCallbacks[0]( - Array.from( - { length: 6_000 }, - (_unused, index): FsChangeEvent => ({ - kind: 'update', - absolutePath: `${WORKTREE_PATH}/file-${index}.ts` - }) - ) + Array.from({ length: 6_000 }, (_unused, index): FsChangeEvent => ({ + kind: 'update', + absolutePath: `${WORKTREE_PATH}/file-${index}.ts` + })) ) await vi.advanceTimersByTimeAsync(150) diff --git a/src/main/ipc/orca-profiles.ts b/src/main/ipc/orca-profiles.ts index 497da481ae0..bf1bf666270 100644 --- a/src/main/ipc/orca-profiles.ts +++ b/src/main/ipc/orca-profiles.ts @@ -169,17 +169,13 @@ export function registerOrcaProfileHandlers( store: Store, options: RegisterOrcaProfileHandlersOptions = {} ): void { - ipcMain.handle( - 'orcaProfiles:list', - (): OrcaProfileListResult => ({ - ...getOrcaProfileListState(), - multiProfileUi: isMultiProfileUiEnabled() - }) - ) + ipcMain.handle('orcaProfiles:list', (): OrcaProfileListResult => ({ + ...getOrcaProfileListState(), + multiProfileUi: isMultiProfileUiEnabled() + })) - ipcMain.handle( - 'orcaProfiles:authStatus', - (): OrcaProfileAuthStatus => getCurrentOrcaProfileAuthStatus(getProfileUserDataPath()) + ipcMain.handle('orcaProfiles:authStatus', (): OrcaProfileAuthStatus => + getCurrentOrcaProfileAuthStatus(getProfileUserDataPath()) ) ipcMain.handle( diff --git a/src/main/ipc/pet.ts b/src/main/ipc/pet.ts index 88cacffc6fa..3521482ef41 100644 --- a/src/main/ipc/pet.ts +++ b/src/main/ipc/pet.ts @@ -81,9 +81,8 @@ export function registerPetHandlers(): void { } }) - ipcMain.handle( - 'pet:importPetBundle', - async (event): Promise => importPetBundle(event) + ipcMain.handle('pet:importPetBundle', async (event): Promise => + importPetBundle(event) ) ipcMain.handle( diff --git a/src/main/ipc/remote-watcher-event-batch.test.ts b/src/main/ipc/remote-watcher-event-batch.test.ts index 99f7afee014..96dfe564724 100644 --- a/src/main/ipc/remote-watcher-event-batch.test.ts +++ b/src/main/ipc/remote-watcher-event-batch.test.ts @@ -182,13 +182,10 @@ describe('createRemoteWatcherEventBatch', () => { for (let i = 0; i < 3; i++) { batch.push( - Array.from( - { length: 50 }, - (_unused, index): FsChangeEvent => ({ - kind: 'update', - absolutePath: `${ROOT}/wave-${i}-${index}.ts` - }) - ) + Array.from({ length: 50 }, (_unused, index): FsChangeEvent => ({ + kind: 'update', + absolutePath: `${ROOT}/wave-${i}-${index}.ts` + })) ) } vi.advanceTimersByTime(150) diff --git a/src/main/ipc/ssh-pty-model-admission-pressure.ts b/src/main/ipc/ssh-pty-model-admission-pressure.ts index 256c37bb8dd..e5ce722f941 100644 --- a/src/main/ipc/ssh-pty-model-admission-pressure.ts +++ b/src/main/ipc/ssh-pty-model-admission-pressure.ts @@ -107,7 +107,7 @@ export class SshPtyModelAdmissionPressure { if (options.disposed) { return } - for (let index = 0; index < this.entries.length; ) { + for (let index = 0; index < this.entries.length;) { const entry = this.entries[index]! const hasEarlierEntryForPty = this.entries .slice(0, index) diff --git a/src/main/ipc/workspace-cleanup-broad-scan.test.ts b/src/main/ipc/workspace-cleanup-broad-scan.test.ts index 0bfa3e3ccc9..66555d216ac 100644 --- a/src/main/ipc/workspace-cleanup-broad-scan.test.ts +++ b/src/main/ipc/workspace-cleanup-broad-scan.test.ts @@ -447,16 +447,13 @@ describe('workspace cleanup broad scan opt-in', () => { it('batches fleet progress instead of emitting one IPC payload per row', async () => { const worktrees = [ GIT_WORKTREES[0], - ...Array.from( - { length: 200 }, - (_, index): GitWorktreeInfo => ({ - path: `/repo-batch-${index}`, - head: `head-${index}`, - branch: `refs/heads/batch-${index}`, - isBare: false, - isMainWorktree: false - }) - ) + ...Array.from({ length: 200 }, (_, index): GitWorktreeInfo => ({ + path: `/repo-batch-${index}`, + head: `head-${index}`, + branch: `refs/heads/batch-${index}`, + isBare: false, + isMainWorktree: false + })) ] listRepoWorktreesMock.mockResolvedValue(worktrees) const onProgress = vi.fn() diff --git a/src/main/ipc/workspace-cleanup.test.ts b/src/main/ipc/workspace-cleanup.test.ts index 6f5d3d74a9b..1bc9d8740b3 100644 --- a/src/main/ipc/workspace-cleanup.test.ts +++ b/src/main/ipc/workspace-cleanup.test.ts @@ -767,18 +767,15 @@ describe('workspace cleanup scan', () => { }) it('summarizes large diff-note lists without hitting argument limits', async () => { - const diffComments = Array.from( - { length: 150_000 }, - (_, index): DiffComment => ({ - id: `comment-${index}`, - worktreeId: 'repo-1::/repo-feature', - filePath: 'src/file.ts', - lineNumber: 12, - body: 'Follow up before deleting', - createdAt: NOW - index, - side: 'modified' - }) - ) + const diffComments = Array.from({ length: 150_000 }, (_, index): DiffComment => ({ + id: `comment-${index}`, + worktreeId: 'repo-1::/repo-feature', + filePath: 'src/file.ts', + lineNumber: 12, + body: 'Follow up before deleting', + createdAt: NOW - index, + side: 'modified' + })) const result = await scanWorkspaceCleanup( makeStore({ diff --git a/src/main/ipc/workspace-cleanup.ts b/src/main/ipc/workspace-cleanup.ts index ef120a837ad..23bbf55602f 100644 --- a/src/main/ipc/workspace-cleanup.ts +++ b/src/main/ipc/workspace-cleanup.ts @@ -127,10 +127,8 @@ export function registerWorkspaceCleanupHandlers( return true }) - ipcMain.handle( - 'workspaceCleanup:getCachedScan', - (): Promise => - readWorkspaceCleanupScanSnapshot(snapshotDirectory) + ipcMain.handle('workspaceCleanup:getCachedScan', (): Promise => + readWorkspaceCleanupScanSnapshot(snapshotDirectory) ) ipcMain.handle('workspaceCleanup:dismiss', (_event, args: WorkspaceCleanupDismissArgs) => { diff --git a/src/main/ipc/workspace-space.ts b/src/main/ipc/workspace-space.ts index 5b10b039b31..e6165dd1059 100644 --- a/src/main/ipc/workspace-space.ts +++ b/src/main/ipc/workspace-space.ts @@ -114,10 +114,8 @@ export function registerWorkspaceSpaceHandlers(store: Store): void { return inFlightScan.promise }) - ipcMain.handle( - 'workspaceSpace:getCachedAnalysis', - (): Promise => - readWorkspaceSpaceAnalysisSnapshot(snapshotDirectory) + ipcMain.handle('workspaceSpace:getCachedAnalysis', (): Promise => + readWorkspaceSpaceAnalysisSnapshot(snapshotDirectory) ) ipcMain.handle('workspaceSpace:cancel', async (): Promise => { diff --git a/src/main/jira/adf-media-destination.ts b/src/main/jira/adf-media-destination.ts index e92cce03083..67deb7a685d 100644 --- a/src/main/jira/adf-media-destination.ts +++ b/src/main/jira/adf-media-destination.ts @@ -25,7 +25,7 @@ export function escapeMarkdownLinkDestination(url: string): string | null { } let encoded = '' - for (let i = 0; i < url.length; ) { + for (let i = 0; i < url.length;) { const char = url[i] ?? '' if (char === '%') { const hex = url.slice(i + 1, i + 3) diff --git a/src/main/native-chat/transcript-read-cache.test.ts b/src/main/native-chat/transcript-read-cache.test.ts index 0d61f25f48e..de6bf34ffed 100644 --- a/src/main/native-chat/transcript-read-cache.test.ts +++ b/src/main/native-chat/transcript-read-cache.test.ts @@ -161,7 +161,9 @@ describe('readNativeChatTranscriptCached', () => { await utimes(fileA, when, when) await utimes(fileC, when, when) - const readText = (result: Awaited>): string => + const readText = ( + result: Awaited> + ): string => 'messages' in result ? result.messages .flatMap((message) => message.blocks) diff --git a/src/main/opencode-usage/snapshot-rollups.ts b/src/main/opencode-usage/snapshot-rollups.ts index a98ddbdca14..c60f251914d 100644 --- a/src/main/opencode-usage/snapshot-rollups.ts +++ b/src/main/opencode-usage/snapshot-rollups.ts @@ -150,25 +150,23 @@ export function buildOpenCodeUsageRecentSessions( filteredSessions: OpenCodeUsageSession[], limit = 10 ): OpenCodeUsageSessionRow[] { - return filteredSessions.slice(0, limit).map( - (session): OpenCodeUsageSessionRow => ({ - sessionId: session.sessionId, - lastActiveAt: session.lastTimestamp, - durationMinutes: Math.max( - 0, - Math.round( - (new Date(session.lastTimestamp).getTime() - new Date(session.firstTimestamp).getTime()) / - 60_000 - ) - ), - projectLabel: session.primaryProjectLabel, - model: session.primaryModel, - events: session.eventCount, - inputTokens: session.totalInputTokens, - cachedInputTokens: session.totalCachedInputTokens, - outputTokens: session.totalOutputTokens, - reasoningOutputTokens: session.totalReasoningOutputTokens, - totalTokens: session.totalTokens - }) - ) + return filteredSessions.slice(0, limit).map((session): OpenCodeUsageSessionRow => ({ + sessionId: session.sessionId, + lastActiveAt: session.lastTimestamp, + durationMinutes: Math.max( + 0, + Math.round( + (new Date(session.lastTimestamp).getTime() - new Date(session.firstTimestamp).getTime()) / + 60_000 + ) + ), + projectLabel: session.primaryProjectLabel, + model: session.primaryModel, + events: session.eventCount, + inputTokens: session.totalInputTokens, + cachedInputTokens: session.totalCachedInputTokens, + outputTokens: session.totalOutputTokens, + reasoningOutputTokens: session.totalReasoningOutputTokens, + totalTokens: session.totalTokens + })) } diff --git a/src/main/pi/agent-status-extension-source.test.ts b/src/main/pi/agent-status-extension-source.test.ts index b735cd60198..97ad9bcc1c5 100644 --- a/src/main/pi/agent-status-extension-source.test.ts +++ b/src/main/pi/agent-status-extension-source.test.ts @@ -338,7 +338,7 @@ describe('getPiAgentStatusExtensionSource', () => { await vi.waitFor(() => expect(harness.fetchMock).toHaveBeenCalledTimes(2)) const payloads = harness.fetchMock.mock.calls.map( - ([_, init]) => JSON.parse(String(init?.body)).payload + ([_event, init]) => JSON.parse(String(init?.body)).payload ) expect(payloads).toEqual([ { hook_event_name: 'session_start' }, @@ -391,7 +391,7 @@ describe('getPiAgentStatusExtensionSource', () => { await vi.waitFor(() => expect(harness.fetchMock).toHaveBeenCalledTimes(3)) expect( - harness.fetchMock.mock.calls.map(([_, init]) => JSON.parse(String(init?.body)).payload) + harness.fetchMock.mock.calls.map(([_event, init]) => JSON.parse(String(init?.body)).payload) ).toEqual([ { hook_event_name: 'agent_start', session_id: 'omp-session-8' }, { diff --git a/src/main/rate-limits/service.ts b/src/main/rate-limits/service.ts index d6f291d6b38..8ce0b167a4a 100644 --- a/src/main/rate-limits/service.ts +++ b/src/main/rate-limits/service.ts @@ -1938,16 +1938,14 @@ export class RateLimitService { allowPtyFallback: this.shouldAllowCodexPtyFallback(), signal }) - ).catch( - (err): ProviderRateLimits => ({ - provider: 'codex', - session: null, - weekly: null, - updatedAt: Date.now(), - error: err instanceof Error ? err.message : 'Unknown error', - status: 'error' - }) - ) + ).catch((err): ProviderRateLimits => ({ + provider: 'codex', + session: null, + weekly: null, + updatedAt: Date.now(), + error: err instanceof Error ? err.message : 'Unknown error', + status: 'error' + })) if (signal.aborted) { return @@ -2008,16 +2006,14 @@ export class RateLimitService { allowUsagePanelSupplement: this.shouldAllowClaudeUsagePanelSupplement(), networkProxySettings: this.networkProxySettingsResolver?.(), signal - }).catch( - (err): ProviderRateLimits => ({ - provider: 'claude', - session: null, - weekly: null, - updatedAt: Date.now(), - error: err instanceof Error ? err.message : 'Unknown error', - status: 'error' - }) - ) + }).catch((err): ProviderRateLimits => ({ + provider: 'claude', + session: null, + weekly: null, + updatedAt: Date.now(), + error: err instanceof Error ? err.message : 'Unknown error', + status: 'error' + })) if (signal.aborted) { return @@ -2060,16 +2056,14 @@ export class RateLimitService { const grok = await fetchGrokRateLimits({ signal, authReadResult: grokAuthReadResult - }).catch( - (err): ProviderRateLimits => ({ - provider: 'grok', - session: null, - weekly: null, - updatedAt: Date.now(), - error: err instanceof Error ? err.message : 'Unknown error', - status: 'error' - }) - ) + }).catch((err): ProviderRateLimits => ({ + provider: 'grok', + session: null, + weekly: null, + updatedAt: Date.now(), + error: err instanceof Error ? err.message : 'Unknown error', + status: 'error' + })) if (signal.aborted) { return diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 41883e05a66..3fd419cb69b 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -6067,22 +6067,18 @@ export class OrcaRuntimeService { ) const worktrees = (await this.listResolvedWorktrees()) .filter((worktree) => repos.has(getRepoIdFromWorktreeId(worktree.id))) - .map( - (worktree): SkillSshWorkspaceAuthority => ({ - kind: 'worktree', - id: worktree.id, - path: worktree.path - }) - ) + .map((worktree): SkillSshWorkspaceAuthority => ({ + kind: 'worktree', + id: worktree.id, + path: worktree.path + })) const folders = this.listFolderWorkspaces() .filter((folder) => folder.connectionId === connectionId) - .map( - (folder): SkillSshWorkspaceAuthority => ({ - kind: 'folder', - id: folder.id, - path: folder.folderPath - }) - ) + .map((folder): SkillSshWorkspaceAuthority => ({ + kind: 'folder', + id: folder.id, + path: folder.folderPath + })) return [...worktrees, ...folders] } diff --git a/src/main/runtime/rpc/terminal-output-frame-chunks.ts b/src/main/runtime/rpc/terminal-output-frame-chunks.ts index 91bbc506680..78d570b87e6 100644 --- a/src/main/runtime/rpc/terminal-output-frame-chunks.ts +++ b/src/main/runtime/rpc/terminal-output-frame-chunks.ts @@ -34,7 +34,7 @@ export function exceedsTerminalStreamChunkBytes(data: string): boolean { return false } let byteLength = 0 - for (let start = 0; start < data.length; ) { + for (let start = 0; start < data.length;) { let end = Math.min(start + TERMINAL_STREAM_BYTE_PROBE_CODE_UNITS, data.length) const high = data.charCodeAt(end - 1) const low = data.charCodeAt(end) diff --git a/src/main/skills/skill-install-filesystem.ts b/src/main/skills/skill-install-filesystem.ts index d51929d79f6..123d500fe92 100644 --- a/src/main/skills/skill-install-filesystem.ts +++ b/src/main/skills/skill-install-filesystem.ts @@ -92,16 +92,14 @@ export const nativeSkillInstallFilesystem: SkillInstallFilesystem = { inspectPaths: async (paths) => new Map( await runSkillCandidateTasks( - paths.map( - (path) => async (): Promise<[string, SkillPathInspection]> => [ - path, - { - kind: statKind(await lstat(path).catch(() => null)), - realpath: await realpath(path).catch(() => null), - mtimeMs: (await stat(path).catch(() => null))?.mtimeMs ?? null - } - ] - ) + paths.map((path) => async (): Promise<[string, SkillPathInspection]> => [ + path, + { + kind: statKind(await lstat(path).catch(() => null)), + realpath: await realpath(path).catch(() => null), + mtimeMs: (await stat(path).catch(() => null))?.mtimeMs ?? null + } + ]) ) ) } diff --git a/src/relay/pty-handler-source-publication.test.ts b/src/relay/pty-handler-source-publication.test.ts index f0041a57ce5..f8082b20af5 100644 --- a/src/relay/pty-handler-source-publication.test.ts +++ b/src/relay/pty-handler-source-publication.test.ts @@ -654,7 +654,10 @@ describe('PtyHandler negotiated source publication', () => { expect(mockPtySpawn).toHaveBeenCalledOnce() expect( - responseResult(replacementWrites.find((buffer) => responseResult(buffer, 4))!, 4) + responseResult( + replacementWrites.find((buffer) => responseResult(buffer, 4))!, + 4 + ) ).toMatchObject({ id: 'pty-1', incarnationId: expect.any(String), diff --git a/src/renderer/src/app-shell/use-persisted-ui-writer.ts b/src/renderer/src/app-shell/use-persisted-ui-writer.ts index f16346b25fb..7d21726619e 100644 --- a/src/renderer/src/app-shell/use-persisted-ui-writer.ts +++ b/src/renderer/src/app-shell/use-persisted-ui-writer.ts @@ -96,34 +96,32 @@ export function usePersistedUIWriter(): void { const persistedUIReady = useAppStore((s) => s.persistedUIReady) const activeView = useAppStore((s) => s.activeView) const ui = useAppStore( - useShallow( - (s): PersistedUIWriteBaseline => ({ - sidebarWidth: s.sidebarWidth, - rightSidebarOpen: s.rightSidebarOpen, - rightSidebarTab: s.rightSidebarTab, - rightSidebarExplorerView: s.rightSidebarExplorerView, - rightSidebarWidth: s.rightSidebarWidth, - markdownTocPanelWidth: s.markdownTocPanelWidth, - combinedDiffFileTreeWidth: s.combinedDiffFileTreeWidth, - groupBy: s.groupBy, - sortBy: s.sortBy, - projectOrderBy: s.projectOrderBy, - showSleepingWorkspaces: s.showSleepingWorkspaces, - hideDefaultBranchWorkspace: s.hideDefaultBranchWorkspace, - hideAutomationGeneratedWorkspaces: s.hideAutomationGeneratedWorkspaces, - hideCliCreatedWorkspaces: s.hideCliCreatedWorkspaces, - hideDetachedHeadWorkspaces: s.hideDetachedHeadWorkspaces, - hideWorkspacesFromOtherDevices: s.hideWorkspacesFromOtherDevices, - alwaysShowDefaultBranchWorkspace: s.alwaysShowDefaultBranchWorkspace, - showDotfilesByWorktree: s.showDotfilesByWorktree, - filterRepoIds: s.filterRepoIds, - // Why: dashboard auto-acks (fire on focus/visibility) and the in-memory ack cleanup - // paths in agent-status.ts (close/dismiss) flow to disk through map identity changes. - // Without persisting, agent rows that survive restart come back bold even when the - // user had already visited them. - acknowledgedAgentsByPaneKey: s.acknowledgedAgentsByPaneKey - }) - ) + useShallow((s): PersistedUIWriteBaseline => ({ + sidebarWidth: s.sidebarWidth, + rightSidebarOpen: s.rightSidebarOpen, + rightSidebarTab: s.rightSidebarTab, + rightSidebarExplorerView: s.rightSidebarExplorerView, + rightSidebarWidth: s.rightSidebarWidth, + markdownTocPanelWidth: s.markdownTocPanelWidth, + combinedDiffFileTreeWidth: s.combinedDiffFileTreeWidth, + groupBy: s.groupBy, + sortBy: s.sortBy, + projectOrderBy: s.projectOrderBy, + showSleepingWorkspaces: s.showSleepingWorkspaces, + hideDefaultBranchWorkspace: s.hideDefaultBranchWorkspace, + hideAutomationGeneratedWorkspaces: s.hideAutomationGeneratedWorkspaces, + hideCliCreatedWorkspaces: s.hideCliCreatedWorkspaces, + hideDetachedHeadWorkspaces: s.hideDetachedHeadWorkspaces, + hideWorkspacesFromOtherDevices: s.hideWorkspacesFromOtherDevices, + alwaysShowDefaultBranchWorkspace: s.alwaysShowDefaultBranchWorkspace, + showDotfilesByWorktree: s.showDotfilesByWorktree, + filterRepoIds: s.filterRepoIds, + // Why: dashboard auto-acks (fire on focus/visibility) and the in-memory ack cleanup + // paths in agent-status.ts (close/dismiss) flow to disk through map identity changes. + // Without persisting, agent rows that survive restart come back bold even when the + // user had already visited them. + acknowledgedAgentsByPaneKey: s.acknowledgedAgentsByPaneKey + })) ) useEffect(() => { // The baseline holds the values this client last saw persisted (newest diff --git a/src/renderer/src/components/TaskPage.tsx b/src/renderer/src/components/TaskPage.tsx index 82e3680b2cf..06cd8e745c9 100644 --- a/src/renderer/src/components/TaskPage.tsx +++ b/src/renderer/src/components/TaskPage.tsx @@ -758,8 +758,8 @@ export default function TaskPage(): React.JSX.Element { // Hard guarantee (K4): always overlay pending after reconcile so list // fetch clobbers never paint unprotected coordinator fields. setPages((current) => - reconcileTaskPagePagesWithWorkItemsCache(current, selectedWorkItemsCacheEntries).map((page) => - page ? (overlayPendingOnTaskPagePages([page])[0] ?? []) : null + reconcileTaskPagePagesWithWorkItemsCache(current, selectedWorkItemsCacheEntries).map( + (page) => (page ? (overlayPendingOnTaskPagePages([page])[0] ?? []) : null) ) ) }, [githubMode, selectedWorkItemsCacheEntries, taskSource, setPages]) diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx index b6408593ed3..f18ba555d68 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.tsx @@ -2022,13 +2022,11 @@ function WorktreeJumpPaletteContent({ fetchLinearIssue: state.fetchLinearIssue }) .catch(() => null) - .then( - (issue): CmdJLinearIssuePreview => ({ - ...pendingPreview, - issue, - loading: false - }) - ) + .then((issue): CmdJLinearIssuePreview => ({ + ...pendingPreview, + issue, + loading: false + })) linearIssueLookupRef.current = { query: createWorktreeName, promise } void promise.then((preview) => { if (linearIssueLookupGenerationRef.current === generation) { @@ -2077,13 +2075,11 @@ function WorktreeJumpPaletteContent({ sourceContext }) .catch(() => null) - .then( - (item): CmdJGitHubWorkItemPreview => ({ - ...pendingPreview, - item: item ?? null, - loading: false - }) - ) + .then((item): CmdJGitHubWorkItemPreview => ({ + ...pendingPreview, + item: item ?? null, + loading: false + })) githubLookupRef.current = { query: createWorktreeName, promise } void promise.then((preview) => { if (githubLookupGenerationRef.current === generation) { diff --git a/src/renderer/src/components/automations/automation-run-output-snapshot-equivalence.test.ts b/src/renderer/src/components/automations/automation-run-output-snapshot-equivalence.test.ts index b988ad96581..cf8f7590fa5 100644 --- a/src/renderer/src/components/automations/automation-run-output-snapshot-equivalence.test.ts +++ b/src/renderer/src/components/automations/automation-run-output-snapshot-equivalence.test.ts @@ -89,7 +89,7 @@ describe('automation run output snapshot queue equivalence', () => { const reference = createShiftReferenceBuffer() const candidate = createAutomationRunOutputSnapshotBuffer() let chunkCount = 0 - for (let offset = 0; offset < source.length; ) { + for (let offset = 0; offset < source.length;) { const chunkLength = 1 + (random() % 73) const chunk = source.slice(offset, offset + chunkLength) reference.append(chunk) diff --git a/src/renderer/src/components/browser-pane/annotate/browser-annotation-output.ts b/src/renderer/src/components/browser-pane/annotate/browser-annotation-output.ts index fa35eb9541f..06f9bc22f38 100644 --- a/src/renderer/src/components/browser-pane/annotate/browser-annotation-output.ts +++ b/src/renderer/src/components/browser-pane/annotate/browser-annotation-output.ts @@ -35,7 +35,7 @@ function inlineText( // scanning only the bounded text we will actually retain. let normalized = '' let pendingSpace = false - for (let index = 0; index < content.length && normalized.length < maxLength; ) { + for (let index = 0; index < content.length && normalized.length < maxLength;) { const code = content.charCodeAt(index) if (isInlineWhitespaceCode(code)) { if (code === 13 && content.charCodeAt(index + 1) === 10) { diff --git a/src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx b/src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx index 7cd7bf94c93..84ea917f372 100644 --- a/src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx +++ b/src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx @@ -14,9 +14,10 @@ const mocks = vi.hoisted(() => ({ onSnapshotRequested: vi.fn(), getPopoutOpen: vi.fn(async () => false), publishSnapshot: vi.fn(async (_snapshot: DashboardSnapshot) => undefined), - buildDashboardSnapshot: vi.fn( - (_state: unknown, now: number): DashboardSnapshot => ({ generatedAt: now, cards: [] }) - ), + buildDashboardSnapshot: vi.fn((_state: unknown, now: number): DashboardSnapshot => ({ + generatedAt: now, + cards: [] + })), offRevealAgent: vi.fn(), offAckAgent: vi.fn(), offPopoutOpenChanged: vi.fn(), diff --git a/src/renderer/src/components/github-item-dialog/discuss-item/issue-conversation-entries.ts b/src/renderer/src/components/github-item-dialog/discuss-item/issue-conversation-entries.ts index 6af66ea62fe..75f08c821dd 100644 --- a/src/renderer/src/components/github-item-dialog/discuss-item/issue-conversation-entries.ts +++ b/src/renderer/src/components/github-item-dialog/discuss-item/issue-conversation-entries.ts @@ -27,24 +27,20 @@ export function getIssueConversationEntries( timelineItems: GitHubIssueTimelineItem[] ): IssueConversationEntry[] { return [ - ...comments.map( - (comment, index): IssueConversationEntry => ({ - kind: 'comment', - id: `comment:${comment.id}`, - createdAt: comment.createdAt, - comment, - index - }) - ), - ...timelineItems.map( - (activity, index): IssueConversationEntry => ({ - kind: 'activity', - id: `activity:${activity.id}`, - createdAt: activity.createdAt, - activity, - index: comments.length + index - }) - ) + ...comments.map((comment, index): IssueConversationEntry => ({ + kind: 'comment', + id: `comment:${comment.id}`, + createdAt: comment.createdAt, + comment, + index + })), + ...timelineItems.map((activity, index): IssueConversationEntry => ({ + kind: 'activity', + id: `activity:${activity.id}`, + createdAt: activity.createdAt, + activity, + index: comments.length + index + })) ].sort((a, b) => { const diff = getTimelineSortValue(a.createdAt) - getTimelineSortValue(b.createdAt) return diff === 0 ? a.index - b.index : diff diff --git a/src/renderer/src/components/native-chat/structured-agent-session-message-projection.ts b/src/renderer/src/components/native-chat/structured-agent-session-message-projection.ts index 9da10c6581a..aebc51c90e0 100644 --- a/src/renderer/src/components/native-chat/structured-agent-session-message-projection.ts +++ b/src/renderer/src/components/native-chat/structured-agent-session-message-projection.ts @@ -25,14 +25,12 @@ export function projectStructuredAgentSessionMessages( ...projectStructuredItemsToNativeChat(items), ...optimistic .filter((entry) => !journalled.has(agentJournalSubmissionKey(entry.clientMessageId))) - .map( - (entry): NativeChatMessage => ({ - id: agentJournalSubmissionKey(entry.clientMessageId), - role: 'user', - source: 'transcript', - timestamp: entry.queuedAt, - blocks: entry.body.blocks - }) - ) + .map((entry): NativeChatMessage => ({ + id: agentJournalSubmissionKey(entry.clientMessageId), + role: 'user', + source: 'transcript', + timestamp: entry.queuedAt, + blocks: entry.body.blocks + })) ] } diff --git a/src/renderer/src/components/provider-check-classification-parity.test.ts b/src/renderer/src/components/provider-check-classification-parity.test.ts index bedb0663739..5e10ad5f1c9 100644 --- a/src/renderer/src/components/provider-check-classification-parity.test.ts +++ b/src/renderer/src/components/provider-check-classification-parity.test.ts @@ -23,16 +23,14 @@ function completed(conclusion: string): PRCheckDetail { function gitLabJobs(...statuses: string[]): PRCheckDetail[] { return gitLabPipelineJobsToPRChecks( - statuses.map( - (status, index): GitLabPipelineJob => ({ - id: index, - name: status, - stage: 'deploy', - status, - webUrl: '', - duration: null - }) - ) + statuses.map((status, index): GitLabPipelineJob => ({ + id: index, + name: status, + stage: 'deploy', + status, + webUrl: '', + duration: null + })) ) } diff --git a/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.test.ts b/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.test.ts index c6d221b4fad..343e33b64a0 100644 --- a/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.test.ts +++ b/src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.test.ts @@ -6,16 +6,14 @@ import { useAppStore } from '@/store' function cacheWithChildren(paths: string[]): DirCache { return { - children: paths.map( - (path): TreeNode => ({ - name: path.split(/[\\/]/).at(-1) ?? path, - path, - relativePath: path, - isDirectory: false, - depth: 0, - operationOwner: { kind: 'local' } - }) - ), + children: paths.map((path): TreeNode => ({ + name: path.split(/[\\/]/).at(-1) ?? path, + path, + relativePath: path, + isDirectory: false, + depth: 0, + operationOwner: { kind: 'local' } + })), loading: false, operationOwner: { kind: 'local' } } diff --git a/src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx b/src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx index c58fb017ba4..6400e71795e 100644 --- a/src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx +++ b/src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx @@ -24,11 +24,9 @@ beforeEach(() => { Object.assign(window, { api: { developerPermissions: { - getStatus: vi.fn( - async (): Promise => [ - { id: 'full-disk-access', status: 'denied' } - ] - ), + getStatus: vi.fn(async (): Promise => [ + { id: 'full-disk-access', status: 'denied' } + ]), request: vi.fn(async ({ id }: { id: string }) => ({ id, status: 'unknown', diff --git a/src/renderer/src/components/settings/McpConfigSection.tsx b/src/renderer/src/components/settings/McpConfigSection.tsx index e4ceac4c892..a4bcc97f8d1 100644 --- a/src/renderer/src/components/settings/McpConfigSection.tsx +++ b/src/renderer/src/components/settings/McpConfigSection.tsx @@ -90,12 +90,10 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele ) const missingInspections = useMemo( () => - MCP_CONFIG_CANDIDATES.map( - (candidate): LoadedMcpConfigInspection => ({ - ...inspectMcpConfigContent(candidate, null), - absolutePath: joinPath(targetRootPath, candidate.relativePath) - }) - ), + MCP_CONFIG_CANDIDATES.map((candidate): LoadedMcpConfigInspection => ({ + ...inspectMcpConfigContent(candidate, null), + absolutePath: joinPath(targetRootPath, candidate.relativePath) + })), [targetRootPath] ) const serverCount = useMemo(() => countServers(configs), [configs]) diff --git a/src/renderer/src/components/settings/appearance-sidebar-search.ts b/src/renderer/src/components/settings/appearance-sidebar-search.ts index bb3ac17e7a3..ab77d2a0ad0 100644 --- a/src/renderer/src/components/settings/appearance-sidebar-search.ts +++ b/src/renderer/src/components/settings/appearance-sidebar-search.ts @@ -3,81 +3,77 @@ import { createLocalizedCatalog } from '@/i18n/localized-catalog' import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' -export const getLeftSidebarAppearanceEntry = createLocalizedCatalog( - (): SettingsSearchEntry => ({ - title: translate( - 'auto.components.settings.appearance.search.leftSidebarAppearance.title', - 'Left Sidebar Appearance' +export const getLeftSidebarAppearanceEntry = createLocalizedCatalog((): SettingsSearchEntry => ({ + title: translate( + 'auto.components.settings.appearance.search.leftSidebarAppearance.title', + 'Left Sidebar Appearance' + ), + description: translate( + 'auto.components.settings.appearance.search.leftSidebarAppearance.description', + 'Make the left sidebar match your terminal, stay default, or use a tint.' + ), + keywords: [ + ...translateSearchKeyword('auto.components.settings.appearance.search.5bff6a2ef0', 'sidebar'), + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.leftSidebarAppearance.project', + 'project' ), - description: translate( - 'auto.components.settings.appearance.search.leftSidebarAppearance.description', - 'Make the left sidebar match your terminal, stay default, or use a tint.' + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.leftSidebarAppearance.terminal', + 'terminal' ), - keywords: [ - ...translateSearchKeyword('auto.components.settings.appearance.search.5bff6a2ef0', 'sidebar'), - ...translateSearchKeyword( - 'auto.components.settings.appearance.search.leftSidebarAppearance.project', - 'project' - ), - ...translateSearchKeyword( - 'auto.components.settings.appearance.search.leftSidebarAppearance.terminal', - 'terminal' - ), - ...translateSearchKeyword( - 'auto.components.settings.appearance.search.leftSidebarAppearance.background', - 'background' - ), - ...translateSearchKeyword( - 'auto.components.settings.appearance.search.leftSidebarAppearance.tint', - 'tint' - ) - ] - }) -) + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.leftSidebarAppearance.background', + 'background' + ), + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.leftSidebarAppearance.tint', + 'tint' + ) + ] +})) -export const getWorkspaceCardLayoutEntry = createLocalizedCatalog( - (): SettingsSearchEntry => ({ - title: translate( - 'auto.components.settings.appearance.search.workspaceCardLayout.title', - 'Workspace Card Layout' +export const getWorkspaceCardLayoutEntry = createLocalizedCatalog((): SettingsSearchEntry => ({ + title: translate( + 'auto.components.settings.appearance.search.workspaceCardLayout.title', + 'Workspace Card Layout' + ), + description: translate( + 'auto.components.settings.appearance.search.workspaceCardLayout.description', + 'Workspace cards can use compact or detailed layouts.' + ), + keywords: [ + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.workspaceCardLayout.compact', + 'compact' ), - description: translate( - 'auto.components.settings.appearance.search.workspaceCardLayout.description', - 'Workspace cards can use compact or detailed layouts.' + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.workspaceCardLayout.compactDisplay', + 'compact display' ), - keywords: [ - ...translateSearchKeyword( - 'auto.components.settings.appearance.search.workspaceCardLayout.compact', - 'compact' - ), - ...translateSearchKeyword( - 'auto.components.settings.appearance.search.workspaceCardLayout.compactDisplay', - 'compact display' - ), - ...translateSearchKeyword( - 'auto.components.settings.appearance.search.workspaceCardLayout.workspaceCards', - 'workspace cards' - ), - ...translateSearchKeyword( - 'auto.components.settings.appearance.search.workspaceCardLayout.worktreeCards', - 'worktree cards' - ), - ...translateSearchKeyword('auto.components.settings.appearance.search.5bff6a2ef0', 'sidebar'), - ...translateSearchKeyword( - 'auto.components.settings.appearance.search.workspaceCardLayout.cardLayout', - 'card layout' - ), - ...translateSearchKeyword( - 'auto.components.settings.appearance.search.workspaceCardLayout.workspaceOptions', - 'workspace options' - ), - ...translateSearchKeyword( - 'auto.components.settings.appearance.search.workspaceCardLayout.detailed', - 'detailed' - ) - ] - }) -) + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.workspaceCardLayout.workspaceCards', + 'workspace cards' + ), + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.workspaceCardLayout.worktreeCards', + 'worktree cards' + ), + ...translateSearchKeyword('auto.components.settings.appearance.search.5bff6a2ef0', 'sidebar'), + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.workspaceCardLayout.cardLayout', + 'card layout' + ), + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.workspaceCardLayout.workspaceOptions', + 'workspace options' + ), + ...translateSearchKeyword( + 'auto.components.settings.appearance.search.workspaceCardLayout.detailed', + 'detailed' + ) + ] +})) export const getShowPinnedWorktreesInGroupsEntry = createLocalizedCatalog( (): SettingsSearchEntry => ({ diff --git a/src/renderer/src/components/settings/ephemeral-vms-search.ts b/src/renderer/src/components/settings/ephemeral-vms-search.ts index 654c5d52dda..8870d515858 100644 --- a/src/renderer/src/components/settings/ephemeral-vms-search.ts +++ b/src/renderer/src/components/settings/ephemeral-vms-search.ts @@ -3,36 +3,31 @@ import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' import { createLocalizedCatalog } from '@/i18n/localized-catalog' -export const getEphemeralVmsSearchEntry = createLocalizedCatalog( - (): SettingsSearchEntry => ({ - title: translate('auto.components.settings.ephemeralVms.search.cloudVmTitle', 'Cloud VM'), - description: translate( - 'auto.components.settings.ephemeralVms.search.description', - 'Learn how repo-owned recipes give each workspace its own on-demand, disposable environment.' +export const getEphemeralVmsSearchEntry = createLocalizedCatalog((): SettingsSearchEntry => ({ + title: translate('auto.components.settings.ephemeralVms.search.cloudVmTitle', 'Cloud VM'), + description: translate( + 'auto.components.settings.ephemeralVms.search.description', + 'Learn how repo-owned recipes give each workspace its own on-demand, disposable environment.' + ), + keywords: [ + ...translateSearchKeyword( + 'auto.components.settings.experimental.search.0d24759f14', + 'experimental' ), - keywords: [ - ...translateSearchKeyword( - 'auto.components.settings.experimental.search.0d24759f14', - 'experimental' - ), - ...translateSearchKeyword('auto.components.settings.ephemeralVms.search.keywordVm', 'vm'), - ...translateSearchKeyword( - 'auto.components.settings.ephemeralVms.search.keywordSandbox', - 'sandbox' - ), - ...translateSearchKeyword( - 'auto.components.settings.ephemeralVms.search.keywordCloud', - 'cloud' - ), - ...translateSearchKeyword( - 'auto.components.settings.ephemeralVms.search.keywordRecipe', - 'recipe' - ), - ...translateSearchKeyword( - 'auto.components.settings.ephemeralVms.search.keywordEphemeral', - 'ephemeral' - ) - ], - targetSectionId: 'ephemeral-vms' - }) -) + ...translateSearchKeyword('auto.components.settings.ephemeralVms.search.keywordVm', 'vm'), + ...translateSearchKeyword( + 'auto.components.settings.ephemeralVms.search.keywordSandbox', + 'sandbox' + ), + ...translateSearchKeyword('auto.components.settings.ephemeralVms.search.keywordCloud', 'cloud'), + ...translateSearchKeyword( + 'auto.components.settings.ephemeralVms.search.keywordRecipe', + 'recipe' + ), + ...translateSearchKeyword( + 'auto.components.settings.ephemeralVms.search.keywordEphemeral', + 'ephemeral' + ) + ], + targetSectionId: 'ephemeral-vms' +})) diff --git a/src/renderer/src/components/settings/mobile-settings-search.ts b/src/renderer/src/components/settings/mobile-settings-search.ts index 6fce884fc12..3afa610f44b 100644 --- a/src/renderer/src/components/settings/mobile-settings-search.ts +++ b/src/renderer/src/components/settings/mobile-settings-search.ts @@ -4,63 +4,46 @@ import { translate } from '@/i18n/i18n' import { translateSearchKeyword } from './settings-search-keywords' import { createLocalizedCatalog } from '@/i18n/localized-catalog' -export const getMobileOverviewSearchEntry = createLocalizedCatalog( - (): SettingsSearchEntry => ({ - title: translate('auto.components.settings.mobile.settings.search.ffd52a96e4', 'Mobile'), - description: translate( - 'auto.components.settings.mobile.settings.search.671eb4173c', - 'Control terminals and agents from your phone.' +export const getMobileOverviewSearchEntry = createLocalizedCatalog((): SettingsSearchEntry => ({ + title: translate('auto.components.settings.mobile.settings.search.ffd52a96e4', 'Mobile'), + description: translate( + 'auto.components.settings.mobile.settings.search.671eb4173c', + 'Control terminals and agents from your phone.' + ), + keywords: [ + ...translateSearchKeyword( + 'auto.components.settings.mobile.settings.search.f213400800', + 'mobile' ), - keywords: [ - ...translateSearchKeyword( - 'auto.components.settings.mobile.settings.search.f213400800', - 'mobile' - ), - ...translateSearchKeyword( - 'auto.components.settings.mobile.settings.search.f4ed142753', - 'phone' - ), - ...translateSearchKeyword( - 'auto.components.settings.mobile.settings.search.5d5af8e041', - 'iphone' - ), - ...translateSearchKeyword( - 'auto.components.settings.mobile.settings.search.cf2c93b479', - 'pair' - ), - ...translateSearchKeyword('auto.components.settings.mobile.settings.search.87816d1c59', 'qr'), - ...translateSearchKeyword( - 'auto.components.settings.mobile.settings.search.59b1d75fd1', - 'code' - ), - ...translateSearchKeyword( - 'auto.components.settings.mobile.settings.search.0b7e585cb9', - 'scan' - ), - ...translateSearchKeyword( - 'auto.components.settings.mobile.settings.search.7e801801ac', - 'remote' - ), - ...translateSearchKeyword( - 'auto.components.settings.mobile.settings.search.e4f4daea0e', - 'relay' - ), - ...translateSearchKeyword( - 'auto.components.settings.mobile.settings.search.a7eececc1d', - 'android' - ), - ...translateSearchKeyword( - 'auto.components.settings.mobile.settings.search.6bfa001752', - 'apk' - ), - ...translateSearchKeyword( - 'auto.components.settings.mobile.settings.search.8d4ba0ef09', - 'beta' - ), - ...translateSearchKeyword('auto.components.settings.mobile.settings.search.b730ff7049', 'app') - ] - }) -) + ...translateSearchKeyword( + 'auto.components.settings.mobile.settings.search.f4ed142753', + 'phone' + ), + ...translateSearchKeyword( + 'auto.components.settings.mobile.settings.search.5d5af8e041', + 'iphone' + ), + ...translateSearchKeyword('auto.components.settings.mobile.settings.search.cf2c93b479', 'pair'), + ...translateSearchKeyword('auto.components.settings.mobile.settings.search.87816d1c59', 'qr'), + ...translateSearchKeyword('auto.components.settings.mobile.settings.search.59b1d75fd1', 'code'), + ...translateSearchKeyword('auto.components.settings.mobile.settings.search.0b7e585cb9', 'scan'), + ...translateSearchKeyword( + 'auto.components.settings.mobile.settings.search.7e801801ac', + 'remote' + ), + ...translateSearchKeyword( + 'auto.components.settings.mobile.settings.search.e4f4daea0e', + 'relay' + ), + ...translateSearchKeyword( + 'auto.components.settings.mobile.settings.search.a7eececc1d', + 'android' + ), + ...translateSearchKeyword('auto.components.settings.mobile.settings.search.6bfa001752', 'apk'), + ...translateSearchKeyword('auto.components.settings.mobile.settings.search.8d4ba0ef09', 'beta'), + ...translateSearchKeyword('auto.components.settings.mobile.settings.search.b730ff7049', 'app') + ] +})) export const getMobileSidebarShortcutSearchEntry = createLocalizedCatalog( (): SettingsSearchEntry => ({ diff --git a/src/renderer/src/components/settings/settings-search.ts b/src/renderer/src/components/settings/settings-search.ts index cbbd8388f65..359fe8b40ef 100644 --- a/src/renderer/src/components/settings/settings-search.ts +++ b/src/renderer/src/components/settings/settings-search.ts @@ -145,13 +145,11 @@ export function rankSettingsSearchItems( } return items - .map( - (item, index): SettingsSearchRankCandidate => ({ - item, - index, - score: scoreSettingsSearch(query, getEntries(item)) - }) - ) + .map((item, index): SettingsSearchRankCandidate => ({ + item, + index, + score: scoreSettingsSearch(query, getEntries(item)) + })) .filter((candidate) => candidate.score > SETTINGS_SEARCH_NO_MATCH_SCORE) .sort((a, b) => b.score - a.score || a.index - b.index) .map(({ item, score }) => ({ item, score })) diff --git a/src/renderer/src/components/task-page-localized-options.tsx b/src/renderer/src/components/task-page-localized-options.tsx index cd14317489d..4bbf2b4de55 100644 --- a/src/renderer/src/components/task-page-localized-options.tsx +++ b/src/renderer/src/components/task-page-localized-options.tsx @@ -211,15 +211,13 @@ export const getLinearDisplayProperties = createLocalizedCatalog( } ) -export const getLinearPriorityLabels = createLocalizedCatalog( - (): Record => ({ - 0: translate('auto.components.TaskPage.713179dfdc', 'No priority'), - 1: translate('auto.components.TaskPage.f373ab1a4f', 'Urgent'), - 2: translate('auto.components.TaskPage.345b169f1f', 'High'), - 3: translate('auto.components.TaskPage.7fd59c18d8', 'Medium'), - 4: translate('auto.components.TaskPage.69591944e7', 'Low') - }) -) +export const getLinearPriorityLabels = createLocalizedCatalog((): Record => ({ + 0: translate('auto.components.TaskPage.713179dfdc', 'No priority'), + 1: translate('auto.components.TaskPage.f373ab1a4f', 'Urgent'), + 2: translate('auto.components.TaskPage.345b169f1f', 'High'), + 3: translate('auto.components.TaskPage.7fd59c18d8', 'Medium'), + 4: translate('auto.components.TaskPage.69591944e7', 'Low') +})) export function getLinearPriorityLabel(priority: number): string { return getLinearPriorityLabels()[priority] ?? `P${priority}` diff --git a/src/renderer/src/components/task-page/hooks/use-task-page-github-source-banners.ts b/src/renderer/src/components/task-page/hooks/use-task-page-github-source-banners.ts index 311179c2d83..6502b86dc91 100644 --- a/src/renderer/src/components/task-page/hooks/use-task-page-github-source-banners.ts +++ b/src/renderer/src/components/task-page/hooks/use-task-page-github-source-banners.ts @@ -82,8 +82,8 @@ export function useTaskPageGitHubSourceBanners({ // Hard guarantee (K4): always overlay pending after reconcile so list // fetch clobbers never paint unprotected coordinator fields. setPages((current) => - reconcileTaskPagePagesWithWorkItemsCache(current, selectedWorkItemsCacheEntries).map((page) => - page ? (overlayPendingOnTaskPagePages([page])[0] ?? []) : null + reconcileTaskPagePagesWithWorkItemsCache(current, selectedWorkItemsCacheEntries).map( + (page) => (page ? (overlayPendingOnTaskPagePages([page])[0] ?? []) : null) ) ) }, [githubMode, selectedWorkItemsCacheEntries, taskSource, setPages]) diff --git a/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts b/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts index f836bdf8b7a..05a59f2a07d 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts @@ -21,10 +21,7 @@ import { selectColdParkedTerminalTabs } from './terminal-hidden-view-parking' import type { ParkVerdictFlipRecord } from './terminal-park-verdict-flip-telemetry' -import { - haveSameTerminalTabIds, - useTerminalParkVerdictPin -} from './use-terminal-park-verdict-pin' +import { haveSameTerminalTabIds, useTerminalParkVerdictPin } from './use-terminal-park-verdict-pin' import { withholdUnparkableTerminalTabs } from './terminal-cold-park-withheld-tabs' import { getTerminalParkingPolicyOverrides } from './terminal-parking-e2e-overrides' import { diff --git a/src/renderer/src/hooks/runtime-client-events-sync.test.ts b/src/renderer/src/hooks/runtime-client-events-sync.test.ts index 9f237ce1c23..bd046856ad9 100644 --- a/src/renderer/src/hooks/runtime-client-events-sync.test.ts +++ b/src/renderer/src/hooks/runtime-client-events-sync.test.ts @@ -225,9 +225,8 @@ describe('createRuntimeClientEventsSync', () => { it('backs off exponentially with a cap while an environment keeps failing', async () => { vi.useFakeTimers() try { - const subscribe = vi.fn( - (): Promise => - Promise.reject(new Error('unreachable')) + const subscribe = vi.fn((): Promise => + Promise.reject(new Error('unreachable')) ) const sync = createRuntimeClientEventsSync({ getDesiredEnvironmentIds: () => ['A'], @@ -266,9 +265,8 @@ describe('createRuntimeClientEventsSync', () => { it('applies jitter below the full backoff delay', async () => { vi.useFakeTimers() try { - const subscribe = vi.fn( - (): Promise => - Promise.reject(new Error('unreachable')) + const subscribe = vi.fn((): Promise => + Promise.reject(new Error('unreachable')) ) const sync = createRuntimeClientEventsSync({ getDesiredEnvironmentIds: () => ['A'], @@ -344,9 +342,8 @@ describe('createRuntimeClientEventsSync', () => { vi.useFakeTimers() try { let subscriptionKey = 'A:1' - const subscribe = vi.fn( - (): Promise => - Promise.reject(new Error('unreachable')) + const subscribe = vi.fn((): Promise => + Promise.reject(new Error('unreachable')) ) const sync = createRuntimeClientEventsSync({ getDesiredEnvironmentIds: () => ['A'], @@ -379,9 +376,8 @@ describe('createRuntimeClientEventsSync', () => { it('an external sync retries a waiting environment immediately (recovery path)', async () => { vi.useFakeTimers() try { - const subscribe = vi.fn( - (): Promise => - Promise.reject(new Error('unreachable')) + const subscribe = vi.fn((): Promise => + Promise.reject(new Error('unreachable')) ) const sync = createRuntimeClientEventsSync({ getDesiredEnvironmentIds: () => ['A'], diff --git a/src/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.ts b/src/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.ts index 3afc0833b77..798fd284fd0 100644 --- a/src/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.ts @@ -107,18 +107,16 @@ describe('useIpcEvents agent status snapshot integration', () => { expandedLeafId: null } } - const snapshot = cases.map( - (entry, index): AgentStatusSetData => ({ - paneKey: makePaneKey(entry.tabId, entry.leafId), - worktreeId: entry.payloadWorktreeId, - ...(entry.connectionId ? { connectionId: entry.connectionId } : {}), - state: 'working', - prompt: `routing case ${index}`, - agentType: 'claude', - receivedAt: 1_700_000_001_000 + index, - stateStartedAt: 1_700_000_001_000 + index - }) - ) + const snapshot = cases.map((entry, index): AgentStatusSetData => ({ + paneKey: makePaneKey(entry.tabId, entry.leafId), + worktreeId: entry.payloadWorktreeId, + ...(entry.connectionId ? { connectionId: entry.connectionId } : {}), + state: 'working', + prompt: `routing case ${index}`, + agentType: 'claude', + receivedAt: 1_700_000_001_000 + index, + stateStartedAt: 1_700_000_001_000 + index + })) const localRepo = { ...TEST_REPO, id: 'repo-routing-local', connectionId: null } const sshRepo = { ...TEST_REPO, id: 'repo-routing-ssh', connectionId: 'ssh-live' } const store = createTestStore() diff --git a/src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-hydration.test.ts b/src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-hydration.test.ts index b273fac9876..0705e323399 100644 --- a/src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-hydration.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-hydration.test.ts @@ -486,17 +486,15 @@ describe('useIpcEvents agent status snapshot integration', () => { return tabId } }) - const snapshot = leafIds.map( - (leafId, index): AgentStatusSetData => ({ - paneKey: makePaneKey(tabId, leafId), - worktreeId, - state: 'working', - prompt: `indexed prompt ${index}`, - agentType: 'claude', - receivedAt: 1_700_000_000_000 + index, - stateStartedAt: 1_700_000_000_000 + index - }) - ) + const snapshot = leafIds.map((leafId, index): AgentStatusSetData => ({ + paneKey: makePaneKey(tabId, leafId), + worktreeId, + state: 'working', + prompt: `indexed prompt ${index}`, + agentType: 'claude', + receivedAt: 1_700_000_000_000 + index, + stateStartedAt: 1_700_000_000_000 + index + })) let resolveSnapshot!: (entries: AgentStatusSetData[]) => void const getSnapshot = vi.fn( () => diff --git a/src/renderer/src/runtime/remote-server-update-coordinator.test.ts b/src/renderer/src/runtime/remote-server-update-coordinator.test.ts index 66cb646f711..3a6b152c416 100644 --- a/src/renderer/src/runtime/remote-server-update-coordinator.test.ts +++ b/src/renderer/src/runtime/remote-server-update-coordinator.test.ts @@ -64,14 +64,12 @@ function transport( getUpdaterStatus: vi.fn(async () => availableSnapshot), check: vi.fn(async () => availableSnapshot), download: vi.fn(async () => availableSnapshot), - install: vi.fn( - async (): Promise => ({ - accepted: true, - fromVersion: '1.4.0', - targetVersion: '1.5.0', - runtimeId: 'runtime-old' - }) - ), + install: vi.fn(async (): Promise => ({ + accepted: true, + fromVersion: '1.4.0', + targetVersion: '1.5.0', + runtimeId: 'runtime-old' + })), wait: vi.fn(async (milliseconds) => { clock += milliseconds }), diff --git a/src/renderer/src/store/slices/browser.ts b/src/renderer/src/store/slices/browser.ts index 21394e8ff36..21fc58ebee5 100644 --- a/src/renderer/src/store/slices/browser.ts +++ b/src/renderer/src/store/slices/browser.ts @@ -1326,8 +1326,8 @@ export const createBrowserSlice: StateCreator = }, browserTabsByWorktree: { ...s.browserTabsByWorktree, - [workspace.worktreeId]: (s.browserTabsByWorktree[workspace.worktreeId] ?? []).map((tab) => - tab.id === workspaceId ? nextWorkspace : tab + [workspace.worktreeId]: (s.browserTabsByWorktree[workspace.worktreeId] ?? []).map( + (tab) => (tab.id === workspaceId ? nextWorkspace : tab) ) }, pendingAddressBarFocusByPageId: shouldFocusAddressBar @@ -1409,8 +1409,8 @@ export const createBrowserSlice: StateCreator = }, browserTabsByWorktree: { ...s.browserTabsByWorktree, - [workspace.worktreeId]: (s.browserTabsByWorktree[workspace.worktreeId] ?? []).map((tab) => - tab.id === workspace.id ? nextWorkspace : tab + [workspace.worktreeId]: (s.browserTabsByWorktree[workspace.worktreeId] ?? []).map( + (tab) => (tab.id === workspace.id ? nextWorkspace : tab) ) }, recentlyClosedBrowserPagesByWorkspace: { @@ -1509,8 +1509,8 @@ export const createBrowserSlice: StateCreator = return { browserTabsByWorktree: { ...s.browserTabsByWorktree, - [workspace.worktreeId]: (s.browserTabsByWorktree[workspace.worktreeId] ?? []).map((tab) => - tab.id === workspaceId ? nextWorkspace : tab + [workspace.worktreeId]: (s.browserTabsByWorktree[workspace.worktreeId] ?? []).map( + (tab) => (tab.id === workspaceId ? nextWorkspace : tab) ) } } @@ -1707,8 +1707,8 @@ export const createBrowserSlice: StateCreator = if (!browserWorkspaceMirrorFieldsEqual(workspace, nextWorkspace)) { nextState.browserTabsByWorktree = { ...s.browserTabsByWorktree, - [workspace.worktreeId]: (s.browserTabsByWorktree[workspace.worktreeId] ?? []).map((tab) => - tab.id === workspace.id ? nextWorkspace : tab + [workspace.worktreeId]: (s.browserTabsByWorktree[workspace.worktreeId] ?? []).map( + (tab) => (tab.id === workspace.id ? nextWorkspace : tab) ) } } @@ -1804,8 +1804,8 @@ export const createBrowserSlice: StateCreator = }, browserTabsByWorktree: { ...s.browserTabsByWorktree, - [workspace.worktreeId]: (s.browserTabsByWorktree[workspace.worktreeId] ?? []).map((tab) => - tab.id === workspace.id ? nextWorkspace : tab + [workspace.worktreeId]: (s.browserTabsByWorktree[workspace.worktreeId] ?? []).map( + (tab) => (tab.id === workspace.id ? nextWorkspace : tab) ) }, ...(shouldClearAnnotations diff --git a/src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-refresh.ts b/src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-refresh.ts index ae584253173..309f39603a1 100644 --- a/src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-refresh.ts +++ b/src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-refresh.ts @@ -49,7 +49,9 @@ export async function listWorktreeLineageForRuntime( lineage?: Record workspaceLineage?: Record } - const normalizeLineageResponse = (value: Record | LineageListResponse) => + const normalizeLineageResponse = ( + value: Record | LineageListResponse + ) => Object.hasOwn(value, 'lineage') || Object.hasOwn(value, 'workspaceLineage') ? { worktreeLineageById: (value as LineageListResponse).lineage ?? {}, diff --git a/src/renderer/src/web/web-preload-api-runtime-environment.test.ts b/src/renderer/src/web/web-preload-api-runtime-environment.test.ts index 58ecb5e96c0..08167e2e140 100644 --- a/src/renderer/src/web/web-preload-api-runtime-environment.test.ts +++ b/src/renderer/src/web/web-preload-api-runtime-environment.test.ts @@ -528,14 +528,13 @@ describe('web runtime environment identity', () => { it('replaces API closures on reinstall while retaining the runtime client singleton', async () => { let clientCount = 0 - const call = vi.fn( - (method: string): Promise> => - Promise.resolve({ - id: method, - ok: true, - result: { method }, - _meta: { runtimeId: 'runtime-a' } - }) + const call = vi.fn((method: string): Promise> => + Promise.resolve({ + id: method, + ok: true, + result: { method }, + _meta: { runtimeId: 'runtime-a' } + }) ) vi.doMock('./web-runtime-client', () => ({ WebRuntimeClient: class { diff --git a/src/shared/map-settled-with-concurrency.test.ts b/src/shared/map-settled-with-concurrency.test.ts index 506d7a4d3e0..3127a31f852 100644 --- a/src/shared/map-settled-with-concurrency.test.ts +++ b/src/shared/map-settled-with-concurrency.test.ts @@ -24,11 +24,10 @@ describe('mapSettledWithConcurrency', () => { expect(peak).toBe(limit) expect(results).toEqual( - items.map( - (item): PromiseSettledResult => - item % 97 === 0 - ? { status: 'rejected', reason: `rejected-${item}` } - : { status: 'fulfilled', value: `fulfilled-${item}` } + items.map((item): PromiseSettledResult => + item % 97 === 0 + ? { status: 'rejected', reason: `rejected-${item}` } + : { status: 'fulfilled', value: `fulfilled-${item}` } ) ) }) diff --git a/src/shared/raster-image-dimensions.test.ts b/src/shared/raster-image-dimensions.test.ts index 1a517a82791..d6e5e3ad3b5 100644 --- a/src/shared/raster-image-dimensions.test.ts +++ b/src/shared/raster-image-dimensions.test.ts @@ -28,7 +28,7 @@ function jpegWithMetadata( extraSegments = 0 ): Buffer { const parts = [Buffer.from([0xff, 0xd8])] - for (let written = 0; written < metadataBytes; ) { + for (let written = 0; written < metadataBytes;) { // A JPEG segment length field is 16 bits, so real files chain many segments to carry a profile. const size = Math.min(65_533, metadataBytes - written) const header = Buffer.alloc(4) diff --git a/tests/e2e/helpers/store.ts b/tests/e2e/helpers/store.ts index 92ca9461dbd..e11c3921f82 100644 --- a/tests/e2e/helpers/store.ts +++ b/tests/e2e/helpers/store.ts @@ -57,12 +57,10 @@ export async function getWorktreeTabs( } const state = store.getState() - return (state.tabsByWorktree[worktreeId] ?? []).map( - (tab): TerminalTabSummary => ({ - id: tab.id, - title: tab.customTitle || tab.title - }) - ) + return (state.tabsByWorktree[worktreeId] ?? []).map((tab): TerminalTabSummary => ({ + id: tab.id, + title: tab.customTitle || tab.title + })) }, worktreeId) } @@ -116,13 +114,11 @@ export async function getBrowserTabs( } const state = store.getState() - return (state.browserTabsByWorktree[worktreeId] ?? []).map( - (tab): BrowserTabSummary => ({ - id: tab.id, - url: tab.url, - title: tab.title - }) - ) + return (state.browserTabsByWorktree[worktreeId] ?? []).map((tab): BrowserTabSummary => ({ + id: tab.id, + url: tab.url, + title: tab.title + })) }, worktreeId) } @@ -140,13 +136,11 @@ export async function getOpenFiles( const state = store.getState() return state.openFiles .filter((file) => file.worktreeId === worktreeId) - .map( - (file): ExplorerFileSummary => ({ - id: file.id, - filePath: file.filePath, - relativePath: file.relativePath - }) - ) + .map((file): ExplorerFileSummary => ({ + id: file.id, + filePath: file.filePath, + relativePath: file.relativePath + })) }, worktreeId) } diff --git a/tests/e2e/terminal-ime-recorded-dom-trace-replay.ts b/tests/e2e/terminal-ime-recorded-dom-trace-replay.ts index 9b73a5b4008..9a4c85fff02 100644 --- a/tests/e2e/terminal-ime-recorded-dom-trace-replay.ts +++ b/tests/e2e/terminal-ime-recorded-dom-trace-replay.ts @@ -166,7 +166,7 @@ export async function replayRecordedImeDomTrace( const samples: ReplayedCompositionSample[] = [] let compositionOpen = false - for (let index = 0; index < trace.dom.length; ) { + for (let index = 0; index < trace.dom.length;) { const group = nextRecordedEventGroup(trace.dom, index) const recorded = group[0] await dispatchRecordedEvents(page, group) From 67fa855b6c0dfb871defd1d20296cd2d1df22873 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:15:45 -0700 Subject: [PATCH 10/80] Make worktree palette hint rows keyboard-clickable (#17272) * Make worktree palette hint rows keyboard-clickable Hint entries like "See more" are now CommandItems that can be navigated with arrow keys and activated with Enter, instead of being non-interactive divs. This allows keyboard-only users to access the expand actions without mouse interaction. * Make worktree palette "See more" keyboard-navigable Preserve cursor position when expanding via keyboard: auto-select the first newly revealed item at the previous index and restore input focus. --- .../src/components/WorktreeJumpPalette.tsx | 39 +++++----- ...jump-palette-interleaved-sections.test.tsx | 75 +++++++++++++++---- .../worktree-palette-create-action.test.ts | 3 +- .../src/lib/worktree-palette-create-action.ts | 5 +- 4 files changed, 87 insertions(+), 35 deletions(-) diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx index f18ba555d68..4e39baf2bea 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.tsx @@ -8,6 +8,7 @@ import React, { useRef, useState } from 'react' +import { flushSync } from 'react-dom' import { useShallow } from 'zustand/react/shallow' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -42,7 +43,6 @@ import { CommandEmpty, CommandItem } from '@/components/ui/command' -import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { parseGitHubIssueOrPRNumber, parseGitHubIssueOrPRLink } from '@/lib/github-links' import { getLinkedWorkItemSuggestedName, getLinkedWorkItemWorkspaceName } from '@/lib/new-workspace' @@ -3258,30 +3258,33 @@ function WorktreeJumpPaletteContent({ } if (entry.type === 'hint') { - // Why: plain div (not CommandItem) so cmdk can't select it; arrow keys skip it via selectableItems. return ( -
{ + const previousIndex = selectionItemIds.indexOf(renderKey) + flushSync(() => entry.onSeeMore?.()) + const expandedItemId = Array.from( + listRef.current?.querySelectorAll('[cmdk-item]') ?? [] + )[previousIndex]?.getAttribute('data-value') + if (expandedItemId) { + setSelectedItemId(expandedItemId) + } + inputRef.current?.focus() + }} + className={cn( + JUMP_PALETTE_ITEM_CLASSNAME, + 'mt-1 min-h-0 gap-2 py-1.5 text-[12px] text-muted-foreground' + )} > {entry.label} {entry.onSeeMore ? ( - + ) : null} -
+ ) } diff --git a/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx b/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx index 854412b5107..f52bf725a5a 100644 --- a/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx +++ b/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx @@ -57,18 +57,33 @@ vi.mock('@/components/ui/command', async () => { return { Command: ({ children }: { children: React.ReactNode }) =>
{children}
, CommandGroup: ({ children }: { children: React.ReactNode }) =>
{children}
, - CommandDialog: ({ children, open }: { children: React.ReactNode; open?: boolean }) => - open ?
{children}
: null, - CommandInput: ({ - value, - onValueChange + CommandDialog: ({ + children, + open, + commandProps }: { - value?: string - onValueChange?: (next: string) => void - }) => { + children: React.ReactNode + open?: boolean + commandProps?: { value?: string } + }) => + open ? ( +
+ {children} +
+ ) : null, + CommandInput: React.forwardRef(function CommandInput( + { + value, + onValueChange + }: { + value?: string + onValueChange?: (next: string) => void + }, + ref: React.ForwardedRef + ) { setCommandQuery = onValueChange ?? null - return {}} /> - }, + return {}} /> + }), CommandList: React.forwardRef(function CommandList( { children }: { children: React.ReactNode }, ref: React.ForwardedRef @@ -82,8 +97,22 @@ vi.mock('@/components/ui/command', async () => { CommandEmpty: ({ children }: { children: React.ReactNode }) => (
{children}
), - CommandItem: ({ children, value }: { children: React.ReactNode; value?: string }) => ( - ) @@ -534,6 +563,13 @@ describe('WorktreeJumpPalette interleaved primary sections', () => { btn.textContent?.includes('See more') ) expect(seeMoreBtn).toBeDefined() + const initialItemIds = Array.from(testContainer.querySelectorAll('[cmdk-item]')).map((item) => + item.getAttribute('data-value') + ) + const seeMoreIndex = initialItemIds.indexOf('__hint_worktree_overflow__') + expect(seeMoreIndex).toBeGreaterThan(0) + const input = testContainer.querySelector('[data-command-input="true"]') + input?.focus() await act(async () => { seeMoreBtn?.click() @@ -541,9 +577,20 @@ describe('WorktreeJumpPalette interleaved primary sections', () => { await flushEffects() // After expanding by 20: 30 worktrees are rendered, 5 more - const renderedItems = testContainer.querySelectorAll('[data-command-item]') + const renderedItems = testContainer.querySelectorAll('[data-command-item^="worktree:"]') expect(renderedItems).toHaveLength(30) expect(testContainer.textContent).toContain('5 more') + const firstRevealedItemId = Array.from(testContainer.querySelectorAll('[cmdk-item]'))[ + seeMoreIndex + ]?.getAttribute('data-value') + expect(firstRevealedItemId).toMatch(/^worktree:/) + expect(firstRevealedItemId).not.toBe(initialItemIds[0]) + expect( + testContainer + .querySelector('[data-command-dialog="true"]') + ?.getAttribute('data-command-value') + ).toBe(firstRevealedItemId) + expect(document.activeElement).toBe(input) // Click again: 30 + 20 = 50 (all 35 fit), hint disappears const seeMoreBtn2 = Array.from(testContainer.querySelectorAll('button')).find((btn) => @@ -554,7 +601,7 @@ describe('WorktreeJumpPalette interleaved primary sections', () => { }) await flushEffects() - const renderedItemsAll = testContainer.querySelectorAll('[data-command-item]') + const renderedItemsAll = testContainer.querySelectorAll('[data-command-item^="worktree:"]') expect(renderedItemsAll).toHaveLength(35) expect(testContainer.textContent).not.toContain('more') }) diff --git a/src/renderer/src/lib/worktree-palette-create-action.test.ts b/src/renderer/src/lib/worktree-palette-create-action.test.ts index 408d6227c45..8f4667a043e 100644 --- a/src/renderer/src/lib/worktree-palette-create-action.test.ts +++ b/src/renderer/src/lib/worktree-palette-create-action.test.ts @@ -182,7 +182,7 @@ describe('worktree-palette-create-action', () => { }) }) - it('derives selection ids from rendered entries while skipping headers and hints', () => { + it('derives selection ids from rendered entries while skipping headers', () => { expect( getWorktreePaletteSelectionItemIds([ { id: '__header_worktrees__', type: 'section-header' }, @@ -198,6 +198,7 @@ describe('worktree-palette-create-action', () => { ).toEqual([ 'worktree:one', CREATE_WORKTREE_ITEM_ID, + '__hint_worktree_cap__', 'settings:ai-provider-accounts', 'quick-action:new-terminal', 'browser-page:one' diff --git a/src/renderer/src/lib/worktree-palette-create-action.ts b/src/renderer/src/lib/worktree-palette-create-action.ts index f054b5d0f52..392bcddc33f 100644 --- a/src/renderer/src/lib/worktree-palette-create-action.ts +++ b/src/renderer/src/lib/worktree-palette-create-action.ts @@ -66,7 +66,8 @@ const SELECTABLE_ENTRY_TYPES = [ 'browser-page', 'workspace-tab', 'simulator-tab', - 'project-target' + 'project-target', + 'hint' ] as const type WorktreePaletteSelectableEntryType = (typeof SELECTABLE_ENTRY_TYPES)[number] @@ -85,7 +86,7 @@ export function getWorktreePaletteSelectionItemIds< T extends WorktreePaletteSelectionCandidateEntry >(entries: readonly T[], renderKeys: readonly string[] = []): string[] { // Why: keyboard focus should mirror rendered order, including synthetic - // action rows, while skipping headers and explanatory hint rows. + // action rows, while skipping only section headers. // Why renderKeys wins: rows render under de-duplicated keys, so naming the bare // id here would leave a duplicate row absent from the list the `includes` check // above consults — arrowing onto it would snap the highlight back to the top. From aa95bdb11ad7b1b6604334399c00210041530900 Mon Sep 17 00:00:00 2001 From: JahyunBaek <60498711+JahyunBaek@users.noreply.github.com> Date: Sun, 30 Aug 2026 06:23:22 +0900 Subject: [PATCH 11/80] test(shared): stop two suites asserting POSIX separators on Windows (#16511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both files describe paths with POSIX literals while their subjects compose paths through `node:path`, so the assertions only hold where the separator happens to be `/`. `node-markdown-document-discovery` keys its fake tree at `/repo/docs` and `/repo/one`, but `discoverMarkdownRelativePaths` descends with `join(absoluteDirectoryPath, entry.name)` — `\repo\docs` on win32. The child lookup misses, `readDirectory` yields nothing, and the walk stops at the root: `docs/guide.mdx` disappears and the depth-limit case never reaches its limit, so it resolves `[]` instead of rejecting. Keying the children with `join` walks the tree the subject actually walks. `git-fetch-head-lock` expects `cwd: '/tmp/repo'` from a subject that returns `path.resolve(cwd, 'repo')`, which is `C:\tmp\repo` on win32. Asserting through `path.resolve` pins the behaviour — that `-C` and `--git-dir` are resolved against the cwd — rather than the separator of whichever machine runs the suite. Verified on Windows 11: the two files go from 3 failed / 12 passed to 14 passed / 1 skipped, and the wider `src/shared` run shows no regression. Co-authored-by: Claude Opus 5 (1M context) --- src/shared/git-fetch-head-lock.test.ts | 6 ++++-- src/shared/node-markdown-document-discovery.test.ts | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/shared/git-fetch-head-lock.test.ts b/src/shared/git-fetch-head-lock.test.ts index bcf1e13a7fc..4133d8d373e 100644 --- a/src/shared/git-fetch-head-lock.test.ts +++ b/src/shared/git-fetch-head-lock.test.ts @@ -26,13 +26,15 @@ describe('runWithGitFetchHeadLock', () => { expect(resolveGitFetchHeadCommand(args, '/repo').needsLock).toBe(expected) }) + // Why resolve() rather than a literal: the subject resolves these through path.resolve, so a + // POSIX literal asserts the separator of the machine running the suite instead of the behaviour. it('resolves -C and --git-dir before deriving the lock identity', () => { expect(resolveGitFetchHeadCommand(['-C', 'repo', 'fetch'], '/tmp')).toMatchObject({ - cwd: '/tmp/repo', + cwd: path.resolve('/tmp', 'repo'), needsLock: true }) expect(resolveGitFetchHeadCommand(['--git-dir=/repo/.git', 'fetch'], '/tmp')).toMatchObject({ - gitDir: '/repo/.git', + gitDir: path.resolve('/tmp', '/repo/.git'), needsLock: true }) }) diff --git a/src/shared/node-markdown-document-discovery.test.ts b/src/shared/node-markdown-document-discovery.test.ts index e151f25ad90..9c5e1bdfd14 100644 --- a/src/shared/node-markdown-document-discovery.test.ts +++ b/src/shared/node-markdown-document-discovery.test.ts @@ -1,4 +1,5 @@ import type { Dirent } from 'node:fs' +import { join } from 'node:path' import { describe, expect, it } from 'vitest' import { MarkdownDocumentListingCapacityError } from './markdown-document-listing-limits' import { discoverMarkdownRelativePaths } from './node-markdown-document-discovery' @@ -22,6 +23,8 @@ function reader(entriesByPath: Record) { describe('bounded Markdown document discovery', () => { it('preserves depth-first discovery and skips excluded and symlinked directories', async () => { + // Why join() for the child key: the subject descends with path.join, so a '/repo/docs' + // literal never matches on Windows and the walk silently stops at the root. const result = await discoverMarkdownRelativePaths('/repo', { readDirectory: reader({ '/repo': [ @@ -30,7 +33,7 @@ describe('bounded Markdown document discovery', () => { entry('docs', 'directory'), entry('linked', 'symlink') ], - '/repo/docs': [entry('guide.mdx'), entry('app.ts')] + [join('/repo', 'docs')]: [entry('guide.mdx'), entry('app.ts')] }), shouldDescend: (_relativePath, name) => name !== '.git' }) @@ -65,7 +68,7 @@ describe('bounded Markdown document discovery', () => { limits: { maxDepth: 1 }, readDirectory: reader({ '/repo': [entry('one', 'directory')], - '/repo/one': [entry('two', 'directory')] + [join('/repo', 'one')]: [entry('two', 'directory')] }), shouldDescend: () => true }) From 7655d20f04ffc11a6221c504652fb435c8e0d88d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:34:50 -0700 Subject: [PATCH 12/80] fix(terminal): complete OMP stale cwd recovery (#17154) --- config/reliability-gates.jsonc | 12 +- .../daemon-bash-rcfile.txt | 11 +- .../daemon-zsh-zshenv.txt | 11 +- .../local-bash-rcfile.txt | 11 +- .../local-zsh-zshenv.txt | 11 +- .../relay-bash-rcfile.txt | 11 +- .../relay-zsh-zshenv.txt | 11 +- .../pty/omp-shell-wrapper.node-pty.test.ts | 111 +++++++++++++----- src/main/pty/omp-shell-wrapper.ts | 11 +- 9 files changed, 141 insertions(+), 59 deletions(-) diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 10143d9019c..4dea7b1e3e2 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -6345,8 +6345,8 @@ "https://github.com/stablyai/orca/issues/16457", "https://github.com/stablyai/orca/pull/17128" ], - "invariant": "A fresh local renderer terminal spawn may recover from a saved startup cwd whose directory no longer exists only by spawning at the selected workspace root and printing a generic in-terminal notice; existing directories — including ones outside the worktree (#7685) — spawn as requested, and reattach, SSH, remote-runtime, runtime/API, and mobile callers keep exact cwd semantics. A long-lived POSIX shell whose cwd inode was deleted and replaced may run an extension-enabled OMP launch from the live path named by its logical PWD only in a subshell; the parent shell cwd, OMP argv, status extension, and exit status remain unchanged, while a genuinely unavailable path fails visibly without invoking OMP. A usable current directory remains authoritative when PWD is unset instead of being remapped to a workspace fallback.", - "oracle": "The shared resolver falls back to the workspace root only when the injected existence probe reports the resolved cwd missing and the workspace root present, and never probes floating terminals or a cwd equal to the root. The renderer sends cwdFallback only for fresh local IPC spawns, main honors it only when connectionId and sessionId are absent, WSL UNC paths never engage the probe-based fallback, main returns fallback metadata only after an actual fallback, the IPC transport preserves that metadata, and the connection layer writes a generic notice that omits the missing path. In real interactive Bash and Zsh PTYs, unset PWD in a usable nested directory and require OMP to stay there rather than remap to ORCA_WORKTREE_PATH; then delete and recreate the active project path, require OMP to observe the replacement inode with byte-exact extension argv and its nonzero status preserved, require the parent shell to remain on the stale inode, and finally delete the replacement and require an actionable failure before the fake OMP binary runs again.", + "invariant": "A fresh local renderer terminal spawn may recover from a saved startup cwd whose directory no longer exists only by spawning at the selected workspace root and printing a generic in-terminal notice; existing directories — including ones outside the worktree (#7685) — spawn as requested, and reattach, SSH, remote-runtime, runtime/API, and mobile callers keep exact cwd semantics. A long-lived POSIX shell whose cwd inode was deleted and replaced may run a wrapped OMP command from the live path named by its logical PWD only in a subshell; the parent shell cwd, OMP argv, status extension, and exit status remain unchanged, while a genuinely unavailable path fails visibly without invoking OMP. A usable physical cwd remains authoritative when PWD is unset or names a removed logical symlink instead of being remapped to a workspace fallback.", + "oracle": "The shared resolver falls back to the workspace root only when the injected existence probe reports the resolved cwd missing and the workspace root present, and never probes floating terminals or a cwd equal to the root. The renderer sends cwdFallback only for fresh local IPC spawns, main honors it only when connectionId and sessionId are absent, WSL UNC paths never engage the probe-based fallback, main returns fallback metadata only after an actual fallback, the IPC transport preserves that metadata, and the connection layer writes a generic notice that omits the missing path. In real interactive Bash and Zsh PTYs, unset PWD and remove a logical symlink while the physical cwd stays usable, requiring OMP to remain there; then delete and recreate the active project path, require extension-enabled and extension-skipping invocations to observe the replacement inode with byte-exact argv and nonzero status preserved, require the parent shell to remain on the stale inode, unset PWD and require recovery through ORCA_WORKTREE_PATH, clear every logical fallback and require an actionable failure without invoking OMP, and finally delete the replacement and require the same for the missing PWD path.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/shared/terminal-startup-cwd.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty-spawn-cwd-fallback.test.ts src/main/ipc/pty-wsl-cwd-validation.test.ts", @@ -6410,6 +6410,10 @@ "assertions": [ "real Bash and Zsh PTYs rebind OMP to a recreated cwd inode without changing the parent shell", "an unset PWD with a usable cwd stays unset and does not remap OMP to ORCA_WORKTREE_PATH", + "a removed logical symlink does not invalidate its still-usable physical cwd", + "extension-skipping commands such as omp --version still recover the cwd without gaining extension argv", + "an unset PWD on a stale inode recovers through ORCA_WORKTREE_PATH", + "an unset PWD on a stale inode without a logical fallback fails visibly before invoking OMP", "extension argv and a nonzero OMP exit status survive the recovery subshell", "a subsequently missing logical cwd prints an actionable error and does not invoke OMP again" ] @@ -6465,7 +6469,7 @@ "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/pty/omp-shell-wrapper.node-pty.test.ts src/main/pty/omp-shell-wrapper-alias-safety.test.ts src/main/shell-wrapper-generated-file-snapshot.test.ts", "result": "passed", "durationSeconds": 3, - "summary": "3 test files passed; real Bash and Zsh PTYs covered unset-PWD authority, stale-inode recovery, parent-shell isolation, extension argv, exit status, missing-path visibility, alias safety, and generated local/daemon/relay parity." + "summary": "3 test files passed; real Bash and Zsh PTYs covered usable physical cwd authority, stale-inode recovery with set and unset PWD, extension-enabled and extension-skipping argv, parent-shell isolation, exit status, missing-path visibility, alias safety, and generated local/daemon/relay parity." } ], "runtimeBudget": { @@ -6482,7 +6486,7 @@ }, "performanceBudget": { "required": true, - "evidence": "The spawn recovery adds at most two statSync probes on the fresh-local spawn path (the provider already stats the same paths during validation) and one bounded terminal write only when fallback actually occurs. Each extension-enabled OMP launch adds a constant number of shell filesystem predicates; only a detected stale/unusable cwd creates one short recovery subshell. No polling, retry, provider listing, hidden-pane work, startup awaits, extra OMP subprocesses, or render-loop work was added." + "evidence": "The spawn recovery adds at most two statSync probes on the fresh-local spawn path (the provider already stats the same paths during validation) and one bounded terminal write only when fallback actually occurs. Each wrapped OMP launch adds a constant number of shell filesystem predicates; only a detected stale/unusable cwd creates one short recovery subshell. No polling, retry, provider listing, hidden-pane work, startup awaits, extra OMP subprocesses, or render-loop work was added." }, "promotionCriteria": [ "Attach CI evidence for all declared test files.", diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt index b91f1065f84..54837bf4d65 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt @@ -45,11 +45,14 @@ __orca_omp_should_skip_extension() { return 1 } __orca_omp_cwd_is_usable() { + local __orca_physical_cwd [[ -x . ]] || return 1 - if [[ -n "${PWD:-}" ]]; then - [[ -d "${PWD}" && "${PWD}" -ef . ]] + if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then + [[ "${PWD}" -ef . ]] else - builtin pwd -P >/dev/null 2>&1 + # Why compare the path: shell builtins can print a cached path for a deleted cwd. + __orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1 + [[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]] fi } __orca_omp_invoke() { @@ -69,7 +72,7 @@ __orca_omp_invoke() { __orca_omp() { local __orca_use_extension=1 __orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0 - if [[ $__orca_use_extension -eq 1 ]] && ! __orca_omp_cwd_is_usable; then + if ! __orca_omp_cwd_is_usable; then local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}" # Why: a restored shell can retain the deleted directory inode after its path is recreated. ( diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt index bce09f52ff0..3d3403ad099 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-zsh-zshenv.txt @@ -83,11 +83,14 @@ __orca_deferred_init() { return 1 } __orca_omp_cwd_is_usable() { + local __orca_physical_cwd [[ -x . ]] || return 1 - if [[ -n "${PWD:-}" ]]; then - [[ -d "${PWD}" && "${PWD}" -ef . ]] + if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then + [[ "${PWD}" -ef . ]] else - builtin pwd -P >/dev/null 2>&1 + # Why compare the path: shell builtins can print a cached path for a deleted cwd. + __orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1 + [[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]] fi } __orca_omp_invoke() { @@ -107,7 +110,7 @@ __orca_deferred_init() { __orca_omp() { local __orca_use_extension=1 __orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0 - if [[ $__orca_use_extension -eq 1 ]] && ! __orca_omp_cwd_is_usable; then + if ! __orca_omp_cwd_is_usable; then local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}" # Why: a restored shell can retain the deleted directory inode after its path is recreated. ( diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt index ee9cb7975ec..d4bb98997c2 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt @@ -48,11 +48,14 @@ __orca_omp_should_skip_extension() { return 1 } __orca_omp_cwd_is_usable() { + local __orca_physical_cwd [[ -x . ]] || return 1 - if [[ -n "${PWD:-}" ]]; then - [[ -d "${PWD}" && "${PWD}" -ef . ]] + if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then + [[ "${PWD}" -ef . ]] else - builtin pwd -P >/dev/null 2>&1 + # Why compare the path: shell builtins can print a cached path for a deleted cwd. + __orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1 + [[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]] fi } __orca_omp_invoke() { @@ -72,7 +75,7 @@ __orca_omp_invoke() { __orca_omp() { local __orca_use_extension=1 __orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0 - if [[ $__orca_use_extension -eq 1 ]] && ! __orca_omp_cwd_is_usable; then + if ! __orca_omp_cwd_is_usable; then local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}" # Why: a restored shell can retain the deleted directory inode after its path is recreated. ( diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt b/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt index 222cab93896..ee18600acb1 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/local-zsh-zshenv.txt @@ -83,11 +83,14 @@ __orca_deferred_init() { return 1 } __orca_omp_cwd_is_usable() { + local __orca_physical_cwd [[ -x . ]] || return 1 - if [[ -n "${PWD:-}" ]]; then - [[ -d "${PWD}" && "${PWD}" -ef . ]] + if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then + [[ "${PWD}" -ef . ]] else - builtin pwd -P >/dev/null 2>&1 + # Why compare the path: shell builtins can print a cached path for a deleted cwd. + __orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1 + [[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]] fi } __orca_omp_invoke() { @@ -107,7 +110,7 @@ __orca_deferred_init() { __orca_omp() { local __orca_use_extension=1 __orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0 - if [[ $__orca_use_extension -eq 1 ]] && ! __orca_omp_cwd_is_usable; then + if ! __orca_omp_cwd_is_usable; then local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}" # Why: a restored shell can retain the deleted directory inode after its path is recreated. ( diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt index 9d90929085d..de9c8f95248 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/relay-bash-rcfile.txt @@ -37,11 +37,14 @@ __orca_omp_should_skip_extension() { return 1 } __orca_omp_cwd_is_usable() { + local __orca_physical_cwd [[ -x . ]] || return 1 - if [[ -n "${PWD:-}" ]]; then - [[ -d "${PWD}" && "${PWD}" -ef . ]] + if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then + [[ "${PWD}" -ef . ]] else - builtin pwd -P >/dev/null 2>&1 + # Why compare the path: shell builtins can print a cached path for a deleted cwd. + __orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1 + [[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]] fi } __orca_omp_invoke() { @@ -61,7 +64,7 @@ __orca_omp_invoke() { __orca_omp() { local __orca_use_extension=1 __orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0 - if [[ $__orca_use_extension -eq 1 ]] && ! __orca_omp_cwd_is_usable; then + if ! __orca_omp_cwd_is_usable; then local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}" # Why: a restored shell can retain the deleted directory inode after its path is recreated. ( diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt b/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt index 335b54c60c3..394bc4a6d10 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/relay-zsh-zshenv.txt @@ -57,11 +57,14 @@ __orca_deferred_init() { return 1 } __orca_omp_cwd_is_usable() { + local __orca_physical_cwd [[ -x . ]] || return 1 - if [[ -n "${PWD:-}" ]]; then - [[ -d "${PWD}" && "${PWD}" -ef . ]] + if [[ -n "${PWD:-}" && -d "${PWD:-}" ]]; then + [[ "${PWD}" -ef . ]] else - builtin pwd -P >/dev/null 2>&1 + # Why compare the path: shell builtins can print a cached path for a deleted cwd. + __orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1 + [[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]] fi } __orca_omp_invoke() { @@ -81,7 +84,7 @@ __orca_deferred_init() { __orca_omp() { local __orca_use_extension=1 __orca_omp_should_skip_extension "${1:-}" && __orca_use_extension=0 - if [[ $__orca_use_extension -eq 1 ]] && ! __orca_omp_cwd_is_usable; then + if ! __orca_omp_cwd_is_usable; then local __orca_logical_cwd="${PWD:-${ORCA_WORKTREE_PATH:-${ORCA_ROOT_PATH:-}}}" # Why: a restored shell can retain the deleted directory inode after its path is recreated. ( diff --git a/src/main/pty/omp-shell-wrapper.node-pty.test.ts b/src/main/pty/omp-shell-wrapper.node-pty.test.ts index 6ab30dfdbae..15bc98bdb3b 100644 --- a/src/main/pty/omp-shell-wrapper.node-pty.test.ts +++ b/src/main/pty/omp-shell-wrapper.node-pty.test.ts @@ -1,11 +1,13 @@ import { spawnSync } from 'node:child_process' import { chmodSync, + existsSync, mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, + symlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' @@ -334,18 +336,82 @@ exit 0 const homeDir = join(tempDir, 'home') const binDir = join(tempDir, 'bin') const extensionDir = join(tempDir, 'extensions') + const logicalProjectLink = join(tempDir, 'logical-project') mkdirSync(projectDir, { recursive: true }) mkdirSync(homeDir) mkdirSync(binDir) mkdirSync(extensionDir) + symlinkSync(projectDir, logicalProjectLink) const expectedProjectDir = realpathSync(projectDir) + const expectedWorkspaceDir = realpathSync(workspaceDir) const statusExtension = join(extensionDir, 'orca-agent-status.ts') writeFileSync(statusExtension, 'export default {}') writeFakeOmp(binDir) const unsetPwdCaptureFile = join(tempDir, 'unset-pwd-capture') + const deletedLinkCaptureFile = join(tempDir, 'deleted-link-capture') const staleCaptureFile = join(tempDir, 'stale-cwd-capture') + const skipCaptureFile = join(tempDir, 'skip-capture') + const staleUnsetCaptureFile = join(tempDir, 'stale-unset-capture') + const noLogicalCaptureFile = join(tempDir, 'no-logical-capture') const resultFile = join(tempDir, 'stale-cwd-result') + const scenarioFile = join(tempDir, 'stale-cwd-scenario') + writeFileSync( + scenarioFile, + `ORCA_CAPTURE_FILE="$ORCA_UNSET_PWD_CAPTURE_FILE" +unset PWD +omp +__orca_test_unset_status=$? +if [[ -z "\${PWD+x}" ]]; then + __orca_test_pwd_state=unset +else + __orca_test_pwd_state=set +fi +builtin cd -- "$ORCA_LOGICAL_PROJECT_LINK" +/bin/rm -- "$ORCA_LOGICAL_PROJECT_LINK" +ORCA_CAPTURE_FILE="$ORCA_DELETED_LINK_CAPTURE_FILE" +omp +__orca_test_deleted_link_status=$? +builtin cd -P -- "$ORCA_STALE_PROJECT_DIR" +ORCA_CAPTURE_FILE="$ORCA_STALE_CAPTURE_FILE" +/bin/rm -rf -- "$ORCA_STALE_PROJECT_DIR" +/bin/mkdir -p -- "$ORCA_STALE_PROJECT_DIR" +omp +__orca_test_first_status=$? +ORCA_CAPTURE_FILE="$ORCA_SKIP_CAPTURE_FILE" +omp --version +__orca_test_skip_status=$? +if [[ "$PWD" -ef . ]]; then + __orca_test_parent_state=live +else + __orca_test_parent_state=stale +fi +unset PWD +ORCA_CAPTURE_FILE="$ORCA_STALE_UNSET_CAPTURE_FILE" +omp +__orca_test_stale_unset_status=$? +unset ORCA_WORKTREE_PATH ORCA_ROOT_PATH +ORCA_CAPTURE_FILE="$ORCA_NO_LOGICAL_CAPTURE_FILE" +omp +__orca_test_no_logical_status=$? +PWD="$ORCA_STALE_PROJECT_DIR" +/bin/rm -rf -- "$ORCA_STALE_PROJECT_DIR" +omp +__orca_test_missing_status=$? +{ + echo "UNSET=$__orca_test_unset_status" + echo "PWD=$__orca_test_pwd_state" + echo "LINK=$__orca_test_deleted_link_status" + echo "FIRST=$__orca_test_first_status" + echo "SKIP=$__orca_test_skip_status" + echo "PARENT=$__orca_test_parent_state" + echo "STALE_UNSET=$__orca_test_stale_unset_status" + echo "NO_LOGICAL=$__orca_test_no_logical_status" + echo "MISSING=$__orca_test_missing_status" +} > "$ORCA_RESULT_FILE" +exit 0 +` + ) const output = await runInteractivePosixPty({ shell, cwd: projectDir, @@ -355,47 +421,30 @@ ${getPosixOmpShellWrapper()}`, INPUTRC: '/dev/null', PROMPT_COMMAND: '', ORCA_STALE_PROJECT_DIR: projectDir, + ORCA_LOGICAL_PROJECT_LINK: logicalProjectLink, ORCA_UNSET_PWD_CAPTURE_FILE: unsetPwdCaptureFile, + ORCA_DELETED_LINK_CAPTURE_FILE: deletedLinkCaptureFile, ORCA_STALE_CAPTURE_FILE: staleCaptureFile, + ORCA_SKIP_CAPTURE_FILE: skipCaptureFile, + ORCA_STALE_UNSET_CAPTURE_FILE: staleUnsetCaptureFile, + ORCA_NO_LOGICAL_CAPTURE_FILE: noLogicalCaptureFile, ORCA_WORKTREE_PATH: workspaceDir, HOME: homeDir, PATH: `${binDir}:/usr/bin:/bin:/usr/sbin:/sbin`, ORCA_OMP_STATUS_EXTENSION: statusExtension, ORCA_CAPTURE_FILE: staleCaptureFile, ORCA_RESULT_FILE: resultFile, + ORCA_SCENARIO_FILE: scenarioFile, ORCA_TEST_FAKE_OMP_EXIT_CODE: '23', TERM: 'xterm-256color' }, - input: `ORCA_CAPTURE_FILE="$ORCA_UNSET_PWD_CAPTURE_FILE" -unset PWD -omp -__orca_test_unset_status=$? -if [[ -z "\${PWD+x}" ]]; then - __orca_test_pwd_state=unset -else - __orca_test_pwd_state=set -fi -builtin cd -P -- "$ORCA_STALE_PROJECT_DIR" -ORCA_CAPTURE_FILE="$ORCA_STALE_CAPTURE_FILE" -/bin/rm -rf -- "$ORCA_STALE_PROJECT_DIR" -/bin/mkdir -p -- "$ORCA_STALE_PROJECT_DIR" -omp -__orca_test_first_status=$? -if [[ "$PWD" -ef . ]]; then - __orca_test_parent_state=live -else - __orca_test_parent_state=stale -fi -/bin/rm -rf -- "$ORCA_STALE_PROJECT_DIR" -omp -__orca_test_missing_status=$? -printf 'UNSET=%s\nPWD=%s\nFIRST=%s\nPARENT=%s\nMISSING=%s\n' "$__orca_test_unset_status" "$__orca_test_pwd_state" "$__orca_test_first_status" "$__orca_test_parent_state" "$__orca_test_missing_status" > "$ORCA_RESULT_FILE" -exit 0 -` + input: 'source "$ORCA_SCENARIO_FILE"\n' }) const unsetPwdCapture = readFileSync(unsetPwdCaptureFile, 'utf8') expect(unsetPwdCapture).toContain(`CWD=${expectedProjectDir}`) + const deletedLinkCapture = readFileSync(deletedLinkCaptureFile, 'utf8') + expect(deletedLinkCapture).toContain(`CWD=${expectedProjectDir}`) const staleCapture = readFileSync(staleCaptureFile, 'utf8') expect(staleCapture).toContain(`CWD=${expectedProjectDir}`) expect(staleCapture.split('\n').filter((line) => line.startsWith('ARG'))).toEqual([ @@ -403,9 +452,17 @@ exit 0 `ARG2=${statusExtension}` ]) expect(staleCapture).not.toContain('--cwd') + const skipCapture = readFileSync(skipCaptureFile, 'utf8') + expect(skipCapture).toContain(`CWD=${expectedProjectDir}`) + expect(skipCapture).toContain('ARG1=--version') + expect(skipCapture).not.toContain('--extension') + const staleUnsetCapture = readFileSync(staleUnsetCaptureFile, 'utf8') + expect(staleUnsetCapture).toContain(`CWD=${expectedWorkspaceDir}`) + expect(existsSync(noLogicalCaptureFile)).toBe(false) expect(readFileSync(resultFile, 'utf8')).toBe( - 'UNSET=23\nPWD=unset\nFIRST=23\nPARENT=stale\nMISSING=1\n' + 'UNSET=23\nPWD=unset\nLINK=23\nFIRST=23\nSKIP=23\nPARENT=stale\nSTALE_UNSET=23\nNO_LOGICAL=1\nMISSING=1\n' ) + expect(output).toContain('no terminal working directory is available') expect(output).toContain('Orca: OMP cannot access the terminal working directory') } diff --git a/src/main/pty/omp-shell-wrapper.ts b/src/main/pty/omp-shell-wrapper.ts index fb6d274dc57..f5bc25421bb 100644 --- a/src/main/pty/omp-shell-wrapper.ts +++ b/src/main/pty/omp-shell-wrapper.ts @@ -52,11 +52,14 @@ __orca_omp_should_skip_extension() { return 1 } __orca_omp_cwd_is_usable() { + local __orca_physical_cwd [[ -x . ]] || return 1 - if [[ -n "\${PWD:-}" ]]; then - [[ -d "\${PWD}" && "\${PWD}" -ef . ]] + if [[ -n "\${PWD:-}" && -d "\${PWD:-}" ]]; then + [[ "\${PWD}" -ef . ]] else - builtin pwd -P >/dev/null 2>&1 + # Why compare the path: shell builtins can print a cached path for a deleted cwd. + __orca_physical_cwd="$(builtin pwd -P 2>/dev/null)" || return 1 + [[ -d "$__orca_physical_cwd" && "$__orca_physical_cwd" -ef . ]] fi } __orca_omp_invoke() { @@ -76,7 +79,7 @@ __orca_omp_invoke() { __orca_omp() { local __orca_use_extension=1 __orca_omp_should_skip_extension "\${1:-}" && __orca_use_extension=0 - if [[ $__orca_use_extension -eq 1 ]] && ! __orca_omp_cwd_is_usable; then + if ! __orca_omp_cwd_is_usable; then local __orca_logical_cwd="\${PWD:-\${ORCA_WORKTREE_PATH:-\${ORCA_ROOT_PATH:-}}}" # Why: a restored shell can retain the deleted directory inode after its path is recreated. ( From e3757a1d30224e5686974fd491c82aab0cc64395 Mon Sep 17 00:00:00 2001 From: JahyunBaek <60498711+JahyunBaek@users.noreply.github.com> Date: Sun, 30 Aug 2026 06:36:24 +0900 Subject: [PATCH 13/80] fix(worktree): ignore orphan remote-tracking refs in branch conflicts (#16699) Refs #16646 Unify native, WSL, and direct SSH conflict checks behind the execution host, remove the duplicated SSH classifier, and cover orphan/configured remote behavior across both paths. --- src/main/git/repo-base-ref-search.ts | 23 ++++- src/main/git/repo-branch-conflict.ts | 63 +++++++----- src/main/git/repo.test.ts | 37 +++++++ ...orktree-remote-ssh-branch-conflict.test.ts | 75 +++++++++++++++ src/main/ipc/worktree-remote.ts | 96 ++----------------- 5 files changed, 181 insertions(+), 113 deletions(-) create mode 100644 src/main/ipc/worktree-remote-ssh-branch-conflict.test.ts diff --git a/src/main/git/repo-base-ref-search.ts b/src/main/git/repo-base-ref-search.ts index 5ea01a7ec1c..213868fe5a7 100644 --- a/src/main/git/repo-base-ref-search.ts +++ b/src/main/git/repo-base-ref-search.ts @@ -218,6 +218,21 @@ export function parseAndFilterSearchRefDetails( .slice(0, Math.max(0, limit)) } +export function resolveConfiguredRemoteBranchName( + fullRef: string, + longestFirstRemoteNames: readonly string[] +): string | null { + const remoteRefPrefix = 'refs/remotes/' + if (!fullRef.startsWith(remoteRefPrefix)) { + return null + } + const remoteAndBranch = fullRef.slice(remoteRefPrefix.length) + const remote = longestFirstRemoteNames.find((candidate) => + remoteAndBranch.startsWith(`${candidate}/`) + ) + return remote ? remoteAndBranch.slice(remote.length + 1) || null : null +} + export function resolveLocalBranchName( fullRef: string, shortRef: string, @@ -227,11 +242,11 @@ export function resolveLocalBranchName( if (!fullRef.startsWith(remoteRefPrefix)) { return shortRef } - const remoteAndBranch = fullRef.slice(remoteRefPrefix.length) - const remote = remotes.find((candidate) => remoteAndBranch.startsWith(`${candidate}/`)) - if (remote) { - return remoteAndBranch.slice(remote.length + 1) + const configuredBranch = resolveConfiguredRemoteBranchName(fullRef, remotes) + if (configuredBranch) { + return configuredBranch } + const remoteAndBranch = fullRef.slice(remoteRefPrefix.length) return remoteAndBranch.split('/').slice(1).join('/') || shortRef } diff --git a/src/main/git/repo-branch-conflict.ts b/src/main/git/repo-branch-conflict.ts index dbadf254d11..fcb6d8dc101 100644 --- a/src/main/git/repo-branch-conflict.ts +++ b/src/main/git/repo-branch-conflict.ts @@ -1,45 +1,50 @@ -import { listRemoteNames, resolveLocalBranchName } from './repo-base-ref-search' -import { gitExecOptions, type LocalGitExecOptions } from './repo-default-base-ref' +import { resolveConfiguredRemoteBranchName } from './repo-base-ref-search' +import { gitExecOptions, type GitExec, type LocalGitExecOptions } from './repo-default-base-ref' import { gitExecFileAsync } from './runner' export type BranchConflictKind = 'local' | 'remote' -async function hasGitRefAsync( - path: string, - ref: string, - options: LocalGitExecOptions = {} -): Promise { +async function hasGitRefAsync(exec: GitExec, ref: string): Promise { try { - await gitExecFileAsync(['rev-parse', '--verify', ref], gitExecOptions(path, options)) - return true + const { stdout } = await exec(['rev-parse', '--verify', ref]) + return stdout.trim().length > 0 } catch { return false } } -export async function getBranchConflictKind( - path: string, +async function listRemoteNamesViaExec(exec: GitExec): Promise { + try { + const { stdout } = await exec(['remote']) + return stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .sort((a, b) => b.length - a.length) + } catch { + return [] + } +} + +/** Run branch-conflict policy through the host that owns Git execution. */ +export async function getBranchConflictKindViaExec( + exec: GitExec, branchName: string, - allowedBaseRef?: string, - options: LocalGitExecOptions = {} + allowedBaseRef?: string ): Promise { - if (await hasGitRefAsync(path, `refs/heads/${branchName}`, options)) { + if (await hasGitRefAsync(exec, `refs/heads/${branchName}`)) { return 'local' } try { - const remoteNames = (await listRemoteNames(path, options)).sort((a, b) => b.length - a.length) - const { stdout } = await gitExecFileAsync( - ['for-each-ref', '--format=%(refname)', 'refs/remotes'], - gitExecOptions(path, options) - ) - const hasRemoteConflict = stdout.split('\n').some((ref) => { + const remoteNames = await listRemoteNamesViaExec(exec) + const { stdout } = await exec(['for-each-ref', '--format=%(refname)', 'refs/remotes']) + const hasRemoteConflict = stdout.split(/\r?\n/).some((ref) => { const trimmed = ref.trim() if (isAllowedRemoteBaseRef(trimmed, allowedBaseRef)) { return false } - const shortRef = trimmed.replace(/^refs\/remotes\//, '') - return resolveLocalBranchName(trimmed, shortRef, remoteNames) === branchName + return resolveConfiguredRemoteBranchName(trimmed, remoteNames) === branchName }) return hasRemoteConflict ? 'remote' : null @@ -48,6 +53,20 @@ export async function getBranchConflictKind( } } +export function getBranchConflictKind( + path: string, + branchName: string, + allowedBaseRef?: string, + options: LocalGitExecOptions = {} +): Promise { + const execOptions = gitExecOptions(path, options) + return getBranchConflictKindViaExec( + (argv) => gitExecFileAsync(argv, execOptions), + branchName, + allowedBaseRef + ) +} + function isAllowedRemoteBaseRef(refName: string, allowedBaseRef: string | undefined): boolean { if (!allowedBaseRef) { return false diff --git a/src/main/git/repo.test.ts b/src/main/git/repo.test.ts index 90199d1c69b..7500735d72f 100644 --- a/src/main/git/repo.test.ts +++ b/src/main/git/repo.test.ts @@ -215,6 +215,7 @@ describe('searchBaseRefs (widened glob)', () => { it('allows creating a local branch from the selected matching remote base ref', async () => { const sha = getHeadSha(tmpDir) + git(tmpDir, ['remote', 'add', 'origin', 'https://example.invalid/repo.git']) createRemoteRef(tmpDir, 'origin/feature/something', sha) const result = await getBranchConflictKind( @@ -228,6 +229,10 @@ describe('searchBaseRefs (widened glob)', () => { it('still reports a remote conflict for a different tracking ref with the same branch name', async () => { const sha = getHeadSha(tmpDir) + // Why register both: a ref only tracks a branch when a remote actually owns its + // prefix, and this case is about a second *tracking* ref, not an orphan. + git(tmpDir, ['remote', 'add', 'origin', 'https://example.invalid/repo.git']) + git(tmpDir, ['remote', 'add', 'upstream', 'https://example.invalid/upstream.git']) createRemoteRef(tmpDir, 'origin/feature/something', sha) createRemoteRef(tmpDir, 'upstream/feature/something', sha) @@ -236,6 +241,38 @@ describe('searchBaseRefs (widened glob)', () => { ).toBe('remote') }) + it('ignores a ref whose prefix matches no configured remote', async () => { + const sha = getHeadSha(tmpDir) + git(tmpDir, ['remote', 'add', 'origin', 'https://example.invalid/repo.git']) + createRemoteRef(tmpDir, 'mimic-fork/my-task', sha) + + expect(await getBranchConflictKind(tmpDir, 'my-task', 'origin/main')).toBeNull() + }) + + it('stops treating leftover refs from a removed remote as conflicts', async () => { + const sha = getHeadSha(tmpDir) + git(tmpDir, ['remote', 'add', 'origin', 'https://example.invalid/repo.git']) + git(tmpDir, ['remote', 'add', 'fork', 'https://example.invalid/fork.git']) + createRemoteRef(tmpDir, 'fork/my-task', sha) + expect(await getBranchConflictKind(tmpDir, 'my-task', 'origin/main')).toBe('remote') + + // Why not `remote remove`: that prunes the tracking refs too, which would pass + // with or without the ownership guard. Dropping only the config leaves the + // orphan ref behind, which is the state this guard exists for. + git(tmpDir, ['config', '--remove-section', 'remote.fork']) + + expect(await getBranchConflictKind(tmpDir, 'my-task', 'origin/main')).toBeNull() + }) + + it('still reports a local conflict when an unrelated ref shares the name', async () => { + const sha = getHeadSha(tmpDir) + git(tmpDir, ['remote', 'add', 'origin', 'https://example.invalid/repo.git']) + createRemoteRef(tmpDir, 'mimic-fork/my-task', sha) + git(tmpDir, ['branch', 'my-task']) + + expect(await getBranchConflictKind(tmpDir, 'my-task', 'origin/main')).toBe('local') + }) + it('reports remote conflicts when the remote name contains a slash', async () => { const sha = getHeadSha(tmpDir) git(tmpDir, ['remote', 'add', 'foo/bar', 'https://example.invalid/repo.git']) diff --git a/src/main/ipc/worktree-remote-ssh-branch-conflict.test.ts b/src/main/ipc/worktree-remote-ssh-branch-conflict.test.ts new file mode 100644 index 00000000000..7ced18eb173 --- /dev/null +++ b/src/main/ipc/worktree-remote-ssh-branch-conflict.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it, vi } from 'vitest' + +import { getSshBranchConflictKind } from './worktree-remote' + +type ConflictProvider = Parameters[0] + +function providerAnswering(answers: { + remotes: string[] + localBranchHead?: string + remoteRefs: string[] +}): ConflictProvider { + const exec: ConflictProvider['exec'] = vi.fn(async (args: string[]) => { + if (args[0] === 'remote') { + return { stdout: `${answers.remotes.join('\n')}\n`, stderr: '' } + } + if (args[0] === 'rev-parse') { + if (!answers.localBranchHead) { + throw new Error('missing ref') + } + return { stdout: `${answers.localBranchHead}\n`, stderr: '' } + } + if (args[0] === 'for-each-ref') { + return { stdout: `${answers.remoteRefs.join('\n')}\n`, stderr: '' } + } + throw new Error(`unexpected git ${args.join(' ')}`) + }) + return { exec } +} + +describe('getSshBranchConflictKind remote ownership', () => { + it('reports a local branch before scanning remote refs', async () => { + const provider = providerAnswering({ + localBranchHead: 'abc123', + remotes: ['origin'], + remoteRefs: [] + }) + + await expect( + getSshBranchConflictKind(provider, '/srv/repo', 'my-task', 'origin/main') + ).resolves.toBe('local') + }) + + it('ignores a ref whose prefix matches no configured remote', async () => { + const provider = providerAnswering({ + remotes: ['origin'], + remoteRefs: ['refs/remotes/origin/main', 'refs/remotes/deleted-fork/my-task'] + }) + + await expect( + getSshBranchConflictKind(provider, '/srv/repo', 'my-task', 'origin/main') + ).resolves.toBeNull() + }) + + it('still reports a ref owned by a configured remote', async () => { + const provider = providerAnswering({ + remotes: ['origin'], + remoteRefs: ['refs/remotes/origin/main', 'refs/remotes/origin/my-task'] + }) + + await expect( + getSshBranchConflictKind(provider, '/srv/repo', 'my-task', 'origin/main') + ).resolves.toBe('remote') + }) + + it('prefers the longest configured remote name', async () => { + const provider = providerAnswering({ + remotes: ['foo', 'foo/bar'], + remoteRefs: ['refs/remotes/foo/bar/my-task'] + }) + + await expect( + getSshBranchConflictKind(provider, '/srv/repo', 'my-task', 'origin/main') + ).resolves.toBe('remote') + }) +}) diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index 581872540df..d9908b7c05c 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -34,6 +34,7 @@ import { resolveDefaultBaseRefViaExec, resolveDefaultBaseRefWithLocalGit } from '../git/repo' +import { getBranchConflictKindViaExec } from '../git/repo-branch-conflict' import { resolveLocalGitUsername, getSshGitUsername } from '../git/git-username' import { hasCommitObjectViaGitExec } from '../git/commit-object-ref' import { probeWorktreeBaseRefPresence } from '../git/worktree-base-ref-probe' @@ -839,98 +840,19 @@ async function canCheckoutExistingLocalBranchSsh( return !worktrees.some((worktree) => normalizeLocalBranchName(worktree.branch) === branchName) } -async function listSshRemoteNames(provider: SshGitProvider, repoPath: string): Promise { - try { - const { stdout } = await provider.exec(['remote'], repoPath) - return stdout - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean) - .sort((a, b) => b.length - a.length) - } catch { - return [] - } -} +type SshGitExecutor = Pick -function isAllowedSshRemoteBaseRef(refName: string, allowedBaseRef: string): boolean { - if (!allowedBaseRef) { - return false - } - const normalizedAllowedRef = allowedBaseRef.startsWith('refs/remotes/') - ? allowedBaseRef - : `refs/remotes/${allowedBaseRef}` - return refName === normalizedAllowedRef -} - -function resolveSshRemoteBranchName(refName: string, remoteNames: string[]): string { - const remotePrefix = 'refs/remotes/' - if (!refName.startsWith(remotePrefix)) { - return refName - } - const remoteAndBranch = refName.slice(remotePrefix.length) - const remote = remoteNames.find((candidate) => remoteAndBranch.startsWith(`${candidate}/`)) - if (remote) { - return remoteAndBranch.slice(remote.length + 1) - } - return remoteAndBranch.split('/').slice(1).join('/') || remoteAndBranch -} - -async function hasSshRemoteBranchConflict( - provider: SshGitProvider, - repoPath: string, - branchName: string, - allowedBaseRef: string -): Promise { - const remoteNames = await listSshRemoteNames(provider, repoPath) - try { - const { stdout } = await provider.exec( - ['for-each-ref', '--format=%(refname)', 'refs/remotes'], - repoPath - ) - return stdout.split(/\r?\n/).some((line) => { - const refName = line.trim() - if (!refName || /^refs\/remotes\/.+\/HEAD$/.test(refName)) { - return false - } - if (isAllowedSshRemoteBaseRef(refName, allowedBaseRef)) { - return false - } - // Why: `git branch --all --list feature/x` doesn't match `remotes/origin/feature/x`; parse remote refs directly. - return resolveSshRemoteBranchName(refName, remoteNames) === branchName - }) - } catch { - return false - } -} - -async function hasSshLocalBranchConflict( - provider: SshGitProvider, - repoPath: string, - branchName: string -): Promise { - try { - const { stdout } = await provider.exec( - ['rev-parse', '--verify', '--quiet', `refs/heads/${branchName}^{commit}`], - repoPath - ) - return stdout.trim().length > 0 - } catch { - return false - } -} - -async function getSshBranchConflictKind( - provider: SshGitProvider, +export function getSshBranchConflictKind( + provider: SshGitExecutor, repoPath: string, branchName: string, allowedBaseRef: string ): Promise<'local' | 'remote' | null> { - if (await hasSshLocalBranchConflict(provider, repoPath, branchName)) { - return 'local' - } - return (await hasSshRemoteBranchConflict(provider, repoPath, branchName, allowedBaseRef)) - ? 'remote' - : null + return getBranchConflictKindViaExec( + (argv) => provider.exec(argv, repoPath), + branchName, + allowedBaseRef + ) } type SelectedReviewBranchInput = Pick< From b826b3fa5b7b3a3d28babd86adc5dba2568bf2f8 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:03:36 -0700 Subject: [PATCH 14/80] feat(editor): raise rich Markdown size limit to 600 KB (#17288) --- .../editor/markdown-rich-size-limit.test.ts | 4 ++++ src/shared/constants.ts | 17 +++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/components/editor/markdown-rich-size-limit.test.ts b/src/renderer/src/components/editor/markdown-rich-size-limit.test.ts index 616e298b175..3539ffc565f 100644 --- a/src/renderer/src/components/editor/markdown-rich-size-limit.test.ts +++ b/src/renderer/src/components/editor/markdown-rich-size-limit.test.ts @@ -3,6 +3,10 @@ import { RICH_MARKDOWN_MAX_SIZE_BYTES } from '../../../../shared/constants' import { exceedsMarkdownRichModeSizeLimit } from './markdown-rich-size-limit' describe('exceedsMarkdownRichModeSizeLimit', () => { + it('uses a 600 KB default rich-mode ceiling', () => { + expect(RICH_MARKDOWN_MAX_SIZE_BYTES).toBe(600 * 1024) + }) + it('allows markdown at the rich-mode byte limit', () => { expect(exceedsMarkdownRichModeSizeLimit('a'.repeat(RICH_MARKDOWN_MAX_SIZE_BYTES))).toBe(false) }) diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 15c47bdd968..96c7901cc08 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -83,16 +83,13 @@ export const getDefaultTerminalRightClickToPaste = ( platform = typeof process !== 'undefined' ? process.platform : '' ): boolean => platform === 'win32' -/** Why: ProseMirror renders the whole document — no virtualization — so opening - * one blocks the main thread for as long as it takes to build the DOM. Measured - * in a packaged build (M-series) on a doc with a code block every ~570 bytes, - * blocking mount / median keystroke: 300 KB 1.7 s / 59 ms · 600 KB 4.2 s / 201 ms - * · 900 KB 8.8 s / 431 ms. The same 300 KB as pure prose is 0.8 s / 16 ms, so - * node-view count drives the cost far more than byte size — but bytes are the - * only thing cheap enough to test before parsing. The blocking mount is what - * pins this ceiling; past it, fall back to source mode (Monaco) with a per-file - * "Open anyway" escape hatch. Real headroom needs #7056. */ -export const RICH_MARKDOWN_MAX_SIZE_BYTES = 300 * 1024 +/** Why: ProseMirror renders the whole document without virtualization. After the + * parser/highlighter work in #17134/#17147/#17158, M-series Electron measurements + * on distinct code blocks every ~600 bytes put visible mount / longest task at + * 300 KB 0.70 / 0.66 s · 450 KB 1.09 / 1.02 s · 600 KB 1.49 / 1.41 s, with + * 600 KB typing at 38 ms median / 39 ms p95. Bytes remain the only cheap + * pre-parse guard; larger files use source mode with an "Open anyway" escape hatch. */ +export const RICH_MARKDOWN_MAX_SIZE_BYTES = 600 * 1024 export const DEFAULT_EDITOR_AUTO_SAVE_DELAY_MS = 1000 export const MIN_EDITOR_AUTO_SAVE_DELAY_MS = 250 From 8b01cf1a48d91cf28a87451cb5412d5c7626e46c Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:07:38 -0700 Subject: [PATCH 15/80] Clarify automation history unavailability and improve recovery UX (#17284) * Clarify automation history unavailability and improve recovery UX - Improve error message to explain run history is unavailable due to host version requirements, not automation failure - Hide misleading "0 runs" count badge when history is unavailable - Deep-link "update server" recovery actions to specific runtime environment in settings instead of pane root - Add test coverage for run count hiding and recovery targeting * Watch for deep-linked settings targets that render asynchronously Some settings panes (such as Remote Orca Servers) fetch and render their rows asynchronously. Deep links can name targets that don't exist yet, but the scroll effect has no way to know when they finally mount. Add a MutationObserver-based watcher to detect when async rows appear and trigger scrolling. --- .../automations/AutomationRunHistory.test.tsx | 3 +- .../automations/AutomationRunHistory.tsx | 2 +- .../AutomationsDetailPane.run-count.test.tsx | 88 ++++++++++++++ .../automations/AutomationsDetailPane.tsx | 4 +- .../AutomationsPage.notice-recovery.test.tsx | 13 ++- .../automation-captured-owner.test.ts | 6 + .../automations/automation-captured-owner.ts | 2 +- .../automation-host-recovery.test.ts | 14 ++- .../automations/automation-host-recovery.ts | 8 +- .../use-automation-host-catalog.ts | 2 +- .../src/components/settings/Settings.tsx | 22 ++++ .../settings-deep-link-target-watcher.test.ts | 108 ++++++++++++++++++ .../settings-deep-link-target-watcher.ts | 54 +++++++++ src/renderer/src/i18n/locales/en.json | 4 +- 14 files changed, 315 insertions(+), 15 deletions(-) create mode 100644 src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx create mode 100644 src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts create mode 100644 src/renderer/src/components/settings/settings-deep-link-target-watcher.ts diff --git a/src/renderer/src/components/automations/AutomationRunHistory.test.tsx b/src/renderer/src/components/automations/AutomationRunHistory.test.tsx index 7742f74b26a..41d839e615a 100644 --- a/src/renderer/src/components/automations/AutomationRunHistory.test.tsx +++ b/src/renderer/src/components/automations/AutomationRunHistory.test.tsx @@ -113,7 +113,8 @@ describe('AutomationRunHistory unanswered history', () => { expect(container.textContent).not.toContain('No runs yet.') // "0 runs" is a count of something nobody managed to read. expect(container.textContent).not.toContain('0 runs') - expect(container.textContent).toContain('Run history could not be loaded') + expect(container.textContent).toContain('Run history is unavailable from this host') + expect(container.textContent).toContain('does not mean the automation failed or has no runs') expect(container.textContent).toContain('web-01 is not connected') }) diff --git a/src/renderer/src/components/automations/AutomationRunHistory.tsx b/src/renderer/src/components/automations/AutomationRunHistory.tsx index cd2ece0d7dc..13395dae6ad 100644 --- a/src/renderer/src/components/automations/AutomationRunHistory.tsx +++ b/src/renderer/src/components/automations/AutomationRunHistory.tsx @@ -167,7 +167,7 @@ export function AutomationRunHistory({

{translate( 'auto.components.automations.AutomationRunHistory.historyUnavailable', - 'Run history could not be loaded for this automation.' + 'Run history is unavailable from this host. This does not mean the automation failed or has no runs.' )}

diff --git a/src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx b/src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx new file mode 100644 index 00000000000..740f1130405 --- /dev/null +++ b/src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx @@ -0,0 +1,88 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { AutomationsDetailPane } from './AutomationsDetailPane' +import { makeAutomation } from './automations-page-fixtures' + +let root: Root | null = null + +beforeEach(() => { + globalThis.IS_REACT_ACT_ENVIRONMENT = true +}) + +afterEach(async () => { + await act(async () => root?.unmount()) + root = null + document.body.innerHTML = '' +}) + +async function renderRunsTab(historyUnavailable: boolean): Promise { + const container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + await act(async () => { + root?.render( + + ) + }) + const runsTab = [...container.querySelectorAll('[role="tab"]')].find((tab) => + tab.textContent?.includes('Runs') + ) + if (!runsTab) { + throw new Error('Runs tab was not rendered') + } + return runsTab +} + +describe('AutomationsDetailPane run count', () => { + it('does not report zero when run history is unavailable', async () => { + expect((await renderRunsTab(true)).textContent?.trim()).toBe('Runs') + }) + + it('reports zero when the host answered with an empty history', async () => { + expect((await renderRunsTab(false)).textContent).toContain('0') + }) +}) diff --git a/src/renderer/src/components/automations/AutomationsDetailPane.tsx b/src/renderer/src/components/automations/AutomationsDetailPane.tsx index 7eb2ff13c1d..0affe311f2c 100644 --- a/src/renderer/src/components/automations/AutomationsDetailPane.tsx +++ b/src/renderer/src/components/automations/AutomationsDetailPane.tsx @@ -213,7 +213,9 @@ export function AutomationsDetailPane({ {translate('auto.components.automations.AutomationsPage.0e110a3469', 'Runs')}{' '} - {selectedRuns.length} + {selectedRunsNotice ? null : ( + {selectedRuns.length} + )}
diff --git a/src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx b/src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx index 503d0877ed5..d99cfb91dac 100644 --- a/src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx +++ b/src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx @@ -19,6 +19,7 @@ import { mocks, renderPage, runtimeHost, + RUNTIME_ID, RUNTIME_REPO_ID, RUNTIME_SELF_FILTER, RUNTIME_WORKSPACE_ID, @@ -88,7 +89,11 @@ describe('AutomationsPage notice recovery', () => { }) expect(settings.open).toHaveBeenCalled() - expect(settings.target).toHaveBeenCalledWith({ pane: 'servers', repoId: null }) + expect(settings.target).toHaveBeenCalledWith({ + pane: 'servers', + repoId: null, + sectionId: RUNTIME_ID + }) }) it('sends a row Update server to the row own host, not to the unfiltered list', async () => { @@ -116,6 +121,10 @@ describe('AutomationsPage notice recovery', () => { // All hosts selects no host, so only the row's captured owner names one. expect(settings.open).toHaveBeenCalled() - expect(settings.target).toHaveBeenCalledWith({ pane: 'servers', repoId: null }) + expect(settings.target).toHaveBeenCalledWith({ + pane: 'servers', + repoId: null, + sectionId: RUNTIME_ID + }) }) }) diff --git a/src/renderer/src/components/automations/automation-captured-owner.test.ts b/src/renderer/src/components/automations/automation-captured-owner.test.ts index ffc998a5b36..ca22b0286e0 100644 --- a/src/renderer/src/components/automations/automation-captured-owner.test.ts +++ b/src/renderer/src/components/automations/automation-captured-owner.test.ts @@ -92,6 +92,12 @@ describe('captured owner availability', () => { const availability = automationActionAvailability(legacy, 'delete') expect(availability.kind).toBe('blocked') expect(availability.kind === 'blocked' && availability.block.recovery).toBe('update-server') + expect(availability.kind === 'blocked' && availability.block.message).toContain( + 'needs an update' + ) + expect(availability.kind === 'blocked' && availability.block.message).toContain( + 'Scheduled runs may still continue' + ) }) it('reports a row with no metadata as uncaptured rather than blocked', () => { diff --git a/src/renderer/src/components/automations/automation-captured-owner.ts b/src/renderer/src/components/automations/automation-captured-owner.ts index 761dd787e78..7a95b517fe5 100644 --- a/src/renderer/src/components/automations/automation-captured-owner.ts +++ b/src/renderer/src/components/automations/automation-captured-owner.ts @@ -71,7 +71,7 @@ function unfencedBlock(): AutomationActionBlock { reason: 'unfenced', message: translate( 'auto.components.automations.capturedOwner.unfenced', - 'This host cannot confirm which automation it is changing, so this automation is read-only here.' + 'Orca on this host needs an update before you can view run history or manage this automation. Scheduled runs may still continue on the host.' ), recovery: 'update-server' } diff --git a/src/renderer/src/components/automations/automation-host-recovery.test.ts b/src/renderer/src/components/automations/automation-host-recovery.test.ts index 8d6419fb128..871f9343114 100644 --- a/src/renderer/src/components/automations/automation-host-recovery.test.ts +++ b/src/renderer/src/components/automations/automation-host-recovery.test.ts @@ -35,9 +35,10 @@ const DESKTOP_SSH = entry({ stableRef: { authority: { kind: 'desktop' }, selector: { kind: 'ssh', targetId: 't1' } }, kind: 'ssh' }) +const RUNTIME_ENVIRONMENT_ID = 'gpu' const RUNTIME_SSH = entry({ stableRef: { - authority: { kind: 'runtime', environmentId: 'gpu' }, + authority: { kind: 'runtime', environmentId: RUNTIME_ENVIRONMENT_ID }, selector: { kind: 'ssh', targetId: 't1' } }, kind: 'ssh' @@ -65,7 +66,7 @@ describe('automation host recovery', () => { { ...RUNTIME_SSH, authorityHealth: 'unavailable' }, target ) - expect(target.connectRuntimeEnvironment).toHaveBeenCalledWith('gpu') + expect(target.connectRuntimeEnvironment).toHaveBeenCalledWith(RUNTIME_ENVIRONMENT_ID) expect(target.connectSshTarget).not.toHaveBeenCalled() }) @@ -78,10 +79,15 @@ describe('automation host recovery', () => { expect(target.retry).toHaveBeenCalledWith(desktopSelf) }) - it('sends a runtime to the servers pane, since nothing here updates a host', () => { + it('deep-links a runtime to its update row in Remote Orca Servers settings', () => { const target = deps() runAutomationHostRecovery('update-server', RUNTIME_SSH, target) - expect(target.openSettings).toHaveBeenCalledWith({ pane: 'servers', repoId: null }) + expect(target.openSettings).toHaveBeenCalledWith({ + pane: 'servers', + repoId: null, + // Runtime environment IDs are nested scroll anchors within the pane. + sectionId: RUNTIME_ENVIRONMENT_ID + }) }) it('sends a stale desktop SSH registration to the SSH pane instead', () => { diff --git a/src/renderer/src/components/automations/automation-host-recovery.ts b/src/renderer/src/components/automations/automation-host-recovery.ts index bc71f08ff18..d443d485b1d 100644 --- a/src/renderer/src/components/automations/automation-host-recovery.ts +++ b/src/renderer/src/components/automations/automation-host-recovery.ts @@ -20,10 +20,14 @@ export type AutomationHostRecoveryDeps = { openSettings: (target: SettingsNavigationTarget) => void } -/** Where a host's version is managed; the app cannot update a host on its own. */ +/** The Remote Orca Servers pane owns each runtime's version status and update action. */ function versionSettingsTarget(entry: AutomationHostCatalogEntry): SettingsNavigationTarget { if (entry.stableRef.authority.kind === 'runtime') { - return { pane: 'servers', repoId: null } + return { + pane: 'servers', + repoId: null, + sectionId: entry.stableRef.authority.environmentId + } } // A desktop SSH host with no registration generation is a stale registration, // repaired by re-adding the target rather than by updating anything. diff --git a/src/renderer/src/components/automations/use-automation-host-catalog.ts b/src/renderer/src/components/automations/use-automation-host-catalog.ts index bd7cd8dbb67..e40a4393e20 100644 --- a/src/renderer/src/components/automations/use-automation-host-catalog.ts +++ b/src/renderer/src/components/automations/use-automation-host-catalog.ts @@ -248,8 +248,8 @@ export function useAutomationHostCatalog( void window.api.runtimeEnvironments.connect({ selector: environmentId }) }, openSettings: (target) => { - openSettingsPage() openSettingsTarget(target) + openSettingsPage() } }), [controller, openSettingsPage, openSettingsTarget, pairingRevision] diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx index fb928aa2ceb..f51a7fe22f5 100644 --- a/src/renderer/src/components/settings/Settings.tsx +++ b/src/renderer/src/components/settings/Settings.tsx @@ -120,6 +120,10 @@ import { getLinearAgentSkillNavInstallStatus } from '@/lib/agent-skill-nav-install-status' import { deriveNeededSectionIds, getInitialMountedSectionIds } from './settings-load-performance' +import { + watchForSettingsDeepLinkTarget, + type SettingsDeepLinkTargetWatch +} from './settings-deep-link-target-watcher' import { translate } from '@/i18n/i18n' import { getProjectHostSetupProjectionFromState } from '../../store/selectors' import { getRepoHostIdentity } from '../../store/slices/repo-host-identity' @@ -277,6 +281,13 @@ function cancelPendingSettingsSubsectionScrollFrame( } } +function cancelPendingSettingsDeepLinkTargetWatch( + watchRef: MutableRefObject +): void { + watchRef.current?.cancel() + watchRef.current = null +} + function isEditableTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) { return false @@ -399,6 +410,7 @@ function Settings(): React.JSX.Element { const pendingNavSectionRef = useRef(null) const pendingScrollTargetRef = useRef(null) const pendingSubsectionScrollFrameRef = useRef(null) + const pendingScrollTargetWatchRef = useRef(null) const repoHooksRequestSeqRef = useRef(0) const shortcutsEscapeConfirmUntilRef = useRef(0) const sourceControlAiWriteQueueRef = useRef>(Promise.resolve()) @@ -446,6 +458,7 @@ function Settings(): React.JSX.Element { } // Why: cancel pending subsection jumps with the scroll container so a stale deep-link frame can't run after close. cancelPendingSettingsSubsectionScrollFrame(pendingSubsectionScrollFrameRef) + cancelPendingSettingsDeepLinkTargetWatch(pendingScrollTargetWatchRef) }, []) useEffect(() => { @@ -1043,6 +1056,8 @@ function Settings(): React.JSX.Element { useEffect(() => { const scrollTargetId = pendingScrollTargetRef.current const pendingNavSectionId = pendingNavSectionRef.current + // Why: this pass re-decides whether to wait for the target, so drop any watch armed by the previous one. + cancelPendingSettingsDeepLinkTargetWatch(pendingScrollTargetWatchRef) // Why: subsection deep links clear a stale filter that could hide the target row; pane-level links keep it to force-open the matching section. if ( @@ -1069,6 +1084,13 @@ function Settings(): React.JSX.Element { if (scrollTargetId !== pendingNavSectionId) { // Why: target can arrive before the lazy section mounts; keep pending refs until it does. if (!getSettingsScrollTarget(scrollTargetId, container)) { + // Why: async panes (the server list, say) mount rows after this pass, and nothing else re-runs the effect. + pendingScrollTargetWatchRef.current = watchForSettingsDeepLinkTarget({ + root: container, + isTargetPresent: () => + getSettingsScrollTarget(scrollTargetId, contentScrollRef.current) !== null, + onTargetPresent: () => setPendingNavRequestTick((tick) => tick + 1) + }) return } const scrollToSubsection = (): void => { diff --git a/src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts b/src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts new file mode 100644 index 00000000000..f3d87ee07c6 --- /dev/null +++ b/src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts @@ -0,0 +1,108 @@ +// @vitest-environment happy-dom + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { watchForSettingsDeepLinkTarget } from './settings-deep-link-target-watcher' + +function flushMutations(): Promise { + return new Promise((resolve) => setTimeout(resolve, 0)) +} + +afterEach(() => { + document.body.innerHTML = '' +}) + +describe('watchForSettingsDeepLinkTarget', () => { + it('reports the target once the async rows render', async () => { + const container = document.createElement('div') + document.body.append(container) + const onTargetPresent = vi.fn() + watchForSettingsDeepLinkTarget({ + root: container, + isTargetPresent: () => container.querySelector('[data-settings-section="env-1"]') !== null, + onTargetPresent + }) + + const row = document.createElement('div') + row.dataset.settingsSection = 'env-1' + container.append(row) + await flushMutations() + + expect(onTargetPresent).toHaveBeenCalledTimes(1) + }) + + it('keeps waiting while unrelated rows render', async () => { + const container = document.createElement('div') + document.body.append(container) + const onTargetPresent = vi.fn() + watchForSettingsDeepLinkTarget({ + root: container, + isTargetPresent: () => container.querySelector('[data-settings-section="env-1"]') !== null, + onTargetPresent + }) + + const other = document.createElement('div') + other.dataset.settingsSection = 'env-2' + container.append(other) + await flushMutations() + expect(onTargetPresent).not.toHaveBeenCalled() + + const row = document.createElement('div') + row.dataset.settingsSection = 'env-1' + container.append(row) + await flushMutations() + expect(onTargetPresent).toHaveBeenCalledTimes(1) + }) + + it('reports at most once even as more rows arrive', async () => { + const container = document.createElement('div') + document.body.append(container) + const onTargetPresent = vi.fn() + watchForSettingsDeepLinkTarget({ + root: container, + isTargetPresent: () => container.querySelector('[data-settings-section="env-1"]') !== null, + onTargetPresent + }) + + const row = document.createElement('div') + row.dataset.settingsSection = 'env-1' + container.append(row) + await flushMutations() + container.append(document.createElement('div')) + await flushMutations() + + expect(onTargetPresent).toHaveBeenCalledTimes(1) + }) + + it('stops reporting after cancel', async () => { + const container = document.createElement('div') + document.body.append(container) + const onTargetPresent = vi.fn() + const watch = watchForSettingsDeepLinkTarget({ + root: container, + isTargetPresent: () => true, + onTargetPresent + }) + watch.cancel() + + container.append(document.createElement('div')) + await flushMutations() + + expect(onTargetPresent).not.toHaveBeenCalled() + }) + + it('falls back to the document body when the pane has no scroll container yet', async () => { + const onTargetPresent = vi.fn() + watchForSettingsDeepLinkTarget({ + root: null, + isTargetPresent: () => document.querySelector('[data-settings-section="env-1"]') !== null, + onTargetPresent + }) + + const row = document.createElement('div') + row.dataset.settingsSection = 'env-1' + document.body.append(row) + await flushMutations() + + expect(onTargetPresent).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/renderer/src/components/settings/settings-deep-link-target-watcher.ts b/src/renderer/src/components/settings/settings-deep-link-target-watcher.ts new file mode 100644 index 00000000000..30d7ee1b103 --- /dev/null +++ b/src/renderer/src/components/settings/settings-deep-link-target-watcher.ts @@ -0,0 +1,54 @@ +/** + * A deep link can name a row that does not exist yet: panes such as Remote Orca + * Servers render their rows only after an async fetch resolves. The scroll + * effect parks its target in a ref, but nothing re-runs it when those rows + * finally mount, so this watcher reports the moment they do. + */ + +/** Give up eventually; a target that never mounts must not leave an observer alive. */ +const TARGET_WAIT_TIMEOUT_MS = 5000 + +export type SettingsDeepLinkTargetWatch = { + cancel: () => void +} + +export function watchForSettingsDeepLinkTarget(args: { + /** Subtree to observe; falls back to the document body when the pane has no scroll container yet. */ + root: HTMLElement | null + isTargetPresent: () => boolean + onTargetPresent: () => void +}): SettingsDeepLinkTargetWatch { + const root = args.root ?? (typeof document === 'undefined' ? null : document.body) + if (!root || typeof MutationObserver === 'undefined') { + return { cancel: () => {} } + } + + let settled = false + let observer: MutationObserver | null = null + let timeout: number | null = null + + const finish = (): void => { + if (settled) { + return + } + settled = true + observer?.disconnect() + observer = null + if (timeout !== null) { + window.clearTimeout(timeout) + timeout = null + } + } + + observer = new MutationObserver(() => { + if (settled || !args.isTargetPresent()) { + return + } + finish() + args.onTargetPresent() + }) + observer.observe(root, { subtree: true, childList: true }) + timeout = window.setTimeout(finish, TARGET_WAIT_TIMEOUT_MS) + + return { cancel: finish } +} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 23f03820b50..6660bb6c200 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -15405,7 +15405,7 @@ "53fc5f07ab": "Run history", "a00e38d1a3": "n/a", "fdb3caa8fb": "known", - "historyUnavailable": "Run history could not be loaded for this automation." + "historyUnavailable": "Run history is unavailable from this host. This does not mean the automation failed or has no runs." }, "AutomationRunPageFrame": { "40a511bed4": "Run context", @@ -15727,7 +15727,7 @@ }, "capturedOwner": { "orphan": "This automation has no host to run on. Delete it, or re-add the host it belongs to.", - "unfenced": "This host cannot confirm which automation it is changing, so this automation is read-only here." + "unfenced": "Orca on this host needs an update before you can view run history or manage this automation. Scheduled runs may still continue on the host." }, "hostStatus": { "authority": { From 2214d29f1513fae7631d6a2b774e285f497bbf03 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:43:36 -0700 Subject: [PATCH 16/80] fix(browser): close guest-owned split tab (#17281) * fix(browser): close guest-owned split tab * fix: check sourceId before toggling floating panel on close The empty-panel toggle is the ambient fallback only. Guest-initiated closes (with sourceId) target the main workspace and should not toggle the panel. * test(browser-split-shortcuts): remove terminal-mirrors close test and un Removes test case that verified Cmd+W closes guest-owned browser splits when active-tab mirrors point to a terminal, along with the helper function and unused fixture properties that only that test required. --- .../browser-guest-shortcut-dispatch.ts | 4 +- .../browser-guest-shortcut-forwarding.test.ts | 8 +- .../browser-manager-guest-shortcuts.test.ts | 4 +- src/preload/api/ui-command-event-api.ts | 4 +- ...close-active-tab-payload-admission.test.ts | 22 +++++ .../close-active-tab-payload-admission.ts | 25 ++++++ src/preload/index.ts | 13 ++- .../ipc-events-close-routing-test-harness.ts | 2 +- .../ipc-events/tab-lifecycle-ipc-bridge.ts | 25 ++++-- ...s-close-routing-active-browser-tab.test.ts | 73 +++++++++++++++++ .../browser-workspace-source-resolution.ts | 27 +++++++ .../floating-workspace-terminal-actions.ts | 16 ++-- ...pec.ts => browser-split-shortcuts.spec.ts} | 81 ++++++++++++++----- 13 files changed, 258 insertions(+), 46 deletions(-) create mode 100644 src/preload/close-active-tab-payload-admission.test.ts create mode 100644 src/preload/close-active-tab-payload-admission.ts create mode 100644 src/renderer/src/lib/browser-workspace-source-resolution.ts rename tests/e2e/{browser-split-find-shortcut.spec.ts => browser-split-shortcuts.spec.ts} (83%) diff --git a/src/main/browser/browser-guest-shortcut-dispatch.ts b/src/main/browser/browser-guest-shortcut-dispatch.ts index fa4bd966994..5d24dce43ea 100644 --- a/src/main/browser/browser-guest-shortcut-dispatch.ts +++ b/src/main/browser/browser-guest-shortcut-dispatch.ts @@ -160,7 +160,9 @@ export function forwardGuestShortcutInput( if (isFloatingGuest) { renderer.send('ui:closeFloatingItem', { sourceId: browserTabId }) } else { - renderer.send('ui:closeActiveTab') + // Why: carry the guest's page id — activeTabType/activeBrowserTabId can be stale in split + // layouts (guest focus doesn't reach the group's focus-capture), silently dropping the close. + renderer.send('ui:closeActiveTab', { sourceId: browserTabId }) } } else if (keybindingMatchesAction('tab.nextSameType', input, process.platform, keybindings)) { renderer.send('ui:switchTab', 1) diff --git a/src/main/browser/browser-guest-shortcut-forwarding.test.ts b/src/main/browser/browser-guest-shortcut-forwarding.test.ts index 19bf03bd2f8..f7f35917fa9 100644 --- a/src/main/browser/browser-guest-shortcut-forwarding.test.ts +++ b/src/main/browser/browser-guest-shortcut-forwarding.test.ts @@ -872,7 +872,9 @@ describe('setupGuestShortcutForwarding', () => { expect(rendererSendMock).toHaveBeenCalledWith('ui:closeFloatingItem', { sourceId: browserTabId }) - expect(rendererSendMock).not.toHaveBeenCalledWith('ui:closeActiveTab') + expect(rendererSendMock.mock.calls.some(([channel]) => channel === 'ui:closeActiveTab')).toBe( + false + ) }) it('routes workspace/tab index chords to ui:selectFloatingIndex for a floating guest', () => { @@ -903,7 +905,7 @@ describe('setupGuestShortcutForwarding', () => { triggerBeforeInput(closeInput) triggerBeforeInput(workspaceIndexInput) - expect(rendererSendMock).toHaveBeenCalledWith('ui:closeActiveTab') + expect(rendererSendMock).toHaveBeenCalledWith('ui:closeActiveTab', { sourceId: browserTabId }) expect(rendererSendMock).toHaveBeenCalledWith('ui:jumpToWorktreeIndex', 0) expect(rendererSendMock).not.toHaveBeenCalledWith('ui:closeFloatingItem', expect.anything()) expect(rendererSendMock).not.toHaveBeenCalledWith('ui:selectFloatingIndex', expect.anything()) @@ -918,7 +920,7 @@ describe('setupGuestShortcutForwarding', () => { triggerBeforeInput(closeInput) - expect(rendererSendMock).toHaveBeenCalledWith('ui:closeActiveTab') + expect(rendererSendMock).toHaveBeenCalledWith('ui:closeActiveTab', { sourceId: browserTabId }) expect(rendererSendMock).not.toHaveBeenCalledWith('ui:closeFloatingItem', expect.anything()) }) diff --git a/src/main/browser/browser-manager-guest-shortcuts.test.ts b/src/main/browser/browser-manager-guest-shortcuts.test.ts index cc9af9b6329..fa84fd99d50 100644 --- a/src/main/browser/browser-manager-guest-shortcuts.test.ts +++ b/src/main/browser/browser-manager-guest-shortcuts.test.ts @@ -297,7 +297,9 @@ describe('browserManager', () => { expect(rendererSendMock).toHaveBeenNthCalledWith(1, 'ui:newBrowserTab') expect(rendererSendMock).toHaveBeenNthCalledWith(2, 'ui:newTerminalTab') - expect(rendererSendMock).toHaveBeenNthCalledWith(3, 'ui:closeActiveTab') + expect(rendererSendMock).toHaveBeenNthCalledWith(3, 'ui:closeActiveTab', { + sourceId: 'browser-1' + }) expect(rendererSendMock).toHaveBeenNthCalledWith(4, 'ui:switchTabAcrossAllTypes', 1) expect(rendererSendMock).toHaveBeenNthCalledWith(5, 'ui:switchTab', 1) expect(rendererSendMock).toHaveBeenNthCalledWith(6, 'ui:switchTerminalTab', 1) diff --git a/src/preload/api/ui-command-event-api.ts b/src/preload/api/ui-command-event-api.ts index c3eefb02115..f287d747aee 100644 --- a/src/preload/api/ui-command-event-api.ts +++ b/src/preload/api/ui-command-event-api.ts @@ -32,6 +32,8 @@ import type { SessionTabCloseResponse } from '../../shared/session-tab-close' +export type CloseActiveTabPayload = { sourceId: string } + export type UiCommandEventApi = { get: () => Promise set: (args: Partial) => Promise @@ -105,7 +107,7 @@ export type UiCommandEventApi = { onBrowserHistoryNavigate: (callback: (direction: 'back' | 'forward') => void) => () => void onZoomBrowserPage: (callback: (direction: 'in' | 'out' | 'reset') => void) => () => void onHardReloadBrowserPage: (callback: () => void) => () => void - onCloseActiveTab: (callback: () => void) => () => void + onCloseActiveTab: (callback: (payload?: CloseActiveTabPayload) => void) => () => void onCloseFloatingItem: (callback: (payload: { sourceId: string }) => void) => () => void onSelectFloatingIndex: (callback: (payload: { index: number }) => void) => () => void onSwitchTab: (callback: (direction: 1 | -1) => void) => () => void diff --git a/src/preload/close-active-tab-payload-admission.test.ts b/src/preload/close-active-tab-payload-admission.test.ts new file mode 100644 index 00000000000..76f0270c34c --- /dev/null +++ b/src/preload/close-active-tab-payload-admission.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import { admitCloseActiveTabPayload } from './close-active-tab-payload-admission' + +describe('admitCloseActiveTabPayload', () => { + it('preserves the legacy omitted payload', () => { + expect(admitCloseActiveTabPayload(undefined)).toEqual({ kind: 'legacy' }) + }) + + it('admits a nonempty source id', () => { + expect(admitCloseActiveTabPayload({ sourceId: 'page-1' })).toEqual({ + kind: 'source', + payload: { sourceId: 'page-1' } + }) + }) + + it.each([null, {}, [], { sourceId: '' }, { sourceId: 1 }, 'page-1'])( + 'rejects malformed payload %#', + (payload) => { + expect(admitCloseActiveTabPayload(payload)).toEqual({ kind: 'invalid' }) + } + ) +}) diff --git a/src/preload/close-active-tab-payload-admission.ts b/src/preload/close-active-tab-payload-admission.ts new file mode 100644 index 00000000000..17428cb6a7a --- /dev/null +++ b/src/preload/close-active-tab-payload-admission.ts @@ -0,0 +1,25 @@ +import type { CloseActiveTabPayload } from './api/ui-command-event-api' + +export type AdmittedCloseActiveTabPayload = + | { kind: 'legacy' } + | { kind: 'source'; payload: CloseActiveTabPayload } + | { kind: 'invalid' } + +export function admitCloseActiveTabPayload(value: unknown): AdmittedCloseActiveTabPayload { + if (value === undefined) { + return { kind: 'legacy' } + } + if ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + typeof (value as Record).sourceId === 'string' && + (value as Record).sourceId !== '' + ) { + return { + kind: 'source', + payload: { sourceId: (value as Record).sourceId as string } + } + } + return { kind: 'invalid' } +} diff --git a/src/preload/index.ts b/src/preload/index.ts index a474eb7560e..c13e7a30a7d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -3,6 +3,8 @@ import { contextBridge, ipcRenderer, webFrame, webUtils } from 'electron' import { electronAPI } from '@electron-toolkit/preload' import { preloadE2EConfig } from './e2e-config' import { glApi } from './gitlab' +import { admitCloseActiveTabPayload } from './close-active-tab-payload-admission' +import type { CloseActiveTabPayload } from './api/ui-command-event-api' import type { SkillDeletePlan, SkillDeleteRequest, @@ -4050,8 +4052,15 @@ const api = { ipcRenderer.on('ui:hardReloadBrowserPage', listener) return () => ipcRenderer.removeListener('ui:hardReloadBrowserPage', listener) }, - onCloseActiveTab: (callback: () => void): (() => void) => { - const listener = (_event: Electron.IpcRendererEvent) => callback() + onCloseActiveTab: (callback: (payload?: CloseActiveTabPayload) => void): (() => void) => { + const listener = (_event: Electron.IpcRendererEvent, payload?: unknown): void => { + const admitted = admitCloseActiveTabPayload(payload) + if (admitted.kind === 'legacy') { + callback() + } else if (admitted.kind === 'source') { + callback(admitted.payload) + } + } ipcRenderer.on('ui:closeActiveTab', listener) return () => ipcRenderer.removeListener('ui:closeActiveTab', listener) }, diff --git a/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts b/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts index eede0d6fa8e..d3424f8fc32 100644 --- a/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts +++ b/src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts @@ -6,7 +6,7 @@ export type RequestTabCloseListener = (data: { tabId: string | null worktreeId?: string }) => void -export type CloseActiveTabListener = () => void +export type CloseActiveTabListener = (payload?: { sourceId: string }) => void export type CloseFloatingItemListener = (payload: { sourceId: string }) => void export type SelectFloatingIndexListener = (payload: { index: number }) => void export type CloseTerminalListener = (data: { tabId: string; paneRuntimeId?: number | null }) => void diff --git a/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.ts b/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.ts index b079dc9ad29..bf60e9dc0a1 100644 --- a/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.ts +++ b/src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.ts @@ -25,6 +25,7 @@ import { } from '@/lib/floating-workspace-guest-bridge' import { useAppStore } from '../../store' +import { resolveBrowserWorkspaceOwner } from '../../lib/browser-workspace-source-resolution' import { handleSwitchRecentTab, handleSwitchTab, @@ -86,15 +87,29 @@ export function registerTabLifecycleIpcBridge(unsubs: (() => void)[]): void { ) unsubs.push( - window.api.ui.onCloseActiveTab(() => { - if (isEmptyFloatingWorkspacePanelVisible()) { + window.api.ui.onCloseActiveTab((payload) => { + // Why: the empty-panel toggle is the ambient fallback only. A guest-originated close names a + // main-workspace target, so an open-but-empty floating panel must not swallow it. + if (!payload?.sourceId && isEmptyFloatingWorkspacePanelVisible()) { window.dispatchEvent(new Event(TOGGLE_FLOATING_TERMINAL_EVENT)) return } const store = useAppStore.getState() - if (store.activeTabType === 'browser' && store.activeBrowserTabId) { - const tabId = store.activeBrowserTabId - const worktreeId = store.activeWorktreeId + // Why: a guest-originated close names its own page; the activeTabType mirror goes stale in + // split layouts (guest focus never reaches the group's focus-capture), so trust the source id. + const explicitTarget = payload?.sourceId + ? resolveBrowserWorkspaceOwner(store, payload.sourceId) + : null + if (payload?.sourceId && !explicitTarget) { + // Stale id (guest closed between keydown and IPC) = no-op, never the ambient fallback. + return + } + if (explicitTarget || (store.activeTabType === 'browser' && store.activeBrowserTabId)) { + const tabId = explicitTarget?.workspaceId ?? store.activeBrowserTabId + const worktreeId = explicitTarget?.worktreeId ?? store.activeWorktreeId + if (!tabId) { + return + } const closeActiveBrowserTab = (): void => { const currentStore = useAppStore.getState() if (!worktreeId) { diff --git a/src/renderer/src/hooks/useIpcEvents-close-routing-active-browser-tab.test.ts b/src/renderer/src/hooks/useIpcEvents-close-routing-active-browser-tab.test.ts index e9a4097fcea..4225e309427 100644 --- a/src/renderer/src/hooks/useIpcEvents-close-routing-active-browser-tab.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-close-routing-active-browser-tab.test.ts @@ -184,4 +184,77 @@ describe('useIpcEvents Close Tab on the active browser tab', () => { expect(closeUnifiedTab).not.toHaveBeenCalled() expect(closeBrowserTab).toHaveBeenCalledWith('workspace-1', undefined) }) + + // Why: in split layouts the activeTabType mirror can say 'terminal' while a browser guest holds + // focus (guest focus never reaches the group's focus-capture); the guest-forwarded source id must + // close the guest's own workspace anyway. + it('closes the source workspace when the active-tab mirror points at a terminal', async () => { + const listenerRef: { current: CloseActiveTabListener | null } = { current: null } + await useIpcEventsForCloseRouting({ + closeActiveTabListenerRef: listenerRef, + getState: () => ({ + ...activeBrowserWorkspace({}), + activeTabType: 'terminal', + activeBrowserTabId: null, + browserPagesByWorkspace: { + // Ownership comes from the canonical maps, not denormalized page fields. + 'workspace-1': [{ id: 'page-1', workspaceId: 'stale-workspace', worktreeId: 'stale-wt' }] + } + }) + }) + + requireListener(listenerRef)({ sourceId: 'page-1' }) + + expect(closeBrowserTab).toHaveBeenCalledWith('workspace-1', undefined) + expect(destroyWorkspaceWebviews).toHaveBeenCalled() + }) + + // Why: an open-but-empty floating panel is the ambient close fallback only; a guest-forwarded + // source id names a main-workspace target and must not be swallowed by the panel toggle. + it('closes the source workspace even while an empty floating panel is visible', async () => { + vi.stubGlobal('document', { querySelector: () => ({}) }) + const listenerRef: { current: CloseActiveTabListener | null } = { current: null } + await useIpcEventsForCloseRouting({ + closeActiveTabListenerRef: listenerRef, + getState: () => activeBrowserWorkspace({}) + }) + + requireListener(listenerRef)({ sourceId: 'page-1' }) + + expect(closeBrowserTab).toHaveBeenCalledWith('workspace-1', undefined) + expect(destroyWorkspaceWebviews).toHaveBeenCalled() + }) + + it('no-ops on a stale source id instead of closing the ambient active tab', async () => { + const listenerRef: { current: CloseActiveTabListener | null } = { current: null } + await useIpcEventsForCloseRouting({ + closeActiveTabListenerRef: listenerRef, + getState: () => activeBrowserWorkspace({}) + }) + + requireListener(listenerRef)({ sourceId: 'page-that-no-longer-exists' }) + + expect(closeBrowserTab).not.toHaveBeenCalled() + expect(closeUnifiedTab).not.toHaveBeenCalled() + expect(closeWebRuntimeSessionTab).not.toHaveBeenCalled() + }) + + it('no-ops when a source page belongs to no live workspace', async () => { + const listenerRef: { current: CloseActiveTabListener | null } = { current: null } + await useIpcEventsForCloseRouting({ + closeActiveTabListenerRef: listenerRef, + getState: () => ({ + ...activeBrowserWorkspace({}), + browserPagesByWorkspace: { + orphan: [{ id: 'orphan-page', workspaceId: 'workspace-1', worktreeId: 'wt-1' }] + } + }) + }) + + requireListener(listenerRef)({ sourceId: 'orphan-page' }) + + expect(closeBrowserTab).not.toHaveBeenCalled() + expect(closeUnifiedTab).not.toHaveBeenCalled() + expect(closeWebRuntimeSessionTab).not.toHaveBeenCalled() + }) }) diff --git a/src/renderer/src/lib/browser-workspace-source-resolution.ts b/src/renderer/src/lib/browser-workspace-source-resolution.ts new file mode 100644 index 00000000000..9e10c6f7043 --- /dev/null +++ b/src/renderer/src/lib/browser-workspace-source-resolution.ts @@ -0,0 +1,27 @@ +import type { AppState } from '../store/types' + +export type BrowserWorkspaceOwner = { + worktreeId: string + workspaceId: string +} + +export function resolveBrowserWorkspaceOwner( + state: Pick, + sourceId: string, + requiredWorktreeId?: string +): BrowserWorkspaceOwner | null { + for (const [worktreeId, workspaces] of Object.entries(state.browserTabsByWorktree)) { + if (requiredWorktreeId && worktreeId !== requiredWorktreeId) { + continue + } + for (const workspace of workspaces) { + if ( + workspace.id === sourceId || + (state.browserPagesByWorkspace[workspace.id] ?? []).some((page) => page.id === sourceId) + ) { + return { worktreeId, workspaceId: workspace.id } + } + } + } + return null +} diff --git a/src/renderer/src/lib/floating-workspace-terminal-actions.ts b/src/renderer/src/lib/floating-workspace-terminal-actions.ts index 484143160d9..d8c81add055 100644 --- a/src/renderer/src/lib/floating-workspace-terminal-actions.ts +++ b/src/renderer/src/lib/floating-workspace-terminal-actions.ts @@ -9,6 +9,7 @@ import { type TypeCyclableTab } from '@/components/terminal/tab-type-cycle' import type { AppState } from '@/store/types' +import { resolveBrowserWorkspaceOwner } from './browser-workspace-source-resolution' import { TOGGLE_FLOATING_TERMINAL_EVENT } from './floating-terminal' import { focusTerminalTabSurface } from './focus-terminal-tab-surface' import { keybindingMatchesAction, type KeybindingOverrides } from '../../../shared/keybindings' @@ -143,17 +144,10 @@ export function resolveFloatingWorkspaceBrowserWorkspaceId( store: Pick, sourceId: string ): string | null { - const workspaces = store.browserTabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? [] - const pagesByWorkspace = store.browserPagesByWorkspace ?? {} - for (const workspace of workspaces) { - if (workspace.id === sourceId) { - return workspace.id - } - if ((pagesByWorkspace[workspace.id] ?? []).some((page) => page.id === sourceId)) { - return workspace.id - } - } - return null + return ( + resolveBrowserWorkspaceOwner(store, sourceId, FLOATING_TERMINAL_WORKTREE_ID)?.workspaceId ?? + null + ) } function activateFloatingWorkspaceCyclableTab( diff --git a/tests/e2e/browser-split-find-shortcut.spec.ts b/tests/e2e/browser-split-shortcuts.spec.ts similarity index 83% rename from tests/e2e/browser-split-find-shortcut.spec.ts rename to tests/e2e/browser-split-shortcuts.spec.ts index a7563fb66f2..4d9c824bbac 100644 --- a/tests/e2e/browser-split-find-shortcut.spec.ts +++ b/tests/e2e/browser-split-shortcuts.spec.ts @@ -4,8 +4,9 @@ import { focusActiveTerminalInput } from './helpers/terminal' import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' const modifier = process.platform === 'darwin' ? 'Meta' : 'Control' +const guestModifier: 'meta' | 'control' = process.platform === 'darwin' ? 'meta' : 'control' -type SplitFindFixture = { +type TerminalBrowserSplitFixture = { browserGroupId: string browserTabId: string terminalGroupId: string @@ -17,7 +18,7 @@ type BrowserSplitFixture = { secondBrowserTabId: string } -async function createTerminalBrowserSplit(page: Page): Promise { +async function createTerminalBrowserSplit(page: Page): Promise { return page.evaluate(() => { const store = window.__store if (!store) { @@ -28,6 +29,13 @@ async function createTerminalBrowserSplit(page: Page): Promise if (!worktreeId) { throw new Error('Active worktree unavailable') } + const terminalTabId = state.activeTabIdByWorktree[worktreeId] ?? state.activeTabId ?? undefined + if ( + !terminalTabId || + !(state.tabsByWorktree[worktreeId] ?? []).some((tab) => tab.id === terminalTabId) + ) { + throw new Error('Active terminal tab unavailable') + } const terminalGroupId = state.ensureWorktreeRootGroup(worktreeId) const browserGroupId = state.createEmptySplitGroup(worktreeId, terminalGroupId, 'right') if (!browserGroupId) { @@ -38,7 +46,15 @@ async function createTerminalBrowserSplit(page: Page): Promise focusAddressBar: false, targetGroupId: browserGroupId }) - return { browserGroupId, browserTabId: browserTab.id, terminalGroupId } + const browserPageId = browserTab.activePageId + if (!browserPageId) { + throw new Error('Active browser page unavailable') + } + return { + browserGroupId, + browserTabId: browserTab.id, + terminalGroupId + } }) } @@ -76,8 +92,12 @@ async function createBrowserSplit(page: Page): Promise { focusAddressBar: false, targetGroupId: secondBrowserGroupId }) + const firstBrowserPageId = firstBrowserTab.activePageId + if (!firstBrowserPageId) { + throw new Error('First active browser page unavailable') + } return { - firstBrowserPageId: firstBrowserTab.activePageId, + firstBrowserPageId, firstBrowserTabId: firstBrowserTab.id, secondBrowserTabId: secondBrowserTab.id } @@ -97,7 +117,7 @@ async function focusBrowserAddressBar(page: Page, browserTabId: string): Promise 'form:has(> [data-orca-browser-address-bar="true"])' ) await expect(addressBarForm).toBeVisible() - await addressBarForm.click() + await addressBar.focus() await expect(addressBar).toBeFocused() } @@ -115,7 +135,7 @@ function browserSplitFindInput(page: Page, browserTabId: string) { .getByPlaceholder('Find in page...') } -async function pressFindInBrowserGuest( +async function waitForBrowserGuestRegistration( page: Page, browserTabId: string, browserPageId: string @@ -123,7 +143,7 @@ async function pressFindInBrowserGuest( await expect .poll(() => page.evaluate( - async ({ targetBrowserPageId, targetBrowserTabId, inputModifier }) => { + async ({ targetBrowserPageId, targetBrowserTabId }) => { const overlay = document.querySelector( `[data-browser-overlay-tab-id="${targetBrowserTabId}"]` ) @@ -140,17 +160,6 @@ async function pressFindInBrowserGuest( if (!registered) { return false } - webview.focus() - await webview.sendInputEvent({ - type: 'keyDown', - keyCode: 'F', - modifiers: [inputModifier] - }) - await webview.sendInputEvent({ - type: 'keyUp', - keyCode: 'F', - modifiers: [inputModifier] - }) return true } catch { return false @@ -158,14 +167,44 @@ async function pressFindInBrowserGuest( }, { targetBrowserPageId: browserPageId, - targetBrowserTabId: browserTabId, - inputModifier: modifier.toLowerCase() + targetBrowserTabId: browserTabId } ) ) .toBe(true) } +async function pressFindInBrowserGuest( + page: Page, + browserTabId: string, + browserPageId: string +): Promise { + await waitForBrowserGuestRegistration(page, browserTabId, browserPageId) + await page.evaluate( + async ({ targetBrowserTabId, inputModifier }) => { + const overlay = document.querySelector( + `[data-browser-overlay-tab-id="${targetBrowserTabId}"]` + ) + const webview = overlay?.querySelector('webview') as Electron.WebviewTag | null + if (!webview) { + throw new Error('Registered browser guest unavailable') + } + webview.focus() + await webview.sendInputEvent({ + type: 'keyDown', + keyCode: 'F', + modifiers: [inputModifier] + }) + await webview.sendInputEvent({ + type: 'keyUp', + keyCode: 'F', + modifiers: [inputModifier] + }) + }, + { targetBrowserTabId: browserTabId, inputModifier: guestModifier } + ) +} + function terminalFindInput(page: Page) { return page.locator('[data-terminal-search-root] input:visible') } @@ -199,7 +238,7 @@ async function focusBrowserGroup(page: Page, groupId: string): Promise { await waitForFocusedGroup(page, groupId) } -test.describe('browser split Find shortcut', () => { +test.describe('browser split shortcuts', () => { test.beforeEach(async ({ orcaPage }) => { await waitForSessionReady(orcaPage) await waitForActiveWorktree(orcaPage) From 0645729f68e12526f8227369747cc931dc920a85 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:01:16 -0700 Subject: [PATCH 17/80] perf(worktrees): resolve watcher targets concurrently (#17166) --- ...ktree-base-directory-watch-targets.test.ts | 232 ++++++++++++++++++ .../worktree-base-directory-watch-targets.ts | 54 +++- 2 files changed, 276 insertions(+), 10 deletions(-) create mode 100644 src/main/ipc/worktree-base-directory-watch-targets.test.ts diff --git a/src/main/ipc/worktree-base-directory-watch-targets.test.ts b/src/main/ipc/worktree-base-directory-watch-targets.test.ts new file mode 100644 index 00000000000..250b3d7625d --- /dev/null +++ b/src/main/ipc/worktree-base-directory-watch-targets.test.ts @@ -0,0 +1,232 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { join, sep } from 'node:path' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type { Repo } from '../../shared/repo-types' + +const { getSshFilesystemProviderMock, readFileMock, realpathMock, statMock } = vi.hoisted(() => ({ + getSshFilesystemProviderMock: vi.fn(), + readFileMock: vi.fn(), + realpathMock: vi.fn(), + statMock: vi.fn() +})) + +vi.mock('node:fs/promises', () => ({ + readFile: readFileMock, + realpath: realpathMock, + stat: statMock +})) + +vi.mock('../providers/ssh-filesystem-dispatch', () => ({ + getSshFilesystemProvider: getSshFilesystemProviderMock +})) + +import { + buildWorktreeBaseDirectoryWatchTargets, + clearWorktreeBaseDirectoryWatchTargetWarnings, + WORKTREE_BASE_TARGET_RESOLUTION_CONCURRENCY +} from './worktree-base-directory-watch-targets' + +const absolutePath = (...parts: string[]): string => join(sep, ...parts) +const WORKTREE_ROOT = absolutePath('workspace', 'worktrees') +const PROJECT_ROOT = absolutePath('workspace', 'projects') +const localDirectoryStat = { isDirectory: () => true } +const remoteDirectoryStat = { type: 'directory', size: 0, mtime: 0 } +const settings = { + workspaceDir: WORKTREE_ROOT, + nestWorkspaces: true +} as GlobalSettings + +function makeRepo(index: number, overrides: Partial = {}): Repo { + return { + id: `repo-${index}`, + path: join(PROJECT_ROOT, `project-${index}`), + displayName: `Project ${index}`, + badgeColor: '#000000', + addedAt: index, + ...overrides + } as Repo +} + +function makeStore(repos: Repo[]) { + return { + getSettings: () => settings, + getRepos: () => repos + } +} + +function makeRemoteProvider() { + return { + stat: vi.fn(async () => remoteDirectoryStat), + realpath: vi.fn(async (path: string) => path), + readFile: vi.fn(async () => ({ content: '', isBinary: false })) + } +} + +describe('worktree base directory watch target resolution', () => { + let warnSpy: ReturnType + + beforeEach(() => { + statMock.mockReset().mockResolvedValue(localDirectoryStat) + realpathMock.mockReset().mockImplementation(async (path: string) => path) + readFileMock.mockReset().mockResolvedValue('') + getSshFilesystemProviderMock.mockReset().mockReturnValue(undefined) + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + }) + + afterEach(() => { + clearWorktreeBaseDirectoryWatchTargetWarnings() + warnSpy.mockRestore() + }) + + it('bounds resolution concurrency and starts one successor per released slot', async () => { + const repos = Array.from( + { length: WORKTREE_BASE_TARGET_RESOLUTION_CONCURRENCY + 4 }, + (_, index) => makeRepo(index) + ) + const gates = repos.map(() => Promise.withResolvers()) + let startedRootStats = 0 + let activeRootStats = 0 + let peakRootStats = 0 + statMock.mockImplementation(async (path: string) => { + if (path === WORKTREE_ROOT) { + const gate = gates[startedRootStats++] + activeRootStats++ + peakRootStats = Math.max(peakRootStats, activeRootStats) + await gate.promise + activeRootStats-- + } + return localDirectoryStat + }) + + const resultPromise = buildWorktreeBaseDirectoryWatchTargets(makeStore(repos) as never) + await vi.waitFor(() => { + expect(startedRootStats).toBe(WORKTREE_BASE_TARGET_RESOLUTION_CONCURRENCY) + }) + expect(peakRootStats).toBe(WORKTREE_BASE_TARGET_RESOLUTION_CONCURRENCY) + + gates[0].resolve() + await vi.waitFor(() => { + expect(startedRootStats).toBe(WORKTREE_BASE_TARGET_RESOLUTION_CONCURRENCY + 1) + }) + expect(peakRootStats).toBe(WORKTREE_BASE_TARGET_RESOLUTION_CONCURRENCY) + + for (const gate of gates) { + gate.resolve() + } + await resultPromise + expect(startedRootStats).toBe(repos.length) + expect(peakRootStats).toBe(WORKTREE_BASE_TARGET_RESOLUTION_CONCURRENCY) + }) + + it('merges targets and shared repo membership in store order after reverse completion', async () => { + const repos = Array.from({ length: 4 }, (_, index) => makeRepo(index)) + const gates = repos.map(() => Promise.withResolvers()) + const gitStatStarted = new Set() + const completionOrder: number[] = [] + statMock.mockImplementation(async (path: string) => { + const index = repos.findIndex((repo) => path === join(repo.path, '.git')) + if (index !== -1) { + gitStatStarted.add(index) + await gates[index].promise + completionOrder.push(index) + } + return localDirectoryStat + }) + + const resultPromise = buildWorktreeBaseDirectoryWatchTargets(makeStore(repos) as never) + await vi.waitFor(() => expect(gitStatStarted.size).toBe(repos.length)) + for (let index = repos.length - 1; index >= 0; index--) { + gates[index].resolve() + await vi.waitFor(() => expect(completionOrder).toContain(index)) + } + const targets = await resultPromise + + expect(completionOrder).toEqual([3, 2, 1, 0]) + expect([...targets.values()].map((target) => [target.kind, target.path])).toEqual([ + ['base', WORKTREE_ROOT], + ...repos.map((repo) => ['git-common', join(repo.path, '.git')]) + ]) + expect([...([...targets.values()][0]?.repos.keys() ?? [])]).toEqual( + repos.map((repo) => repo.id) + ) + }) + + it('isolates missing paths to the affected repo', async () => { + const missing = Object.assign(new Error('missing'), { code: 'ENOENT' }) + const goodBefore = makeRepo(0, { worktreeBasePath: absolutePath('worktrees', 'before') }) + const unavailable = makeRepo(1, { worktreeBasePath: absolutePath('worktrees', 'missing') }) + const goodAfter = makeRepo(2, { worktreeBasePath: absolutePath('worktrees', 'after') }) + statMock.mockImplementation(async (path: string) => { + if (path === unavailable.worktreeBasePath || path === join(unavailable.path, '.git')) { + throw missing + } + return localDirectoryStat + }) + + const targets = await buildWorktreeBaseDirectoryWatchTargets( + makeStore([goodBefore, unavailable, goodAfter]) as never + ) + const repoIds = [...targets.values()].flatMap((target) => [...target.repos.keys()]) + + expect(repoIds).toContain(goodBefore.id) + expect(repoIds).not.toContain(unavailable.id) + expect(repoIds).toContain(goodAfter.id) + }) + + it('keeps identical SSH paths isolated by connection and provider', async () => { + const providerA = makeRemoteProvider() + const providerB = makeRemoteProvider() + getSshFilesystemProviderMock.mockImplementation((connectionId: string) => + connectionId === 'ssh-a' ? providerA : connectionId === 'ssh-b' ? providerB : undefined + ) + const sharedPath = '/srv/project' + const repos = [ + makeRepo(0, { path: sharedPath, connectionId: 'ssh-a' }), + makeRepo(1, { path: sharedPath, connectionId: 'ssh-b' }) + ] + + const targets = await buildWorktreeBaseDirectoryWatchTargets(makeStore(repos) as never) + + expect(providerA.stat).toHaveBeenCalledTimes(2) + expect(providerA.realpath).toHaveBeenCalledTimes(2) + expect(providerB.stat).toHaveBeenCalledTimes(2) + expect(providerB.realpath).toHaveBeenCalledTimes(2) + expect([...targets.values()].map((target) => target.connectionId)).toEqual([ + 'ssh-a', + 'ssh-a', + 'ssh-b', + 'ssh-b' + ]) + expect(new Set(targets.keys()).size).toBe(4) + }) + + it('skips folder workspaces, unavailable SSH providers, and WSL UNC roots', async () => { + const folder = makeRepo(0, { kind: 'folder' }) + const unavailableSsh = makeRepo(1, { connectionId: 'missing', path: '/srv/missing' }) + const wslPath = '\\\\wsl.localhost\\Ubuntu\\home\\alice\\project' + const wsl = makeRepo(2, { path: wslPath, worktreeBasePath: wslPath }) + + const targets = await buildWorktreeBaseDirectoryWatchTargets( + makeStore([folder, unavailableSsh, wsl]) as never + ) + + expect(targets.size).toBe(0) + expect(statMock).not.toHaveBeenCalled() + expect(getSshFilesystemProviderMock).toHaveBeenCalledWith('missing') + }) + + it('does not publish ordered partial targets when a repo resolver rejects unexpectedly', async () => { + const completed = makeRepo(0) + const broken = makeRepo(1) + Object.defineProperty(broken, 'path', { + get: () => { + throw new Error('unexpected resolver failure') + } + }) + + await expect( + buildWorktreeBaseDirectoryWatchTargets(makeStore([completed, broken]) as never) + ).rejects.toThrow('unexpected resolver failure') + expect(statMock).toHaveBeenCalled() + }) +}) diff --git a/src/main/ipc/worktree-base-directory-watch-targets.ts b/src/main/ipc/worktree-base-directory-watch-targets.ts index cfa825e09ab..5300475593d 100644 --- a/src/main/ipc/worktree-base-directory-watch-targets.ts +++ b/src/main/ipc/worktree-base-directory-watch-targets.ts @@ -15,6 +15,7 @@ import { resolveRuntimePath } from '../../shared/cross-platform-path' import { isWslUncPath } from '../../shared/wsl-paths' +import { mapWithConcurrency } from '../../shared/map-with-concurrency' import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch' import { computeWorkspaceRoot, @@ -32,6 +33,9 @@ import type { const missingRootWarnings = new Set() const skippedWslWarnings = new Set() +// Why: match existing worktree probe caps while bounding aggregate SSH filesystem RPC pressure. +export const WORKTREE_BASE_TARGET_RESOLUTION_CONCURRENCY = 8 + function normalizeWatchKey(pathValue: string): string { return normalizeRuntimePathForComparison(normalize(pathValue)) } @@ -177,21 +181,51 @@ async function maybeAddBaseTarget( } } +async function resolveRepoTargets( + repo: Repo, + settings: GlobalSettings +): Promise> { + const targets = new Map() + if (isFolderRepo(repo)) { + return targets + } + const executionHostId = getRepoExecutionHostId(repo) + if (executionHostId === LOCAL_EXECUTION_HOST_ID) { + await maybeAddBaseTarget(targets, repo, settings) + } else if (repo.connectionId) { + await maybeAddBaseTarget(targets, repo, settings, repo.connectionId) + } + return targets +} + +function mergeRepoTargets( + targets: Map, + repoTargets: Map +): void { + for (const [key, target] of repoTargets) { + const existing = targets.get(key) + if (!existing) { + targets.set(key, target) + continue + } + for (const [repoId, config] of target.repos) { + existing.repos.set(repoId, config) + } + } +} + export async function buildWorktreeBaseDirectoryWatchTargets( store: Store ): Promise> { const settings = store.getSettings() + const resolvedRepoTargets = await mapWithConcurrency( + store.getRepos(), + WORKTREE_BASE_TARGET_RESOLUTION_CONCURRENCY, + (repo) => resolveRepoTargets(repo, settings) + ) const targets = new Map() - for (const repo of store.getRepos()) { - if (isFolderRepo(repo)) { - continue - } - const executionHostId = getRepoExecutionHostId(repo) - if (executionHostId === LOCAL_EXECUTION_HOST_ID) { - await maybeAddBaseTarget(targets, repo, settings) - } else if (repo.connectionId) { - await maybeAddBaseTarget(targets, repo, settings, repo.connectionId) - } + for (const repoTargets of resolvedRepoTargets) { + mergeRepoTargets(targets, repoTargets) } return targets } From bf884ea8667817727f7dce6a86aa2c0e68c42be6 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:01:20 -0700 Subject: [PATCH 18/80] perf(startup): skip disabled diagnostic serialization (#17176) --- ...rsistence-loading-store-extraction.test.ts | 81 ++++++++++++++++++- .../loading-store/loaded-state-parsing.ts | 14 ++-- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/main/persistence-loading-store-extraction.test.ts b/src/main/persistence-loading-store-extraction.test.ts index dafb6deadee..e48971d6c5c 100644 --- a/src/main/persistence-loading-store-extraction.test.ts +++ b/src/main/persistence-loading-store-extraction.test.ts @@ -2,14 +2,22 @@ import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'v import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { getDefaultWorkspaceSession } from '../shared/constants' +import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../shared/constants' import type { PersistedState } from '../shared/persisted-state-types' +import type * as StartupDiagnosticsModule from './startup/startup-diagnostics' import type { Store as PersistenceStore } from './persistence/loading-store/store' -import { createStore, dataFile, makeRepo, testState } from './persistence-test-harness' +import { + createStore, + dataFile, + makeRepo, + testState, + writeDataFile +} from './persistence-test-harness' -const { trackMock, getCohortAtEmitMock } = vi.hoisted(() => ({ +const { trackMock, getCohortAtEmitMock, logStartupDiagnosticMock } = vi.hoisted(() => ({ trackMock: vi.fn(), - getCohortAtEmitMock: vi.fn(() => ({ nth_repo_added: 2 })) + getCohortAtEmitMock: vi.fn(() => ({ nth_repo_added: 2 })), + logStartupDiagnosticMock: vi.fn() })) vi.mock('electron', () => ({ @@ -33,6 +41,10 @@ vi.mock('./ssh/ssh-config-parser', () => ({ loadUserSshConfig: vi.fn(() => ({ hosts: [] })), sshConfigHostsToTargets: vi.fn(() => []) })) +vi.mock('./startup/startup-diagnostics', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, logStartupDiagnostic: logStartupDiagnosticMock } +}) describe('loading Store extraction seams', () => { beforeEach(() => { @@ -40,9 +52,70 @@ describe('loading Store extraction seams', () => { }) afterEach(() => { + vi.unstubAllEnvs() + logStartupDiagnosticMock.mockReset() rmSync(testState.dir, { recursive: true, force: true }) }) + it('does not serialize the workspace session when startup diagnostics are disabled', () => { + const sentinel = 'startup-diagnostics-workspace-session-sentinel-disabled' + vi.stubEnv('ORCA_STARTUP_DIAGNOSTICS', '') + const state = getDefaultPersistedState(testState.dir) + state.workspaceSession = { ...state.workspaceSession, activeTabId: sentinel } + writeDataFile(state) + + const stringifySpy = vi.spyOn(JSON, 'stringify') + const store = createStore() + store.freezeWrites() + + const workspaceSessionStringifyCalls = stringifySpy.mock.calls.filter( + ([value]) => + value && + typeof value === 'object' && + (value as { activeTabId?: unknown }).activeTabId === sentinel + ) + stringifySpy.mockRestore() + + expect(store.getWorkspaceSession().activeTabId).toBe(sentinel) + expect(workspaceSessionStringifyCalls).toHaveLength(0) + expect( + logStartupDiagnosticMock.mock.calls.some(([event]) => event === 'persistence-load-done') + ).toBe(false) + }) + + it('reports the unchanged workspace-session byte count when startup diagnostics are enabled', () => { + const sentinel = 'startup-diagnostics-workspace-session-sentinel-enabled' + vi.stubEnv('ORCA_STARTUP_DIAGNOSTICS', '1') + const state = getDefaultPersistedState(testState.dir) + state.workspaceSession = { ...state.workspaceSession, activeTabId: sentinel } + writeDataFile(state) + + const stringifySpy = vi.spyOn(JSON, 'stringify') + const store = createStore() + store.freezeWrites() + + const workspaceSessionStringifyCalls = stringifySpy.mock.calls.filter( + ([value]) => + value && + typeof value === 'object' && + (value as { activeTabId?: unknown }).activeTabId === sentinel + ) + stringifySpy.mockRestore() + + expect(store.getWorkspaceSession().activeTabId).toBe(sentinel) + expect(workspaceSessionStringifyCalls).toHaveLength(1) + const loadDoneCall = logStartupDiagnosticMock.mock.calls.find( + ([event]) => event === 'persistence-load-done' + ) + expect(loadDoneCall).toBeDefined() + const details = loadDoneCall?.[1] as Record | undefined + expect(details).toEqual({ + t: expect.any(Number), + repos: state.repos.length, + workspaceSessionBytes: Buffer.byteLength(JSON.stringify(store.getWorkspaceSession())) + }) + }) + it('accepts the first JSON-parseable backup even when an older backup has richer state', async () => { mkdirSync(testState.dir, { recursive: true }) writeFileSync(dataFile(), '{{corrupt-primary', 'utf-8') diff --git a/src/main/persistence/loading-store/loaded-state-parsing.ts b/src/main/persistence/loading-store/loaded-state-parsing.ts index 2f28c771508..5bd6e572ab1 100644 --- a/src/main/persistence/loading-store/loaded-state-parsing.ts +++ b/src/main/persistence/loading-store/loaded-state-parsing.ts @@ -42,13 +42,17 @@ import { prepareLoadedTerminalSettings } from './prepare-loaded-terminal-setting import { prepareLoadedProfileSettings } from './prepare-loaded-profile-settings' import { normalizeLoadedProfileState } from './normalize-loaded-profile-state' +type PersistenceStartupDetails = Record | (() => Record) + function logPersistenceStartupMilestone( event: string, - details: Record = {} + details: PersistenceStartupDetails = {} ): void { - if (isStartupDiagnosticsEnabled()) { - logStartupDiagnostic(event, { t: Math.round(performance.now()), ...details }) + if (!isStartupDiagnosticsEnabled()) { + return } + const resolvedDetails = typeof details === 'function' ? details() : details + logStartupDiagnostic(event, { t: Math.round(performance.now()), ...resolvedDetails }) } import type { StoreRuntimeState } from './store-runtime-state' @@ -279,10 +283,10 @@ export class LoadedStateParsingOperations { migrated.githubCache = readGithubCacheSnapshot(this.runtime.dataFile) ?? migrated.githubCache } - logPersistenceStartupMilestone('persistence-load-done', { + logPersistenceStartupMilestone('persistence-load-done', () => ({ repos: migrated.repos.length, workspaceSessionBytes: Buffer.byteLength(JSON.stringify(migrated.workspaceSession)) - }) + })) return migrated } } From 80a0595449d78f12b9610c704da80b5ff40df6a9 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:01:23 -0700 Subject: [PATCH 19/80] perf(startup): stop rescanning persisted pane tabs (#17179) * perf(startup): index persisted pane tabs once * perf(startup): lazily resolve persisted pane tabs --- .../pane-identity-migration.ts | 66 ++++++- ...workspace-pane-normalization-index.test.ts | 184 ++++++++++++++++++ .../workspace-pane-normalization.ts | 9 +- 3 files changed, 247 insertions(+), 12 deletions(-) create mode 100644 src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts diff --git a/src/main/persistence/restoring-sessions/pane-identity-migration.ts b/src/main/persistence/restoring-sessions/pane-identity-migration.ts index e07e9995286..98d97a649a3 100644 --- a/src/main/persistence/restoring-sessions/pane-identity-migration.ts +++ b/src/main/persistence/restoring-sessions/pane-identity-migration.ts @@ -1,5 +1,5 @@ import type { LegacyPaneKeyAliasEntry } from '../../../shared/persisted-state-types' -import type { TerminalLayoutSnapshot } from '../../../shared/terminal-tab-types' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import { isTerminalLeafId, makePaneKey } from '../../../shared/stable-pane-id' import { agentHookServer } from '../../agent-hooks/server' @@ -17,23 +17,69 @@ export function findWorktreeIdForTab( return undefined } +export type TerminalTabLookup = { + get(tabId: string): TerminalTab | undefined +} + +/** Resolves tabs on demand while retaining first-match ordering and scan progress. */ +export function createLazyTerminalTabLookup(session: WorkspaceSessionState): TerminalTabLookup { + const tabsByWorktree = Object.entries(session.tabsByWorktree ?? {}) + const tabsById = new Map() + let worktreeIndex = 0 + let tabIndex = 0 + let exhausted = false + + return { + get(requestedTabId: string): TerminalTab | undefined { + if (tabsById.has(requestedTabId)) { + return tabsById.get(requestedTabId) + } + if (exhausted) { + return undefined + } + + while (worktreeIndex < tabsByWorktree.length) { + const tabs = tabsByWorktree[worktreeIndex][1] + while (tabIndex < tabs.length) { + const currentIndex = tabIndex + tabIndex += 1 + // Array#some, used by the prior lookup, skips sparse holes. + if (!(currentIndex in tabs)) { + continue + } + const tab = tabs[currentIndex] + const tabId = tab.id + // Preserve first-match behavior when persisted IDs collide. + if (!tabsById.has(tabId)) { + tabsById.set(tabId, tab) + } + if (tabId === requestedTabId) { + return tabsById.get(requestedTabId) + } + } + worktreeIndex += 1 + tabIndex = 0 + } + + exhausted = true + return undefined + } + } +} + /** Bridges a tab's legacy numeric pane keys to stable ones; returns the alias rows worth persisting. */ export function registerLegacyPaneKeyAliasesForTab(args: { - session: WorkspaceSessionState tabId: string + tab: TerminalTab | undefined inputLayout: TerminalLayoutSnapshot normalizedLayout: TerminalLayoutSnapshot leafIdByInputLeafId: Map }): LegacyPaneKeyAliasEntry[] { - const worktreeId = findWorktreeIdForTab(args.session, args.tabId) - const tab = worktreeId - ? args.session.tabsByWorktree?.[worktreeId]?.find((entry) => entry.id === args.tabId) - : undefined const legacyPaneKeyAliasEntries: LegacyPaneKeyAliasEntry[] = [] const registeredLegacyPaneKeys = new Set() const hasLeafPtyBindings = Object.keys(args.inputLayout.ptyIdsByLeafId ?? {}).length > 0 const fallbackPtyId = - !hasLeafPtyBindings && typeof tab?.ptyId === 'string' ? tab.ptyId : undefined + !hasLeafPtyBindings && typeof args.tab?.ptyId === 'string' ? args.tab.ptyId : undefined const registerLegacyAlias = (inputLeafId: string, leafId: string, ptyId?: string): boolean => { if (!isTerminalLeafId(leafId)) { return false @@ -80,7 +126,7 @@ export function registerLegacyPaneKeyAliasesForTab(args: { ) } } - if (tab?.ptyId && !hasLeafPtyBindings) { + if (args.tab?.ptyId && !hasLeafPtyBindings) { const fallbackLeafId = args.normalizedLayout.activeLeafId ?? firstLayoutLeafId(args.normalizedLayout.root) let paneKey: string | undefined @@ -96,9 +142,9 @@ export function registerLegacyPaneKeyAliasesForTab(args: { if (registeredLegacyPaneKeys.has(legacyPaneKey)) { continue } - agentHookServer.registerPaneKeyAlias(legacyPaneKey, paneKey, tab.ptyId) + agentHookServer.registerPaneKeyAlias(legacyPaneKey, paneKey, args.tab.ptyId) legacyPaneKeyAliasEntries.push({ - ptyId: tab.ptyId, + ptyId: args.tab.ptyId, legacyPaneKey, stablePaneKey: paneKey, updatedAt: Date.now() diff --git a/src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts b/src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts new file mode 100644 index 00000000000..9cebe041544 --- /dev/null +++ b/src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts @@ -0,0 +1,184 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { TerminalLayoutSnapshot, TerminalTab } from '../../../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import { createLazyTerminalTabLookup } from './pane-identity-migration' +import { normalizeWorkspaceSessionPaneIdentities } from './workspace-pane-normalization' + +const { registerPaneKeyAliasMock } = vi.hoisted(() => ({ + registerPaneKeyAliasMock: vi.fn() +})) + +vi.mock('../../agent-hooks/server', () => ({ + agentHookServer: { + registerPaneKeyAlias: registerPaneKeyAliasMock + } +})) + +const STABLE_LEAF_ID = '00000000-0000-4000-8000-000000000001' + +function terminalTab(id: string, worktreeId: string, ptyId: string | null): TerminalTab { + return { + id, + worktreeId, + ptyId, + title: 'Terminal', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } +} + +function workspaceSession( + tabsByWorktree: Record, + terminalLayoutsByTabId: Record +): WorkspaceSessionState { + return { + activeRepoId: null, + activeWorktreeId: null, + activeTabId: null, + tabsByWorktree, + terminalLayoutsByTabId + } +} + +function legacyLayout(): TerminalLayoutSnapshot { + return { + root: { type: 'leaf', leafId: 'pane:1' }, + activeLeafId: 'pane:1', + expandedLeafId: null + } +} + +function stableLayout(): TerminalLayoutSnapshot { + return { + root: { type: 'leaf', leafId: STABLE_LEAF_ID }, + activeLeafId: STABLE_LEAF_ID, + expandedLeafId: null + } +} + +describe('workspace pane normalization tab index', () => { + beforeEach(() => { + registerPaneKeyAliasMock.mockReset() + }) + + it('keeps the first tab when duplicate ids occur across worktrees', () => { + const result = normalizeWorkspaceSessionPaneIdentities( + workspaceSession( + { + first: [terminalTab('duplicate-tab', 'first', 'first-pty')], + second: [terminalTab('duplicate-tab', 'second', 'second-pty')] + }, + { 'duplicate-tab': legacyLayout() } + ) + ) + + expect(result.legacyPaneKeyAliasEntries).not.toHaveLength(0) + expect(result.legacyPaneKeyAliasEntries.every((entry) => entry.ptyId === 'first-pty')).toBe( + true + ) + }) + + it('keeps missing tab ids without inventing persisted aliases', () => { + const result = normalizeWorkspaceSessionPaneIdentities( + workspaceSession({}, { 'missing-tab': legacyLayout() }) + ) + + expect(result.legacyPaneKeyAliasEntries).toEqual([]) + }) + + it('reads each tab id once regardless of the number of layouts', () => { + const tabCount = 256 + let idReads = 0 + const tabs = Array.from({ length: tabCount }, (_, index) => { + const id = `tab-${index}` + const tab = terminalTab(id, 'worktree', null) + Object.defineProperty(tab, 'id', { + enumerable: true, + get: () => { + idReads += 1 + return id + } + }) + return tab + }) + const layouts = Object.fromEntries( + Array.from({ length: tabCount }, (_, index) => [`tab-${index}`, stableLayout()]) + ) + + normalizeWorkspaceSessionPaneIdentities(workspaceSession({ worktree: tabs }, layouts)) + + expect(idReads).toBe(tabCount) + }) + + it('does not inspect tabs after an early match', () => { + const first = terminalTab('first', 'worktree', 'first-pty') + const later = terminalTab('later', 'worktree', 'later-pty') + Object.defineProperty(later, 'id', { + enumerable: true, + get: () => { + throw new Error('later tab should not be inspected') + } + }) + + const lookup = createLazyTerminalTabLookup(workspaceSession({ worktree: [first, later] }, {})) + + expect(lookup.get('first')).toBe(first) + }) + + it('resumes scanning and makes later misses constant-time', () => { + const reads: string[] = [] + const tabs = ['first', 'second', 'third'].map((id) => { + const tab = terminalTab(id, 'worktree', null) + Object.defineProperty(tab, 'id', { + enumerable: true, + get: () => { + reads.push(id) + return id + } + }) + return tab + }) + const lookup = createLazyTerminalTabLookup(workspaceSession({ worktree: tabs }, {})) + + expect(lookup.get('first')).toBe(tabs[0]) + expect(reads).toEqual(['first']) + expect(lookup.get('second')).toBe(tabs[1]) + expect(reads).toEqual(['first', 'second']) + expect(lookup.get('missing')).toBeUndefined() + expect(reads).toEqual(['first', 'second', 'third']) + expect(lookup.get('missing')).toBeUndefined() + expect(reads).toEqual(['first', 'second', 'third']) + }) + + it('skips sparse tab-array holes', () => { + const tabs: TerminalTab[] = [] + tabs.length = 3 + tabs[1] = terminalTab('target', 'worktree', null) + const lookup = createLazyTerminalTabLookup(workspaceSession({ worktree: tabs }, {})) + + expect(lookup.get('target')).toBe(tabs[1]) + expect(lookup.get('missing')).toBeUndefined() + }) + + it('does not read tab ids when every layout is skipped', () => { + const tab = terminalTab('skipped', 'worktree', null) + let idReads = 0 + Object.defineProperty(tab, 'id', { + enumerable: true, + get: () => { + idReads += 1 + return 'skipped' + } + }) + + normalizeWorkspaceSessionPaneIdentities( + workspaceSession({ worktree: [tab] }, { skipped: legacyLayout() }), + {}, + { skipAliasTabIds: new Set(['skipped']) } + ) + + expect(idReads).toBe(0) + }) +}) diff --git a/src/main/persistence/restoring-sessions/workspace-pane-normalization.ts b/src/main/persistence/restoring-sessions/workspace-pane-normalization.ts index a977134bd0e..2bfafebae9f 100644 --- a/src/main/persistence/restoring-sessions/workspace-pane-normalization.ts +++ b/src/main/persistence/restoring-sessions/workspace-pane-normalization.ts @@ -10,7 +10,10 @@ import { import type { SshRemotePtyLease } from '../../../shared/ssh-types' import { isTerminalLeafId, makePaneKey, parsePaneKey } from '../../../shared/stable-pane-id' import { findCrossHostPaneTabIds, withoutPaneTabIds } from './cross-host-pane-tab-ids' -import { registerLegacyPaneKeyAliasesForTab } from './pane-identity-migration' +import { + createLazyTerminalTabLookup, + registerLegacyPaneKeyAliasesForTab +} from './pane-identity-migration' import { normalizeTerminalLayoutSnapshotForPersistence } from './terminal-layout-normalization' import { legacyMigrationUnsupportedRowsToAliasEntries, @@ -40,6 +43,7 @@ export function normalizeWorkspaceSessionPaneIdentities( const migrationUnsupportedEntries: MigrationUnsupportedPtyEntry[] = [] const legacyPaneKeyAliasEntries: LegacyPaneKeyAliasEntry[] = [] const terminalLayoutsByTabId: Record = {} + let tabsById: ReturnType | null = null for (const [tabId, layout] of Object.entries(session.terminalLayoutsByTabId ?? {})) { const normalized = normalizeTerminalLayoutSnapshotForPersistence( layout, @@ -48,9 +52,10 @@ export function normalizeWorkspaceSessionPaneIdentities( terminalLayoutsByTabId[tabId] = normalized.snapshot leafIdByInputLeafIdByTabId.set(tabId, normalized.leafIdByInputLeafId) if (!options.skipAliasTabIds?.has(tabId)) { + tabsById ??= createLazyTerminalTabLookup(session) const tabAliasEntries = registerLegacyPaneKeyAliasesForTab({ - session, tabId, + tab: tabsById.get(tabId), inputLayout: layout, normalizedLayout: normalized.snapshot, leafIdByInputLeafId: normalized.leafIdByInputLeafId From eb6cd6721965f959476cedc550a0fb2e69fa715e Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:01:27 -0700 Subject: [PATCH 20/80] perf(editor): skip unchanged review-note reflows (#17212) --- .../DiffCommentCard.resize.test.tsx | 51 ++++++++++++++++++- .../diff-comments/DiffCommentCard.tsx | 18 ++++--- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/diff-comments/DiffCommentCard.resize.test.tsx b/src/renderer/src/components/diff-comments/DiffCommentCard.resize.test.tsx index f17dfbb7613..648b33357d4 100644 --- a/src/renderer/src/components/diff-comments/DiffCommentCard.resize.test.tsx +++ b/src/renderer/src/components/diff-comments/DiffCommentCard.resize.test.tsx @@ -1,5 +1,5 @@ // @vitest-environment happy-dom -import { act, cleanup, render } from '@testing-library/react' +import { act, cleanup, fireEvent, render } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { DiffCommentCard } from './DiffCommentCard' @@ -44,6 +44,7 @@ describe('DiffCommentCard content resize', () => { afterEach(() => { cleanup() + vi.restoreAllMocks() vi.unstubAllGlobals() }) @@ -101,4 +102,52 @@ describe('DiffCommentCard content resize', () => { expect(constructObserver).not.toHaveBeenCalled() expect(onContentResize).not.toHaveBeenCalled() }) + + it('requests rail reflow only when the editing textarea height changes', () => { + const onContentResize = vi.fn() + let scrollHeight = 60 + vi.spyOn(HTMLTextAreaElement.prototype, 'scrollHeight', 'get').mockImplementation( + () => scrollHeight + ) + const view = render( + true} + /> + ) + + fireEvent.click(view.getByRole('button', { name: 'Edit note' })) + expect(onContentResize).toHaveBeenCalledOnce() + + const textarea = view.getByRole('textbox') + expect(textarea.style.height).toBe('60px') + fireEvent.change(textarea, { target: { value: 'A saved note!' } }) + expect(onContentResize).toHaveBeenCalledOnce() + + for (let index = 0; index < 50; index++) { + fireEvent.change(textarea, { target: { value: `A saved note! ${index}` } }) + } + expect(textarea.style.height).toBe('60px') + expect(onContentResize).toHaveBeenCalledOnce() + + scrollHeight = 400 + fireEvent.change(textarea, { target: { value: 'A much taller saved note' } }) + expect(textarea.style.height).toBe('240px') + expect(onContentResize).toHaveBeenCalledTimes(2) + + scrollHeight = 480 + fireEvent.change(textarea, { target: { value: 'A still taller saved note' } }) + expect(textarea.style.height).toBe('240px') + expect(onContentResize).toHaveBeenCalledTimes(2) + + scrollHeight = 80 + fireEvent.change(textarea, { target: { value: 'A shorter saved note' } }) + expect(textarea.style.height).toBe('80px') + expect(onContentResize).toHaveBeenCalledTimes(3) + + fireEvent.click(view.getByRole('button', { name: 'Cancel' })) + expect(onContentResize).toHaveBeenCalledTimes(4) + }) }) diff --git a/src/renderer/src/components/diff-comments/DiffCommentCard.tsx b/src/renderer/src/components/diff-comments/DiffCommentCard.tsx index 9e7ddf229ce..6becfe1fb34 100644 --- a/src/renderer/src/components/diff-comments/DiffCommentCard.tsx +++ b/src/renderer/src/components/diff-comments/DiffCommentCard.tsx @@ -35,6 +35,13 @@ type Props = { headerActions?: ReactNode } +function resizeDiffCommentTextarea(textarea: HTMLTextAreaElement): boolean { + const previousHeight = textarea.style.height + textarea.style.height = 'auto' + textarea.style.height = `${Math.min(textarea.scrollHeight, 240)}px` + return textarea.style.height !== previousHeight +} + export function DiffCommentCard({ lineNumber, startLine, @@ -116,8 +123,7 @@ export function DiffCommentCard({ if (!el) { return } - el.style.height = 'auto' - el.style.height = `${Math.min(el.scrollHeight, 240)}px` + resizeDiffCommentTextarea(el) el.focus() el.setSelectionRange(el.value.length, el.value.length) onContentResizeRef.current?.() @@ -281,10 +287,10 @@ export function DiffCommentCard({ value={draft} onChange={(e) => { setDraft(e.target.value) - const el = e.currentTarget - el.style.height = 'auto' - el.style.height = `${Math.min(el.scrollHeight, 240)}px` - onContentResizeRef.current?.() + // Why: rich-review layout measures every note; skip it when this card stayed put. + if (resizeDiffCommentTextarea(e.currentTarget)) { + onContentResizeRef.current?.() + } }} onKeyDown={(e) => { if (e.key === 'Escape') { From 7879dead69b1101301c9b861e494702800ee8220 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:01:29 -0700 Subject: [PATCH 21/80] perf(ssh): cache legacy worktree clean fallback (#17213) --- .../ssh-git-provider-worktree.test.ts | 225 ++++++++++++++++-- .../providers/ssh-git-worktree-provider.ts | 69 +++--- 2 files changed, 247 insertions(+), 47 deletions(-) diff --git a/src/main/providers/ssh-git-provider-worktree.test.ts b/src/main/providers/ssh-git-provider-worktree.test.ts index 10f36acc8fd..6ddff0a1f68 100644 --- a/src/main/providers/ssh-git-provider-worktree.test.ts +++ b/src/main/providers/ssh-git-provider-worktree.test.ts @@ -1,6 +1,16 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' import { SshGitProvider } from './ssh-git-provider' -import { createMockMux, type MockMultiplexer } from './ssh-git-provider-test-harness' +import { + createMockMux, + waitForRequestCount, + type MockMultiplexer +} from './ssh-git-provider-test-harness' + +function methodNotFound(method: string): Error & { code: number } { + return Object.assign(new Error(`Method not found: ${method}`), { code: -32601 }) +} + +const CLEAN_STATUS = { entries: [], conflictOperation: 'unknown' } describe('SshGitProvider', () => { let mux: MockMultiplexer @@ -67,6 +77,40 @@ describe('SshGitProvider', () => { expect(result).toEqual(cleanResult) }) + it('keeps supported clean checks on the preferred relay RPC', async () => { + let resolveProbe!: (result: { clean: boolean }) => void + let resolveRemaining!: (result: { clean: boolean }) => void + const probe = new Promise<{ clean: boolean }>((resolve) => { + resolveProbe = resolve + }) + const remaining = new Promise<{ clean: boolean }>((resolve) => { + resolveRemaining = resolve + }) + let preferredRequestCount = 0 + mux.request.mockImplementation((method) => { + expect(method).toBe('git.worktreeIsClean') + preferredRequestCount += 1 + return preferredRequestCount === 1 ? probe : remaining + }) + const checks = Array.from({ length: 10 }, (_, index) => + provider.worktreeIsClean(`/repo/worktree-${index}`) + ) + + await waitForRequestCount(mux.request, 1) + expect(mux.request).toHaveBeenCalledOnce() + resolveProbe({ clean: true }) + await waitForRequestCount(mux.request, 10) + expect(mux.request).toHaveBeenCalledTimes(10) + resolveRemaining({ clean: true }) + await expect(Promise.all(checks)).resolves.toEqual( + Array.from({ length: 10 }, () => ({ clean: true })) + ) + expect( + mux.request.mock.calls.filter(([method]) => method === 'git.worktreeIsClean') + ).toHaveLength(10) + expect(mux.request.mock.calls.some(([method]) => method === 'git.status')).toBe(false) + }) + it('worktreeIsClean can ignore untracked files', async () => { const cleanResult = { clean: true } mux.request.mockResolvedValue(cleanResult) @@ -113,10 +157,7 @@ describe('SshGitProvider', () => { }) it('worktreeIsClean falls back to git.status for old relays', async () => { - const methodNotFound = Object.assign(new Error('Method not found: git.worktreeIsClean'), { - code: -32601 - }) - mux.request.mockRejectedValueOnce(methodNotFound).mockResolvedValueOnce({ + mux.request.mockRejectedValueOnce(methodNotFound('git.worktreeIsClean')).mockResolvedValueOnce({ entries: [{ path: 'scratch.txt', status: 'untracked', area: 'untracked' }], conflictOperation: 'unknown' }) @@ -140,26 +181,180 @@ describe('SshGitProvider', () => { } }) - it('worktreeIsClean filters untracked entries in old-relay fallback', async () => { - const methodNotFound = Object.assign(new Error('Method not found: git.worktreeIsClean'), { - code: -32601 + it('probes an old relay once across sequential clean checks', async () => { + mux.request.mockImplementation(async (method) => { + if (method === 'git.worktreeIsClean') { + throw methodNotFound(method) + } + return CLEAN_STATUS }) - mux.request.mockRejectedValueOnce(methodNotFound).mockResolvedValueOnce({ - entries: [{ path: 'scratch.txt', status: 'untracked', area: 'untracked' }], - conflictOperation: 'unknown' + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const worktreePaths = Array.from({ length: 10 }, (_, index) => `/repo/worktree-${index}`) + + try { + for (const worktreePath of worktreePaths) { + await expect(provider.worktreeIsClean(worktreePath)).resolves.toEqual({ clean: true }) + } + + expect( + mux.request.mock.calls.filter(([method]) => method === 'git.worktreeIsClean') + ).toHaveLength(1) + expect(mux.request.mock.calls.filter(([method]) => method === 'git.status')).toHaveLength(10) + expect(mux.request).toHaveBeenCalledTimes(11) + expect(warnSpy).toHaveBeenCalledOnce() + } finally { + warnSpy.mockRestore() + } + }) + + it('shares one old-relay probe across concurrent clean checks', async () => { + let rejectProbe!: (error: Error) => void + const probe = new Promise((_resolve, reject) => { + rejectProbe = reject + }) + mux.request.mockImplementation((method) => { + if (method === 'git.worktreeIsClean') { + return probe + } + return Promise.resolve(CLEAN_STATUS) + }) + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const checks = Array.from({ length: 10 }, (_, index) => + provider.worktreeIsClean(`/repo/worktree-${index}`) + ) + + try { + await waitForRequestCount(mux.request, 1) + rejectProbe(methodNotFound('git.worktreeIsClean')) + await expect(Promise.all(checks)).resolves.toEqual( + Array.from({ length: 10 }, () => ({ clean: true })) + ) + + expect( + mux.request.mock.calls.filter(([method]) => method === 'git.worktreeIsClean') + ).toHaveLength(1) + expect(mux.request.mock.calls.filter(([method]) => method === 'git.status')).toHaveLength(10) + expect(mux.request).toHaveBeenCalledTimes(11) + expect(warnSpy).toHaveBeenCalledOnce() + } finally { + warnSpy.mockRestore() + } + }) + + it('retries the preferred clean RPC after a transport failure', async () => { + const transportError = Object.assign(new Error('Method not found: git.worktreeIsClean'), { + code: -32602 + }) + mux.request.mockRejectedValueOnce(transportError).mockResolvedValueOnce({ clean: true }) + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + try { + await expect(provider.worktreeIsClean('/repo/first')).rejects.toBe(transportError) + await expect(provider.worktreeIsClean('/repo/second')).resolves.toEqual({ clean: true }) + expect(mux.request.mock.calls.map(([method]) => method)).toEqual([ + 'git.worktreeIsClean', + 'git.worktreeIsClean' + ]) + expect(warnSpy).not.toHaveBeenCalled() + } finally { + warnSpy.mockRestore() + } + }) + + it('lets concurrent waiters retry after a transient probe failure', async () => { + const transportError = new Error('connection closed') + let rejectProbe!: (error: Error) => void + let resolveRetries!: (result: { clean: boolean }) => void + const probe = new Promise((_resolve, reject) => { + rejectProbe = reject + }) + const retries = new Promise<{ clean: boolean }>((resolve) => { + resolveRetries = resolve + }) + let preferredRequestCount = 0 + mux.request.mockImplementation((method) => { + expect(method).toBe('git.worktreeIsClean') + preferredRequestCount += 1 + return preferredRequestCount === 1 ? probe : retries + }) + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const checks = Array.from({ length: 3 }, (_, index) => + provider.worktreeIsClean(`/repo/worktree-${index}`) + ) + const settledChecks = Promise.allSettled(checks) + + try { + await waitForRequestCount(mux.request, 1) + rejectProbe(transportError) + await waitForRequestCount(mux.request, 3) + resolveRetries({ clean: true }) + + const results = await settledChecks + expect(results[0]).toEqual({ status: 'rejected', reason: transportError }) + expect(results.slice(1)).toEqual([ + { status: 'fulfilled', value: { clean: true } }, + { status: 'fulfilled', value: { clean: true } } + ]) + await expect(provider.worktreeIsClean('/repo/later')).resolves.toEqual({ clean: true }) + expect(mux.request).toHaveBeenCalledTimes(4) + expect(warnSpy).not.toHaveBeenCalled() + } finally { + warnSpy.mockRestore() + } + }) + + it('re-probes clean support after the provider is replaced', async () => { + mux.request.mockImplementation(async (method) => { + if (method === 'git.worktreeIsClean') { + throw methodNotFound(method) + } + return CLEAN_STATUS }) const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) try { + await expect(provider.worktreeIsClean('/repo/first')).resolves.toEqual({ clean: true }) + const replacement = new SshGitProvider('conn-1', mux as never) + await expect(replacement.worktreeIsClean('/repo/second')).resolves.toEqual({ clean: true }) + + expect( + mux.request.mock.calls.filter(([method]) => method === 'git.worktreeIsClean') + ).toHaveLength(2) + expect(warnSpy).toHaveBeenCalledTimes(2) + } finally { + warnSpy.mockRestore() + } + }) + + it('worktreeIsClean filters untracked entries in old-relay fallback', async () => { + mux.request.mockImplementation(async (method) => { + if (method === 'git.worktreeIsClean') { + throw methodNotFound(method) + } + return { + entries: [{ path: 'scratch.txt', status: 'untracked', area: 'untracked' }], + conflictOperation: 'unknown' + } + }) + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + try { + await expect(provider.worktreeIsClean('/home/user/feat')).resolves.toEqual({ + clean: false, + stdout: 'untracked untracked: scratch.txt' + }) await expect( provider.worktreeIsClean('/home/user/feat', { includeUntracked: false }) ).resolves.toEqual({ clean: true }) expect(mux.request).toHaveBeenNthCalledWith( - 2, + 3, 'git.status', { worktreePath: '/home/user/feat' }, { signal: expect.any(AbortSignal) } ) + expect( + mux.request.mock.calls.filter(([method]) => method === 'git.worktreeIsClean') + ).toHaveLength(1) } finally { warnSpy.mockRestore() } @@ -183,11 +378,7 @@ describe('SshGitProvider', () => { }) it('forceDeletePreservedBranch maps old relays to the reconnect message', async () => { - const methodNotFound = Object.assign( - new Error('Method not found: git.forceDeletePreservedBranch'), - { code: -32601 } - ) - mux.request.mockRejectedValueOnce(methodNotFound) + mux.request.mockRejectedValueOnce(methodNotFound('git.forceDeletePreservedBranch')) await expect( provider.forceDeletePreservedBranch('/home/user/repo', 'you/fix-auth', 'abc123') diff --git a/src/main/providers/ssh-git-worktree-provider.ts b/src/main/providers/ssh-git-worktree-provider.ts index ce509ccf847..d5cf9b5a5e1 100644 --- a/src/main/providers/ssh-git-worktree-provider.ts +++ b/src/main/providers/ssh-git-worktree-provider.ts @@ -1,9 +1,12 @@ import type { GitStatusResult } from '../../shared/git-status-types' import type { RemoveWorktreeResult } from '../../shared/worktree/create-types' import type { GitWorktreeInfo } from '../../shared/worktree/types' +import { CapabilityProbeCache } from '../../shared/capability-probe-cache' import { isJsonRpcMethodNotFoundError } from './ssh-git-relay-errors' import { SshGitReviewHeadProvider } from './ssh-git-review-head-provider' +const WORKTREE_IS_CLEAN_CAPABILITY = 'git.worktreeIsClean' as const + function formatStatusEntriesForCleanCheck(entries: GitStatusResult['entries']): string | undefined { if (entries.length === 0) { return undefined @@ -20,6 +23,10 @@ function filterUntrackedPorcelainStatus(stdout: string | undefined): string | un export class SshGitWorktreeProvider extends SshGitReviewHeadProvider { private loggedWorktreeIsCleanFallback = false + // Why: reconnect replaces this provider, so an upgraded relay is naturally re-probed. + private readonly worktreeIsCleanCapabilityCache = new CapabilityProbeCache< + typeof WORKTREE_IS_CLEAN_CAPABILITY + >(Number.POSITIVE_INFINITY) async listWorktrees( repoPath: string, @@ -67,37 +74,39 @@ export class SshGitWorktreeProvider extends SshGitReviewHeadProvider { worktreePath: string, options: { includeUntracked?: boolean } = {} ): Promise<{ clean: boolean; stdout?: string }> { - try { - const result = (await this.mux.request('git.worktreeIsClean', { - worktreePath, - ...(options.includeUntracked === false ? { includeUntracked: false } : {}) - })) as { clean: boolean; stdout?: string } - if (options.includeUntracked === false) { - if (!result.clean && result.stdout === undefined) { - return result + return this.worktreeIsCleanCapabilityCache.runWithFallback( + WORKTREE_IS_CLEAN_CAPABILITY, + async () => { + const result = (await this.mux.request('git.worktreeIsClean', { + worktreePath, + ...(options.includeUntracked === false ? { includeUntracked: false } : {}) + })) as { clean: boolean; stdout?: string } + if (options.includeUntracked === false) { + if (!result.clean && result.stdout === undefined) { + return result + } + const trackedStdout = filterUntrackedPorcelainStatus(result.stdout) + return { clean: !trackedStdout, ...(trackedStdout ? { stdout: trackedStdout } : {}) } } - const trackedStdout = filterUntrackedPorcelainStatus(result.stdout) - return { clean: !trackedStdout, ...(trackedStdout ? { stdout: trackedStdout } : {}) } - } - return result - } catch (error) { - if (!isJsonRpcMethodNotFoundError(error)) { - throw error - } - if (!this.loggedWorktreeIsCleanFallback) { - this.loggedWorktreeIsCleanFallback = true - console.warn( - '[ssh-git] Relay does not implement git.worktreeIsClean; falling back to git.status clean check' - ) - } - const status = await this.getStatus(worktreePath) - const entries = - options.includeUntracked === false - ? status.entries.filter((entry) => entry.area !== 'untracked') - : status.entries - const clean = entries.length === 0 - return { clean, stdout: formatStatusEntriesForCleanCheck(entries) } - } + return result + }, + async () => { + if (!this.loggedWorktreeIsCleanFallback) { + this.loggedWorktreeIsCleanFallback = true + console.warn( + '[ssh-git] Relay does not implement git.worktreeIsClean; falling back to git.status clean check' + ) + } + const status = await this.getStatus(worktreePath) + const entries = + options.includeUntracked === false + ? status.entries.filter((entry) => entry.area !== 'untracked') + : status.entries + const clean = entries.length === 0 + return { clean, stdout: formatStatusEntriesForCleanCheck(entries) } + }, + isJsonRpcMethodNotFoundError + ) } async refreshLocalBaseRefForWorktreeCreate(args: { From ad499df8937e11559909205afe8577a38b49b4ee Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:01:33 -0700 Subject: [PATCH 22/80] perf(markdown): reuse rich eligibility state (#17219) --- ...orContent.markdown-classification.test.tsx | 292 ++++++++++++++++++ .../EditorContent.monaco-lifecycle.test.tsx | 5 + .../components/editor/EditorContent.test.tsx | 4 + .../src/components/editor/EditorContent.tsx | 4 + .../editor/EditorEditFileSurface.tsx | 4 + .../editor/EditorMarkdownFileSurface.tsx | 17 +- .../components/editor/EditorPanelShell.tsx | 1 + .../editor/editor-panel-render-model.ts | 46 ++- .../components/editor/markdown-render-mode.ts | 5 + 9 files changed, 355 insertions(+), 23 deletions(-) create mode 100644 src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx diff --git a/src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx b/src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx new file mode 100644 index 00000000000..7d8767071be --- /dev/null +++ b/src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx @@ -0,0 +1,292 @@ +// @vitest-environment happy-dom +import { cleanup, render } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { OpenFile } from '@/store/slices/editor' +import type { FileContent } from './editor-panel-content-types' + +const classifiers = vi.hoisted(() => ({ + getUnsupportedMessage: vi.fn<(content: string) => string | null>(), + exceedsSizeLimit: vi.fn<(content: string) => boolean>() +})) + +vi.mock('./markdown-rich-mode', () => ({ + getMarkdownRichModeUnsupportedMessage: classifiers.getUnsupportedMessage +})) + +vi.mock('./markdown-rich-size-limit', () => ({ + exceedsMarkdownRichModeSizeLimit: classifiers.exceedsSizeLimit +})) + +vi.mock('./editor-lazy-views', () => { + const view = (name: string) => () =>
+ return { + MonacoEditor: view('source'), + DiffViewer: view('diff'), + CombinedDiffViewer: view('combined-diff'), + RichMarkdownEditor: view('rich-editor'), + MarkdownPreview: view('preview'), + ImageViewer: view('image'), + ImageDiffViewer: view('image-diff'), + MermaidViewer: view('mermaid'), + CsvViewer: view('csv'), + IpynbViewer: view('notebook') + } +}) + +vi.mock('./RichMarkdownErrorBoundary', () => ({ + RichMarkdownErrorBoundary: ({ children }: { children: React.ReactNode }) => children +})) + +vi.mock('./useMarkdownDocuments', () => ({ + useMarkdownDocuments: () => ({ + markdownDocuments: [], + onOpenDocLink: () => {}, + previewProps: { markdownDocuments: [], onOpenDocument: async () => {} }, + mdSave: async () => true + }) +})) + +vi.mock('./useEditorConflictNavigation', () => ({ + useEditorConflictNavigation: () => () => undefined +})) + +vi.mock('@/store', () => { + const state = { + markdownRichModeSizeOverride: {}, + setMarkdownRichModeSizeOverride: () => {}, + reloadOpenCheckRunDetailsTab: () => {} + } + return { + useAppStore: Object.assign( + (selector: (storeState: typeof state) => unknown) => selector(state), + { getState: () => state } + ) + } +}) + +import { EditorContent } from './EditorContent' +import { getEditorPanelRenderModel } from './editor-panel-render-model' + +function openFile( + language: 'markdown' | 'typescript' = 'markdown', + mode: 'edit' | 'markdown-preview' = 'edit' +): OpenFile { + const extension = language === 'markdown' ? 'md' : 'ts' + return { + id: `/repo/notes.${extension}`, + filePath: `/repo/notes.${extension}`, + relativePath: `notes.${extension}`, + worktreeId: 'wt-1', + language, + mode, + isDirty: false + } +} + +function renderEditPath({ + content, + language = 'markdown', + viewMode = 'rich', + mode = 'edit' +}: { + content: string + language?: 'markdown' | 'typescript' + viewMode?: 'source' | 'rich' | 'preview' + mode?: 'edit' | 'markdown-preview' +}) { + const activeFile = openFile(language, mode) + const fileContents = { + [activeFile.id]: { content: '# Saved', isBinary: false } + } + const editorDrafts = { [activeFile.id]: content } + const model = getEditorPanelRenderModel({ + activeFile, + fileContents, + editorDrafts, + gitStatusEntries: undefined, + gitBranchEntries: undefined, + markdownViewMode: { [activeFile.id]: viewMode }, + markdownRichModeSizeOverride: {}, + isChangesMode: false, + canOpenWorkspaceFileBrowser: true + }) + const view = render( + + ) + + return { model, view } +} + +function getGuardedRenderModel({ + activeFileOverrides, + fileContent = { content: '# Saved', isBinary: false }, + includeFileContent = true, + isChangesMode = false +}: { + activeFileOverrides?: Partial + fileContent?: FileContent + includeFileContent?: boolean + isChangesMode?: boolean +}) { + const activeFile = { ...openFile(), ...activeFileOverrides } + return getEditorPanelRenderModel({ + activeFile, + fileContents: includeFileContent ? { [activeFile.id]: fileContent } : {}, + editorDrafts: { [activeFile.id]: '# Draft' }, + gitStatusEntries: undefined, + gitBranchEntries: undefined, + markdownViewMode: { [activeFile.id]: 'rich' }, + markdownRichModeSizeOverride: {}, + isChangesMode, + canOpenWorkspaceFileBrowser: true + }) +} + +beforeEach(() => { + classifiers.getUnsupportedMessage.mockImplementation((content) => + content.includes('[reference]:') ? 'Reference links require source mode.' : null + ) + classifiers.exceedsSizeLimit.mockImplementation((content) => content.includes('oversized')) +}) + +afterEach(() => { + cleanup() + vi.clearAllMocks() +}) + +describe('inline Markdown render classification', () => { + it('classifies each rich draft once, including after content changes', () => { + const first = renderEditPath({ content: '# First draft' }) + + expect(first.view.container.innerHTML).toContain('data-editor-view="rich-editor"') + expect(classifiers.getUnsupportedMessage).toHaveBeenCalledTimes(1) + expect(classifiers.getUnsupportedMessage).toHaveBeenLastCalledWith('# First draft') + expect(classifiers.exceedsSizeLimit).toHaveBeenCalledTimes(1) + expect(classifiers.exceedsSizeLimit).toHaveBeenLastCalledWith('# First draft') + + first.view.unmount() + const second = renderEditPath({ content: '# Changed draft' }) + + expect(second.view.container.innerHTML).toContain('data-editor-view="rich-editor"') + expect(classifiers.getUnsupportedMessage).toHaveBeenCalledTimes(2) + expect(classifiers.getUnsupportedMessage).toHaveBeenLastCalledWith('# Changed draft') + expect(classifiers.exceedsSizeLimit).toHaveBeenCalledTimes(2) + expect(classifiers.exceedsSizeLimit).toHaveBeenLastCalledWith('# Changed draft') + }) + + it.each([ + { + name: 'source Markdown', + args: { content: '# Source', viewMode: 'source' as const }, + expectedView: 'source', + canExport: false + }, + { + name: 'Markdown preview', + args: { content: '# Preview', mode: 'markdown-preview' as const }, + expectedView: 'preview', + canExport: true + }, + { + name: 'TypeScript', + args: { content: 'const value = 1', language: 'typescript' as const }, + expectedView: 'source', + canExport: false + } + ])('skips rich eligibility scans for $name', ({ args, expectedView, canExport }) => { + const result = renderEditPath(args) + + expect(result.view.container.innerHTML).toContain(`data-editor-view="${expectedView}"`) + expect(result.model.canExportMarkdownToPdf).toBe(canExport) + expect(classifiers.getUnsupportedMessage).not.toHaveBeenCalled() + expect(classifiers.exceedsSizeLimit).not.toHaveBeenCalled() + }) + + it.each([ + { name: 'Changes mode', args: { isChangesMode: true } }, + { name: 'content that is still loading', args: { includeFileContent: false } }, + { + name: 'binary content', + args: { fileContent: { content: '', isBinary: true } } + }, + { + name: 'content with a load error', + args: { fileContent: { content: '', isBinary: false, loadError: 'Read failed' } } + }, + { + name: 'read-only Markdown', + args: { activeFileOverrides: { readOnly: true } } + }, + { + name: 'an unresolved conflict', + args: { + activeFileOverrides: { + conflict: { + kind: 'conflict-editable' as const, + conflictKind: 'both_modified' as const, + conflictStatus: 'unresolved' as const, + conflictStatusSource: 'git' as const + } + } + } + }, + { + name: 'a conflict placeholder', + args: { + activeFileOverrides: { + conflict: { + kind: 'conflict-placeholder' as const, + conflictKind: 'both_deleted' as const, + conflictStatus: 'unresolved' as const, + conflictStatusSource: 'git' as const + } + } + } + } + ])('skips rich eligibility scans for $name', ({ args }) => { + const model = getGuardedRenderModel(args) + + expect(model.inlineMarkdownRenderState).toBeNull() + expect(classifiers.getUnsupportedMessage).not.toHaveBeenCalled() + expect(classifiers.exceedsSizeLimit).not.toHaveBeenCalled() + }) + + it('preserves unsupported and oversized rich-mode fallbacks', () => { + const unsupported = renderEditPath({ content: '[reference]: https://example.com' }) + + expect(unsupported.model.canExportMarkdownToPdf).toBe(false) + expect(unsupported.view.getByText('Reference links require source mode.')).toBeTruthy() + expect(unsupported.view.queryByText('Open anyway')).toBeNull() + unsupported.view.unmount() + + const oversized = renderEditPath({ content: '# oversized' }) + + expect(oversized.model.canExportMarkdownToPdf).toBe(false) + expect(oversized.view.getByText(/File is larger than the .* rich editing limit/)).toBeTruthy() + expect(oversized.view.getByText('Open anyway')).toBeTruthy() + }) +}) diff --git a/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx b/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx index 53eec01d0d8..00ded9527d2 100644 --- a/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx +++ b/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx @@ -184,6 +184,7 @@ function props(activeFile: OpenFile, content: string) { isCsv: false, isNotebook: false, mdViewMode: 'rich' as const, + inlineMarkdownRenderState: null, isChangesMode: false, sideBySide: false, pendingEditorReveal: null, @@ -326,6 +327,10 @@ describe('EditorContent Monaco lifecycle boundary', () => { resolvedLanguage="markdown" isMarkdown mdViewMode="rich" + inlineMarkdownRenderState={{ + renderMode: 'rich-editor', + richModeUnsupportedMessage: null + }} /> ) diff --git a/src/renderer/src/components/editor/EditorContent.test.tsx b/src/renderer/src/components/editor/EditorContent.test.tsx index 130c92abe35..8086851a81c 100644 --- a/src/renderer/src/components/editor/EditorContent.test.tsx +++ b/src/renderer/src/components/editor/EditorContent.test.tsx @@ -67,6 +67,7 @@ describe('EditorContent', () => { isCsv={false} isNotebook mdViewMode="rich" + inlineMarkdownRenderState={null} isChangesMode={false} sideBySide={false} pendingEditorReveal={null} @@ -108,6 +109,7 @@ describe('EditorContent', () => { isCsv={false} isNotebook={false} mdViewMode="rich" + inlineMarkdownRenderState={null} isChangesMode={false} sideBySide={false} pendingEditorReveal={null} @@ -153,6 +155,7 @@ describe('EditorContent', () => { isCsv={false} isNotebook={false} mdViewMode="rich" + inlineMarkdownRenderState={null} isChangesMode={false} sideBySide={false} pendingEditorReveal={null} @@ -200,6 +203,7 @@ describe('EditorContent', () => { isCsv={false} isNotebook={false} mdViewMode="preview" + inlineMarkdownRenderState={null} isChangesMode={false} sideBySide={false} pendingEditorReveal={null} diff --git a/src/renderer/src/components/editor/EditorContent.tsx b/src/renderer/src/components/editor/EditorContent.tsx index ca12823e846..9fab1cd5665 100644 --- a/src/renderer/src/components/editor/EditorContent.tsx +++ b/src/renderer/src/components/editor/EditorContent.tsx @@ -12,6 +12,7 @@ import type { FileContent } from './editor-panel-content-types' import { translate } from '@/i18n/i18n' import { useEditorConflictNavigation } from './useEditorConflictNavigation' import { useMarkdownDocuments } from './useMarkdownDocuments' +import type { MarkdownRenderState } from './markdown-render-mode' const noopCloseMarkdownTableOfContents = (): void => {} @@ -47,6 +48,7 @@ export function EditorContent({ isCsv, isNotebook, mdViewMode, + inlineMarkdownRenderState, isChangesMode, sideBySide, showMarkdownTableOfContents = false, @@ -74,6 +76,7 @@ export function EditorContent({ isCsv: boolean isNotebook: boolean mdViewMode: MarkdownViewMode + inlineMarkdownRenderState: MarkdownRenderState | null isChangesMode: boolean sideBySide: boolean showMarkdownTableOfContents?: boolean @@ -238,6 +241,7 @@ export function EditorContent({ isCsv={isCsv} isNotebook={isNotebook} mdViewMode={mdViewMode} + inlineMarkdownRenderState={inlineMarkdownRenderState} isChangesMode={isChangesMode} sideBySide={sideBySide} showMarkdownTableOfContents={showMarkdownTableOfContents} diff --git a/src/renderer/src/components/editor/EditorEditFileSurface.tsx b/src/renderer/src/components/editor/EditorEditFileSurface.tsx index d0b83539ee6..06a7bb3f909 100644 --- a/src/renderer/src/components/editor/EditorEditFileSurface.tsx +++ b/src/renderer/src/components/editor/EditorEditFileSurface.tsx @@ -17,6 +17,7 @@ import type { FileContent } from './editor-panel-content-types' import { ExternalFileChangeBanner } from './ExternalFileChangeBanner' import type { useMarkdownDocuments } from './useMarkdownDocuments' import { EditorMarkdownFileSurface } from './EditorMarkdownFileSurface' +import type { MarkdownRenderState } from './markdown-render-mode' const noopEditorContentChange = (_content: string): void => {} const noopEditorSave = async (_content: string): Promise => false @@ -39,6 +40,7 @@ export function EditorEditFileSurface({ isCsv, isNotebook, mdViewMode, + inlineMarkdownRenderState, isChangesMode, sideBySide, showMarkdownTableOfContents, @@ -69,6 +71,7 @@ export function EditorEditFileSurface({ isCsv: boolean isNotebook: boolean mdViewMode: MarkdownViewMode + inlineMarkdownRenderState: MarkdownRenderState | null isChangesMode: boolean sideBySide: boolean showMarkdownTableOfContents: boolean @@ -210,6 +213,7 @@ export function EditorEditFileSurface({ editorViewStateKey={editorViewStateKey} currentContent={currentContent} mdViewMode={mdViewMode} + inlineMarkdownRenderState={inlineMarkdownRenderState} showMarkdownTableOfContents={showMarkdownTableOfContents} showMarkdownFrontmatter={showMarkdownFrontmatter} onCloseMarkdownTableOfContents={onCloseMarkdownTableOfContents} diff --git a/src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx b/src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx index 6d5cfea1bdf..fda81b98462 100644 --- a/src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx +++ b/src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx @@ -5,10 +5,8 @@ import { Button } from '@/components/ui/button' import { RICH_MARKDOWN_MAX_SIZE_BYTES } from '../../../../shared/constants' import { formatBytes } from '../status-bar/workspace-space-format' import { MarkdownPreview, RichMarkdownEditor } from './editor-lazy-views' -import { exceedsMarkdownRichModeSizeLimit } from './markdown-rich-size-limit' import { extractFrontMatter, prependFrontMatter } from './markdown-frontmatter' -import { getMarkdownRenderMode } from './markdown-render-mode' -import { getMarkdownRichModeUnsupportedMessage } from './markdown-rich-mode' +import type { MarkdownRenderState } from './markdown-render-mode' import { RichMarkdownErrorBoundary } from './RichMarkdownErrorBoundary' import type { useMarkdownDocuments } from './useMarkdownDocuments' @@ -20,6 +18,7 @@ export function EditorMarkdownFileSurface({ editorViewStateKey, currentContent, mdViewMode, + inlineMarkdownRenderState, showMarkdownTableOfContents, showMarkdownFrontmatter, onCloseMarkdownTableOfContents, @@ -35,6 +34,7 @@ export function EditorMarkdownFileSurface({ editorViewStateKey: string currentContent: string mdViewMode: MarkdownViewMode + inlineMarkdownRenderState: MarkdownRenderState | null showMarkdownTableOfContents: boolean showMarkdownFrontmatter: boolean onCloseMarkdownTableOfContents: () => void @@ -45,18 +45,15 @@ export function EditorMarkdownFileSurface({ handleDirtyStateHint: (dirty: boolean) => void monacoEditor: React.JSX.Element }): React.JSX.Element { - const sizeOverridden = useAppStore((s) => s.markdownRichModeSizeOverride[activeFile.id] === true) const setSizeOverride = useAppStore((s) => s.setMarkdownRichModeSizeOverride) - const richModeUnsupportedMessage = getMarkdownRichModeUnsupportedMessage(currentContent) - const renderMode = getMarkdownRenderMode({ - exceedsRichModeSizeLimit: !sizeOverridden && exceedsMarkdownRichModeSizeLimit(currentContent), - hasRichModeUnsupportedContent: richModeUnsupportedMessage !== null, - viewMode: mdViewMode - }) if (activeFile.conflict?.conflictStatus === 'unresolved') { return
{monacoEditor}
} + if (!inlineMarkdownRenderState) { + return
{monacoEditor}
+ } + const { renderMode, richModeUnsupportedMessage } = inlineMarkdownRenderState if (renderMode === 'source' && mdViewMode === 'rich') { // Why: only a size fallback is recoverable — unsupported syntax would round-trip badly, so it gets no override. const isSizeFallback = richModeUnsupportedMessage === null diff --git a/src/renderer/src/components/editor/EditorPanelShell.tsx b/src/renderer/src/components/editor/EditorPanelShell.tsx index 62b399c3b3c..dd15231cefe 100644 --- a/src/renderer/src/components/editor/EditorPanelShell.tsx +++ b/src/renderer/src/components/editor/EditorPanelShell.tsx @@ -149,6 +149,7 @@ export function EditorPanelShell({ isCsv={model.isCsv} isNotebook={model.isNotebook} mdViewMode={model.mdViewMode} + inlineMarkdownRenderState={model.inlineMarkdownRenderState} isChangesMode={model.isDiffSurface && !model.isSingleDiff} sideBySide={sideBySide} pendingEditorReveal={pendingEditorReveal} diff --git a/src/renderer/src/components/editor/editor-panel-render-model.ts b/src/renderer/src/components/editor/editor-panel-render-model.ts index 2dc9fe87382..8a479794f83 100644 --- a/src/renderer/src/components/editor/editor-panel-render-model.ts +++ b/src/renderer/src/components/editor/editor-panel-render-model.ts @@ -12,7 +12,7 @@ import { getEditorHeaderOpenFileState } from './editor-header' import type { EditorToggleValue } from './EditorViewToggle' import type { FileContent } from './editor-panel-content-types' import { canUseChangesModeForFile } from './editor-panel-file-mode' -import { getMarkdownRenderMode } from './markdown-render-mode' +import { getMarkdownRenderMode, type MarkdownRenderState } from './markdown-render-mode' import { getMarkdownRichModeUnsupportedMessage } from './markdown-rich-mode' import { exceedsMarkdownRichModeSizeLimit } from './markdown-rich-size-limit' @@ -129,17 +129,36 @@ export function getEditorPanelRenderModel({ const shouldShowMarkdownExportAction = viewerLanguage === 'markdown' && (activeFile.mode === 'edit' || activeFile.mode === 'markdown-preview') - const inlineMarkdownRenderMode = - activeFile.mode === 'edit' && inlineMarkdownContent !== null - ? getMarkdownRenderMode({ - exceedsRichModeSizeLimit: - markdownRichModeSizeOverride[activeFile.id] !== true && - exceedsMarkdownRichModeSizeLimit(inlineMarkdownContent), - hasRichModeUnsupportedContent: - getMarkdownRichModeUnsupportedMessage(inlineMarkdownContent) !== null, - viewMode: mdViewMode - }) + const inlineFileContent = activeFile.mode === 'edit' ? fileContents[activeFile.id] : undefined + const canRenderInlineMarkdown = + viewerLanguage === 'markdown' && + activeFile.mode === 'edit' && + inlineMarkdownContent !== null && + !isChangesMode && + inlineFileContent !== undefined && + inlineFileContent.isBinary !== true && + !inlineFileContent.loadError && + activeFile.conflict?.kind !== 'conflict-placeholder' && + activeFile.conflict?.conflictStatus !== 'unresolved' + let inlineMarkdownRenderState: MarkdownRenderState | null = null + if (canRenderInlineMarkdown) { + const shouldClassifyRichMode = mdViewMode === 'rich' + const exceedsRichModeSizeLimit = + shouldClassifyRichMode && + markdownRichModeSizeOverride[activeFile.id] !== true && + exceedsMarkdownRichModeSizeLimit(inlineMarkdownContent) + const richModeUnsupportedMessage = shouldClassifyRichMode + ? getMarkdownRichModeUnsupportedMessage(inlineMarkdownContent) : null + inlineMarkdownRenderState = { + renderMode: getMarkdownRenderMode({ + exceedsRichModeSizeLimit, + hasRichModeUnsupportedContent: richModeUnsupportedMessage !== null, + viewMode: mdViewMode + }), + richModeUnsupportedMessage + } + } const canExportMarkdownToPdf = shouldShowMarkdownExportAction && ((activeFile.mode === 'markdown-preview' && @@ -149,8 +168,8 @@ export function getEditorPanelRenderModel({ (activeFile.mode === 'edit' && fileContents[activeFile.id] !== undefined && !isChangesMode && - inlineMarkdownRenderMode !== null && - inlineMarkdownRenderMode !== 'source' && + inlineMarkdownRenderState !== null && + inlineMarkdownRenderState.renderMode !== 'source' && fileContents[activeFile.id]?.isBinary !== true && !fileContents[activeFile.id]?.loadError && activeFile.conflict?.conflictStatus !== 'unresolved')) @@ -180,6 +199,7 @@ export function getEditorPanelRenderModel({ isMarkdownTableOfContentsDisabled: hasViewModeToggle && mdViewMode === 'source', shouldShowMarkdownExportAction, canExportMarkdownToPdf, + inlineMarkdownRenderState, canShowMarkdownTableOfContents: viewerLanguage === 'markdown' && (hasViewModeToggle || activeFile.mode === 'markdown-preview'), diff --git a/src/renderer/src/components/editor/markdown-render-mode.ts b/src/renderer/src/components/editor/markdown-render-mode.ts index b8cbc499986..f188dcbb814 100644 --- a/src/renderer/src/components/editor/markdown-render-mode.ts +++ b/src/renderer/src/components/editor/markdown-render-mode.ts @@ -2,6 +2,11 @@ import type { MarkdownViewMode } from '@/store/slices/editor' export type MarkdownRenderMode = 'source' | 'rich-editor' | 'preview' +export type MarkdownRenderState = { + renderMode: MarkdownRenderMode + richModeUnsupportedMessage: string | null +} + export function getMarkdownRenderMode({ exceedsRichModeSizeLimit, hasRichModeUnsupportedContent, From 5e3c4f391cc2588728e99d79fea30c92e51c795d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:01:36 -0700 Subject: [PATCH 23/80] perf(terminal): bound stalled setup-split polling (#17228) --- .../terminal-startup-grid-settle.test.ts | 24 +++++++++++++++++++ .../terminal-startup-grid-settle.ts | 15 ++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.test.ts b/src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.test.ts index ebe5064be3c..dd7e56ab328 100644 --- a/src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.test.ts @@ -115,6 +115,30 @@ describe('waitForStableStartupGrid', () => { expect(onSettled).toHaveBeenCalledWith({ cols: 88, rows: 50 }) }) + it('stops polling at the default cap when the external split gate never opens', () => { + const scheduler = createFrameScheduler() + const onSettled = vi.fn() + const measure = vi.fn(() => ({ cols: 88, rows: 50 })) + + waitForStableStartupGrid({ + isAlive: () => true, + isReadyToSettle: () => false, + measure, + onSettled, + requestFrame: scheduler.requestFrame, + cancelFrame: scheduler.cancelFrame + }) + + expect(scheduler.run(119)).toBe(119) + expect(scheduler.pending()).toBe(1) + expect(onSettled).not.toHaveBeenCalled() + + expect(scheduler.run(10)).toBe(1) + expect(scheduler.pending()).toBe(0) + expect(measure).toHaveBeenCalledTimes(120) + expect(onSettled).toHaveBeenCalledWith({ cols: 88, rows: 50 }) + }) + it('uses the latest usable grid at the frame cap when dimensions keep changing', () => { const scheduler = createFrameScheduler() const onSettled = vi.fn() diff --git a/src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.ts b/src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.ts index 5ed9d704670..8df3d42d6c6 100644 --- a/src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.ts +++ b/src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.ts @@ -13,6 +13,7 @@ export type TerminalStartupGridSettleOptions = { minFrames?: number stableFrames?: number maxFrames?: number + maxReadinessWaitFrames?: number } export type TerminalStartupGridSettleHandle = { @@ -22,6 +23,8 @@ export type TerminalStartupGridSettleHandle = { const DEFAULT_MIN_FRAMES = 6 const DEFAULT_STABLE_FRAMES = 2 const DEFAULT_MAX_FRAMES = 12 +// Why: ten settle windows tolerate slow split layout without permitting permanent polling. +const DEFAULT_MAX_READINESS_WAIT_FRAMES = DEFAULT_MAX_FRAMES * 10 function usableDimensions( dimensions: TerminalStartupGridDimensions | null @@ -42,6 +45,10 @@ export function waitForStableStartupGrid( const minFrames = Math.max(1, options.minFrames ?? DEFAULT_MIN_FRAMES) const stableFrames = Math.max(1, options.stableFrames ?? DEFAULT_STABLE_FRAMES) const maxFrames = Math.max(minFrames, options.maxFrames ?? DEFAULT_MAX_FRAMES) + const maxReadinessWaitFrames = Math.max( + 1, + options.maxReadinessWaitFrames ?? DEFAULT_MAX_READINESS_WAIT_FRAMES + ) let frame = 0 let stableFrameCount = 0 let previous: TerminalStartupGridDimensions | null = null @@ -50,6 +57,7 @@ export function waitForStableStartupGrid( let pendingFrame: number | null = null let cancelled = false let readyFrame = 0 + let latestReadinessWaitUsable: TerminalStartupGridDimensions | null = null const usesReadinessGate = options.isReadyToSettle !== undefined const settle = (dimensions: TerminalStartupGridDimensions | null): void => { @@ -73,6 +81,13 @@ export function waitForStableStartupGrid( const measured = options.measure() const readyToSettle = options.isReadyToSettle?.() ?? true if (!readyToSettle) { + if (usableDimensions(measured)) { + latestReadinessWaitUsable = measured + } + if (frame >= maxReadinessWaitFrames) { + settle(latestReadinessWaitUsable) + return + } previous = null stableFrameCount = 0 observedGridChange = false From be68aa271887865ef5567bd5ea97548c38a5ef16 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:01:39 -0700 Subject: [PATCH 24/80] perf(browser): release popup origin bar contents (#17234) --- .../browser/popup-origin-bar-window.test.ts | 45 +++++++++++++++++++ src/main/browser/popup-origin-bar-window.ts | 19 +++++--- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/main/browser/popup-origin-bar-window.test.ts b/src/main/browser/popup-origin-bar-window.test.ts index 3e816e74b07..fedcd4dfdd0 100644 --- a/src/main/browser/popup-origin-bar-window.test.ts +++ b/src/main/browser/popup-origin-bar-window.test.ts @@ -285,6 +285,7 @@ describe('openPopupWithOriginBar', () => { it('closes the window when the popup content is destroyed, without re-closing the contents', () => { const adopted = createFakeWebContents() openPopupWithOriginBar({ webContents: adopted as never }, 'https://example.com/') + const { bar } = lastViews() adopted.emit('destroyed') @@ -292,6 +293,7 @@ describe('openPopupWithOriginBar', () => { // The window's closed handler must not call close() on already-destroyed // contents — that throws in real Electron. expect(adopted.close).not.toHaveBeenCalled() + expect(bar.webContents.close).toHaveBeenCalledTimes(1) }) it('re-asserts the origin when the popup finishes loading', () => { @@ -326,11 +328,54 @@ describe('openPopupWithOriginBar', () => { const adopted = createFakeWebContents() const onClosed = vi.fn() const popup = openPopupWithOriginBar({ webContents: adopted as never }, 'https://example.com/') + const { bar } = lastViews() popup.onClosed(onClosed) popup.close() expect(adopted.close).toHaveBeenCalledTimes(1) + expect(bar.webContents.close).toHaveBeenCalledTimes(1) expect(onClosed).toHaveBeenCalledTimes(1) }) + + it('releases every origin bar across repeated popup lifecycles', () => { + const cycleCount = 25 + const originBars: FakeWebContents[] = [] + + for (let cycle = 0; cycle < cycleCount; cycle += 1) { + const popup = openPopupWithOriginBar({}, `https://example.com/${cycle}`) + originBars.push(lastViews().bar.webContents) + popup.close() + } + + expect(originBars.filter((contents) => contents.close.mock.calls.length === 1)).toHaveLength( + cycleCount + ) + }) + + it('tolerates an origin bar destroyed before its window closes', () => { + const adopted = createFakeWebContents() + const onClosed = vi.fn() + const popup = openPopupWithOriginBar({ webContents: adopted as never }, 'https://example.com/') + popup.onClosed(onClosed) + const { bar } = lastViews() + const barWebContents = bar.webContents + barWebContents.emit('destroyed') + Object.defineProperty(bar, 'webContents', { get: () => undefined }) + + expect(() => popup.close()).not.toThrow() + expect(adopted.close).toHaveBeenCalledTimes(1) + expect(barWebContents.close).not.toHaveBeenCalled() + expect(onClosed).toHaveBeenCalledTimes(1) + }) + + it('closes both child contents when popup preparation fails', () => { + const adopted = createFakeWebContents() + + openPopupWithOriginBar({ webContents: adopted as never }, 'https://example.com/', () => false) + + expect(adopted.close).toHaveBeenCalledTimes(1) + expect(lastViews().bar.webContents.close).toHaveBeenCalledTimes(1) + expect(lastWindow().close).toHaveBeenCalledTimes(1) + }) }) diff --git a/src/main/browser/popup-origin-bar-window.ts b/src/main/browser/popup-origin-bar-window.ts index 376c318d1ae..cfbe3ef3d3b 100644 --- a/src/main/browser/popup-origin-bar-window.ts +++ b/src/main/browser/popup-origin-bar-window.ts @@ -92,7 +92,8 @@ function clampPopupContentSize(options: PopupChildWindowOptions): { // caller's popup bookkeeping cannot leak an entry for a window that no longer exists. function closeUnpreparedPopup( window: BaseWindow, - contentWebContents: Electron.WebContents + contentWebContents: Electron.WebContents, + originBarWebContents: Electron.WebContents ): PopupOriginBarWindow { const closedListeners: (() => void)[] = [] let closed = false @@ -105,6 +106,9 @@ function closeUnpreparedPopup( if (!contentWebContents.isDestroyed()) { contentWebContents.close() } + if (!originBarWebContents.isDestroyed()) { + originBarWebContents.close() + } if (!window.isDestroyed()) { window.close() } @@ -159,6 +163,7 @@ export function openPopupWithOriginBar( const originBarView = new WebContentsView({ webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true } }) + const originBarWebContents = originBarView.webContents const contentView = new WebContentsView({ // Why: Electron rejects an explicitly undefined webContents; omitting it // lets WebContentsView create contents for Cmd/Ctrl-click popups. @@ -188,7 +193,7 @@ export function openPopupWithOriginBar( const contentWebContents = contentView.webContents if (prepareContent && !prepareContent(contentWebContents)) { - return closeUnpreparedPopup(window, contentWebContents) + return closeUnpreparedPopup(window, contentWebContents, originBarWebContents) } let currentUrl = initialUrl const renderOrigin = (): void => { @@ -202,15 +207,15 @@ export function openPopupWithOriginBar( } // Why: textContent + JSON encoding — the URL is attacker-controlled and // must never be interpolated into the bar's markup. - void originBarView.webContents + void originBarWebContents .executeJavaScript( `document.body.classList.toggle('insecure', ${insecure ? 'true' : 'false'});` + `document.getElementById('origin').textContent = ${JSON.stringify(label)};` ) .catch(() => {}) } - originBarView.webContents.once('did-finish-load', renderOrigin) - void originBarView.webContents.loadURL( + originBarWebContents.once('did-finish-load', renderOrigin) + void originBarWebContents.loadURL( `data:text/html;charset=utf-8,${encodeURIComponent(ORIGIN_BAR_HTML)}` ) @@ -253,6 +258,10 @@ export function openPopupWithOriginBar( // pages often notify the opener from unload. contentWebContents.close() } + // Why: BaseWindow does not destroy child WebContentsView contents when it closes. + if (!originBarWebContents.isDestroyed()) { + originBarWebContents.close() + } for (const listener of closedListeners) { listener() } From 79aea811734962d35a0177f5cf734b817543dea2 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:01:42 -0700 Subject: [PATCH 25/80] perf(renderer): index unread completion tabs (#17246) --- .../terminal-tab-activity-status.test.ts | 55 +++++++++++++++++++ .../tab-bar/terminal-tab-activity-status.ts | 50 ++++++++++++----- 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts index d2bed923686..bf367f93667 100644 --- a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts +++ b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts @@ -3,6 +3,7 @@ import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import type { TerminalTab } from '../../../../shared/terminal-tab-types' import { hasUnreadAgentCompletionForTerminalTab, + resetUnreadAgentCompletionTabIdsCacheForTest, resetTerminalTabActivityFlagsCacheForTest, resolveTerminalTabActivityStatus, resolveTerminalTabAttentionBadge, @@ -41,6 +42,7 @@ const LIVE_PTY = { [TAB_ID]: ['pty-1'] } beforeEach(() => { resetTerminalTabActivityFlagsCacheForTest() + resetUnreadAgentCompletionTabIdsCacheForTest() vi.useFakeTimers() vi.setSystemTime(NOW) }) @@ -48,6 +50,7 @@ beforeEach(() => { afterEach(() => { vi.useRealTimers() resetTerminalTabActivityFlagsCacheForTest() + resetUnreadAgentCompletionTabIdsCacheForTest() }) describe('resolveTerminalTabActivityStatus', () => { @@ -267,6 +270,7 @@ describe('hasUnreadAgentCompletionForTerminalTab', () => { expect( hasUnreadAgentCompletionForTerminalTab({ [`tab-2:${SECOND_LEAF_ID}`]: true }, TAB_ID) ).toBe(false) + expect(hasUnreadAgentCompletionForTerminalTab(undefined, TAB_ID)).toBe(false) }) // Why: the param accepts boolean maps, so a cleared-to-`false` marker must not read as unread. @@ -275,6 +279,57 @@ describe('hasUnreadAgentCompletionForTerminalTab', () => { hasUnreadAgentCompletionForTerminalTab({ [`${TAB_ID}:${FIRST_LEAF_ID}`]: false }, TAB_ID) ).toBe(false) }) + + it('preserves first-colon ownership for legacy and malformed pane keys', () => { + expect(hasUnreadAgentCompletionForTerminalTab({ [TAB_ID]: true }, TAB_ID)).toBe(true) + expect(hasUnreadAgentCompletionForTerminalTab({ [`${TAB_ID}:3`]: true }, TAB_ID)).toBe(true) + expect( + hasUnreadAgentCompletionForTerminalTab( + { [`${TAB_ID}:${FIRST_LEAF_ID}:suffix`]: true }, + TAB_ID + ) + ).toBe(true) + expect( + hasUnreadAgentCompletionForTerminalTab({ [`${TAB_ID}0:${SECOND_LEAF_ID}`]: true }, TAB_ID) + ).toBe(false) + }) + + it('changes only the owning tab when immutable marker snapshots add and clear unread', () => { + const before = { [`tab-2:${SECOND_LEAF_ID}`]: true } + const added = { ...before, [`${TAB_ID}:${FIRST_LEAF_ID}`]: true } + const cleared = { ...added, [`${TAB_ID}:${FIRST_LEAF_ID}`]: false } + + expect(hasUnreadAgentCompletionForTerminalTab(before, TAB_ID)).toBe(false) + expect(hasUnreadAgentCompletionForTerminalTab(added, TAB_ID)).toBe(true) + expect(hasUnreadAgentCompletionForTerminalTab(cleared, TAB_ID)).toBe(false) + expect(hasUnreadAgentCompletionForTerminalTab(before, 'tab-2')).toBe(true) + expect(hasUnreadAgentCompletionForTerminalTab(added, 'tab-2')).toBe(true) + expect(hasUnreadAgentCompletionForTerminalTab(cleared, 'tab-2')).toBe(true) + }) + + it('indexes one immutable marker snapshot once across all mounted tab lookups', () => { + const ownKeys = vi.fn(Reflect.ownKeys) + let valueReads = 0 + const markers: Record = Object.fromEntries( + Array.from({ length: 1_000 }, (_, index) => [`owner-${index}:leaf`, true] as const) + ) + const unread = new Proxy>(markers, { + ownKeys, + get: (target, property, receiver) => { + valueReads += 1 + return Reflect.get(target, property, receiver) + } + }) + + for (let publication = 0; publication < 5; publication += 1) { + for (let tab = 0; tab < 500; tab += 1) { + expect(hasUnreadAgentCompletionForTerminalTab(unread, `absent-${tab}`)).toBe(false) + } + } + + expect(ownKeys).toHaveBeenCalledTimes(1) + expect(valueReads).toBe(1_000) + }) }) describe('resolveTerminalTabAttentionBadge', () => { diff --git a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts index 97f511a1d41..a801855a588 100644 --- a/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts +++ b/src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts @@ -256,29 +256,49 @@ export function terminalTabHasUnreadActivity({ ) } +// Why: production writes replace this map; WeakMap supports retained snapshots without pinning them. +let unreadAgentCompletionTabIdsBySnapshot = new WeakMap< + Record, + ReadonlySet +>() + +function getUnreadAgentCompletionTabIds( + unreadAgentCompletionPanes: Record +): ReadonlySet { + const cached = unreadAgentCompletionTabIdsBySnapshot.get(unreadAgentCompletionPanes) + if (cached) { + return cached + } + + // Why: every mounted tab runs this selector per store write; index each immutable marker snapshot once. + const tabIds = new Set() + for (const paneKey of Object.keys(unreadAgentCompletionPanes)) { + if (!unreadAgentCompletionPanes[paneKey]) { + continue + } + const separatorIndex = paneKey.indexOf(':') + tabIds.add(separatorIndex === -1 ? paneKey : paneKey.slice(0, separatorIndex)) + } + unreadAgentCompletionTabIdsBySnapshot.set(unreadAgentCompletionPanes, tabIds) + return tabIds +} + /** Match pane-level unread completion markers to their owning terminal tab. */ export function hasUnreadAgentCompletionForTerminalTab( unreadAgentCompletionPanes: Record | undefined, tabId: string ): boolean { - for (const [paneKey, unread] of Object.entries(unreadAgentCompletionPanes ?? {})) { - // Why entries, not keys: the widened value type lets a cleared marker linger as `false`. - if (!unread) { - continue - } - // paneKey is `${tabId}:${leafId}` and tab ids never contain ":", so the - // prefix up to the first ":" is the owning tab id (see - // selectFloatingWorkspaceHasUnread). Prefix-match to keep legacy keys. - const separatorIndex = paneKey.indexOf(':') - const owningTabId = separatorIndex === -1 ? paneKey : paneKey.slice(0, separatorIndex) - if (owningTabId === tabId) { - return true - } - } - return false + return unreadAgentCompletionPanes + ? getUnreadAgentCompletionTabIds(unreadAgentCompletionPanes).has(tabId) + : false } /** Test-only: clear the memoized per-tab flag cache between cases. */ export function resetTerminalTabActivityFlagsCacheForTest(): void { flagsCache = null } + +/** Test-only: clear the unread marker snapshot index between cases. */ +export function resetUnreadAgentCompletionTabIdsCacheForTest(): void { + unreadAgentCompletionTabIdsBySnapshot = new WeakMap() +} From e25878588445931b42db2fb05323e62733e7a646 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:02:10 -0700 Subject: [PATCH 26/80] perf(terminal): index accepted input pane ownership (#17287) --- .../runtime-terminal-inspection.test.ts | 204 ++++++++++++++++++ .../runtime/runtime-terminal-inspection.ts | 78 +++++-- 2 files changed, 266 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/runtime/runtime-terminal-inspection.test.ts b/src/renderer/src/runtime/runtime-terminal-inspection.test.ts index 4223f6d2b82..766e80f3f6e 100644 --- a/src/renderer/src/runtime/runtime-terminal-inspection.test.ts +++ b/src/renderer/src/runtime/runtime-terminal-inspection.test.ts @@ -209,6 +209,53 @@ describe('runtime terminal owner routing', () => { }) }) + it('attributes a delayed runtime acknowledgement to the current layout owner', async () => { + const pendingSend = Promise.withResolvers<{ + ok: true + result: { send: { handle: string; accepted: true; bytesWritten: number } } + _meta: { runtimeId: string } + }>() + runtimeCall.mockReturnValue(pendingSend.promise) + useAppStore.setState({ + terminalLayoutsByTabId: { + 'tab-1': { + root: { type: 'leaf', leafId: LEAF_ID }, + activeLeafId: LEAF_ID, + expandedLeafId: null, + ptyIdsByLeafId: { [LEAF_ID]: 'remote:env-1@@terminal-1' } + } + } + }) + recordRuntimeTerminalInputForPtyId('remote:env-1@@terminal-1', 123) + useAppStore.setState({ lastTerminalInputAtByPaneKey: {} }) + + expect( + sendRuntimePtyInput({ activeRuntimeEnvironmentId: 'env-2' }, 'remote:env-1@@terminal-1', 'x') + ).toBe(true) + const nextLeafId = '22222222-2222-4222-8222-222222222222' + useAppStore.setState({ + terminalLayoutsByTabId: { + 'tab-2': { + root: { type: 'leaf', leafId: nextLeafId }, + activeLeafId: nextLeafId, + expandedLeafId: null, + ptyIdsByLeafId: { [nextLeafId]: 'remote:env-1@@terminal-1' } + } + } + }) + pendingSend.resolve({ + ok: true, + result: { send: { handle: 'terminal-1', accepted: true, bytesWritten: 1 } }, + _meta: { runtimeId: 'runtime-1' } + }) + + await vi.waitFor(() => { + expect(useAppStore.getState().lastTerminalInputAtByPaneKey).toEqual({ + [`tab-2:${nextLeafId}`]: expect.any(Number) + }) + }) + }) + it('does not record declined fire-and-forget runtime input', async () => { runtimeCall.mockResolvedValue({ ok: true, @@ -479,6 +526,163 @@ describe('runtime terminal owner routing', () => { expect(useAppStore.getState().lastTerminalInputAtByPaneKey[PANE_KEY]).toBe(123) }) + it('indexes a stable layout identity once across repeated terminal input', () => { + const layoutCount = 500 + let layoutEnumerations = 0 + let leafEnumerations = 0 + const layouts = Object.fromEntries( + Array.from({ length: layoutCount }, (_, index) => { + const leafId = `00000000-0000-4000-8000-${index.toString(16).padStart(12, '0')}` + const ptyIdsByLeafId = new Proxy( + { [leafId]: `pty-${index}` }, + { + ownKeys: (target) => { + leafEnumerations += 1 + return Reflect.ownKeys(target) + } + } + ) + return [ + `tab-${index}`, + { + root: { type: 'leaf' as const, leafId }, + activeLeafId: leafId, + expandedLeafId: null, + ptyIdsByLeafId + } + ] + }) + ) + const observedLayouts = new Proxy(layouts, { + ownKeys: (target) => { + layoutEnumerations += 1 + return Reflect.ownKeys(target) + } + }) + useAppStore.setState({ terminalLayoutsByTabId: observedLayouts }) + + recordRuntimeTerminalInputForPtyId('pty-0', 10_000) + expect({ layoutEnumerations, leafEnumerations }).toEqual({ + layoutEnumerations: 1, + leafEnumerations: 1 + }) + layoutEnumerations = 0 + leafEnumerations = 0 + + for (let timestamp = 1; timestamp <= 100; timestamp += 1) { + recordRuntimeTerminalInputForPtyId(`pty-${layoutCount - 1}`, timestamp * 10_000) + } + + expect({ layoutEnumerations, leafEnumerations }).toEqual({ + layoutEnumerations: 1, + leafEnumerations: layoutCount + }) + expect( + useAppStore.getState().lastTerminalInputAtByPaneKey[ + `tab-${layoutCount - 1}:00000000-0000-4000-8000-${(layoutCount - 1) + .toString(16) + .padStart(12, '0')}` + ] + ).toBe(1_000_000) + }) + + it('reindexes a PTY when the immutable layout identity changes', () => { + const nextLeafId = '22222222-2222-4222-8222-222222222222' + useAppStore.setState({ + terminalLayoutsByTabId: { + 'tab-1': { + root: { type: 'leaf', leafId: LEAF_ID }, + activeLeafId: LEAF_ID, + expandedLeafId: null, + ptyIdsByLeafId: { [LEAF_ID]: 'rebound-pty' } + } + } + }) + recordRuntimeTerminalInputForPtyId('rebound-pty', 123) + + useAppStore.setState({ + terminalLayoutsByTabId: { + 'tab-2': { + root: { type: 'leaf', leafId: nextLeafId }, + activeLeafId: nextLeafId, + expandedLeafId: null, + ptyIdsByLeafId: { [nextLeafId]: 'rebound-pty' } + } + } + }) + recordRuntimeTerminalInputForPtyId('rebound-pty', 1_000) + + expect(useAppStore.getState().lastTerminalInputAtByPaneKey).toEqual({ + [PANE_KEY]: 123, + [`tab-2:${nextLeafId}`]: 1_000 + }) + }) + + it('reindexes stale owners without caching misses within the same layout identity', () => { + const nextLeafId = '22222222-2222-4222-8222-222222222222' + useAppStore.setState({ + terminalLayoutsByTabId: { + 'tab-1': { + root: { type: 'leaf', leafId: LEAF_ID }, + activeLeafId: LEAF_ID, + expandedLeafId: null, + ptyIdsByLeafId: { [LEAF_ID]: 'rebound-pty' } + } + } + }) + const layouts = useAppStore.getState().terminalLayoutsByTabId + recordRuntimeTerminalInputForPtyId('rebound-pty', 123) + recordRuntimeTerminalInputForPtyId('late-pty', 456) + + const firstLayout = layouts['tab-1'] + expect(firstLayout).toBeDefined() + if (!firstLayout) { + return + } + firstLayout.ptyIdsByLeafId = Object.create({ [LEAF_ID]: 'rebound-pty' }) + Object.setPrototypeOf(layouts, { 'tab-1': firstLayout }) + delete layouts['tab-1'] + layouts['tab-2'] = { + root: { type: 'leaf', leafId: nextLeafId }, + activeLeafId: nextLeafId, + expandedLeafId: null, + ptyIdsByLeafId: { [nextLeafId]: 'rebound-pty', [LEAF_ID]: 'late-pty' } + } + recordRuntimeTerminalInputForPtyId('rebound-pty', 1_000) + recordRuntimeTerminalInputForPtyId('late-pty', 2_000) + + expect(useAppStore.getState().terminalLayoutsByTabId).toBe(layouts) + expect(useAppStore.getState().lastTerminalInputAtByPaneKey).toEqual({ + [PANE_KEY]: 123, + [`tab-2:${nextLeafId}`]: 1_000, + [`tab-2:${LEAF_ID}`]: 2_000 + }) + }) + + it('preserves a malformed first match instead of routing a duplicate PTY', () => { + const nextLeafId = '22222222-2222-4222-8222-222222222222' + useAppStore.setState({ + terminalLayoutsByTabId: { + 'legacy:tab': { + root: { type: 'leaf', leafId: LEAF_ID }, + activeLeafId: LEAF_ID, + expandedLeafId: null, + ptyIdsByLeafId: { [LEAF_ID]: 'duplicate-pty' } + }, + 'tab-2': { + root: { type: 'leaf', leafId: nextLeafId }, + activeLeafId: nextLeafId, + expandedLeafId: null, + ptyIdsByLeafId: { [nextLeafId]: 'duplicate-pty' } + } + } + }) + + recordRuntimeTerminalInputForPtyId('duplicate-pty', 123) + + expect(useAppStore.getState().lastTerminalInputAtByPaneKey).toEqual({}) + }) + it('reports success after fallback fire-and-forget writes when local acceptance cannot be verified', async () => { localWriteAccepted.mockResolvedValue(false) diff --git a/src/renderer/src/runtime/runtime-terminal-inspection.ts b/src/renderer/src/runtime/runtime-terminal-inspection.ts index 29be1aed435..7578c161116 100644 --- a/src/renderer/src/runtime/runtime-terminal-inspection.ts +++ b/src/renderer/src/runtime/runtime-terminal-inspection.ts @@ -1,6 +1,6 @@ import type { GlobalSettings } from '../../../shared/global-settings-types' import type { RuntimeTerminalSend } from '../../../shared/runtime-types' -import { makePaneKey } from '../../../shared/stable-pane-id' +import { makePaneKey, type PaneKey } from '../../../shared/stable-pane-id' import { isTerminalInputTooLargeWithDeferredMeasurement } from '../../../shared/terminal-input' import { useAppStore } from '../store' import { RuntimeRpcCallError, callRuntimeRpc, getActiveRuntimeTarget } from './runtime-rpc-client' @@ -18,6 +18,56 @@ export type RuntimeTerminalProcessInspection = { const REMOTE_PTY_ID_PREFIX = 'remote:' const DESKTOP_RUNTIME_CLIENT = { id: 'orca-desktop', type: 'desktop' } as const +type TerminalLayoutsByTabId = ReturnType['terminalLayoutsByTabId'] +type TerminalPaneOwner = { + tabId: string + leafId: string + paneKey: PaneKey +} + +const paneOwnersByPtyIdByLayoutIdentity = new WeakMap< + TerminalLayoutsByTabId, + Map +>() + +function resolvePaneKeyForPtyId(layouts: TerminalLayoutsByTabId, ptyId: string): PaneKey | null { + let paneOwnersByPtyId = paneOwnersByPtyIdByLayoutIdentity.get(layouts) + if (!paneOwnersByPtyId) { + paneOwnersByPtyId = new Map() + paneOwnersByPtyIdByLayoutIdentity.set(layouts, paneOwnersByPtyId) + } + const cachedOwner = paneOwnersByPtyId.get(ptyId) + if (cachedOwner) { + const layout = Object.prototype.propertyIsEnumerable.call(layouts, cachedOwner.tabId) + ? layouts[cachedOwner.tabId] + : undefined + const ptyIdsByLeafId = layout?.ptyIdsByLeafId + if ( + ptyIdsByLeafId && + Object.prototype.propertyIsEnumerable.call(ptyIdsByLeafId, cachedOwner.leafId) && + ptyIdsByLeafId[cachedOwner.leafId] === ptyId + ) { + return cachedOwner.paneKey + } + paneOwnersByPtyId.delete(ptyId) + } + for (const [tabId, layout] of Object.entries(layouts)) { + for (const [leafId, leafPtyId] of Object.entries(layout?.ptyIdsByLeafId ?? {})) { + if (leafPtyId !== ptyId) { + continue + } + try { + const paneKey = makePaneKey(tabId, leafId) + paneOwnersByPtyId.set(ptyId, { tabId, leafId, paneKey }) + return paneKey + } catch { + // Preserve first-match behavior for malformed legacy layout rows. + return null + } + } + } + return null +} function isRuntimePtyInputTooLarge(data: string): boolean | Promise { return isTerminalInputTooLargeWithDeferredMeasurement(data) @@ -49,21 +99,17 @@ function isTerminalGoneError(error: unknown): boolean { export function recordRuntimeTerminalInputForPtyId(ptyId: string, timestamp = Date.now()): void { const state = useAppStore.getState() - for (const [tabId, layout] of Object.entries(state.terminalLayoutsByTabId)) { - for (const [leafId, leafPtyId] of Object.entries(layout?.ptyIdsByLeafId ?? {})) { - if (leafPtyId !== ptyId) { - continue - } - try { - // Why: paired/runtime sends can bypass xterm.onData, so hibernation - // needs the same user-input marker from the PTY-id route. - state.recordTerminalInput(makePaneKey(tabId, leafId), timestamp) - } catch { - // Ignore malformed legacy layout data; the planner will stay - // conservative when a live PTY cannot be matched to an eligible pane. - } - return - } + const paneKey = resolvePaneKeyForPtyId(state.terminalLayoutsByTabId, ptyId) + if (!paneKey) { + return + } + try { + // Why: paired/runtime sends can bypass xterm.onData, so hibernation + // needs the same user-input marker from the PTY-id route. + state.recordTerminalInput(paneKey, timestamp) + } catch { + // Ignore malformed legacy layout data; the planner will stay + // conservative when a live PTY cannot be matched to an eligible pane. } } From 1fe558dd284477f03dcacbed2128feca49af2d59 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:02:29 -0700 Subject: [PATCH 27/80] perf(automations): narrow status observer updates (#17249) --- ...tomation-agent-status-entry-change.test.ts | 66 ++++++++++++++ .../automation-agent-status-entry-change.ts | 15 ++++ ...automation-dispatch-observer-test-probe.ts | 30 +++++++ .../hooks/useAutomationDispatchEvents.test.ts | 4 +- .../src/hooks/useAutomationDispatchEvents.ts | 89 +++++++++++-------- 5 files changed, 164 insertions(+), 40 deletions(-) create mode 100644 src/renderer/src/hooks/automation-agent-status-entry-change.test.ts create mode 100644 src/renderer/src/hooks/automation-agent-status-entry-change.ts create mode 100644 src/renderer/src/hooks/automation-dispatch-observer-test-probe.ts diff --git a/src/renderer/src/hooks/automation-agent-status-entry-change.test.ts b/src/renderer/src/hooks/automation-agent-status-entry-change.test.ts new file mode 100644 index 00000000000..7079f81af8d --- /dev/null +++ b/src/renderer/src/hooks/automation-agent-status-entry-change.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest' +import type { AgentStatusEntry } from '../../../shared/agent-status-types' +import { + selectAutomationAgentStatusEntryChange, + UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY +} from './automation-agent-status-entry-change' + +function makeEntry(paneKey: string): AgentStatusEntry { + return { + paneKey, + state: 'working', + prompt: 'turn', + updatedAt: 1, + stateStartedAt: 1, + stateHistory: [] + } +} + +describe('selectAutomationAgentStatusEntryChange', () => { + it('reads only the target and skips an unchanged entry', () => { + const targetPaneKey = 'target-tab:leaf' + const targetEntry = makeEntry(targetPaneKey) + const entries = Object.fromEntries( + Array.from({ length: 499 }, (_, index) => { + const paneKey = `other-tab:${index}` + return [paneKey, makeEntry(paneKey)] + }) + ) + entries[targetPaneKey] = targetEntry + let enumerations = 0 + const measuredEntries = new Proxy(entries, { + ownKeys: (target) => { + enumerations += 1 + return Reflect.ownKeys(target) + } + }) + + expect(selectAutomationAgentStatusEntryChange(measuredEntries, targetPaneKey, undefined)).toBe( + targetEntry + ) + expect( + selectAutomationAgentStatusEntryChange(measuredEntries, targetPaneKey, targetEntry) + ).toBe(UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY) + expect(enumerations).toBe(0) + }) + + it('reports removal and ignores inherited or non-enumerable entries', () => { + const targetPaneKey = 'target-tab:leaf' + const previousEntry = makeEntry(targetPaneKey) + const inheritedEntries = Object.create({ [targetPaneKey]: previousEntry }) as Record< + string, + AgentStatusEntry + > + const nonEnumerableEntries = Object.defineProperty({}, targetPaneKey, { + value: previousEntry + }) as Record + + expect(selectAutomationAgentStatusEntryChange({}, targetPaneKey, previousEntry)).toBeUndefined() + expect(selectAutomationAgentStatusEntryChange(inheritedEntries, targetPaneKey, undefined)).toBe( + UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY + ) + expect( + selectAutomationAgentStatusEntryChange(nonEnumerableEntries, targetPaneKey, undefined) + ).toBe(UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY) + }) +}) diff --git a/src/renderer/src/hooks/automation-agent-status-entry-change.ts b/src/renderer/src/hooks/automation-agent-status-entry-change.ts new file mode 100644 index 00000000000..1c919e6b417 --- /dev/null +++ b/src/renderer/src/hooks/automation-agent-status-entry-change.ts @@ -0,0 +1,15 @@ +import type { AgentStatusEntry } from '../../../shared/agent-status-types' + +export const UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY = Symbol('unchanged-agent-status-entry') + +export function selectAutomationAgentStatusEntryChange( + entries: Readonly>, + targetPaneKey: string, + previousEntry: AgentStatusEntry | undefined +): AgentStatusEntry | undefined | typeof UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY { + const entry = Object.prototype.propertyIsEnumerable.call(entries, targetPaneKey) + ? entries[targetPaneKey] + : undefined + // Why: status writes replace entries; unchanged identity proves an unrelated publication. + return entry === previousEntry ? UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY : entry +} diff --git a/src/renderer/src/hooks/automation-dispatch-observer-test-probe.ts b/src/renderer/src/hooks/automation-dispatch-observer-test-probe.ts new file mode 100644 index 00000000000..302c052782c --- /dev/null +++ b/src/renderer/src/hooks/automation-dispatch-observer-test-probe.ts @@ -0,0 +1,30 @@ +import type { AgentStatusEntry } from '../../../shared/agent-status-types' + +const TARGET_PANE_KEY = 'agent-tab:7c6fb4e5-3bf1-4ff4-8259-03f7ae81c40d' + +export function countUnchangedObserverHistoryReads( + state: { agentStatusByPaneKey: Record }, + subscriber: (() => void) | null +): number { + if (!subscriber) { + throw new Error('agent status observer was not registered') + } + let historyReads = 0 + state.agentStatusByPaneKey = { + [TARGET_PANE_KEY]: { + paneKey: TARGET_PANE_KEY, + state: 'working', + prompt: 'turn', + updatedAt: Date.now() + 1, + stateStartedAt: Date.now() + 1, + get stateHistory() { + historyReads += 1 + return [] + } + } + } + subscriber() + historyReads = 0 + subscriber() + return historyReads +} diff --git a/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts b/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts index 81fc23a5728..b8c1cb49c17 100644 --- a/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts +++ b/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts @@ -1,6 +1,7 @@ import type * as ReactModule from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { AUTOMATIONS_CHANGED_EVENT } from '@/lib/automations-changed-window-event' +import { countUnchangedObserverHistoryReads } from './automation-dispatch-observer-test-probe' const mockDispatchEvent = vi.fn() @@ -620,10 +621,11 @@ describe('useAutomationDispatchEvents setup launch', () => { await vi.waitFor(() => expect(mockFinalizeTerminalOwnership).toHaveBeenCalledOnce()) }) - it('persists assistant output from a batched working→done→working transition', async () => { + it('skips unchanged status and persists batched working→done→working output', async () => { const paneKey = 'agent-tab:7c6fb4e5-3bf1-4ff4-8259-03f7ae81c40d' await registerAndDispatch() + expect(countUnchangedObserverHistoryReads(state, latestStoreSubscriber)).toBe(0) const transitionStartedAt = Date.now() + 1 state.agentStatusByPaneKey = { [paneKey]: { diff --git a/src/renderer/src/hooks/useAutomationDispatchEvents.ts b/src/renderer/src/hooks/useAutomationDispatchEvents.ts index 1e46b315f28..e06eefa6e61 100644 --- a/src/renderer/src/hooks/useAutomationDispatchEvents.ts +++ b/src/renderer/src/hooks/useAutomationDispatchEvents.ts @@ -32,8 +32,12 @@ import { toSshExecutionHostId } from '../../../shared/execution-host' import { parseWorkspaceKey } from '../../../shared/workspace-scope' -import type { AgentStateHistoryEntry } from '../../../shared/agent-status-types' +import type { AgentStateHistoryEntry, AgentStatusEntry } from '../../../shared/agent-status-types' import { resolveFolderWorkspaceHost } from '../../../shared/folder-workspace-execution-host' +import { + selectAutomationAgentStatusEntryChange, + UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY +} from './automation-agent-status-entry-change' const activeReuseDispatchTabIds = new Set() @@ -450,57 +454,64 @@ export function useAutomationDispatchEvents(): void { ): void => { let sawWorkingAfterStart = false let observedStateHistory: AgentStateHistoryEntry[] = [] + let observedEntry: AgentStatusEntry | undefined const checkCurrentStatus = (): void => { - const { agentStatusByPaneKey } = useAppStore.getState() - for (const [paneKey, entry] of Object.entries(agentStatusByPaneKey)) { - if (paneKey !== targetPaneKey || entry.updatedAt < startedAfter) { + const entryChange = selectAutomationAgentStatusEntryChange( + useAppStore.getState().agentStatusByPaneKey, + targetPaneKey, + observedEntry + ) + if (entryChange === UNCHANGED_AUTOMATION_AGENT_STATUS_ENTRY) { + return + } + const entry = entryChange + observedEntry = entry + if (!entry || entry.updatedAt < startedAfter) { + return + } + const historyOverlap = getAgentStateHistoryOverlap( + observedStateHistory, + entry.stateHistory + ) + // Why: sawWorkingAfterStart stays monotonic — a recreated entry + // (transport loss, PTY exit, cap eviction) arrives with an empty + // stateHistory, so clearing it here would strand reuseSession runs + // with nothing left to re-derive the working edge from. + for (const historicalState of entry.stateHistory.slice(historyOverlap)) { + if (historicalState.startedAt < startedAfter) { continue } - const historyOverlap = getAgentStateHistoryOverlap( - observedStateHistory, - entry.stateHistory - ) - // Why: sawWorkingAfterStart stays monotonic — a recreated entry - // (transport loss, PTY exit, cap eviction) arrives with an empty - // stateHistory, so clearing it here would strand reuseSession runs - // with nothing left to re-derive the working edge from. - for (const historicalState of entry.stateHistory.slice(historyOverlap)) { - if (historicalState.startedAt < startedAfter) { - continue - } - if (historicalState.state === 'working') { - sawWorkingAfterStart = true - } - if ( - historicalState.state === 'done' && - (!options?.requireWorkingAfterStart || sawWorkingAfterStart) - ) { - // Why: this `done` already rolled out of the live entry, so its output - // survives only in the entry-level completed slot. - latestAssistantMessage = - entry.lastCompletedAssistantMessage?.trim() || latestAssistantMessage - handleAgentDone() - return - } - } - observedStateHistory = [...entry.stateHistory] - if (entry.state === 'working') { + if (historicalState.state === 'working') { sawWorkingAfterStart = true } if ( - entry.state === 'done' && - // Why: a session-boundary done is the agent CONNECTING (Claude SessionStart - // fires at launch, before the argv prompt submits) — completing here would - // close the tab and record an empty run result. - entry.sessionBoundary !== true && + historicalState.state === 'done' && (!options?.requireWorkingAfterStart || sawWorkingAfterStart) ) { + // Why: this `done` already rolled out of the live entry, so its output + // survives only in the entry-level completed slot. latestAssistantMessage = - entry.lastAssistantMessage?.trim() || latestAssistantMessage + entry.lastCompletedAssistantMessage?.trim() || latestAssistantMessage handleAgentDone() return } } + observedStateHistory = [...entry.stateHistory] + if (entry.state === 'working') { + sawWorkingAfterStart = true + } + if ( + entry.state === 'done' && + // Why: a session-boundary done is the agent CONNECTING (Claude SessionStart + // fires at launch, before the argv prompt submits) — completing here would + // close the tab and record an empty run result. + entry.sessionBoundary !== true && + (!options?.requireWorkingAfterStart || sawWorkingAfterStart) + ) { + latestAssistantMessage = + entry.lastAssistantMessage?.trim() || latestAssistantMessage + handleAgentDone() + } } // Why: Codex/Claude completion normally arrives through the global // hook IPC listener, not the hidden PTY OSC fallback. From 9c777ca89e175169fad79ef34bccfdbb36613822 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:03:35 -0700 Subject: [PATCH 28/80] perf(renderer): gate pending agent-status retries (#17254) --- .../ipc-events/agent-status-ipc-bridge.ts | 11 +- .../agent-status-pending-retry-gate.test.ts | 191 ++++++++++++++++++ .../agent-status-pending-retry-gate.ts | 29 +++ ...Events-agent-status-queue-ordering.test.ts | 27 ++- ...vents-agent-status-snapshot-replay.test.ts | 6 +- 5 files changed, 249 insertions(+), 15 deletions(-) create mode 100644 src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.test.ts create mode 100644 src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.ts diff --git a/src/renderer/src/hooks/ipc-events/agent-status-ipc-bridge.ts b/src/renderer/src/hooks/ipc-events/agent-status-ipc-bridge.ts index 3a99527b1d3..26058a3e9e0 100644 --- a/src/renderer/src/hooks/ipc-events/agent-status-ipc-bridge.ts +++ b/src/renderer/src/hooks/ipc-events/agent-status-ipc-bridge.ts @@ -13,6 +13,7 @@ import type { AgentStatusBatchEvent, PendingAgentStatusEvent } from './agent-status-bridge-types' +import { shouldRetryPendingAgentStatusesAfterStoreUpdate } from './agent-status-pending-retry-gate' const PENDING_AGENT_STATUS_RETRY_MS = 100 const PENDING_AGENT_STATUS_TTL_MS = 15_000 @@ -94,8 +95,8 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu } } finally { isFlushingAgentStatuses = false + schedulePendingAgentStatusFlush() } - schedulePendingAgentStatusFlush() } const applyAgentStatus = createAgentStatusEventApplicator({ @@ -261,7 +262,13 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu requestAgentStatusSnapshotIfReady() const unsubscribeAgentStatusStore = useAppStore.subscribe((state, previousState) => { requestAgentStatusSnapshotIfReady() - flushPendingAgentStatuses() + // Why: the timer covers module-owned rekeys; unrelated store writes cannot change attribution and must not rebuild its routing index. + if ( + pendingAgentStatusEvents.length > 0 && + shouldRetryPendingAgentStatusesAfterStoreUpdate(state, previousState) + ) { + flushPendingAgentStatuses() + } syncAgentHookCompletionNotificationsForStoreUpdate(state, previousState) }) diff --git a/src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.test.ts b/src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.test.ts new file mode 100644 index 00000000000..51e8161a8fc --- /dev/null +++ b/src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.test.ts @@ -0,0 +1,191 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { AgentStatusBatchUpdate } from '../../store/slices/agent-status' +import { + buildStoreState, + FUTURE_LEAF_ID, + FUTURE_PANE_KEY, + type AgentStatusSetData, + type StoreLike, + type StoreSubscribeListener +} from '../ipc-events-agent-status-store-test-fixtures' +import { shouldRetryPendingAgentStatusesAfterStoreUpdate } from './agent-status-pending-retry-gate' + +type RetryState = Parameters[0] + +function createRetryState(): RetryState { + return { + workspaceSessionReady: true, + tabsByWorktree: {}, + unifiedTabsByWorktree: {}, + terminalLayoutsByTabId: {}, + worktreesByRepo: {}, + repos: [], + recentlyClosedAgentStatusTabIds: {}, + recentlyRetiredAgentStatusPaneKeys: {} + } +} + +type PendingRetryHarness = { + bridge: { disposeAsyncState: () => void; unsubscribeStore: () => void } + emitPendingStatus: () => void + publish: (mutate: (state: StoreLike) => void) => void + setAgentStatuses: ReturnType + transactAgentStatuses: ReturnType +} + +async function createPendingRetryHarness(): Promise { + const subscribeListenerRef: { current: StoreSubscribeListener | null } = { current: null } + const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { + current: null + } + const setAgentStatuses = vi.fn((updates: readonly AgentStatusBatchUpdate[]) => + updates.map(() => true) + ) + const storeState = buildStoreState({ + setAgentStatuses, + workspaceSessionReady: true, + tabsByWorktree: {}, + terminalLayoutsByTabId: {}, + settings: { terminalFontSize: 13, notifications: { enabled: false } } + }) + const transactImplementation = storeState.transactAgentStatuses as (...args: unknown[]) => unknown + const transactAgentStatuses = vi.fn(transactImplementation) + storeState.transactAgentStatuses = transactAgentStatuses + + vi.doMock('../../store', () => ({ + useAppStore: { + subscribe: vi.fn((listener: StoreSubscribeListener) => { + subscribeListenerRef.current = listener + return () => { + subscribeListenerRef.current = null + } + }), + getState: () => storeState + } + })) + vi.doMock('../agent-hook-completion-notifications', () => ({ + observeAgentHookCompletionForNotification: vi.fn(), + syncAgentHookCompletionNotificationsForStoreUpdate: vi.fn() + })) + vi.stubGlobal('window', { + api: { + agentStatus: { + onSet: (listener: (data: AgentStatusSetData) => void) => { + onSetListenerRef.current = listener + return () => { + onSetListenerRef.current = null + } + } + } + } + }) + + const { registerAgentStatusIpcBridge } = await import('./agent-status-ipc-bridge') + const bridge = registerAgentStatusIpcBridge([]) + if (!subscribeListenerRef.current || !onSetListenerRef.current) { + throw new Error('Expected agent-status bridge listeners') + } + + return { + bridge, + emitPendingStatus: () => { + onSetListenerRef.current?.({ + paneKey: FUTURE_PANE_KEY, + state: 'working', + prompt: 'pending attribution', + agentType: 'claude', + receivedAt: 1_700_000_000_000, + stateStartedAt: 1_700_000_000_000 + }) + }, + publish: (mutate) => { + const previousState = { ...storeState } + mutate(storeState) + subscribeListenerRef.current?.(storeState, previousState) + }, + setAgentStatuses, + transactAgentStatuses + } +} + +describe('agent status pending retry gate', () => { + beforeEach(() => { + vi.resetModules() + vi.useFakeTimers() + vi.setSystemTime(1_700_000_100_000) + }) + + afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() + }) + + it('tracks every store-owned input that can resolve or retire pending attribution', () => { + const previous = createRetryState() + const changes: RetryState[] = [ + { ...previous, workspaceSessionReady: false }, + { ...previous, tabsByWorktree: {} }, + { ...previous, unifiedTabsByWorktree: {} }, + { ...previous, terminalLayoutsByTabId: {} }, + { ...previous, worktreesByRepo: {} }, + { ...previous, repos: [] }, + { ...previous, recentlyClosedAgentStatusTabIds: {} }, + { ...previous, recentlyRetiredAgentStatusPaneKeys: {} } + ] + + expect(shouldRetryPendingAgentStatusesAfterStoreUpdate({ ...previous }, previous)).toBe(false) + for (const current of changes) { + expect(shouldRetryPendingAgentStatusesAfterStoreUpdate(current, previous)).toBe(true) + } + }) + + it('skips retry transactions for unrelated publications and retries on hydration', async () => { + const harness = await createPendingRetryHarness() + harness.emitPendingStatus() + + for (let updateIndex = 0; updateIndex < 2_400; updateIndex += 1) { + harness.publish((state) => { + state.cosmeticUpdateIndex = updateIndex + }) + } + + expect(harness.transactAgentStatuses).not.toHaveBeenCalled() + + harness.publish((state) => { + state.tabsByWorktree = { + 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Future' }] + } + state.terminalLayoutsByTabId = { + 'tab-future': { + root: { type: 'leaf', leafId: FUTURE_LEAF_ID }, + activeLeafId: FUTURE_LEAF_ID, + expandedLeafId: null + } + } + }) + + expect(harness.transactAgentStatuses).toHaveBeenCalledTimes(1) + expect(harness.setAgentStatuses).toHaveBeenCalledWith([ + expect.objectContaining({ paneKey: FUTURE_PANE_KEY }) + ]) + vi.advanceTimersByTime(100) + expect(harness.transactAgentStatuses).toHaveBeenCalledTimes(1) + harness.bridge.unsubscribeStore() + harness.bridge.disposeAsyncState() + }) + + it('keeps the timer fallback when routing references stay unchanged', async () => { + const harness = await createPendingRetryHarness() + harness.emitPendingStatus() + harness.publish((state) => { + state.cosmeticUpdateIndex = 1 + }) + + expect(harness.transactAgentStatuses).not.toHaveBeenCalled() + vi.advanceTimersByTime(100) + expect(harness.transactAgentStatuses).toHaveBeenCalledTimes(1) + expect(harness.setAgentStatuses).toHaveBeenCalledWith([]) + harness.bridge.unsubscribeStore() + harness.bridge.disposeAsyncState() + }) +}) diff --git a/src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.ts b/src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.ts new file mode 100644 index 00000000000..4b8dcc36a9e --- /dev/null +++ b/src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.ts @@ -0,0 +1,29 @@ +import type { AppState } from '../../store/types' + +type AgentStatusPendingRetryState = Pick< + AppState, + | 'workspaceSessionReady' + | 'tabsByWorktree' + | 'unifiedTabsByWorktree' + | 'terminalLayoutsByTabId' + | 'worktreesByRepo' + | 'repos' + | 'recentlyClosedAgentStatusTabIds' + | 'recentlyRetiredAgentStatusPaneKeys' +> + +export function shouldRetryPendingAgentStatusesAfterStoreUpdate( + current: AgentStatusPendingRetryState, + previous: AgentStatusPendingRetryState +): boolean { + return ( + current.workspaceSessionReady !== previous.workspaceSessionReady || + current.tabsByWorktree !== previous.tabsByWorktree || + current.unifiedTabsByWorktree !== previous.unifiedTabsByWorktree || + current.terminalLayoutsByTabId !== previous.terminalLayoutsByTabId || + current.worktreesByRepo !== previous.worktreesByRepo || + current.repos !== previous.repos || + current.recentlyClosedAgentStatusTabIds !== previous.recentlyClosedAgentStatusTabIds || + current.recentlyRetiredAgentStatusPaneKeys !== previous.recentlyRetiredAgentStatusPaneKeys + ) +} diff --git a/src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts b/src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts index 322e253e541..eb613f21e70 100644 --- a/src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { AgentStatusUpdate } from '../store/slices/agent-status' import { YOLO_TUI_AGENT_ARGS } from '../../../shared/tui-agent-permissions' import { @@ -22,6 +22,10 @@ describe('useIpcEvents agent status snapshot integration', () => { vi.unstubAllGlobals() }) + afterEach(() => { + vi.useRealTimers() + }) + it('preserves queued set-clear order for working removal and done retention', async () => { vi.useFakeTimers() let storeState: StoreLike @@ -163,7 +167,7 @@ describe('useIpcEvents agent status snapshot integration', () => { it('does not recurse when flushing a pending status re-enters via the store subscriber', async () => { // Repro for crash 9fc89529 (RangeError: Maximum call stack size exceeded): - // the store subscriber calls flushPendingAgentStatuses() on every update. + // the store subscriber retries pending statuses synchronously on hydration. // flush -> applyAgentStatus -> store.setAgentStatus notifies subscribers // synchronously (like Zustand) -> subscriber -> flush again while the same // event is still queued -> infinite recursion. Model setAgentStatus with a @@ -173,10 +177,10 @@ describe('useIpcEvents agent status snapshot integration', () => { current: null } let setAgentStatusCalls = 0 - const notify = (): void => { + const notify = (previousState: StoreLike = storeState): void => { const listener = subscribeListenerRef.current if (listener) { - listener(storeState, storeState) + listener(storeState, previousState) } } const storeState: StoreLike = buildStoreState({ @@ -243,6 +247,7 @@ describe('useIpcEvents agent status snapshot integration', () => { // Tab hydrates; the next store update flushes the pending event. Without the // re-entrancy guard this overflows the stack instead of applying once. + const beforeHydration = { ...storeState } storeState.tabsByWorktree = { 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Future Tab' }] } @@ -254,7 +259,7 @@ describe('useIpcEvents agent status snapshot integration', () => { } } - expect(() => notify()).not.toThrow() + expect(() => notify(beforeHydration)).not.toThrow() // Applied exactly once — the re-entrant flush is a no-op, not a loop. expect(setAgentStatusCalls).toBe(1) }) @@ -263,6 +268,7 @@ describe('useIpcEvents agent status snapshot integration', () => { // buffered event permanently. Before batching the queue was only replaced after the loop, // so a throw left it intact — keep that. it('keeps pending statuses queued when the retry fold throws', async () => { + vi.useFakeTimers() const subscribeListenerRef: { current: StoreSubscribeListener | null } = { current: null } const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = { current: null @@ -284,8 +290,6 @@ describe('useIpcEvents agent status snapshot integration', () => { tabsByWorktree: {}, terminalLayoutsByTabId: {} }) - const notify = (): void => subscribeListenerRef.current?.(storeState, storeState) - stubReactSyncEffect() vi.doMock('../store', () => ({ useAppStore: { @@ -325,7 +329,10 @@ describe('useIpcEvents agent status snapshot integration', () => { stateStartedAt: 1_700_000_000_100 }) - // Tab hydrates, so the next store update flushes the pending event — and throws. + // The timer-owned retry throws after clearing its handle. + expect(() => vi.advanceTimersByTime(100)).toThrow('fold blew up') + + // Hydrate without publishing so only the re-armed timer can recover the event. storeState.tabsByWorktree = { 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'Future Tab' }] } @@ -336,10 +343,8 @@ describe('useIpcEvents agent status snapshot integration', () => { expandedLeafId: null } } - expect(() => notify()).toThrow('fold blew up') - // The event must still be queued, so the next flush replays it. - notify() + vi.advanceTimersByTime(100) const replayedAfterThrow = setAgentStatuses.mock.calls .slice(1) .flatMap((call) => call[0].map((update) => update.payload.prompt)) diff --git a/src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts b/src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts index cbb4992e7f2..e26448f0b63 100644 --- a/src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts +++ b/src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts @@ -87,6 +87,7 @@ describe('useIpcEvents agent status snapshot integration', () => { expect(setAgentStatus).not.toHaveBeenCalled() + const beforeHydration = { ...storeState } Object.assign(storeState, { tabsByWorktree: { 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'SSH Tab' }] @@ -99,7 +100,7 @@ describe('useIpcEvents agent status snapshot integration', () => { } } }) - subscribeListenerRef.current?.(storeState, storeState) + subscribeListenerRef.current?.(storeState, beforeHydration) expect(setAgentStatus).toHaveBeenCalledTimes(1) expect(setAgentStatus).toHaveBeenCalledWith( @@ -192,6 +193,7 @@ describe('useIpcEvents agent status snapshot integration', () => { expect(setAgentStatus).not.toHaveBeenCalled() + const beforeHydration = { ...storeState } Object.assign(storeState, { tabsByWorktree: { 'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'SSH Tab' }] @@ -204,7 +206,7 @@ describe('useIpcEvents agent status snapshot integration', () => { } } }) - subscribeListenerRef.current?.(storeState, storeState) + subscribeListenerRef.current?.(storeState, beforeHydration) expect(setAgentStatus).toHaveBeenCalledWith( FUTURE_PANE_KEY, From 7ae916cebd7b14f9d1e55f3866c042102e51583b Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:06:15 -0700 Subject: [PATCH 29/80] perf(worktrees): batch-prune stale local metadata (#17278) --- .../ipc/registered-worktree-roots-cache.ts | 16 + ...thoritative-local-metadata-pruning.test.ts | 611 ++++++++++++++++++ src/main/ipc/worktrees-test-harness.ts | 9 +- src/main/ipc/worktrees-test-ipc-surface.ts | 4 + .../worktrees-test-metadata-pruning-store.ts | 35 + src/main/ipc/worktrees-windows.test.ts | 16 + ...itative-local-worktree-metadata-pruning.ts | 141 ++++ .../listing/detected-provider-listing.ts | 23 +- .../listing/detected-worktree-scan-cache.ts | 118 +++- .../register-worktree-catalog-handlers.ts | 29 +- src/main/local-worktree-path-presence.test.ts | 219 +++++++ src/main/local-worktree-path-presence.ts | 72 +++ src/main/local-worktree-scan-generation.ts | 25 + src/main/persistence-initial-load.test.ts | 11 +- src/main/persistence-repo-lifecycle.test.ts | 39 ++ src/main/persistence-settings-update.test.ts | 19 + src/main/persistence-update-repo.test.ts | 23 + .../applying-settings/settings-update.ts | 11 + .../metadata-lineage-batch-pruning.test.ts | 64 ++ .../metadata-lineage-operations.ts | 31 + .../loading-store/profile-preferences.ts | 2 + .../project-collection-operations.ts | 2 + .../repo-lifecycle-operations.ts | 15 + .../worktree-identity-metadata.ts | 13 +- .../session-worktree-ownership.test.ts | 166 +++++ .../session-worktree-ownership.ts | 246 +++++++ ...ktree-metadata-batch-pruning.bench.test.ts | 106 +++ ...worktree-metadata-scan-expectation.test.ts | 242 +++++++ ...ocal-worktree-metadata-scan-expectation.ts | 243 +++++++ ...ng-local-worktree-metadata-pruning.test.ts | 477 ++++++++++++++ ...missing-local-worktree-metadata-pruning.ts | 153 +++++ .../tracking-repos/project-host-operations.ts | 4 + .../tracking-repos/repo-update-operations.ts | 6 + src/main/worktree-lineage-pruning.test.ts | 137 +++- src/main/worktree-lineage-pruning.ts | 35 +- .../worktree-retention-path-comparison.ts | 24 + src/shared/priority-semaphore.test.ts | 15 + src/shared/priority-semaphore.ts | 26 +- 38 files changed, 3382 insertions(+), 46 deletions(-) create mode 100644 src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts create mode 100644 src/main/ipc/worktrees-test-metadata-pruning-store.ts create mode 100644 src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts create mode 100644 src/main/local-worktree-path-presence.test.ts create mode 100644 src/main/local-worktree-path-presence.ts create mode 100644 src/main/local-worktree-scan-generation.ts create mode 100644 src/main/persistence/loading-store/metadata-lineage-batch-pruning.test.ts create mode 100644 src/main/persistence/restoring-sessions/session-worktree-ownership.test.ts create mode 100644 src/main/persistence/restoring-sessions/session-worktree-ownership.ts create mode 100644 src/main/persistence/tracking-repos/local-worktree-metadata-batch-pruning.bench.test.ts create mode 100644 src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.test.ts create mode 100644 src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.ts create mode 100644 src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts create mode 100644 src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.ts create mode 100644 src/main/worktree-retention-path-comparison.ts diff --git a/src/main/ipc/registered-worktree-roots-cache.ts b/src/main/ipc/registered-worktree-roots-cache.ts index 8ca19984298..92ae4fc334f 100644 --- a/src/main/ipc/registered-worktree-roots-cache.ts +++ b/src/main/ipc/registered-worktree-roots-cache.ts @@ -7,6 +7,9 @@ import { isDescendantOrEqual, normalizeExistingPath } from './filesystem-path-co const registeredWorktreeRoots = new Set() const registeredWorktreeRootsByRepo = new Map>() const registeredWorktreeRootRepoIds = new Set() +const registeredWorktreeRootsRevisionByRepo = new Map() +let registeredWorktreeRootsRevisionSequence = 0 +let registeredWorktreeRootsBaseRevision = 0 let registeredWorktreeRootsDirty = true let registeredWorktreeRootsRefresh: Promise | null = null const AUTHORIZED_ROOTS_REBUILD_CONCURRENCY = 8 @@ -17,6 +20,8 @@ export function invalidateAuthorizedRootsCache(): void { registeredWorktreeRoots.clear() registeredWorktreeRootsByRepo.clear() registeredWorktreeRootRepoIds.clear() + registeredWorktreeRootsBaseRevision = ++registeredWorktreeRootsRevisionSequence + registeredWorktreeRootsRevisionByRepo.clear() } export async function rebuildAuthorizedRootsCache(store: Store): Promise { @@ -55,6 +60,8 @@ export async function rebuildAuthorizedRootsCache(store: Store): Promise { registeredWorktreeRootRepoIds.add(repoId) } registeredWorktreeRootsDirty = false + registeredWorktreeRootsBaseRevision = ++registeredWorktreeRootsRevisionSequence + registeredWorktreeRootsRevisionByRepo.clear() } async function mapWithConcurrency( @@ -87,6 +94,10 @@ export function registerWorktreeRootsForRepo( if (!localRepoIds.has(registeredRepoId)) { registeredWorktreeRootsByRepo.delete(registeredRepoId) registeredWorktreeRootRepoIds.delete(registeredRepoId) + registeredWorktreeRootsRevisionByRepo.set( + registeredRepoId, + ++registeredWorktreeRootsRevisionSequence + ) } } @@ -98,10 +109,15 @@ export function registerWorktreeRootsForRepo( registeredWorktreeRootsByRepo.set(repoId, new Set(worktreeRoots.map((root) => resolve(root)))) registeredWorktreeRootRepoIds.add(repoId) + registeredWorktreeRootsRevisionByRepo.set(repoId, ++registeredWorktreeRootsRevisionSequence) refreshRegisteredWorktreeRoots() registeredWorktreeRootsDirty = !allLocalRepoRootsRegistered(localRepoIds) } +export function getRegisteredWorktreeRootsRevision(repoId: string): number { + return registeredWorktreeRootsRevisionByRepo.get(repoId) ?? registeredWorktreeRootsBaseRevision +} + export async function ensureAuthorizedRootsCache(store: Store): Promise { if (!registeredWorktreeRootsDirty) { return diff --git a/src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts b/src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts new file mode 100644 index 00000000000..aa3fff56f77 --- /dev/null +++ b/src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts @@ -0,0 +1,611 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { GitWorktreeInfo } from '../../shared/worktree/types' +import type { NativeLocalWorktreeMetadataScanExpectation } from '../persistence/tracking-repos/missing-local-worktree-metadata-pruning' +import { notifyWorktreesChanged } from './worktree-remote' +import { + listWorktreesMock, + pruneCleanupScanSnapshotsMock, + pruneSpaceAnalysisSnapshotsMock +} from './worktrees-test-module-mocks' +import { handlers, mainWindow, setupWorktreeHandlers, store } from './worktrees-test-harness' +import { mockSelectedWslProjectRuntime } from './worktrees-test-fixtures' +import { pruneMetadataMissingFromAuthoritativeLocalScan } from './worktrees/listing/authoritative-local-worktree-metadata-pruning' +import { listDetectedWorktreesForCapturedRepo } from './worktrees/listing/detected-provider-listing' +import { getLocalWorktreeScanGeneration } from '../local-worktree-scan-generation' +import { + isRegisteredWorktreePath, + registerWorktreeRootsForRepo +} from './registered-worktree-roots-cache' + +const localWorktreePathPresenceMock = vi.hoisted(() => vi.fn()) + +vi.mock('electron', async () => + (await import('./worktrees-test-module-mocks')).electronModuleMock() +) +vi.mock('../git/worktree', async () => + (await import('./worktrees-test-module-mocks')).gitWorktreeModuleMock() +) +vi.mock('../git/runner', async () => + (await import('./worktrees-test-module-mocks')).gitRunnerModuleMock() +) +vi.mock('../git/repo', async () => + (await import('./worktrees-test-module-mocks')).gitRepoModuleMock() +) +vi.mock('../git/git-username', async (importOriginal) => ({ + ...(await importOriginal>()), + resolveLocalGitUsername: (await import('./worktrees-test-module-mocks')) + .resolveLocalGitUsernameMock +})) +vi.mock('../github/client', async () => + (await import('./worktrees-test-module-mocks')).githubClientModuleMock() +) +vi.mock('../source-control/hosted-review', async () => + (await import('./worktrees-test-module-mocks')).hostedReviewModuleMock() +) +vi.mock('../providers/ssh-git-dispatch', async () => + (await import('./worktrees-test-module-mocks')).sshGitDispatchModuleMock() +) +vi.mock('../providers/ssh-filesystem-dispatch', async () => + (await import('./worktrees-test-module-mocks')).sshFilesystemDispatchModuleMock() +) +vi.mock('./worktree-symlinks', async () => + (await import('./worktrees-test-module-mocks')).worktreeSymlinksModuleMock() +) +vi.mock('./ssh', async () => (await import('./worktrees-test-module-mocks')).sshModuleMock()) +vi.mock('../ssh/ssh-target-registry', async () => + (await import('./worktrees-test-module-mocks')).sshTargetRegistryModuleMock() +) +vi.mock('../hooks', async () => (await import('./worktrees-test-module-mocks')).hooksModuleMock()) +vi.mock('../setup-runner-script-text', async (importOriginal) => + (await import('./worktrees-test-module-mocks')).setupRunnerScriptTextModuleMock( + (await importOriginal()) as Record + ) +) +vi.mock('../worktree-runner-script', async (importOriginal) => + (await import('./worktrees-test-module-mocks')).worktreeRunnerScriptModuleMock( + (await importOriginal()) as Record + ) +) +vi.mock('../effective-hook-config', async (importOriginal) => + (await import('./worktrees-test-module-mocks')).effectiveHookConfigModuleMock( + (await importOriginal()) as Record + ) +) +vi.mock('../setup-hook-env-vars', async (importOriginal) => + (await import('./worktrees-test-module-mocks')).setupHookEnvVarsModuleMock( + (await importOriginal()) as Record + ) +) +vi.mock('./worktree-logic', async (importOriginal) => + (await import('./worktrees-test-module-mocks')).worktreeLogicModuleMock( + (await importOriginal()) as Record + ) +) +vi.mock('../terminal-history-deletion', async () => + (await import('./worktrees-test-module-mocks')).terminalHistoryDeletionModuleMock() +) +vi.mock('../ports/advertised-url-watcher', async () => + (await import('./worktrees-test-module-mocks')).advertisedUrlWatcherModuleMock() +) +vi.mock('../workspace-cleanup-scan-snapshot', async () => + (await import('./worktrees-test-module-mocks')).workspaceCleanupScanSnapshotModuleMock() +) +vi.mock('../workspace-space-analysis-snapshot', async () => + (await import('./worktrees-test-module-mocks')).workspaceSpaceAnalysisSnapshotModuleMock() +) +vi.mock('../workspace-cleanup-removal-snapshot-prune', async () => + (await import('./worktrees-test-module-mocks')).workspaceCleanupRemovalSnapshotPruneModuleMock() +) +vi.mock('../local-worktree-path-presence', async (importOriginal) => ({ + ...(await importOriginal>()), + localWorktreePathsExistOrAreUnverifiable: localWorktreePathPresenceMock +})) +vi.mock('../runtime/worktree-teardown', async () => + (await import('./worktrees-test-module-mocks')).worktreeTeardownModuleMock() +) +vi.mock('./pty', async () => (await import('./worktrees-test-module-mocks')).ptyModuleMock()) + +const REPO_ID = 'repo-1' +const REPO_PATH = '/workspace/repo' +const LOCAL_HOST_ID = 'local' + +function worktree(path: string, overrides: Partial = {}): GitWorktreeInfo { + return { + path, + head: path, + branch: `refs/heads/${path.split('/').at(-1) ?? 'main'}`, + isBare: false, + isMainWorktree: path === REPO_PATH, + ...overrides + } +} + +function scanExpectation( + worktreeIds: readonly string[] +): NativeLocalWorktreeMetadataScanExpectation { + const expectedRepo = { + id: REPO_ID, + path: REPO_PATH, + displayName: 'repo', + badgeColor: '#000', + addedAt: 0 + } + return { + repo: { + id: REPO_ID, + path: REPO_PATH, + kind: 'git', + expectedRepo + }, + routing: { + expectedProject: undefined, + expectedProjectUpdatedAt: undefined, + expectedSettings: + {} as NativeLocalWorktreeMetadataScanExpectation['routing']['expectedSettings'] + }, + metadata: worktreeIds.map((worktreeId) => ({ + worktreeId, + expectedLegacy: { + expectedPresent: true, + expectedMeta: undefined, + expectedSerialized: undefined, + expectedInstanceId: undefined, + expectedHostId: LOCAL_HOST_ID + }, + expectedAliases: [] + })) + } +} + +function listDetected(): Promise { + return handlers['worktrees:listDetected'](null, { repoId: REPO_ID }) as Promise +} + +const localListingCalls = [ + ['worktrees:listDetected', listDetected], + [ + 'worktrees:list', + () => handlers['worktrees:list'](null, { repoId: REPO_ID }) as Promise + ], + ['worktrees:listAll', () => handlers['worktrees:listAll'](null, undefined) as Promise] +] as const + +describe('authoritative local worktree metadata pruning integration', () => { + beforeEach(() => { + setupWorktreeHandlers() + localWorktreePathPresenceMock.mockReset() + localWorktreePathPresenceMock.mockImplementation( + async (pathValues: readonly string[]) => + new Map(pathValues.map((pathValue) => [pathValue, false])) + ) + }) + + it('captures metadata expectations before starting the Git scan', async () => { + const order: string[] = [] + store.captureNativeLocalWorktreeMetadataScanExpectation.mockImplementation(() => { + order.push('capture') + return scanExpectation([]) + }) + listWorktreesMock.mockImplementation(() => { + order.push('scan') + return Promise.resolve([worktree(REPO_PATH)]) + }) + + await listDetected() + + expect(order).toEqual(['capture', 'scan']) + }) + + it('prunes once for the fresh producer, while coalesced and cached callers do nothing', async () => { + const staleId = `${REPO_ID}::/workspace/stale` + const rows = [worktree(REPO_PATH), worktree('/workspace/live')] + store.captureNativeLocalWorktreeMetadataScanExpectation.mockReturnValue( + scanExpectation([staleId]) + ) + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mockReturnValue([staleId]) + listWorktreesMock.mockImplementation(async () => { + await Promise.resolve() + return rows + }) + + await Promise.all([listDetected(), listDetected(), listDetected()]) + await listDetected() + + expect(store.captureNativeLocalWorktreeMetadataScanExpectation).toHaveBeenCalledTimes(1) + expect(store.pruneSessionlessMissingLocalWorktreeMetadataForRepo).toHaveBeenCalledTimes(1) + const firstPruneCall = store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mock + .calls[0] as [unknown, readonly { worktreeId: string }[]] | undefined + expect(firstPruneCall?.[1].map(({ worktreeId }) => worktreeId)).toEqual([staleId]) + expect(pruneCleanupScanSnapshotsMock).toHaveBeenCalledTimes(1) + expect(pruneCleanupScanSnapshotsMock).toHaveBeenCalledWith('/profile-a', [ + { worktreeId: staleId, executionHostId: LOCAL_HOST_ID } + ]) + expect(pruneSpaceAnalysisSnapshotsMock).toHaveBeenCalledTimes(1) + expect(pruneSpaceAnalysisSnapshotsMock).toHaveBeenCalledWith('/profile-a', [ + { worktreeId: staleId, executionHostId: LOCAL_HOST_ID } + ]) + }) + + it('does not prune when the authoritative scan is invalidated while in flight', async () => { + const staleId = `${REPO_ID}::/workspace/stale` + let resolveScan: (rows: GitWorktreeInfo[]) => void = () => {} + store.captureNativeLocalWorktreeMetadataScanExpectation.mockReturnValue( + scanExpectation([staleId]) + ) + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mockReturnValue([staleId]) + listWorktreesMock.mockImplementation( + () => + new Promise((resolve) => { + resolveScan = resolve as (rows: GitWorktreeInfo[]) => void + }) + ) + + const pending = listDetected() + await Promise.resolve() + notifyWorktreesChanged(mainWindow as never, REPO_ID) + resolveScan([worktree(REPO_PATH), worktree('/workspace/live')]) + await pending + + expect(store.captureNativeLocalWorktreeMetadataScanExpectation).toHaveBeenCalledTimes(1) + expect(store.pruneSessionlessMissingLocalWorktreeMetadataForRepo).not.toHaveBeenCalled() + expect(pruneCleanupScanSnapshotsMock).not.toHaveBeenCalled() + expect(pruneSpaceAnalysisSnapshotsMock).not.toHaveBeenCalled() + }) + + it('does not prune when the completed scan is invalidated before its caller resumes', async () => { + const staleId = `${REPO_ID}::/workspace/stale` + let resolveScan: (rows: GitWorktreeInfo[]) => void = () => {} + store.captureNativeLocalWorktreeMetadataScanExpectation.mockReturnValue( + scanExpectation([staleId]) + ) + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mockReturnValue([staleId]) + listWorktreesMock.mockImplementation( + () => + new Promise((resolve) => { + resolveScan = resolve as (rows: GitWorktreeInfo[]) => void + }) + ) + + const pending = listDetected() + await Promise.resolve() + resolveScan([worktree(REPO_PATH)]) + queueMicrotask(() => notifyWorktreesChanged(mainWindow as never, REPO_ID)) + await pending + + expect(store.pruneSessionlessMissingLocalWorktreeMetadataForRepo).not.toHaveBeenCalled() + }) + + it.each(localListingCalls)( + 'skips stale WSL root and lineage side effects after %s caller resumption', + async (_channel, listWorktrees) => { + const orphanId = `${REPO_ID}::/workspace/orphan` + let resolveScan: (rows: GitWorktreeInfo[]) => void = () => {} + mockSelectedWslProjectRuntime() + store.getAllWorktreeLineage.mockReturnValue({ + [orphanId]: { + worktreeId: orphanId, + worktreeInstanceId: 'orphan-instance', + parentWorktreeId: `${REPO_ID}::${REPO_PATH}`, + parentWorktreeInstanceId: 'main-instance', + origin: 'manual', + capture: { source: 'manual-action', confidence: 'explicit' }, + createdAt: 0 + } + }) + listWorktreesMock.mockImplementation( + () => + new Promise((resolve) => { + resolveScan = resolve as (rows: GitWorktreeInfo[]) => void + }) + ) + + const pending = listWorktrees() + await Promise.resolve() + resolveScan([worktree(REPO_PATH)]) + queueMicrotask(() => notifyWorktreesChanged(mainWindow as never, REPO_ID)) + await pending + + expect(store.removeWorktreeLineage).not.toHaveBeenCalled() + expect(isRegisteredWorktreePath(REPO_PATH)).toBe(false) + } + ) + + it('preserves WSL roots and lineage created while an older Git scan is pending', async () => { + const newPath = '/workspace/new-wsl-worktree' + const newId = `${REPO_ID}::${newPath}` + let resolveScan: (rows: GitWorktreeInfo[]) => void = () => {} + mockSelectedWslProjectRuntime() + listWorktreesMock.mockImplementation( + () => + new Promise((resolve) => { + resolveScan = resolve as (rows: GitWorktreeInfo[]) => void + }) + ) + + const pending = listDetected() + await Promise.resolve() + + resolveScan([worktree(REPO_PATH)]) + queueMicrotask(() => { + registerWorktreeRootsForRepo(store as never, REPO_ID, [REPO_PATH, newPath]) + store.setWorktreeMeta(newId, { hostId: LOCAL_HOST_ID, instanceId: 'new-instance' }) + store.getAllWorktreeLineage.mockReturnValue({ + [newId]: { + worktreeId: newId, + worktreeInstanceId: 'new-instance', + parentWorktreeId: `${REPO_ID}::${REPO_PATH}`, + parentWorktreeInstanceId: 'main-instance', + origin: 'manual', + capture: { source: 'manual-action', confidence: 'explicit' }, + createdAt: 0 + } + }) + }) + await pending + + expect(store.removeWorktreeLineage).not.toHaveBeenCalled() + expect(isRegisteredWorktreePath(newPath)).toBe(true) + }) + + it.each(['scan generation', 'caller request'] as const)( + 'skips metadata, root, and lineage mutations when the %s is invalidated during path probes', + async (invalidation) => { + const staleId = `${REPO_ID}::/workspace/stale` + const orphanId = `${REPO_ID}::/workspace/orphan` + let callerCurrent = true + let resolvePresence: (presence: ReadonlyMap) => void = () => {} + store.captureNativeLocalWorktreeMetadataScanExpectation.mockReturnValue( + scanExpectation([staleId]) + ) + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mockReturnValue([staleId]) + store.getAllWorktreeLineage.mockReturnValue({ + [orphanId]: { + worktreeId: orphanId, + worktreeInstanceId: 'orphan-instance', + parentWorktreeId: `${REPO_ID}::${REPO_PATH}`, + parentWorktreeInstanceId: 'main-instance', + origin: 'manual', + capture: { source: 'manual-action', confidence: 'explicit' }, + createdAt: 0 + } + }) + localWorktreePathPresenceMock.mockImplementationOnce( + () => + new Promise((resolve) => { + resolvePresence = resolve + }) + ) + listWorktreesMock.mockResolvedValue([worktree(REPO_PATH)]) + + const pending = listDetectedWorktreesForCapturedRepo( + store as never, + store.getRepo(REPO_ID) as never, + () => callerCurrent + ) + await vi.waitFor(() => expect(localWorktreePathPresenceMock).toHaveBeenCalledTimes(1)) + if (invalidation === 'scan generation') { + notifyWorktreesChanged(mainWindow as never, REPO_ID) + } else { + callerCurrent = false + } + resolvePresence(new Map([['/workspace/stale', false]])) + await pending + + expect(store.pruneSessionlessMissingLocalWorktreeMetadataForRepo).not.toHaveBeenCalled() + expect(store.removeWorktreeLineage).not.toHaveBeenCalled() + expect(isRegisteredWorktreePath(REPO_PATH)).toBe(false) + expect(pruneCleanupScanSnapshotsMock).not.toHaveBeenCalled() + expect(pruneSpaceAnalysisSnapshotsMock).not.toHaveBeenCalled() + } + ) + + it('preserves roots and lineage created while an older path probe is pending', async () => { + const staleId = `${REPO_ID}::/workspace/stale` + const newPath = '/workspace/new-worktree' + const newId = `${REPO_ID}::${newPath}` + let resolvePresence: (presence: ReadonlyMap) => void = () => {} + const metadata = new Map() + store.getWorktreeMeta.mockImplementation((worktreeId) => metadata.get(worktreeId)) + store.captureNativeLocalWorktreeMetadataScanExpectation.mockReturnValue( + scanExpectation([staleId]) + ) + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mockReturnValue([staleId]) + localWorktreePathPresenceMock.mockImplementationOnce( + () => + new Promise((resolve) => { + resolvePresence = resolve + }) + ) + listWorktreesMock.mockResolvedValue([worktree(REPO_PATH)]) + + const pending = listDetected() + await vi.waitFor(() => expect(localWorktreePathPresenceMock).toHaveBeenCalledTimes(1)) + + registerWorktreeRootsForRepo(store as never, REPO_ID, [REPO_PATH, newPath]) + const newMetadata = { hostId: LOCAL_HOST_ID, instanceId: 'new-instance' } + metadata.set(newId, newMetadata) + store.setWorktreeMeta(newId, newMetadata) + store.getAllWorktreeLineage.mockReturnValue({ + [newId]: { + worktreeId: newId, + worktreeInstanceId: newMetadata.instanceId, + parentWorktreeId: `${REPO_ID}::${REPO_PATH}`, + parentWorktreeInstanceId: 'main-instance', + origin: 'manual', + capture: { source: 'manual-action', confidence: 'explicit' }, + createdAt: 0 + } + }) + resolvePresence(new Map([['/workspace/stale', false]])) + await pending + + expect(store.pruneSessionlessMissingLocalWorktreeMetadataForRepo).toHaveBeenCalledTimes(1) + expect(store.removeWorktreeLineage).not.toHaveBeenCalled() + expect(isRegisteredWorktreePath(newPath)).toBe(true) + }) + + it('does not capture or prune on an initial WSL scan', async () => { + mockSelectedWslProjectRuntime() + listWorktreesMock.mockResolvedValue([worktree(REPO_PATH)]) + + await listDetected() + + expect(listWorktreesMock).toHaveBeenCalledWith(REPO_PATH, { wslDistro: 'Ubuntu' }) + expect(store.captureNativeLocalWorktreeMetadataScanExpectation).not.toHaveBeenCalled() + expect(store.pruneSessionlessMissingLocalWorktreeMetadataForRepo).not.toHaveBeenCalled() + }) + + it('does not let a native scan that changes to WSL routing become destructive', async () => { + const staleId = `${REPO_ID}::/workspace/stale` + const resolvers: ((rows: GitWorktreeInfo[]) => void)[] = [] + store.captureNativeLocalWorktreeMetadataScanExpectation.mockReturnValue( + scanExpectation([staleId]) + ) + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mockReturnValue([staleId]) + listWorktreesMock.mockImplementation( + () => + new Promise((resolve) => { + resolvers.push(resolve as (rows: GitWorktreeInfo[]) => void) + }) + ) + + const nativePending = listDetected() + await Promise.resolve() + mockSelectedWslProjectRuntime() + const wslPending = listDetected() + await Promise.resolve() + + resolvers[1]?.([worktree(REPO_PATH)]) + await wslPending + resolvers[0]?.([worktree(REPO_PATH), worktree('/workspace/live')]) + await nativePending + + expect(store.captureNativeLocalWorktreeMetadataScanExpectation).toHaveBeenCalledTimes(1) + expect(store.pruneSessionlessMissingLocalWorktreeMetadataForRepo).not.toHaveBeenCalled() + expect(pruneCleanupScanSnapshotsMock).not.toHaveBeenCalled() + expect(pruneSpaceAnalysisSnapshotsMock).not.toHaveBeenCalled() + }) + + it.each([ + ['empty', [] as GitWorktreeInfo[]], + ['failed', new Error('git worktree list failed')] + ] as const)('does nothing for an %s authoritative scan', async (_label, result) => { + const staleId = `${REPO_ID}::/workspace/stale` + store.captureNativeLocalWorktreeMetadataScanExpectation.mockReturnValue( + scanExpectation([staleId]) + ) + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mockReturnValue([staleId]) + if (result instanceof Error) { + listWorktreesMock.mockRejectedValue(result) + } else { + listWorktreesMock.mockResolvedValue(result) + } + + await listDetected() + + expect(store.pruneSessionlessMissingLocalWorktreeMetadataForRepo).not.toHaveBeenCalled() + expect(pruneCleanupScanSnapshotsMock).not.toHaveBeenCalled() + expect(pruneSpaceAnalysisSnapshotsMock).not.toHaveBeenCalled() + }) + + it('keeps prunable Git registrations live and does not probe their paths', async () => { + const staleId = `${REPO_ID}::/workspace/stale` + const liveId = `${REPO_ID}::/workspace/live` + const prunableId = `${REPO_ID}::/workspace/prunable` + store.captureNativeLocalWorktreeMetadataScanExpectation.mockReturnValue( + scanExpectation([staleId, liveId, prunableId]) + ) + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mockReturnValue([staleId]) + listWorktreesMock.mockResolvedValue([ + worktree(REPO_PATH), + worktree('/workspace/live'), + worktree('/workspace/prunable', { prunable: true }) + ]) + + await listDetected() + + expect(store.pruneSessionlessMissingLocalWorktreeMetadataForRepo).toHaveBeenCalledTimes(1) + const missing = store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mock.calls[0]?.[1] as + | readonly { worktreeId: string }[] + | undefined + expect(missing?.map(({ worktreeId }) => worktreeId)).toEqual([staleId]) + expect(missing?.map(({ worktreeId }) => worktreeId)).not.toContain(liveId) + expect(missing?.map(({ worktreeId }) => worktreeId)).not.toContain(prunableId) + expect(localWorktreePathPresenceMock).toHaveBeenCalledWith(['/workspace/stale'], { + signal: undefined + }) + }) + + it('keeps the configured repo path live when Git reports a canonical spelling', async () => { + const configuredMainId = `${REPO_ID}::${REPO_PATH}` + const staleId = `${REPO_ID}::/workspace/stale` + store.captureNativeLocalWorktreeMetadataScanExpectation.mockReturnValue( + scanExpectation([configuredMainId, staleId]) + ) + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mockReturnValue([staleId]) + listWorktreesMock.mockResolvedValue([worktree('/canonical/workspace/repo')]) + + await listDetected() + + const missing = store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mock.calls[0]?.[1] as + | readonly { worktreeId: string }[] + | undefined + expect(missing?.map(({ worktreeId }) => worktreeId)).toEqual([staleId]) + expect(missing?.map(({ worktreeId }) => worktreeId)).not.toContain(configuredMainId) + }) + + it('keeps a linked worktree whose persisted symlink spelling still exists', async () => { + const aliasId = `${REPO_ID}::/workspace/alias/linked` + const staleId = `${REPO_ID}::/workspace/stale` + const scan = scanExpectation([aliasId, staleId]) + const repo = scan.repo.expectedRepo! + const prune = vi.fn( + ( + _scan: NativeLocalWorktreeMetadataScanExpectation, + _missing: readonly { worktreeId: string }[] + ) => [] as string[] + ) + + await pruneMetadataMissingFromAuthoritativeLocalScan({ + store: { + getRepos: () => [repo], + pruneSessionlessMissingLocalWorktreeMetadataForRepo: prune + } as never, + repo, + gitWorktrees: [worktree('/workspace/real/linked')], + scan, + scanGeneration: getLocalWorktreeScanGeneration(REPO_ID), + pathsExistOrAreUnverifiable: async (pathValues) => + new Map(pathValues.map((pathValue) => [pathValue, pathValue === '/workspace/alias/linked'])) + }) + + expect(prune.mock.calls[0]?.[1].map(({ worktreeId }) => worktreeId)).toEqual([staleId]) + }) + + it('does not rematerialize a removed lineage parent metadata row', async () => { + const staleId = `${REPO_ID}::/workspace/stale` + store.captureNativeLocalWorktreeMetadataScanExpectation.mockReturnValue( + scanExpectation([staleId]) + ) + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mockReturnValue([staleId]) + store.getAllWorktreeLineage.mockReturnValue({ + [`${REPO_ID}::/workspace/live-child`]: { + worktreeId: `${REPO_ID}::/workspace/live-child`, + worktreeInstanceId: 'child-instance', + parentWorktreeId: staleId, + parentWorktreeInstanceId: 'stale-instance', + origin: 'manual', + capture: { source: 'manual-action', confidence: 'explicit' }, + createdAt: 0 + } + }) + listWorktreesMock.mockResolvedValue([worktree(REPO_PATH), worktree('/workspace/live')]) + + await listDetected() + + expect(store.pruneSessionlessMissingLocalWorktreeMetadataForRepo).toHaveBeenCalledTimes(1) + expect(store.setWorktreeMeta.mock.calls.some(([worktreeId]) => worktreeId === staleId)).toBe( + false + ) + }) +}) diff --git a/src/main/ipc/worktrees-test-harness.ts b/src/main/ipc/worktrees-test-harness.ts index 0eacf52e472..1888156a692 100644 --- a/src/main/ipc/worktrees-test-harness.ts +++ b/src/main/ipc/worktrees-test-harness.ts @@ -7,6 +7,7 @@ import { resetRetirementCollisionKeyCacheForTests } from '../worktree-name-retir import { resetSshProviderAuthorities } from '../ssh/ssh-provider-authority' import { createWorktreeRuntimeStub, type WorktreeRuntimeStub } from './worktrees-test-runtime-stub' import { handlers, mainWindow, store } from './worktrees-test-ipc-surface' +import { configureMetadataPruningStoreMocks } from './worktrees-test-metadata-pruning-store' import { ORIGINAL_PLATFORM, setPlatform, @@ -129,10 +130,12 @@ export function setupWorktreeHandlers(): WorktreeRuntimeStub { store.getWorktreeMeta, store.getWorktreeMetaForHost, store.getAllWorktreeMeta, + store.captureNativeLocalWorktreeMetadataScanExpectation, store.setWorktreeMeta, store.setWorktreeMetaForHost, store.getProjectHostSetups, store.removeWorktreeMeta, + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo, store.removeWorkspaceSessionStateForWorktree, store.getAllWorktreeLineage, store.removeWorktreeLineage, @@ -187,12 +190,13 @@ export function setupWorktreeHandlers(): WorktreeRuntimeStub { store.getRepo.mockReturnValue({ ...repo, worktreeBaseRef: null }) store.getProjects.mockReturnValue([]) store.getSparsePresets.mockReturnValue([]) - store.getSettings.mockReturnValue({ + const settings = { branchPrefix: 'none', nestWorkspaces: false, refreshLocalBaseRefOnWorktreeCreate: false, workspaceDir: '/workspace' - }) + } + store.getSettings.mockReturnValue(settings) store.getWorktreeMeta.mockReturnValue(undefined) // Host-qualified accessors delegate by default so payload assertions stay on one spy; // host routing itself is covered by worktree-identity-persistence.test.ts. @@ -200,6 +204,7 @@ export function setupWorktreeHandlers(): WorktreeRuntimeStub { store.getWorktreeMeta(args[0] as string) ) store.getAllWorktreeMeta.mockReturnValue({}) + configureMetadataPruningStoreMocks(store, settings) store.getRetiredWorktreeNameRegistry.mockReturnValue({ exhaustedTiers: 0, names: [] }) resetRetirementCollisionKeyCacheForTests() store.setWorktreeMeta.mockReturnValue({}) diff --git a/src/main/ipc/worktrees-test-ipc-surface.ts b/src/main/ipc/worktrees-test-ipc-surface.ts index cc4b497b1bc..a858aacf6b8 100644 --- a/src/main/ipc/worktrees-test-ipc-surface.ts +++ b/src/main/ipc/worktrees-test-ipc-surface.ts @@ -24,10 +24,12 @@ export type TestStore = { getWorktreeMeta: KeyedStoreMock getWorktreeMetaForHost: StoreMock getAllWorktreeMeta: StoreMock + captureNativeLocalWorktreeMetadataScanExpectation: StoreMock setWorktreeMeta: KeyedStoreWriteMock setWorktreeMetaForHost: StoreMock getProjectHostSetups: StoreMock removeWorktreeMeta: KeyedStoreMock + pruneSessionlessMissingLocalWorktreeMetadataForRepo: StoreMock removeWorkspaceSessionStateForWorktree: KeyedStoreMock getAllWorktreeLineage: StoreMock removeWorktreeLineage: KeyedStoreMock @@ -58,10 +60,12 @@ export const store: TestStore = { getWorktreeMeta: vi.fn(), getWorktreeMetaForHost: vi.fn(), getAllWorktreeMeta: vi.fn(), + captureNativeLocalWorktreeMetadataScanExpectation: vi.fn(), setWorktreeMeta: vi.fn(), setWorktreeMetaForHost: vi.fn(), getProjectHostSetups: vi.fn(), removeWorktreeMeta: vi.fn(), + pruneSessionlessMissingLocalWorktreeMetadataForRepo: vi.fn(), removeWorkspaceSessionStateForWorktree: vi.fn(), getAllWorktreeLineage: vi.fn(), removeWorktreeLineage: vi.fn(), diff --git a/src/main/ipc/worktrees-test-metadata-pruning-store.ts b/src/main/ipc/worktrees-test-metadata-pruning-store.ts new file mode 100644 index 00000000000..dd60acaa027 --- /dev/null +++ b/src/main/ipc/worktrees-test-metadata-pruning-store.ts @@ -0,0 +1,35 @@ +import type { TestStore } from './worktrees-test-ipc-surface' + +export function configureMetadataPruningStoreMocks( + store: TestStore, + expectedSettings: unknown +): void { + store.captureNativeLocalWorktreeMetadataScanExpectation.mockImplementation( + (...args: unknown[]) => { + const repo = args[0] as { + id: string + path: string + connectionId?: string | null + executionHostId?: string | null + kind?: string + } + return { + repo: { + id: repo.id, + path: repo.path, + connectionId: repo.connectionId, + executionHostId: repo.executionHostId, + kind: repo.kind === 'folder' ? 'folder' : 'git', + expectedRepo: repo + }, + routing: { + expectedProject: undefined, + expectedProjectUpdatedAt: undefined, + expectedSettings + }, + metadata: [] + } + } + ) + store.pruneSessionlessMissingLocalWorktreeMetadataForRepo.mockReturnValue([]) +} diff --git a/src/main/ipc/worktrees-windows.test.ts b/src/main/ipc/worktrees-windows.test.ts index b22d7decd84..3826f5493a8 100644 --- a/src/main/ipc/worktrees-windows.test.ts +++ b/src/main/ipc/worktrees-windows.test.ts @@ -166,6 +166,7 @@ describe('registerWorktreeHandlers – Windows path handling', () => { getSettings: vi.fn(), getWorktreeMeta: vi.fn(), getAllWorktreeMeta: vi.fn(), + captureNativeLocalWorktreeMetadataScanExpectation: vi.fn(), setWorktreeMeta: vi.fn(), removeWorktreeMeta: vi.fn(), addRetiredWorktreeName: vi.fn(), @@ -211,6 +212,7 @@ describe('registerWorktreeHandlers – Windows path handling', () => { store.getSettings.mockReset() store.getWorktreeMeta.mockReset() store.getAllWorktreeMeta.mockReset() + store.captureNativeLocalWorktreeMetadataScanExpectation.mockReset() store.setWorktreeMeta.mockReset() store.removeWorktreeMeta.mockReset() store.addRetiredWorktreeName.mockReset() @@ -254,6 +256,20 @@ describe('registerWorktreeHandlers – Windows path handling', () => { resolveSetupRunnerShellMock.mockReturnValue(undefined) store.getWorktreeMeta.mockReturnValue(undefined) store.getAllWorktreeMeta.mockReturnValue({}) + store.captureNativeLocalWorktreeMetadataScanExpectation.mockImplementation((repo) => ({ + repo: { + id: repo.id, + path: repo.path, + kind: 'git', + expectedRepo: repo + }, + routing: { + expectedProject: undefined, + expectedProjectUpdatedAt: undefined, + expectedSettings: store.getSettings() + }, + metadata: [] + })) store.getRetiredWorktreeNameRegistry.mockReturnValue({ exhaustedTiers: 0, names: [] }) store.setWorktreeMeta.mockReturnValue({}) resolveLocalGitUsernameMock.mockResolvedValue('') diff --git a/src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts b/src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts new file mode 100644 index 00000000000..c5ceb1acafb --- /dev/null +++ b/src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts @@ -0,0 +1,141 @@ +import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' +import { isWindowsAbsolutePathLike } from '../../../../shared/cross-platform-path' +import { isFolderRepo } from '../../../../shared/repo-kind' +import type { Repo } from '../../../../shared/repo-types' +import { + FOLDER_WORKSPACE_INSTANCE_SEPARATOR, + splitWorktreeId +} from '../../../../shared/worktree/id' +import type { GitWorktreeInfo } from '../../../../shared/worktree/types' +import { isWslUncPath } from '../../../../shared/wsl-paths' +import type { Store } from '../../../persistence/loading-store/store' +import type { NativeLocalWorktreeMetadataScanExpectation } from '../../../persistence/tracking-repos/missing-local-worktree-metadata-pruning' +import { pruneWorkspaceCleanupScanSnapshots } from '../../../workspace-cleanup-scan-snapshot' +import { pruneWorkspaceSpaceAnalysisSnapshots } from '../../../workspace-space-analysis-snapshot' +import { isLocalWorktreeScanGenerationCurrent } from '../../../local-worktree-scan-generation' +import { localWorktreePathsExistOrAreUnverifiable } from '../../../local-worktree-path-presence' +import { worktreeRetentionPathComparisonKey } from '../../../worktree-retention-path-comparison' + +type AuthoritativeLocalMetadataPruneArgs = { + store: Store + repo: Repo + gitWorktrees: readonly GitWorktreeInfo[] + scan: NativeLocalWorktreeMetadataScanExpectation + scanGeneration: number + platform?: NodeJS.Platform + pathsExistOrAreUnverifiable?: ( + pathValues: readonly string[], + options?: { signal?: AbortSignal } + ) => Promise> + isCallerCurrent?: () => boolean + signal?: AbortSignal +} + +export type AuthoritativeLocalMetadataPruneResult = Readonly<{ + removedWorktreeIds: readonly string[] + preservedMetadataCandidateIds: ReadonlySet + scanGenerationCurrent: boolean +}> + +export async function pruneMetadataMissingFromAuthoritativeLocalScan({ + store, + repo, + gitWorktrees, + scan, + scanGeneration, + platform = process.platform, + pathsExistOrAreUnverifiable = localWorktreePathsExistOrAreUnverifiable, + isCallerCurrent = () => true, + signal +}: AuthoritativeLocalMetadataPruneArgs): Promise { + const capturedCandidateIds = scan.metadata.map(({ worktreeId }) => worktreeId) + const result = ( + removedWorktreeIds: readonly string[], + scanGenerationCurrent: boolean + ): AuthoritativeLocalMetadataPruneResult => { + const removedIds = new Set(removedWorktreeIds) + return { + removedWorktreeIds, + preservedMetadataCandidateIds: new Set( + capturedCandidateIds.filter((worktreeId) => !removedIds.has(worktreeId)) + ), + scanGenerationCurrent + } + } + const generationCurrent = () => isLocalWorktreeScanGenerationCurrent(repo.id, scanGeneration) + const repoOwners = store.getRepos().filter((candidate) => candidate.id === repo.id) + if ( + !generationCurrent() || + !isCallerCurrent() || + gitWorktrees.length === 0 || + repoOwners.length !== 1 || + repo.id !== scan.repo.id || + repo.path !== scan.repo.path || + repo.connectionId || + isFolderRepo(repo) || + getRepoExecutionHostId(repo) !== LOCAL_EXECUTION_HOST_ID || + (platform === 'win32' && + (isWslUncPath(repo.path) || gitWorktrees.some(({ path }) => isWslUncPath(path)))) + ) { + return result([], generationCurrent()) + } + + const livePathKeys = new Set([ + // Why: Git can canonicalize a symlinked main checkout differently from the configured path. + worktreeRetentionPathComparisonKey(repo.path, platform), + ...gitWorktrees.map((worktree) => worktreeRetentionPathComparisonKey(worktree.path, platform)) + ]) + const probeCandidates = scan.metadata.flatMap((metadata) => { + const { worktreeId } = metadata + const parsed = splitWorktreeId(worktreeId) + const nativeAbsolute = parsed + ? platform === 'win32' + ? isWindowsAbsolutePathLike(parsed.worktreePath) + : parsed.worktreePath.startsWith('/') + : false + if ( + parsed?.repoId !== repo.id || + !nativeAbsolute || + isWslUncPath(parsed.worktreePath) || + worktreeId.includes(FOLDER_WORKSPACE_INSTANCE_SEPARATOR) || + livePathKeys.has(worktreeRetentionPathComparisonKey(parsed.worktreePath, platform)) + ) { + return [] + } + return [{ metadata, pathValue: parsed.worktreePath }] + }) + const presenceByPath = await pathsExistOrAreUnverifiable( + probeCandidates.map(({ pathValue }) => pathValue), + { signal } + ) + if (!generationCurrent() || !isCallerCurrent()) { + return result([], generationCurrent()) + } + const missingMetadata = probeCandidates.flatMap(({ metadata, pathValue }) => + presenceByPath.get(pathValue) === false ? [metadata] : [] + ) + if (missingMetadata.length === 0) { + return result([], generationCurrent()) + } + + if (!generationCurrent() || !isCallerCurrent()) { + return result([], generationCurrent()) + } + const removedIds = store.pruneSessionlessMissingLocalWorktreeMetadataForRepo( + scan, + missingMetadata + ) + if (removedIds.length > 0) { + const snapshotDirectory = store.getProfileStorageDirectory() + const targets: { + worktreeId: string + executionHostId: typeof LOCAL_EXECUTION_HOST_ID + }[] = removedIds.map((worktreeId) => ({ + worktreeId, + executionHostId: LOCAL_EXECUTION_HOST_ID + })) + void pruneWorkspaceCleanupScanSnapshots(snapshotDirectory, targets) + void pruneWorkspaceSpaceAnalysisSnapshots(snapshotDirectory, targets) + } + return result(removedIds, generationCurrent()) +} diff --git a/src/main/ipc/worktrees/listing/detected-provider-listing.ts b/src/main/ipc/worktrees/listing/detected-provider-listing.ts index d855e582161..45ccd4183bb 100644 --- a/src/main/ipc/worktrees/listing/detected-provider-listing.ts +++ b/src/main/ipc/worktrees/listing/detected-provider-listing.ts @@ -4,7 +4,6 @@ import { getSshGitProvider } from '../../../providers/ssh-git-dispatch' import type { DetectedWorktreeListResult, GitWorktreeInfo } from '../../../../shared/worktree/types' import { isFolderRepo } from '../../../../shared/repo-kind' import { projectResolvedWorktreeLineage } from '../../../../shared/resolved-worktree-lineage' -import { pruneLineageForMissingRepoWorktrees } from '../../../worktree-lineage-pruning' import type { DirectSshDetectedWorktreeRequest } from '../../../../shared/detected-worktree-provider-contract' import { isAdmissibleDirectSshAuthority } from '../../../../shared/ssh-retained-payload-admission' import type { ListDesktopLineageForHostArgs } from '../../../../shared/host-lineage-contract' @@ -20,8 +19,10 @@ import { import { isFolderWorkspaceIdForRepo } from '../folder-workspace-model' import { hasConflictingStoredWorktreeOwner } from './worktree-host-ownership' import { + applyFreshDetectedWorktreeScanSideEffects, listDetectedGitWorktrees, - rememberLocalWorktreeRoots + type DetectedWorktreeMetadataPrune, + type DetectedWorktreeSideEffectToken } from './detected-worktree-scan-cache' import { loggedWorktreeListFailures, warnOnce } from './worktree-listing-diagnostics' import { readAllWorktreeMetaForHost } from '../../../persistence/host-qualified-worktree-meta' @@ -48,6 +49,8 @@ export async function listDetectedWorktreesForCapturedRepo( try { let gitWorktrees: GitWorktreeInfo[] let freshScan = true + let sideEffectToken: DetectedWorktreeSideEffectToken | undefined + let metadataPrune: DetectedWorktreeMetadataPrune | undefined if (isFolderRepo(repo)) { if (!isCurrent()) { return null @@ -97,6 +100,8 @@ export async function listDetectedWorktreesForCapturedRepo( const scan = await listDetectedGitWorktrees(store, repo) gitWorktrees = scan.gitWorktrees freshScan = scan.fresh + sideEffectToken = scan.sideEffectToken + metadataPrune = scan.metadataPrune } const aborted = abortedResult() if (aborted) { @@ -115,8 +120,18 @@ export async function listDetectedWorktreesForCapturedRepo( } } if (freshScan) { - rememberLocalWorktreeRoots(store, repo, gitWorktrees) - pruneLineageForMissingRepoWorktrees(store, repo, gitWorktrees) + await applyFreshDetectedWorktreeScanSideEffects(store, repo, gitWorktrees, metadataPrune, { + isCurrent: () => isCurrent() && !providerAbort?.signal.aborted, + sideEffectToken, + signal: providerAbort?.signal + }) + const aborted = abortedResult() + if (aborted) { + return aborted + } + if (!isCurrent()) { + return null + } } loggedWorktreeListFailures.delete(`${repo.id}:${repo.path}`) return { diff --git a/src/main/ipc/worktrees/listing/detected-worktree-scan-cache.ts b/src/main/ipc/worktrees/listing/detected-worktree-scan-cache.ts index 55b3d75e3ed..fea200f1a8b 100644 --- a/src/main/ipc/worktrees/listing/detected-worktree-scan-cache.ts +++ b/src/main/ipc/worktrees/listing/detected-worktree-scan-cache.ts @@ -4,7 +4,19 @@ import type { Repo } from '../../../../shared/repo-types' import { getLocalProjectWorktreeGitOptions } from '../../../project-runtime-git-options' import { isFolderRepo } from '../../../../shared/repo-kind' import { listRepoWorktrees } from '../../../repo-worktrees' -import { registerWorktreeRootsForRepo } from '../../registered-worktree-roots-cache' +import { + getRegisteredWorktreeRootsRevision, + registerWorktreeRootsForRepo +} from '../../registered-worktree-roots-cache' +import type { NativeLocalWorktreeMetadataScanExpectation } from '../../../persistence/tracking-repos/missing-local-worktree-metadata-pruning' +import { + bumpLocalWorktreeScanGeneration, + getLocalWorktreeScanGeneration, + isLocalWorktreeScanGenerationCurrent, + resetLocalWorktreeScanGenerationsForTests +} from '../../../local-worktree-scan-generation' +import { pruneLineageForMissingRepoWorktrees } from '../../../worktree-lineage-pruning' +import { pruneMetadataMissingFromAuthoritativeLocalScan } from './authoritative-local-worktree-metadata-pruning' // Why: absorb renderer polling bursts while bounding external worktree-change lag to one short refresh window. export const DETECTED_WORKTREE_SCAN_CACHE_TTL_MS = 5_000 @@ -17,17 +29,31 @@ export type DetectedWorktreeScanCacheEntry = { export type DetectedWorktreeScan = { invalidated: boolean promise: Promise + sideEffectToken: DetectedWorktreeSideEffectToken + metadataPrune?: DetectedWorktreeMetadataPrune } +export type DetectedWorktreeSideEffectToken = Readonly<{ + generation: number + authorizedRootsRevision: number +}> + +export type DetectedWorktreeMetadataPrune = Readonly<{ + expectation: NativeLocalWorktreeMetadataScanExpectation +}> + export type DetectedWorktreeScanResult = { gitWorktrees: GitWorktreeInfo[] fresh: boolean + sideEffectToken?: DetectedWorktreeSideEffectToken + metadataPrune?: DetectedWorktreeMetadataPrune } export const detectedWorktreeScanCache = new Map() export const detectedWorktreeScanInFlight = new Map() export function invalidateDetectedWorktreeScanCache(repoId: string): void { + bumpLocalWorktreeScanGeneration(repoId) const keyPrefix = `${repoId}\0` for (const key of new Set([ ...detectedWorktreeScanCache.keys(), @@ -53,6 +79,7 @@ export function __resetDetectedWorktreeScanCacheForTests(): void { } detectedWorktreeScanCache.clear() detectedWorktreeScanInFlight.clear() + resetLocalWorktreeScanGenerationsForTests() } export function __getDetectedWorktreeScanCacheStatsForTests(): { @@ -88,21 +115,47 @@ export async function listDetectedGitWorktrees( return { gitWorktrees: await inFlight.promise, fresh: false } } + // Why: capture before invoking Git because listing can mutate synchronously before its first await. + // WSL listings can use UNC paths while legacy metadata keeps Linux paths; v1 cannot prove + // those aliases equivalent, so only native-host scans carry destructive expectations. + const generation = getLocalWorktreeScanGeneration(repo.id) + const authorizedRootsRevision = getRegisteredWorktreeRootsRevision(repo.id) + const metadataPruneExpectation = localWorktreeGitOptions.wslDistro + ? undefined + : store.captureNativeLocalWorktreeMetadataScanExpectation(repo) const scan: DetectedWorktreeScan = { invalidated: false, - promise: listRepoWorktrees(repo, localWorktreeGitOptions) + promise: listRepoWorktrees(repo, localWorktreeGitOptions), + sideEffectToken: { generation, authorizedRootsRevision }, + ...(metadataPruneExpectation + ? { + metadataPrune: { + expectation: metadataPruneExpectation + } + } + : {}) } detectedWorktreeScanInFlight.set(cacheKey, scan) try { const gitWorktrees = await scan.promise + const routingUnchanged = + getDetectedWorktreeScanCacheKey(repo.id, getLocalProjectWorktreeGitOptions(store, repo)) === + cacheKey // Why: a create/remove notification can invalidate mid-scan; don't let that stale scan repopulate the cache afterward. - if (!scan.invalidated) { + const generationCurrent = isLocalWorktreeScanGenerationCurrent(repo.id, generation) + if (!scan.invalidated && routingUnchanged && generationCurrent) { detectedWorktreeScanCache.set(cacheKey, { worktrees: gitWorktrees, expiresAt: Date.now() + DETECTED_WORKTREE_SCAN_CACHE_TTL_MS }) } - return { gitWorktrees, fresh: !scan.invalidated } + const fresh = !scan.invalidated && routingUnchanged && generationCurrent + return { + gitWorktrees, + fresh, + ...(fresh ? { sideEffectToken: scan.sideEffectToken } : {}), + ...(fresh && scan.metadataPrune ? { metadataPrune: scan.metadataPrune } : {}) + } } finally { if (detectedWorktreeScanInFlight.get(cacheKey) === scan) { detectedWorktreeScanInFlight.delete(cacheKey) @@ -110,6 +163,63 @@ export async function listDetectedGitWorktrees( } } +export async function applyFreshDetectedWorktreeScanSideEffects( + store: Store, + repo: Repo, + gitWorktrees: GitWorktreeInfo[], + metadataPrune?: DetectedWorktreeMetadataPrune, + options: { + isCurrent?: () => boolean + sideEffectToken?: DetectedWorktreeSideEffectToken + signal?: AbortSignal + } = {} +): Promise { + const { isCurrent = () => true, sideEffectToken, signal } = options + const generationCurrent = () => + sideEffectToken === undefined || + isLocalWorktreeScanGenerationCurrent(repo.id, sideEffectToken.generation) + if (!generationCurrent() || !isCurrent()) { + return false + } + let preservedMetadataCandidateIds: ReadonlySet | undefined + if (metadataPrune) { + if (!sideEffectToken) { + return false + } + const pruneResult = await pruneMetadataMissingFromAuthoritativeLocalScan({ + store, + repo, + gitWorktrees, + scan: metadataPrune.expectation, + scanGeneration: sideEffectToken.generation, + isCallerCurrent: isCurrent, + signal + }) + if (!pruneResult.scanGenerationCurrent || !generationCurrent() || !isCurrent()) { + return false + } + preservedMetadataCandidateIds = pruneResult.preservedMetadataCandidateIds + } + if (!generationCurrent() || !isCurrent()) { + return false + } + + if ( + sideEffectToken && + getRegisteredWorktreeRootsRevision(repo.id) !== sideEffectToken.authorizedRootsRevision + ) { + return false + } + rememberLocalWorktreeRoots(store, repo, gitWorktrees) + pruneLineageForMissingRepoWorktrees( + store, + repo, + gitWorktrees, + preservedMetadataCandidateIds ? { preservedMetadataCandidateIds } : undefined + ) + return true +} + export function getDetectedWorktreeScanCacheKey( repoId: string, localWorktreeGitOptions: { wslDistro?: string } = {} diff --git a/src/main/ipc/worktrees/listing/register-worktree-catalog-handlers.ts b/src/main/ipc/worktrees/listing/register-worktree-catalog-handlers.ts index ac98130b5eb..4a882ff24b7 100644 --- a/src/main/ipc/worktrees/listing/register-worktree-catalog-handlers.ts +++ b/src/main/ipc/worktrees/listing/register-worktree-catalog-handlers.ts @@ -2,7 +2,6 @@ import { ipcMain } from 'electron' import { isFolderRepo } from '../../../../shared/repo-kind' import { getRepoExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host' import { getSshGitProvider } from '../../../providers/ssh-git-dispatch' -import { pruneLineageForMissingRepoWorktrees } from '../../../worktree-lineage-pruning' import { EMPTY_RETIRED_NAME_REGISTRY } from '../../../../shared/worktree/retired-name-registry' import { getRetiredNameRegistryForRepo } from '../../../worktree-name-retirement' import { @@ -13,8 +12,10 @@ import { } from './ssh-worktree-fallback' import { listVisibleFolderWorkspaces } from './folder-workspace-catalog' import { + applyFreshDetectedWorktreeScanSideEffects, listDetectedGitWorktrees, - rememberLocalWorktreeRoots + type DetectedWorktreeMetadataPrune, + type DetectedWorktreeSideEffectToken } from './detected-worktree-scan-cache' import { loggedUnavailableSshGitProviders, @@ -88,6 +89,8 @@ export function registerWorktreeCatalogHandlers(context: WorktreeIpcContext): vo try { let gitWorktrees let freshScan = true + let sideEffectToken: DetectedWorktreeSideEffectToken | undefined + let metadataPrune: DetectedWorktreeMetadataPrune | undefined if (isFolderRepo(repo)) { return listVisibleFolderWorkspaces(store, repo) } else if (repo.connectionId) { @@ -116,10 +119,19 @@ export function registerWorktreeCatalogHandlers(context: WorktreeIpcContext): vo const scan = await listDetectedGitWorktrees(store, repo) gitWorktrees = scan.gitWorktrees freshScan = scan.fresh + sideEffectToken = scan.sideEffectToken + metadataPrune = scan.metadataPrune } if (freshScan) { - rememberLocalWorktreeRoots(store, repo, gitWorktrees) - pruneLineageForMissingRepoWorktrees(store, repo, gitWorktrees) + await applyFreshDetectedWorktreeScanSideEffects( + store, + repo, + gitWorktrees, + metadataPrune, + { + sideEffectToken + } + ) } loggedWorktreeListFailures.delete(`${repo.id}:${repo.path}`) const metadata = metadataForRepo(repo) @@ -169,6 +181,8 @@ export function registerWorktreeCatalogHandlers(context: WorktreeIpcContext): vo try { let gitWorktrees let freshScan = true + let sideEffectToken: DetectedWorktreeSideEffectToken | undefined + let metadataPrune: DetectedWorktreeMetadataPrune | undefined if (isFolderRepo(repo)) { return listVisibleFolderWorkspaces(store, repo) } else if (repo.connectionId) { @@ -197,10 +211,13 @@ export function registerWorktreeCatalogHandlers(context: WorktreeIpcContext): vo const scan = await listDetectedGitWorktrees(store, repo) gitWorktrees = scan.gitWorktrees freshScan = scan.fresh + sideEffectToken = scan.sideEffectToken + metadataPrune = scan.metadataPrune } if (freshScan) { - rememberLocalWorktreeRoots(store, repo, gitWorktrees) - pruneLineageForMissingRepoWorktrees(store, repo, gitWorktrees) + await applyFreshDetectedWorktreeScanSideEffects(store, repo, gitWorktrees, metadataPrune, { + sideEffectToken + }) } loggedWorktreeListFailures.delete(`${repo.id}:${repo.path}`) const metadata = allMeta ?? readAllWorktreeMetaForHost(store, getRepoExecutionHostId(repo)) diff --git a/src/main/local-worktree-path-presence.test.ts b/src/main/local-worktree-path-presence.test.ts new file mode 100644 index 00000000000..d54284f7c6b --- /dev/null +++ b/src/main/local-worktree-path-presence.test.ts @@ -0,0 +1,219 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { statMock } = vi.hoisted(() => ({ + statMock: vi.fn() +})) + +vi.mock('node:fs/promises', () => ({ stat: statMock })) + +import { + LOCAL_WORKTREE_PATH_PROBE_TIMEOUT_MS, + localWorktreePathExistsOrIsUnverifiable, + localWorktreePathsExistOrAreUnverifiable +} from './local-worktree-path-presence' + +function fsError(code: string): NodeJS.ErrnoException { + return Object.assign(new Error(code), { code }) +} + +function installFakeAbortSignalTimeout(): AbortController[] { + const controllers: AbortController[] = [] + vi.spyOn(AbortSignal, 'timeout').mockImplementation((timeoutMs) => { + const controller = new AbortController() + controllers.push(controller) + setTimeout( + () => controller.abort(new DOMException('The operation timed out.', 'TimeoutError')), + timeoutMs + ) + return controller.signal + }) + return controllers +} + +describe('local worktree path presence', () => { + beforeEach(() => { + statMock.mockReset() + }) + + afterEach(() => { + vi.useRealTimers() + vi.restoreAllMocks() + }) + + it('returns one result per distinct path and only treats ENOENT/ENOTDIR as missing', async () => { + statMock.mockImplementation(async (pathValue: string) => { + if (pathValue === '/missing') { + throw fsError('ENOENT') + } + if (pathValue === '/not-a-directory') { + throw fsError('ENOTDIR') + } + if (pathValue === '/permission-denied') { + throw fsError('EACCES') + } + return {} + }) + + const result = await localWorktreePathsExistOrAreUnverifiable([ + '/present', + '/missing', + '/not-a-directory', + '/permission-denied', + '/present' + ]) + + expect([...result.entries()]).toEqual([ + ['/present', true], + ['/missing', false], + ['/not-a-directory', false], + ['/permission-denied', true] + ]) + expect(statMock).toHaveBeenCalledTimes(4) + }) + + it('does not allocate a deadline when there are no paths to probe', async () => { + const timeout = vi.spyOn(AbortSignal, 'timeout') + + await expect(localWorktreePathsExistOrAreUnverifiable([])).resolves.toEqual(new Map()) + + expect(timeout).not.toHaveBeenCalled() + expect(statMock).not.toHaveBeenCalled() + }) + + it('globally bounds stat calls across simultaneous bulk probes', async () => { + let active = 0 + let maximumActive = 0 + const pending: (() => void)[] = [] + statMock.mockImplementation( + () => + new Promise((resolve) => { + active += 1 + maximumActive = Math.max(maximumActive, active) + pending.push(() => { + active -= 1 + resolve({}) + }) + }) + ) + + const first = localWorktreePathsExistOrAreUnverifiable([ + '/first-1', + '/first-2', + '/first-3', + '/first-4' + ]) + const second = localWorktreePathsExistOrAreUnverifiable([ + '/second-1', + '/second-2', + '/second-3', + '/second-4' + ]) + + let resolved = 0 + while (resolved < 8) { + while (pending.length === 0) { + await Promise.resolve() + } + pending.shift()?.() + resolved += 1 + await Promise.resolve() + } + + await Promise.all([first, second]) + + expect(maximumActive).toBe(2) + expect(statMock).toHaveBeenCalledTimes(8) + }) + + it('uses the same fail-closed classification for a single probe', async () => { + statMock.mockRejectedValueOnce(fsError('ENOENT')) + statMock.mockRejectedValueOnce(fsError('ENOTDIR')) + statMock.mockRejectedValueOnce(fsError('EIO')) + + await expect(localWorktreePathExistsOrIsUnverifiable('/missing')).resolves.toBe(false) + await expect(localWorktreePathExistsOrIsUnverifiable('/parent-missing')).resolves.toBe(false) + await expect(localWorktreePathExistsOrIsUnverifiable('/unverifiable')).resolves.toBe(true) + }) + + it('settles timed-out batches without exceeding or leaking the global stat bound', async () => { + vi.useFakeTimers() + const timeoutControllers = installFakeAbortSignalTimeout() + const releaseStats: (() => void)[] = [] + let returnMissing = false + statMock.mockImplementation(() => + returnMissing + ? Promise.reject(fsError('ENOENT')) + : new Promise((resolve) => releaseStats.push(() => resolve({}))) + ) + + try { + const firstPaths = ['/first-1', '/first-2', '/first-3', '/first-4'] + const first = localWorktreePathsExistOrAreUnverifiable(firstPaths) + let firstSettled = false + void first.then(() => { + firstSettled = true + }) + await vi.advanceTimersByTimeAsync(0) + + expect(statMock).toHaveBeenCalledTimes(2) + await vi.advanceTimersByTimeAsync(LOCAL_WORKTREE_PATH_PROBE_TIMEOUT_MS - 1) + expect(firstSettled).toBe(false) + await vi.advanceTimersByTimeAsync(1) + await expect(first).resolves.toEqual( + new Map(firstPaths.map((pathValue) => [pathValue, true])) + ) + expect(statMock).toHaveBeenCalledTimes(2) + + const secondPaths = ['/second-1', '/second-2', '/second-3'] + const second = localWorktreePathsExistOrAreUnverifiable(secondPaths) + await vi.advanceTimersByTimeAsync(LOCAL_WORKTREE_PATH_PROBE_TIMEOUT_MS) + await expect(second).resolves.toEqual( + new Map(secondPaths.map((pathValue) => [pathValue, true])) + ) + expect(statMock).toHaveBeenCalledTimes(2) + + returnMissing = true + releaseStats.splice(0).forEach((release) => release()) + await vi.advanceTimersByTimeAsync(0) + + await expect(localWorktreePathsExistOrAreUnverifiable(['/after-release'])).resolves.toEqual( + new Map([['/after-release', false]]) + ) + expect(statMock).toHaveBeenCalledTimes(3) + } finally { + timeoutControllers.forEach((controller) => controller.abort()) + releaseStats.splice(0).forEach((release) => release()) + await Promise.resolve() + await Promise.resolve() + } + }) + + it('settles promptly and fail closed when the caller aborts hung probes', async () => { + vi.useFakeTimers() + const timeoutControllers = installFakeAbortSignalTimeout() + const releaseStats: (() => void)[] = [] + statMock.mockImplementation( + () => new Promise((resolve) => releaseStats.push(() => resolve({}))) + ) + const controller = new AbortController() + const paths = ['/abort-1', '/abort-2', '/abort-3'] + + try { + const result = localWorktreePathsExistOrAreUnverifiable(paths, { + signal: controller.signal + }) + await vi.advanceTimersByTimeAsync(0) + expect(statMock).toHaveBeenCalledTimes(2) + + controller.abort() + + await expect(result).resolves.toEqual(new Map(paths.map((pathValue) => [pathValue, true]))) + expect(vi.getTimerCount()).toBeGreaterThan(0) + } finally { + timeoutControllers.forEach((timeoutController) => timeoutController.abort()) + releaseStats.splice(0).forEach((release) => release()) + await Promise.resolve() + await Promise.resolve() + } + }) +}) diff --git a/src/main/local-worktree-path-presence.ts b/src/main/local-worktree-path-presence.ts new file mode 100644 index 00000000000..673305d9513 --- /dev/null +++ b/src/main/local-worktree-path-presence.ts @@ -0,0 +1,72 @@ +import { stat } from 'node:fs/promises' +import { mapWithConcurrency } from '../shared/map-with-concurrency' +import { PrioritySemaphore } from '../shared/priority-semaphore' + +// Why: share one bound across polling callers so slow mounts cannot multiply libuv work. +const LOCAL_WORKTREE_PATH_PROBE_CONCURRENCY = 2 +export const LOCAL_WORKTREE_PATH_PROBE_TIMEOUT_MS = 2_000 +const localWorktreePathProbeSemaphore = new PrioritySemaphore(LOCAL_WORKTREE_PATH_PROBE_CONCURRENCY) + +function probeDeadline(signal?: AbortSignal): AbortSignal { + const timeout = AbortSignal.timeout(LOCAL_WORKTREE_PATH_PROBE_TIMEOUT_MS) + return signal ? AbortSignal.any([signal, timeout]) : timeout +} + +async function probeLocalWorktreePath(pathValue: string, signal: AbortSignal): Promise { + let release: () => void + try { + release = await localWorktreePathProbeSemaphore.acquire(0, signal) + } catch { + return true + } + if (signal.aborted) { + release() + return true + } + const presence = stat(pathValue) + .then( + () => true, + (error) => { + const code = (error as NodeJS.ErrnoException | undefined)?.code + return code !== 'ENOENT' && code !== 'ENOTDIR' + } + ) + .finally(release) + if (signal.aborted) { + return true + } + return new Promise((resolve) => { + const finish = (value: boolean): void => { + signal.removeEventListener('abort', onAbort) + resolve(value) + } + const onAbort = (): void => finish(true) + signal.addEventListener('abort', onAbort, { once: true }) + void presence.then(finish) + }) +} + +/** A non-absence filesystem error cannot prove a native-local worktree disappeared. */ +export function localWorktreePathExistsOrIsUnverifiable( + pathValue: string, + options: { signal?: AbortSignal } = {} +): Promise { + return probeLocalWorktreePath(pathValue, probeDeadline(options.signal)) +} + +export async function localWorktreePathsExistOrAreUnverifiable( + pathValues: readonly string[], + options: { signal?: AbortSignal } = {} +): Promise> { + const uniquePaths = [...new Set(pathValues)] + if (uniquePaths.length === 0) { + return new Map() + } + const signal = probeDeadline(options.signal) + const results = await mapWithConcurrency( + uniquePaths, + LOCAL_WORKTREE_PATH_PROBE_CONCURRENCY, + async (pathValue) => [pathValue, await probeLocalWorktreePath(pathValue, signal)] as const + ) + return new Map(results) +} diff --git a/src/main/local-worktree-scan-generation.ts b/src/main/local-worktree-scan-generation.ts new file mode 100644 index 00000000000..c8cc3bd0ff9 --- /dev/null +++ b/src/main/local-worktree-scan-generation.ts @@ -0,0 +1,25 @@ +const generationByRepoId = new Map() +let generationSequence = 0 + +export function getLocalWorktreeScanGeneration(repoId: string): number { + const existing = generationByRepoId.get(repoId) + if (existing !== undefined) { + return existing + } + const generation = ++generationSequence + generationByRepoId.set(repoId, generation) + return generation +} + +export function bumpLocalWorktreeScanGeneration(repoId: string): void { + generationByRepoId.set(repoId, ++generationSequence) +} + +export function isLocalWorktreeScanGenerationCurrent(repoId: string, generation: number): boolean { + return getLocalWorktreeScanGeneration(repoId) === generation +} + +export function resetLocalWorktreeScanGenerationsForTests(): void { + generationSequence += 1 + generationByRepoId.clear() +} diff --git a/src/main/persistence-initial-load.test.ts b/src/main/persistence-initial-load.test.ts index e1926a67877..6d0c0f11d5e 100644 --- a/src/main/persistence-initial-load.test.ts +++ b/src/main/persistence-initial-load.test.ts @@ -16,6 +16,10 @@ import { makeProjectHostSetup } from './persistence-test-harness' import { TEST_LEAF_1 } from './persistence-session-fixtures' +import { + getLocalWorktreeScanGeneration, + isLocalWorktreeScanGenerationCurrent +} from './local-worktree-scan-generation' // Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config. const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({ @@ -304,12 +308,13 @@ describe('Store', () => { it('updates and persists a project Windows runtime preference', async () => { const project = makeProject({ - id: 'project-1', + id: 'repo:r1', sourceRepoIds: ['r1'], localWindowsRuntimePreference: { kind: 'inherit-global' } }) writeDataFile({ ...getDefaultPersistedState(testState.dir), + repos: [makeRepo({ id: 'r1' })], projects: [project], projectHostSetups: [ makeProjectHostSetup({ @@ -320,12 +325,14 @@ describe('Store', () => { ] }) const store = await createStore() + const scanGeneration = getLocalWorktreeScanGeneration('r1') - const updated = store.updateProject('project-1', { + const updated = store.updateProject(project.id, { localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' } }) expect(updated?.localWindowsRuntimePreference).toEqual({ kind: 'wsl', distro: 'Ubuntu' }) + expect(isLocalWorktreeScanGenerationCurrent('r1', scanGeneration)).toBe(false) store.flush() const reloaded = await createStore() expect(reloaded.getProjects()[0]?.localWindowsRuntimePreference).toEqual({ diff --git a/src/main/persistence-repo-lifecycle.test.ts b/src/main/persistence-repo-lifecycle.test.ts index 53b759a7d01..1653f78297e 100644 --- a/src/main/persistence-repo-lifecycle.test.ts +++ b/src/main/persistence-repo-lifecycle.test.ts @@ -14,6 +14,10 @@ import { makeTerminalTab, makeWorktreeLineage } from './persistence-test-harness' +import { + getLocalWorktreeScanGeneration, + isLocalWorktreeScanGenerationCurrent +} from './local-worktree-scan-generation' // Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config. const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({ @@ -79,6 +83,23 @@ describe('Store', () => { expect(fetched!.gitUsername).toBe('') }) + it('invalidates local scans across add, remove, and same-id re-add', async () => { + const store = await createStore() + const repoId = 'scan-lifecycle' + const beforeAdd = getLocalWorktreeScanGeneration(repoId) + + store.addRepo(makeRepo({ id: repoId })) + expect(isLocalWorktreeScanGenerationCurrent(repoId, beforeAdd)).toBe(false) + + const beforeRemove = getLocalWorktreeScanGeneration(repoId) + store.removeProject(repoId) + expect(isLocalWorktreeScanGenerationCurrent(repoId, beforeRemove)).toBe(false) + + const beforeReAdd = getLocalWorktreeScanGeneration(repoId) + store.addRepo(makeRepo({ id: repoId, path: '/replacement' })) + expect(isLocalWorktreeScanGenerationCurrent(repoId, beforeReAdd)).toBe(false) + }) + it('setResolvedRepoGitUsername persists the enriched username for hydration', async () => { const store = await createStore() store.addRepo(makeRepo()) @@ -369,6 +390,24 @@ describe('Store', () => { // ── 6b. removeProjectForHost is host-scoped ─────────────────────────── + it('invalidates local scans when one host registration is removed', async () => { + const store = await createStore() + store.addRepo(makeRepo({ id: 'shared', path: '/local/repo' })) + store.addRepo( + makeRepo({ + id: 'shared', + path: '/remote/repo', + connectionId: 'ssh-old', + executionHostId: 'ssh:ssh-old' + }) + ) + const beforeRemove = getLocalWorktreeScanGeneration('shared') + + store.removeProjectForHost('shared', 'ssh:ssh-old') + + expect(isLocalWorktreeScanGenerationCurrent('shared', beforeRemove)).toBe(false) + }) + it('removeProjectForHost removes only the target host row for a shared repo id', async () => { const store = await createStore() // Same repo id on both local and an SSH host. diff --git a/src/main/persistence-settings-update.test.ts b/src/main/persistence-settings-update.test.ts index 330386a48a2..dc3a3c7ebb5 100644 --- a/src/main/persistence-settings-update.test.ts +++ b/src/main/persistence-settings-update.test.ts @@ -14,6 +14,10 @@ import { makeRepo, makeTerminalTab } from './persistence-test-harness' +import { + getLocalWorktreeScanGeneration, + isLocalWorktreeScanGenerationCurrent +} from './local-worktree-scan-generation' // Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config. const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({ @@ -227,6 +231,21 @@ describe('Store', () => { expect(listener).not.toHaveBeenCalled() }) + it('invalidates local worktree scans when the global Windows runtime changes', async () => { + writeDataFile({ repos: [makeRepo()] }) + const store = await createStore() + const generation = getLocalWorktreeScanGeneration('r1') + + store.updateSettings({ theme: 'dark' }) + expect(isLocalWorktreeScanGenerationCurrent('r1', generation)).toBe(true) + + store.updateSettings({ + localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' } + }) + + expect(isLocalWorktreeScanGenerationCurrent('r1', generation)).toBe(false) + }) + it('migrates missing terminal scrollback rows to the row default and writes back rows only', async () => { writeDataFile({ settings: {} }) diff --git a/src/main/persistence-update-repo.test.ts b/src/main/persistence-update-repo.test.ts index d2266103615..a89378d6c71 100644 --- a/src/main/persistence-update-repo.test.ts +++ b/src/main/persistence-update-repo.test.ts @@ -11,6 +11,10 @@ import { makeProject, makeProjectHostSetup } from './persistence-test-harness' +import { + getLocalWorktreeScanGeneration, + isLocalWorktreeScanGenerationCurrent +} from './local-worktree-scan-generation' // Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config. const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({ @@ -75,6 +79,25 @@ describe('Store', () => { expect(store.getRepo('r1')!.displayName).toBe('renamed') }) + it('invalidates local worktree scans across a git-folder-git update round trip', async () => { + const store = await createStore() + store.addRepo(makeRepo({ id: 'scan-round-trip', kind: 'git' })) + + const initialGeneration = getLocalWorktreeScanGeneration('scan-round-trip') + expect(isLocalWorktreeScanGenerationCurrent('scan-round-trip', initialGeneration)).toBe(true) + + const folder = store.updateRepo('scan-round-trip', { kind: 'folder' }) + expect(folder?.kind).toBe('folder') + expect(isLocalWorktreeScanGenerationCurrent('scan-round-trip', initialGeneration)).toBe(false) + + const folderGeneration = getLocalWorktreeScanGeneration('scan-round-trip') + expect(isLocalWorktreeScanGenerationCurrent('scan-round-trip', folderGeneration)).toBe(true) + + const git = store.updateRepo('scan-round-trip', { kind: 'git' }) + expect(git?.kind).toBe('git') + expect(isLocalWorktreeScanGenerationCurrent('scan-round-trip', folderGeneration)).toBe(false) + }) + it('updateRepo targets one host when repo ids collide', async () => { const store = await createStore() store.addRepo(makeRepo({ id: 'shared', path: '/local/repo' })) diff --git a/src/main/persistence/applying-settings/settings-update.ts b/src/main/persistence/applying-settings/settings-update.ts index c4ded8da753..20614061bdd 100644 --- a/src/main/persistence/applying-settings/settings-update.ts +++ b/src/main/persistence/applying-settings/settings-update.ts @@ -41,6 +41,7 @@ import { export type SettingsMutationOperations = { state: PersistedState + bumpLocalWorktreeScanGeneration: (repoId: string) => void removeRetainedBlob: ( slot: Parameters[0] ) => void @@ -242,6 +243,16 @@ export function updateSettings( }), ...(mergedTelemetry !== undefined ? { telemetry: mergedTelemetry } : {}) } + if ( + !Object.is( + previousSettings.localWindowsRuntimeDefault, + operations.state.settings.localWindowsRuntimeDefault + ) + ) { + for (const repoId of new Set(operations.state.repos.map(({ id }) => id))) { + operations.bumpLocalWorktreeScanGeneration(repoId) + } + } operations.scheduleSave() const changedUpdates = {} as Partial & Record for (const key of Object.keys(sanitizedUpdates) as (keyof GlobalSettings)[]) { diff --git a/src/main/persistence/loading-store/metadata-lineage-batch-pruning.test.ts b/src/main/persistence/loading-store/metadata-lineage-batch-pruning.test.ts new file mode 100644 index 00000000000..9e7b71734b6 --- /dev/null +++ b/src/main/persistence/loading-store/metadata-lineage-batch-pruning.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, vi } from 'vitest' +import { getDefaultPersistedState } from '../../../shared/constants' +import type { Repo } from '../../../shared/repo-types' +import type { WorktreeMeta } from '../../../shared/worktree/meta-types' +import type * as WriteSchedulingModule from './write-scheduling' +import { MetadataLineageOperations } from './metadata-lineage-operations' + +const { scheduleSaveMock } = vi.hoisted(() => ({ scheduleSaveMock: vi.fn() })) + +vi.mock('./write-scheduling', async (importOriginal) => ({ + ...(await importOriginal()), + scheduleSave: scheduleSaveMock +})) + +const REPO: Repo = { + id: 'repo-1', + path: '/workspace/repo', + displayName: 'repo', + badgeColor: '#000', + addedAt: 0 +} + +function makeMeta(worktreeId: string): WorktreeMeta { + return { + instanceId: `instance-${worktreeId}`, + hostId: 'local', + displayName: worktreeId, + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0 + } +} + +describe('MetadataLineageOperations batch metadata pruning', () => { + it('schedules one save for thousands of removals and none for a no-op retry', () => { + const state = getDefaultPersistedState('/home/test') + state.repos = [REPO] + const staleIds = Array.from( + { length: 2_709 }, + (_, index) => `${REPO.id}::/workspace/stale-${index}` + ) + for (const worktreeId of staleIds) { + state.worktreeMeta[worktreeId] = makeMeta(worktreeId) + } + const operations = new MetadataLineageOperations({ state } as never, {} as never, {} as never) + const scan = operations.captureNativeLocalWorktreeMetadataScanExpectation(REPO) + + expect( + operations.pruneSessionlessMissingLocalWorktreeMetadataForRepo(scan, scan.metadata) + ).toHaveLength(staleIds.length) + expect(scheduleSaveMock).toHaveBeenCalledTimes(1) + + expect( + operations.pruneSessionlessMissingLocalWorktreeMetadataForRepo(scan, scan.metadata) + ).toEqual([]) + expect(scheduleSaveMock).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/main/persistence/loading-store/metadata-lineage-operations.ts b/src/main/persistence/loading-store/metadata-lineage-operations.ts index f12af5d1583..7d4867ce476 100644 --- a/src/main/persistence/loading-store/metadata-lineage-operations.ts +++ b/src/main/persistence/loading-store/metadata-lineage-operations.ts @@ -1,6 +1,7 @@ import type { WorkspaceKey } from '../../../shared/folder-workspace-types' import type { WorkspaceLineage, WorktreeLineage } from '../../../shared/worktree/lineage-types' import type { WorktreeMeta } from '../../../shared/worktree/meta-types' +import type { Repo } from '../../../shared/repo-types' import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/execution-host' import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' import { hasWorktreeRemovalRepoOwnerOnOtherHost } from '../../worktree-removal-repo-owner' @@ -28,6 +29,12 @@ import { setWorktreeMetaForHost as setWorktreeMetaForHostOperation } from './worktree-identity-metadata' import { mergeWorktreeMetaForWrite } from './worktree-meta-write-normalization' +import { + captureNativeLocalWorktreeMetadataScanExpectation as captureNativeLocalWorktreeMetadataScanExpectationOperation, + pruneSessionlessMissingLocalWorktreeMetadataForRepo as pruneSessionlessMissingLocalWorktreeMetadataForRepoOperation, + type LocalWorktreeMetadataPruneExpectation, + type NativeLocalWorktreeMetadataScanExpectation +} from '../tracking-repos/missing-local-worktree-metadata-pruning' type MetadataLineageOperationsRuntime = Pick @@ -89,6 +96,15 @@ export class MetadataLineageOperations { ) } + captureNativeLocalWorktreeMetadataScanExpectation( + repo: Repo + ): NativeLocalWorktreeMetadataScanExpectation { + return captureNativeLocalWorktreeMetadataScanExpectationOperation( + this[metadataLineageOperationsContext].runtime.state, + repo + ) + } + setWorktreeMeta(worktreeId: string, meta: Partial): WorktreeMeta { const state = this[metadataLineageOperationsContext].runtime.state const stored = state.worktreeMeta[worktreeId] @@ -184,6 +200,21 @@ export class MetadataLineageOperations { scheduleSave(this[metadataLineageOperationsContext].scheduling) } + pruneSessionlessMissingLocalWorktreeMetadataForRepo( + scan: NativeLocalWorktreeMetadataScanExpectation, + missingMetadata: readonly LocalWorktreeMetadataPruneExpectation[] + ): string[] { + const removed = pruneSessionlessMissingLocalWorktreeMetadataForRepoOperation( + this[metadataLineageOperationsContext].runtime.state, + scan, + missingMetadata + ) + if (removed.length > 0) { + scheduleSave(this[metadataLineageOperationsContext].scheduling) + } + return removed + } + getWorktreeLineage(worktreeId: string): WorktreeLineage | undefined { return this[metadataLineageOperationsContext].runtime.state.worktreeLineageById[worktreeId] } diff --git a/src/main/persistence/loading-store/profile-preferences.ts b/src/main/persistence/loading-store/profile-preferences.ts index 228229d2584..0ec4e383c34 100644 --- a/src/main/persistence/loading-store/profile-preferences.ts +++ b/src/main/persistence/loading-store/profile-preferences.ts @@ -17,6 +17,7 @@ import { import type { StoreRuntimeState } from './store-runtime-state' import type { WriteSchedulingOperations } from './write-scheduling' import { scheduleSave } from './write-scheduling' +import { bumpLocalWorktreeScanGeneration } from '../../local-worktree-scan-generation' type ProfilePreferencesRuntime = Pick< StoreRuntimeState, @@ -155,6 +156,7 @@ export function getSettingsMutationOperations( ): SettingsMutationOperations { return { state: owner[profilePreferencesContext].runtime.state, + bumpLocalWorktreeScanGeneration, removeRetainedBlob: (slot) => owner[profilePreferencesContext].runtime.protectedSecrets.removeRetainedBlob(slot), scheduleSave: () => scheduleSave(owner[profilePreferencesContext].scheduling), diff --git a/src/main/persistence/loading-store/project-collection-operations.ts b/src/main/persistence/loading-store/project-collection-operations.ts index e7100478037..ecb1130072f 100644 --- a/src/main/persistence/loading-store/project-collection-operations.ts +++ b/src/main/persistence/loading-store/project-collection-operations.ts @@ -28,6 +28,7 @@ import { } from './repo-lifecycle-operations' import { scheduleSave } from './write-scheduling' import { removeWorkspaceLineageForFolderParent } from './metadata-lineage-operations' +import { bumpLocalWorktreeScanGeneration } from '../../local-worktree-scan-generation' type ProjectCollectionOperationsRuntime = Pick< StoreRuntimeState, @@ -159,6 +160,7 @@ export function getProjectHostOperations( new ProjectHostPersistenceOperations({ state: owner[projectCollectionOperationsContext].runtime.state, gitUsernameCache: owner[projectCollectionOperationsContext].runtime.gitUsernameCache, + bumpLocalWorktreeScanGeneration, hydrateRepo: (repo) => hydrateRepo(owner[projectCollectionOperationsContext].repos, repo), updateRepoBackedProjectHostSetup: (setup, repo, updates) => updateRepoBackedProjectHostSetup( diff --git a/src/main/persistence/loading-store/repo-lifecycle-operations.ts b/src/main/persistence/loading-store/repo-lifecycle-operations.ts index bdd3718d9a6..095090ee1ca 100644 --- a/src/main/persistence/loading-store/repo-lifecycle-operations.ts +++ b/src/main/persistence/loading-store/repo-lifecycle-operations.ts @@ -15,6 +15,7 @@ import { pruneWorktreeStateForRepo as pruneWorktreeStateForRepoOperation } from import { hydrateRepo as hydrateRepoOperation } from '../tracking-repos/repo-hydration' import { RepoUpdatePersistenceOperations } from '../tracking-repos/repo-update-operations' import { ProjectHostSetupPersistenceOperations } from '../tracking-repos/project-host-setup-update' +import { bumpLocalWorktreeScanGeneration } from '../../local-worktree-scan-generation' import type { StoreRuntimeState } from './store-runtime-state' import type { WriteSchedulingOperations } from './write-scheduling' @@ -43,6 +44,7 @@ export class RepoLifecycleOperations { } addRepo(repo: Repo): void { + bumpLocalWorktreeScanGeneration(repo.id) getRepoOrderOperations(this).addRepo(repo) } @@ -55,9 +57,15 @@ export class RepoLifecycleOperations { } removeProject(id: string): void { + const repoRemoved = this[repoLifecycleOperationsContext].runtime.state.repos.some( + (repo) => repo.id === id + ) this[repoLifecycleOperationsContext].runtime.state.repos = this[ repoLifecycleOperationsContext ].runtime.state.repos.filter((r) => r.id !== id) + if (repoRemoved) { + bumpLocalWorktreeScanGeneration(id) + } syncProjectHostSetupCompatibilityState(this) // Why: presets are repo-scoped and unreachable once the repo is gone, so drop them with it. delete this[repoLifecycleOperationsContext].runtime.state.sparsePresetsByRepo[id] @@ -77,9 +85,15 @@ export class RepoLifecycleOperations { } removeProjectForHost(id: string, hostId: ExecutionHostId): void { + const repoRemoved = this[repoLifecycleOperationsContext].runtime.state.repos.some( + (repo) => repo.id === id && getRepoExecutionHostId(repo) === hostId + ) this[repoLifecycleOperationsContext].runtime.state.repos = this[ repoLifecycleOperationsContext ].runtime.state.repos.filter((r) => !(r.id === id && getRepoExecutionHostId(r) === hostId)) + if (repoRemoved) { + bumpLocalWorktreeScanGeneration(id) + } const idStillPresent = this[repoLifecycleOperationsContext].runtime.state.repos.some( (r) => r.id === id ) @@ -204,6 +218,7 @@ export function getRepoUpdateOperations( owner[repoLifecycleOperationsContext].runtime.repoUpdateOperations ??= new RepoUpdatePersistenceOperations({ state: owner[repoLifecycleOperationsContext].runtime.state, + bumpLocalWorktreeScanGeneration, syncProjectHostSetupCompatibilityState: () => syncProjectHostSetupCompatibilityState(owner), scheduleSave: () => scheduleSave(owner[repoLifecycleOperationsContext].scheduling), hydrateRepo: (repo) => hydrateRepo(owner, repo) diff --git a/src/main/persistence/loading-store/worktree-identity-metadata.ts b/src/main/persistence/loading-store/worktree-identity-metadata.ts index 396c2ffae85..21fea4ac15c 100644 --- a/src/main/persistence/loading-store/worktree-identity-metadata.ts +++ b/src/main/persistence/loading-store/worktree-identity-metadata.ts @@ -36,11 +36,18 @@ function resolveAliasIdentityKey(state: PersistedState, alias: string): string | } /** Drop identity metadata no alias points at any more. */ -export function pruneUnreferencedWorktreeIdentityMeta(state: PersistedState): boolean { +export function pruneUnreferencedWorktreeIdentityMeta( + state: PersistedState, + candidates?: ReadonlySet +): boolean { const referenced = new Set(Object.values(state.worktreeIdentityAliases ?? {}).flat()) let changed = false - for (const identityKey of Object.keys(state.worktreeMetaByIdentity ?? {})) { - if (!referenced.has(identityKey)) { + const identityKeys = candidates ?? Object.keys(state.worktreeMetaByIdentity ?? {}) + for (const identityKey of identityKeys) { + if ( + Object.hasOwn(state.worktreeMetaByIdentity ?? {}, identityKey) && + !referenced.has(identityKey) + ) { delete state.worktreeMetaByIdentity?.[identityKey] changed = true } diff --git a/src/main/persistence/restoring-sessions/session-worktree-ownership.test.ts b/src/main/persistence/restoring-sessions/session-worktree-ownership.test.ts new file mode 100644 index 00000000000..1e84cf4c506 --- /dev/null +++ b/src/main/persistence/restoring-sessions/session-worktree-ownership.test.ts @@ -0,0 +1,166 @@ +import { describe, expect, it } from 'vitest' +import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../../shared/constants' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import { worktreeWorkspaceKey } from '../../../shared/workspace-scope' +import { + collectPersistedSessionWorktreeOwners, + collectWorkspaceSessionWorktreeOwners +} from './session-worktree-ownership' + +const TARGET = 'repo-1::/workspace/target' +const OTHER = 'repo-1::/workspace/other' + +function sessionWith(field: keyof WorkspaceSessionState, value: unknown): WorkspaceSessionState { + return { ...getDefaultWorkspaceSession(), [field]: value } as WorkspaceSessionState +} + +function collected(session: WorkspaceSessionState): string[] { + return [...collectWorkspaceSessionWorktreeOwners(session, new Set([TARGET]))] +} + +const OWNER_KEYED_FIELD_VALUES = [ + ['tabsByWorktree', []], + ['openFilesByWorktree', []], + ['activeFileIdByWorktree', null], + ['browserTabsByWorktree', []], + ['activeBrowserTabIdByWorktree', null], + ['clientHostedBrowserPagesByWorktree', []], + ['activeTabTypeByWorktree', 'terminal'], + ['activeTabIdByWorktree', null], + ['unifiedTabs', []], + ['tabGroups', []], + ['tabGroupLayouts', { type: 'leaf', groupId: 'group-1' }], + ['activeGroupIdByWorktree', 'group-1'], + ['lastVisitedAtByWorktreeId', 0], + ['defaultTerminalTabsAppliedByWorktreeId', true] +] as const satisfies readonly [keyof WorkspaceSessionState, unknown][] + +describe('workspace session worktree ownership', () => { + it.each(OWNER_KEYED_FIELD_VALUES)( + 'treats a key in %s as ownership even when its bucket is empty or null', + (field, value) => { + expect(collected(sessionWith(field, { [TARGET]: value }))).toEqual([TARGET]) + } + ) + + it.each([ + ['active worktree', sessionWith('activeWorktreeId', TARGET)], + ['active workspace', sessionWith('activeWorkspaceKey', worktreeWorkspaceKey(TARGET))], + ['shutdown owner', sessionWith('activeWorktreeIdsOnShutdown', [TARGET])], + [ + 'host-qualified recency owner', + sessionWith('lastVisitedAtByWorktreeId', { [`ssh:builder|${TARGET}`]: 1 }) + ] + ])('collects the %s', (_label, session) => { + expect(collected(session)).toEqual([TARGET]) + }) + + it.each([ + ['terminal tab', 'tabsByWorktree', { [OTHER]: [{ worktreeId: TARGET }] }], + ['open file', 'openFilesByWorktree', { [OTHER]: [{ worktreeId: TARGET }] }], + ['unified tab', 'unifiedTabs', { [OTHER]: [{ worktreeId: TARGET }] }], + ['tab group', 'tabGroups', { [OTHER]: [{ worktreeId: TARGET }] }], + ['browser workspace', 'browserTabsByWorktree', { [OTHER]: [{ worktreeId: TARGET }] }], + [ + 'browser workspace document', + 'browserTabsByWorktree', + { [OTHER]: [{ worktreeId: OTHER, docLocation: { worktreeId: TARGET } }] } + ], + ['browser page', 'browserPagesByWorkspace', { page: [{ worktreeId: TARGET }] }], + [ + 'browser page document', + 'browserPagesByWorkspace', + { page: [{ worktreeId: OTHER, docLocation: { worktreeId: TARGET } }] } + ], + [ + 'browser close intent', + 'clientHostedBrowserCloseIntentsByEnvironment', + { environment: [{ worktreeId: TARGET }] } + ], + ['sleeping agent', 'sleepingAgentSessionsByPaneKey', { pane: { worktreeId: TARGET } }], + ['terminal tombstone', 'terminalSurfaceTombstonesByPaneKey', { pane: { worktreeId: TARGET } }], + [ + 'closed-terminal tombstone', + 'closedTerminalTabTombstonesByTabId', + { tab: { worktreeId: TARGET } } + ] + ] as const)( + 'collects a %s value even when its enclosing key names something else', + (_label, field, value) => { + expect(collected(sessionWith(field, value))).toEqual([TARGET]) + } + ) + + it('collects ownership from every persisted host partition', () => { + const state = getDefaultPersistedState('/home/test') + state.workspaceSessionsByHostId = { + 'ssh:builder': sessionWith('tabsByWorktree', { [TARGET]: [] }), + 'runtime:environment': sessionWith('activeWorktreeIdsOnShutdown', [OTHER]) + } + + expect( + [...collectPersistedSessionWorktreeOwners(state, new Set([TARGET, OTHER]))].sort() + ).toEqual([OTHER, TARGET]) + }) + + it('prefers an exact candidate id over workspace-key prefix parsing', () => { + const prefixedId = 'folder::/workspace/target' + const session = sessionWith('tabsByWorktree', { [prefixedId]: [] }) + + expect([...collectWorkspaceSessionWorktreeOwners(session, new Set([prefixedId]))]).toEqual([ + prefixedId + ]) + }) + + it('matches canonically equivalent Unicode worktree owners', () => { + const candidateId = 'repo-1::/workspace/Café'.normalize('NFC') + const ownerId = candidateId.normalize('NFD') + const session = sessionWith('activeWorktreeId', ownerId) + + expect([...collectWorkspaceSessionWorktreeOwners(session, new Set([candidateId]))]).toEqual([ + candidateId + ]) + }) + + it('matches Windows worktree owners across drive case and slash spelling', () => { + const candidateId = 'repo-1::D:/Agentic/game2' + const session = sessionWith('activeWorktreeId', 'repo-1::d:\\agentic\\game2') + + expect([...collectWorkspaceSessionWorktreeOwners(session, new Set([candidateId]))]).toEqual([ + candidateId + ]) + }) + + it('matches macOS /private/tmp session owners to /tmp metadata', () => { + const candidateId = 'repo-1::/private/tmp/orca/target' + const session = sessionWith('activeWorktreeId', 'repo-1::/tmp/orca/target') + + expect([ + ...collectWorkspaceSessionWorktreeOwners(session, new Set([candidateId]), 'darwin') + ]).toEqual([candidateId]) + }) + + it('preserves every candidate that shares an owner comparison key', () => { + const nfcId = 'repo-1::/workspace/Café'.normalize('NFC') + const nfdId = nfcId.normalize('NFD') + const session = sessionWith('activeWorktreeId', nfcId) + + expect( + [...collectWorkspaceSessionWorktreeOwners(session, new Set([nfcId, nfdId]))].sort() + ).toEqual([nfcId, nfdId].sort()) + }) + + it('ignores fields whose keys cannot identify a worktree owner', () => { + const session = { + ...getDefaultWorkspaceSession(), + activeRepoId: TARGET, + activeTabId: TARGET, + terminalLayoutsByTabId: { [TARGET]: { root: null } }, + remoteSessionIdsByTabId: { [TARGET]: TARGET }, + terminalPtyIncarnationsByPaneKey: { [TARGET]: TARGET }, + terminalTopologyRevisionByRepoId: { [TARGET]: 1 } + } as unknown as WorkspaceSessionState + + expect(collected(session)).toEqual([]) + }) +}) diff --git a/src/main/persistence/restoring-sessions/session-worktree-ownership.ts b/src/main/persistence/restoring-sessions/session-worktree-ownership.ts new file mode 100644 index 00000000000..dc572e79278 --- /dev/null +++ b/src/main/persistence/restoring-sessions/session-worktree-ownership.ts @@ -0,0 +1,246 @@ +import type { PersistedState } from '../../../shared/persisted-state-types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import { parseWorkspaceKey } from '../../../shared/workspace-scope' +import { + getWorktreeIdFromHostIdentity, + isWorktreeHostIdentity +} from '../../../shared/worktree/host-qualified-identity' +import { worktreeRetentionIdComparisonKey } from '../../worktree-retention-path-comparison' + +type WorkspaceSessionWorktreeReferenceKind = + | 'none' + | 'direct' + | 'owner-keyed' + | 'owner-keyed-row-arrays' + | 'owner-keyed-browser-row-arrays' + | 'worktree-id-array' + | 'row-record' + | 'row-arrays' + | 'browser-row-arrays' + +/** Every persisted session field must state how, if at all, it can name a worktree owner. */ +export const WORKSPACE_SESSION_WORKTREE_REFERENCE_KIND = { + activeRepoId: 'none', + activeWorkspaceKey: 'direct', + activeWorkspaceExecutionHostId: 'none', + activeWorktreeId: 'direct', + activeTabId: 'none', + tabsByWorktree: 'owner-keyed-row-arrays', + terminalLayoutsByTabId: 'none', + activeWorktreeIdsOnShutdown: 'worktree-id-array', + openFilesByWorktree: 'owner-keyed-row-arrays', + activeFileIdByWorktree: 'owner-keyed', + markdownFrontmatterVisible: 'none', + browserTabsByWorktree: 'owner-keyed-browser-row-arrays', + browserPagesByWorkspace: 'browser-row-arrays', + activeBrowserTabIdByWorktree: 'owner-keyed', + clientHostedBrowserPagesByWorktree: 'owner-keyed', + clientHostedBrowserCloseIntentsByEnvironment: 'row-arrays', + activeTabTypeByWorktree: 'owner-keyed', + browserUrlHistory: 'none', + activeTabIdByWorktree: 'owner-keyed', + unifiedTabs: 'owner-keyed-row-arrays', + tabGroups: 'owner-keyed-row-arrays', + tabGroupLayouts: 'owner-keyed', + activeGroupIdByWorktree: 'owner-keyed', + activeConnectionIdsAtShutdown: 'none', + remoteSessionIdsByTabId: 'none', + lastVisitedAtByWorktreeId: 'owner-keyed', + defaultTerminalTabsAppliedByWorktreeId: 'owner-keyed', + sleepingAgentSessionsByPaneKey: 'row-record', + terminalPtyIncarnationsByPaneKey: 'none', + terminalTopologyRevisionByRepoId: 'none', + terminalSurfaceTombstonesByPaneKey: 'row-record', + closedTerminalTabTombstonesByTabId: 'row-record' +} as const satisfies Record + +type WorktreeRow = { worktreeId?: string | null } +type BrowserWorktreeRow = WorktreeRow & { + docLocation?: { worktreeId?: string | null } | null +} + +export type WorktreeOwnerCandidateCollector = Readonly<{ + owners: Set + addOwner: (ownerKey: string | null | undefined) => void +}> + +export function getPersistedWorktreeOwnerId(ownerKey: string | null | undefined): string | null { + if (!ownerKey) { + return null + } + if (isWorktreeHostIdentity(ownerKey)) { + return getWorktreeIdFromHostIdentity(ownerKey) || null + } + const scope = parseWorkspaceKey(ownerKey) + if (scope?.type === 'folder') { + return null + } + return scope?.type === 'worktree' ? scope.worktreeId : ownerKey +} + +export function createWorktreeOwnerCandidateCollector( + candidateIds: ReadonlySet, + platform: NodeJS.Platform = process.platform, + owners: Set = new Set() +): WorktreeOwnerCandidateCollector { + const candidateIdsByComparisonKey = new Map() + for (const candidateId of candidateIds) { + const comparisonKey = worktreeRetentionIdComparisonKey(candidateId, platform) + if (!comparisonKey) { + continue + } + const matches = candidateIdsByComparisonKey.get(comparisonKey) ?? [] + matches.push(candidateId) + candidateIdsByComparisonKey.set(comparisonKey, matches) + } + return { + owners, + addOwner: (ownerKey) => { + const worktreeId = + ownerKey && candidateIds.has(ownerKey) ? ownerKey : getPersistedWorktreeOwnerId(ownerKey) + if (!worktreeId) { + return + } + const comparisonKey = worktreeRetentionIdComparisonKey(worktreeId, platform) + const equivalentCandidates = comparisonKey + ? candidateIdsByComparisonKey.get(comparisonKey) + : undefined + if (equivalentCandidates) { + for (const candidateId of equivalentCandidates) { + owners.add(candidateId) + } + } else if (candidateIds.has(worktreeId)) { + owners.add(worktreeId) + } + } + } +} + +function collectRecordKeys( + value: unknown, + add: (ownerKey: string | null | undefined) => void +): void { + for (const ownerKey of Object.keys((value ?? {}) as Record)) { + add(ownerKey) + } +} + +function collectWorktreeRows( + rows: unknown, + add: (ownerKey: string | null | undefined) => void +): void { + if (!Array.isArray(rows)) { + return + } + for (const row of rows as WorktreeRow[]) { + add(row?.worktreeId) + } +} + +function collectBrowserRows( + rows: unknown, + add: (ownerKey: string | null | undefined) => void +): void { + if (!Array.isArray(rows)) { + return + } + for (const row of rows as BrowserWorktreeRow[]) { + add(row?.worktreeId) + add(row?.docLocation?.worktreeId) + } +} + +function collectSessionFieldOwners( + value: unknown, + kind: WorkspaceSessionWorktreeReferenceKind, + add: (ownerKey: string | null | undefined) => void +): void { + switch (kind) { + case 'none': + return + case 'direct': + add(typeof value === 'string' ? value : null) + return + case 'owner-keyed': + collectRecordKeys(value, add) + return + case 'owner-keyed-row-arrays': + case 'owner-keyed-browser-row-arrays': + for (const [ownerKey, rows] of Object.entries((value ?? {}) as Record)) { + add(ownerKey) + if (kind === 'owner-keyed-browser-row-arrays') { + collectBrowserRows(rows, add) + } else { + collectWorktreeRows(rows, add) + } + } + return + case 'worktree-id-array': + for (const ownerKey of (value ?? []) as string[]) { + add(ownerKey) + } + return + case 'row-record': + for (const row of Object.values((value ?? {}) as Record)) { + add(row?.worktreeId) + } + return + case 'row-arrays': + case 'browser-row-arrays': + for (const rows of Object.values((value ?? {}) as Record)) { + if (kind === 'browser-row-arrays') { + collectBrowserRows(rows, add) + } else { + collectWorktreeRows(rows, add) + } + } + } +} + +export function collectWorkspaceSessionWorktreeOwners( + session: WorkspaceSessionState, + candidateIds: ReadonlySet, + platform: NodeJS.Platform = process.platform, + owners: Set = new Set() +): Set { + const collector = createWorktreeOwnerCandidateCollector(candidateIds, platform, owners) + addWorkspaceSessionWorktreeOwners(session, collector) + return owners +} + +function addWorkspaceSessionWorktreeOwners( + session: WorkspaceSessionState, + collector: WorktreeOwnerCandidateCollector +): void { + for (const field of Object.keys( + WORKSPACE_SESSION_WORKTREE_REFERENCE_KIND + ) as (keyof WorkspaceSessionState)[]) { + collectSessionFieldOwners( + session[field], + WORKSPACE_SESSION_WORKTREE_REFERENCE_KIND[field], + collector.addOwner + ) + } +} + +export function addPersistedSessionWorktreeOwners( + state: Pick, + collector: WorktreeOwnerCandidateCollector +): void { + addWorkspaceSessionWorktreeOwners(state.workspaceSession, collector) + for (const session of Object.values(state.workspaceSessionsByHostId ?? {})) { + if (session) { + addWorkspaceSessionWorktreeOwners(session, collector) + } + } +} + +export function collectPersistedSessionWorktreeOwners( + state: Pick, + candidateIds: ReadonlySet, + platform: NodeJS.Platform = process.platform +): Set { + const collector = createWorktreeOwnerCandidateCollector(candidateIds, platform) + addPersistedSessionWorktreeOwners(state, collector) + return collector.owners +} diff --git a/src/main/persistence/tracking-repos/local-worktree-metadata-batch-pruning.bench.test.ts b/src/main/persistence/tracking-repos/local-worktree-metadata-batch-pruning.bench.test.ts new file mode 100644 index 00000000000..0f5a85a80c6 --- /dev/null +++ b/src/main/persistence/tracking-repos/local-worktree-metadata-batch-pruning.bench.test.ts @@ -0,0 +1,106 @@ +// Opt in: ORCA_LOCAL_METADATA_PRUNE_BENCH=1 pnpm test src/main/persistence/tracking-repos/local-worktree-metadata-batch-pruning.bench.test.ts +import { performance } from 'node:perf_hooks' +import { describe, expect, it, vi } from 'vitest' +import { getDefaultPersistedState } from '../../../shared/constants' +import type { PersistedState } from '../../../shared/persisted-state-types' +import type { Repo } from '../../../shared/repo-types' +import type { WorktreeMeta } from '../../../shared/worktree/meta-types' +import { removeWorkspaceSessionOwner } from '../restoring-sessions/session-owner-removal' +import { + pruneUnreferencedWorktreeIdentityMeta, + removeWorktreeMetadataForHost +} from '../loading-store/worktree-identity-metadata' +import { + captureNativeLocalWorktreeMetadataScanExpectation, + pruneSessionlessMissingLocalWorktreeMetadataForRepo +} from './missing-local-worktree-metadata-pruning' + +const describeBench = process.env.ORCA_LOCAL_METADATA_PRUNE_BENCH ? describe : describe.skip +const ROW_COUNT = 2_709 +const REPO: Repo = { + id: 'repo-1', + path: '/workspace/repo', + displayName: 'repo', + badgeColor: '#000', + addedAt: 0 +} + +function makeMeta(index: number): WorktreeMeta { + return { + instanceId: `instance-${index}`, + hostId: 'local', + displayName: `stale-${index}`, + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: index, + lastActivityAt: 0 + } +} + +function makeState(): { state: PersistedState; staleIds: string[] } { + const state = getDefaultPersistedState('/home/test') + state.repos = [REPO] + state.worktreeMetaByIdentity = {} + state.worktreeIdentityAliases = {} + const staleIds = Array.from( + { length: ROW_COUNT }, + (_, index) => `${REPO.id}::/workspace/stale-${index}` + ) + for (const [index, worktreeId] of staleIds.entries()) { + const meta = makeMeta(index) + const identityKey = `identity-${index}` + state.worktreeMeta[worktreeId] = meta + state.worktreeMetaByIdentity[identityKey] = meta + state.worktreeIdentityAliases[`local|${worktreeId}`] = [identityKey] + } + return { state, staleIds } +} + +describeBench('authoritative local metadata batch pruning', () => { + it('measures the reported 2,709-row sequential and batch shapes', () => { + const legacy = makeState() + const batch = makeState() + const cloneSpy = vi.spyOn(globalThis, 'structuredClone') + const legacyStartedAt = performance.now() + for (const worktreeId of legacy.staleIds) { + removeWorktreeMetadataForHost(legacy.state, worktreeId, undefined) + delete legacy.state.worktreeMeta[worktreeId] + legacy.state.workspaceSession = removeWorkspaceSessionOwner( + legacy.state.workspaceSession, + worktreeId + )! + } + pruneUnreferencedWorktreeIdentityMeta(legacy.state) + const legacyMs = performance.now() - legacyStartedAt + const legacySessionClones = cloneSpy.mock.calls.length + cloneSpy.mockClear() + + const batchStartedAt = performance.now() + const scan = captureNativeLocalWorktreeMetadataScanExpectation(batch.state, REPO) + const removed = pruneSessionlessMissingLocalWorktreeMetadataForRepo( + batch.state, + scan, + scan.metadata + ) + const batchMs = performance.now() - batchStartedAt + const batchSessionClones = cloneSpy.mock.calls.length + cloneSpy.mockRestore() + + expect(removed).toHaveLength(ROW_COUNT) + expect(Object.keys(legacy.state.worktreeMeta)).toHaveLength(0) + expect(Object.keys(batch.state.worktreeMeta)).toHaveLength(0) + expect(legacySessionClones).toBe(ROW_COUNT) + expect(batchSessionClones).toBe(0) + + console.log( + `[bench] rows=${ROW_COUNT} metadata+session=${legacyMs.toFixed(2)}ms -> ${batchMs.toFixed(2)}ms; ` + + `gitScans=${ROW_COUNT} -> 1; sessionClones=${legacySessionClones} -> ${batchSessionClones}; ` + + `minimumSaveSchedules=${ROW_COUNT * 2} -> 1` + ) + }) +}) diff --git a/src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.test.ts b/src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.test.ts new file mode 100644 index 00000000000..85c883fe666 --- /dev/null +++ b/src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.test.ts @@ -0,0 +1,242 @@ +import { describe, expect, it } from 'vitest' +import { getDefaultPersistedState } from '../../../shared/constants' +import type { PersistedState } from '../../../shared/persisted-state-types' +import type { Repo } from '../../../shared/repo-types' +import type { WorktreeMeta } from '../../../shared/worktree/meta-types' +import { + captureNativeLocalWorktreeMetadataScanExpectation, + pruneSessionlessMissingLocalWorktreeMetadataForRepo +} from './missing-local-worktree-metadata-pruning' + +const REPO_ID = 'repo-1' +const WORKTREE_ID = `${REPO_ID}::/workspace/stale` +const LOCAL_ALIAS = `local|${WORKTREE_ID}` +const IDENTITY_KEY = 'local-identity' + +function makeRepo(id = REPO_ID): Repo { + return { + id, + path: `/workspace/${id}`, + displayName: id, + badgeColor: '#000', + addedAt: 0 + } +} + +function makeMeta(worktreeId = WORKTREE_ID): WorktreeMeta { + return { + instanceId: `instance-${worktreeId}`, + hostId: 'local', + displayName: worktreeId, + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0 + } +} + +function makeState(): PersistedState { + const state = getDefaultPersistedState('/home/test') + state.repos = [makeRepo()] + return state +} + +function makeCanonicalOnlyState(): { + state: PersistedState + meta: WorktreeMeta +} { + const state = makeState() + const meta = makeMeta() + state.worktreeMetaByIdentity = { [IDENTITY_KEY]: meta } + state.worktreeIdentityAliases = { [LOCAL_ALIAS]: [IDENTITY_KEY] } + return { state, meta } +} + +function pruneCaptured(state: PersistedState): string[] { + const scan = captureNativeLocalWorktreeMetadataScanExpectation(state, state.repos[0]!) + return pruneSessionlessMissingLocalWorktreeMetadataForRepo(state, scan, scan.metadata) +} + +describe('local worktree metadata scan expectations', () => { + it('captures canonical-only local metadata and removes its orphaned identity row', () => { + const { state } = makeCanonicalOnlyState() + + expect(pruneCaptured(state)).toEqual([WORKTREE_ID]) + expect(state.worktreeMeta[WORKTREE_ID]).toBeUndefined() + expect(state.worktreeIdentityAliases?.[LOCAL_ALIAS]).toBeUndefined() + expect(state.worktreeMetaByIdentity?.[IDENTITY_KEY]).toBeUndefined() + }) + + it('removes equal legacy and canonical projections after a persistence round trip', () => { + const state = makeState() + const meta = makeMeta() + state.worktreeMeta[WORKTREE_ID] = structuredClone(meta) + state.worktreeMetaByIdentity = { [IDENTITY_KEY]: structuredClone(meta) } + state.worktreeIdentityAliases = { [LOCAL_ALIAS]: [IDENTITY_KEY] } + + expect(pruneCaptured(state)).toEqual([WORKTREE_ID]) + expect(state.worktreeMeta[WORKTREE_ID]).toBeUndefined() + expect(state.worktreeIdentityAliases[LOCAL_ALIAS]).toBeUndefined() + expect(state.worktreeMetaByIdentity[IDENTITY_KEY]).toBeUndefined() + }) + + it('does not sweep an unrelated unaliased canonical row during pruning', () => { + const { state, meta } = makeCanonicalOnlyState() + const unrelated = makeMeta(`${REPO_ID}::/workspace/unrelated`) + state.worktreeMetaByIdentity!.unrelated = unrelated + + expect(pruneCaptured(state)).toEqual([WORKTREE_ID]) + expect(state.worktreeMetaByIdentity).toEqual({ unrelated }) + expect(state.worktreeMetaByIdentity?.[IDENTITY_KEY]).not.toBeDefined() + expect(meta).not.toBe(unrelated) + }) + + it('allocates expectations only for the scanned repository', () => { + const state = makeState() + const otherRepo = makeRepo('repo-2') + const otherId = `${otherRepo.id}::/workspace/other` + const canonicalId = `${REPO_ID}::/workspace/canonical` + const canonicalKey = 'canonical-key' + state.repos.push(otherRepo) + state.worktreeMeta[WORKTREE_ID] = makeMeta() + state.worktreeMeta[otherId] = makeMeta(otherId) + state.worktreeMetaByIdentity = { + [canonicalKey]: makeMeta(canonicalId), + 'other-key': makeMeta(otherId) + } + state.worktreeIdentityAliases = { + [`local|${canonicalId}`]: [canonicalKey], + [`local|${otherId}`]: ['other-key'] + } + + const scan = captureNativeLocalWorktreeMetadataScanExpectation(state, state.repos[0]!) + + expect(scan.metadata.map(({ worktreeId }) => worktreeId).sort()).toEqual( + [WORKTREE_ID, canonicalId].sort() + ) + }) + + it('preserves metadata when an alias array is replaced with identical contents', () => { + const { state } = makeCanonicalOnlyState() + const scan = captureNativeLocalWorktreeMetadataScanExpectation(state, state.repos[0]!) + state.worktreeIdentityAliases![LOCAL_ALIAS] = [IDENTITY_KEY] + + expect(pruneSessionlessMissingLocalWorktreeMetadataForRepo(state, scan, scan.metadata)).toEqual( + [] + ) + expect(state.worktreeMetaByIdentity?.[IDENTITY_KEY]).toBeDefined() + }) + + it('preserves metadata when the captured alias array is mutated in place', () => { + const { state } = makeCanonicalOnlyState() + const scan = captureNativeLocalWorktreeMetadataScanExpectation(state, state.repos[0]!) + state.worktreeIdentityAliases![LOCAL_ALIAS]!.push('new-identity') + state.worktreeMetaByIdentity!['new-identity'] = makeMeta() + + expect(pruneSessionlessMissingLocalWorktreeMetadataForRepo(state, scan, scan.metadata)).toEqual( + [] + ) + }) + + it.each([ + [ + 'replaced', + (state: PersistedState) => { + state.worktreeMetaByIdentity![IDENTITY_KEY] = { + ...state.worktreeMetaByIdentity![IDENTITY_KEY]! + } + } + ], + [ + 'host-mutated', + (state: PersistedState) => { + state.worktreeMetaByIdentity![IDENTITY_KEY]!.hostId = 'ssh:builder' + } + ], + [ + 'instance-mutated', + (state: PersistedState) => { + state.worktreeMetaByIdentity![IDENTITY_KEY]!.instanceId = 'new-instance' + } + ], + [ + 'content-mutated', + (state: PersistedState) => { + state.worktreeMetaByIdentity![IDENTITY_KEY]!.comment = 'new comment' + } + ] + ] as const)('preserves metadata when a canonical row is %s', (_label, mutate) => { + const { state } = makeCanonicalOnlyState() + const scan = captureNativeLocalWorktreeMetadataScanExpectation(state, state.repos[0]!) + mutate(state) + + expect(pruneSessionlessMissingLocalWorktreeMetadataForRepo(state, scan, scan.metadata)).toEqual( + [] + ) + }) + + it.each([ + [ + 'added', + (state: PersistedState) => { + state.worktreeIdentityAliases![`ssh:builder|${WORKTREE_ID}`] = [IDENTITY_KEY] + } + ], + [ + 'removed', + (state: PersistedState) => { + delete state.worktreeIdentityAliases![LOCAL_ALIAS] + } + ] + ] as const)('preserves metadata when an alias is %s during the scan', (_label, mutate) => { + const { state } = makeCanonicalOnlyState() + const scan = captureNativeLocalWorktreeMetadataScanExpectation(state, state.repos[0]!) + mutate(state) + + expect(pruneSessionlessMissingLocalWorktreeMetadataForRepo(state, scan, scan.metadata)).toEqual( + [] + ) + expect(state.worktreeMetaByIdentity?.[IDENTITY_KEY]).toBeDefined() + }) + + it('preserves a legacy row created after a canonical-only scan capture', () => { + const { state } = makeCanonicalOnlyState() + const scan = captureNativeLocalWorktreeMetadataScanExpectation(state, state.repos[0]!) + state.worktreeMeta[WORKTREE_ID] = makeMeta() + + expect(pruneSessionlessMissingLocalWorktreeMetadataForRepo(state, scan, scan.metadata)).toEqual( + [] + ) + expect(state.worktreeMeta[WORKTREE_ID]).toBeDefined() + }) + + it.each([WORKTREE_ID, `bogus|${WORKTREE_ID}`, `ssh:builder|${WORKTREE_ID}`])( + 'preserves metadata with an unqualified or foreign alias: %s', + (alias) => { + const state = makeState() + state.worktreeMeta[WORKTREE_ID] = makeMeta() + state.worktreeMetaByIdentity = { [IDENTITY_KEY]: state.worktreeMeta[WORKTREE_ID]! } + state.worktreeIdentityAliases = { [alias]: [IDENTITY_KEY] } + + expect(pruneCaptured(state)).toEqual([]) + expect(state.worktreeMeta[WORKTREE_ID]).toBeDefined() + } + ) + + it('preserves an unqualified alias whose worktree path contains a pipe', () => { + const state = makeState() + const pipedId = `${REPO_ID}::/workspace/a|b` + state.worktreeMeta[pipedId] = makeMeta(pipedId) + state.worktreeMetaByIdentity = { [IDENTITY_KEY]: state.worktreeMeta[pipedId]! } + state.worktreeIdentityAliases = { [pipedId]: [IDENTITY_KEY] } + + expect(pruneCaptured(state)).toEqual([]) + expect(state.worktreeMeta[pipedId]).toBeDefined() + expect(state.worktreeIdentityAliases[pipedId]).toEqual([IDENTITY_KEY]) + }) +}) diff --git a/src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.ts b/src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.ts new file mode 100644 index 00000000000..fca925d0986 --- /dev/null +++ b/src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.ts @@ -0,0 +1,243 @@ +import { isDeepStrictEqual } from 'node:util' +import type { PersistedState } from '../../../shared/persisted-state-types' +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { Project } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' +import { getRepoKind } from '../../../shared/repo-kind' +import { splitWorktreeId } from '../../../shared/worktree/id' +import { + composeWorktreeHostIdentity, + getWorktreeIdFromHostIdentity, + isWorktreeHostIdentity +} from '../../../shared/worktree/host-qualified-identity' +import type { WorktreeMeta } from '../../../shared/worktree/meta-types' +import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host' + +type CapturedLocalRepoIdentity = Readonly< + Pick & { + kind: ReturnType + expectedRepo: Repo | undefined + } +> + +type CapturedLocalRoutingIdentity = Readonly<{ + expectedProject: Project | undefined + expectedProjectUpdatedAt: number | undefined + expectedSettings: GlobalSettings +}> + +type MetadataRowExpectation = Readonly<{ + expectedPresent: boolean + expectedMeta: WorktreeMeta | undefined + expectedSerialized: string | null | undefined + expectedInstanceId: string | undefined + expectedHostId: WorktreeMeta['hostId'] +}> + +type MetadataAliasExpectation = Readonly<{ + alias: string + expectedAlias: string[] + expectedIdentityKeys: readonly string[] + expectedIdentityRows: readonly (MetadataRowExpectation & { identityKey: string })[] +}> + +export type LocalWorktreeMetadataPruneExpectation = Readonly<{ + worktreeId: string + expectedLegacy: MetadataRowExpectation + expectedAliases: readonly MetadataAliasExpectation[] +}> + +export type NativeLocalWorktreeMetadataScanExpectation = Readonly<{ + repo: CapturedLocalRepoIdentity + routing: CapturedLocalRoutingIdentity + metadata: readonly LocalWorktreeMetadataPruneExpectation[] +}> + +type MetadataAliasEntry = readonly [alias: string, identityKeys: string[]] + +function serializeMetadataRow(meta: WorktreeMeta | undefined): string | null | undefined { + if (!meta) { + return undefined + } + try { + return JSON.stringify(meta) + } catch { + return null + } +} + +function captureRow( + record: Readonly> | undefined, + key: string +): MetadataRowExpectation { + const expectedPresent = Object.hasOwn(record ?? {}, key) + const expectedMeta = record?.[key] + return { + expectedPresent, + expectedMeta, + expectedSerialized: serializeMetadataRow(expectedMeta), + expectedInstanceId: expectedMeta?.instanceId, + expectedHostId: expectedMeta?.hostId + } +} + +export function indexMetadataAliasesForWorktreeIds( + state: PersistedState, + worktreeIds: ReadonlySet +): Map { + const indexed = new Map() + for (const [alias, identityKeys] of Object.entries(state.worktreeIdentityAliases ?? {})) { + const matches = new Set() + if (worktreeIds.has(alias)) { + matches.add(alias) + } + const separator = alias.indexOf('|') + const suffix = separator === -1 ? alias : alias.slice(separator + 1) + if (worktreeIds.has(suffix)) { + matches.add(suffix) + } + for (const worktreeId of matches) { + const entries = indexed.get(worktreeId) ?? [] + entries.push([alias, identityKeys]) + indexed.set(worktreeId, entries) + } + } + return indexed +} + +export function captureNativeLocalWorktreeMetadataScanExpectation( + state: PersistedState, + repo: Repo +): NativeLocalWorktreeMetadataScanExpectation { + const matchingRepos = state.repos.filter((entry) => entry.id === repo.id) + const expectedRepo = matchingRepos.length === 1 ? matchingRepos[0] : undefined + const expectedProject = state.projects.find((project) => project.sourceRepoIds.includes(repo.id)) + const candidateIds = new Set() + for (const [worktreeId, meta] of Object.entries(state.worktreeMeta)) { + if ( + splitWorktreeId(worktreeId)?.repoId === repo.id && + (!meta.hostId || meta.hostId === LOCAL_EXECUTION_HOST_ID) + ) { + candidateIds.add(worktreeId) + } + } + for (const alias of Object.keys(state.worktreeIdentityAliases ?? {})) { + if (!isWorktreeHostIdentity(alias)) { + continue + } + const worktreeId = getWorktreeIdFromHostIdentity(alias) + if ( + alias === composeWorktreeHostIdentity(LOCAL_EXECUTION_HOST_ID, worktreeId) && + splitWorktreeId(worktreeId)?.repoId === repo.id + ) { + candidateIds.add(worktreeId) + } + } + const aliasesByWorktreeId = indexMetadataAliasesForWorktreeIds(state, candidateIds) + return { + repo: { + id: repo.id, + path: repo.path, + connectionId: repo.connectionId, + executionHostId: repo.executionHostId, + kind: getRepoKind(repo), + expectedRepo + }, + routing: { + expectedProject, + expectedProjectUpdatedAt: expectedProject?.updatedAt, + expectedSettings: state.settings + }, + metadata: [...candidateIds].map((worktreeId) => ({ + worktreeId, + expectedLegacy: captureRow(state.worktreeMeta, worktreeId), + expectedAliases: (aliasesByWorktreeId.get(worktreeId) ?? []).map(([alias, identityKeys]) => ({ + alias, + expectedAlias: identityKeys, + expectedIdentityKeys: [...identityKeys], + expectedIdentityRows: identityKeys.map((identityKey) => ({ + identityKey, + ...captureRow(state.worktreeMetaByIdentity, identityKey) + })) + })) + })) + } +} + +function rowStillMatches( + record: Readonly> | undefined, + key: string, + expected: MetadataRowExpectation +): boolean { + const current = record?.[key] + return ( + Object.hasOwn(record ?? {}, key) === expected.expectedPresent && + current === expected.expectedMeta && + expected.expectedSerialized !== null && + serializeMetadataRow(current) === expected.expectedSerialized && + current?.instanceId === expected.expectedInstanceId && + current?.hostId === expected.expectedHostId + ) +} + +function aliasesStillMatch( + state: PersistedState, + expected: LocalWorktreeMetadataPruneExpectation, + currentAliases: readonly MetadataAliasEntry[] +): boolean { + if (currentAliases.length !== expected.expectedAliases.length) { + return false + } + const currentByAlias = new Map(currentAliases) + return expected.expectedAliases.every((aliasExpectation) => { + const current = currentByAlias.get(aliasExpectation.alias) + return Boolean( + current && + current === aliasExpectation.expectedAlias && + current.length === aliasExpectation.expectedIdentityKeys.length && + current.every((key, index) => key === aliasExpectation.expectedIdentityKeys[index]) && + aliasExpectation.expectedIdentityRows.every(({ identityKey, ...row }) => + rowStillMatches(state.worktreeMetaByIdentity, identityKey, row) + ) + ) + }) +} + +export function removeRevalidatedLocalWorktreeMetadata( + state: PersistedState, + expected: LocalWorktreeMetadataPruneExpectation, + currentAliases: readonly MetadataAliasEntry[], + removedIdentityKeys?: Set +): boolean { + if ( + !rowStillMatches(state.worktreeMeta, expected.worktreeId, expected.expectedLegacy) || + !aliasesStillMatch(state, expected, currentAliases) + ) { + return false + } + const localAlias = composeWorktreeHostIdentity(LOCAL_EXECUTION_HOST_ID, expected.worktreeId) + if (currentAliases.some(([alias]) => alias !== localAlias)) { + return false + } + const legacy = state.worktreeMeta[expected.worktreeId] + const identityKeys = state.worktreeIdentityAliases?.[localAlias] + if (identityKeys && identityKeys.length !== 1) { + return false + } + const canonical = identityKeys?.[0] ? state.worktreeMetaByIdentity?.[identityKeys[0]] : undefined + if ( + (legacy?.hostId && legacy.hostId !== LOCAL_EXECUTION_HOST_ID) || + (canonical?.hostId && canonical.hostId !== LOCAL_EXECUTION_HOST_ID) || + (identityKeys && !canonical) || + (legacy && canonical && !isDeepStrictEqual(legacy, canonical)) || + (!legacy && !canonical) + ) { + return false + } + if (identityKeys?.[0]) { + removedIdentityKeys?.add(identityKeys[0]) + } + delete state.worktreeMeta[expected.worktreeId] + delete state.worktreeIdentityAliases?.[localAlias] + return true +} diff --git a/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts b/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts new file mode 100644 index 00000000000..cdad14034e2 --- /dev/null +++ b/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts @@ -0,0 +1,477 @@ +import { describe, expect, it, vi } from 'vitest' +import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../../shared/constants' +import type { PersistedState } from '../../../shared/persisted-state-types' +import type { Project } from '../../../shared/project-types' +import type { Repo } from '../../../shared/repo-types' +import { worktreeWorkspaceKey } from '../../../shared/workspace-scope' +import type { WorktreeMeta } from '../../../shared/worktree/meta-types' +import { + captureNativeLocalWorktreeMetadataScanExpectation, + pruneSessionlessMissingLocalWorktreeMetadataForRepo +} from './missing-local-worktree-metadata-pruning' + +const REPO_ID = 'repo-1' +const LIVE_ID = `${REPO_ID}::/workspace/live` + +function makeRepo(overrides: Partial = {}): Repo { + return { + id: REPO_ID, + path: '/workspace/repo', + displayName: 'repo', + badgeColor: '#000', + addedAt: 0, + ...overrides + } +} + +function makeMeta(worktreeId: string): WorktreeMeta { + return { + instanceId: `instance-${worktreeId}`, + hostId: 'local', + displayName: worktreeId, + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0 + } +} + +function makeState(repo = makeRepo()): PersistedState { + const state = getDefaultPersistedState('/home/test') + state.repos = [repo] + return state +} + +function capture(state: PersistedState, repo = state.repos[0]!) { + return captureNativeLocalWorktreeMetadataScanExpectation(state, repo) +} + +function pruneCaptured( + state: PersistedState, + scan: ReturnType, + ids: readonly string[], + platform?: NodeJS.Platform +): string[] { + const wanted = new Set(ids) + return pruneSessionlessMissingLocalWorktreeMetadataForRepo( + state, + scan, + scan.metadata.filter(({ worktreeId }) => wanted.has(worktreeId)), + platform + ) +} + +describe('pruneSessionlessMissingLocalWorktreeMetadataForRepo', () => { + it('removes 2,709 sessionless rows in one batch without cloning or changing session state', () => { + const state = makeState() + const staleIds = Array.from( + { length: 2_709 }, + (_, index) => `${REPO_ID}::/workspace/stale-${index}` + ) + const allIds = [LIVE_ID, ...staleIds] + state.worktreeMetaByIdentity = {} + state.worktreeIdentityAliases = {} + for (const [index, worktreeId] of allIds.entries()) { + const meta = makeMeta(worktreeId) + const identityKey = `identity-${index}` + state.worktreeMeta[worktreeId] = meta + state.worktreeMetaByIdentity[identityKey] = meta + state.worktreeIdentityAliases[`local|${worktreeId}`] = [identityKey] + } + state.worktreeLineageById[staleIds[0]!] = { worktreeId: staleIds[0] } as never + state.workspaceLineageByChildKey[worktreeWorkspaceKey(staleIds[0]!)] = { + childWorkspaceKey: worktreeWorkspaceKey(staleIds[0]!) + } as never + state.workspaceSession.terminalTopologyRevisionByRepoId = { [REPO_ID]: 7 } + const hostSession = { + ...getDefaultWorkspaceSession(), + terminalTopologyRevisionByRepoId: { [REPO_ID]: 11 } + } + state.workspaceSessionsByHostId = { 'runtime:mirror': hostSession } + const scan = capture(state) + const session = state.workspaceSession + const sessionSnapshot = structuredClone(session) + const hostSessionSnapshot = structuredClone(hostSession) + const cloneSpy = vi.spyOn(globalThis, 'structuredClone') + + const removed = pruneCaptured(state, scan, staleIds) + + expect(removed).toHaveLength(staleIds.length) + expect(cloneSpy).not.toHaveBeenCalled() + cloneSpy.mockRestore() + expect(Object.keys(state.worktreeMeta)).toEqual([LIVE_ID]) + expect(Object.keys(state.worktreeMetaByIdentity ?? {})).toEqual(['identity-0']) + expect(Object.keys(state.worktreeIdentityAliases ?? {})).toEqual([`local|${LIVE_ID}`]) + expect(state.worktreeLineageById[staleIds[0]!]).toBeUndefined() + expect(state.workspaceLineageByChildKey[worktreeWorkspaceKey(staleIds[0]!)]).toBeUndefined() + expect(state.workspaceSession).toBe(session) + expect(state.workspaceSession).toEqual(sessionSnapshot) + expect(state.workspaceSessionsByHostId?.['runtime:mirror']).toBe(hostSession) + expect(state.workspaceSessionsByHostId?.['runtime:mirror']).toEqual(hostSessionSnapshot) + }) + + it('fails closed when metadata is mutated or replaced after the scan starts', () => { + const inPlaceState = makeState() + const inPlaceId = `${REPO_ID}::/workspace/in-place` + inPlaceState.worktreeMeta[inPlaceId] = makeMeta(inPlaceId) + const inPlaceScan = capture(inPlaceState) + inPlaceState.worktreeMeta[inPlaceId]!.instanceId = 'replacement-instance' + + expect(pruneCaptured(inPlaceState, inPlaceScan, [inPlaceId])).toEqual([]) + + const replacedState = makeState() + const replacedId = `${REPO_ID}::/workspace/replaced` + const original = makeMeta(replacedId) + replacedState.worktreeMeta[replacedId] = original + const replacedScan = capture(replacedState) + replacedState.worktreeMeta[replacedId] = { ...original } + + expect(pruneCaptured(replacedState, replacedScan, [replacedId])).toEqual([]) + expect(replacedState.worktreeMeta[replacedId]).not.toBe(original) + }) + + it('fails closed when an identical repo row is removed and re-added during the scan', () => { + const state = makeState() + const worktreeId = `${REPO_ID}::/workspace/re-added-repo` + state.worktreeMeta[worktreeId] = makeMeta(worktreeId) + const scan = capture(state) + const replacement = { ...state.repos[0]! } + state.repos.splice(0, 1, replacement) + + expect(pruneCaptured(state, scan, [worktreeId])).toEqual([]) + expect(state.repos[0]).toBe(replacement) + expect(state.worktreeMeta[worktreeId]).toBeDefined() + }) + + it('uses the effective Git kind when the raw legacy repo omitted kind', () => { + const state = makeState() + const worktreeId = `${REPO_ID}::/workspace/legacy-git-kind` + const rawRepo = state.repos[0]! + state.worktreeMeta[worktreeId] = makeMeta(worktreeId) + const scan = capture(state, { ...rawRepo, kind: 'git' }) + + expect(rawRepo.kind).toBeUndefined() + expect(pruneCaptured(state, scan, [worktreeId])).toEqual([worktreeId]) + }) + + it.each([ + 'tabsByWorktree', + 'browserTabsByWorktree', + 'openFilesByWorktree', + 'activeFileIdByWorktree', + 'activeBrowserTabIdByWorktree', + 'clientHostedBrowserPagesByWorktree', + 'activeTabTypeByWorktree', + 'activeTabIdByWorktree', + 'unifiedTabs', + 'tabGroups', + 'tabGroupLayouts', + 'activeGroupIdByWorktree', + 'lastVisitedAtByWorktreeId', + 'defaultTerminalTabsAppliedByWorktreeId' + ] as const)('preserves an owner named by workspaceSession.%s', (field) => { + const state = makeState() + const worktreeId = `${REPO_ID}::/workspace/${field}` + state.worktreeMeta[worktreeId] = makeMeta(worktreeId) + const scan = capture(state) + const ownerKey = field === 'lastVisitedAtByWorktreeId' ? `local|${worktreeId}` : worktreeId + ;(state.workspaceSession as unknown as Record)[field] = { + [ownerKey]: [] + } + + expect(pruneCaptured(state, scan, [worktreeId])).toEqual([]) + expect(state.worktreeMeta[worktreeId]).toBeDefined() + }) + + it('preserves scalar, nested, remote-partition, mobile, and relay recovery owners', () => { + const ownerMutations: ((state: PersistedState, worktreeId: string) => void)[] = [ + (state, worktreeId) => { + state.workspaceSession.activeWorktreeId = worktreeId + }, + (state, worktreeId) => { + state.workspaceSession.activeWorkspaceKey = worktreeWorkspaceKey(worktreeId) + }, + (state, worktreeId) => { + state.workspaceSession.activeWorktreeIdsOnShutdown = [worktreeId] + }, + (state, worktreeId) => { + state.ui.lastActiveWorktreeId = worktreeId + }, + (state, worktreeId) => { + state.workspaceSession.sleepingAgentSessionsByPaneKey = { + pane: { worktreeId } as never + } + }, + (state, worktreeId) => { + state.workspaceSession.terminalSurfaceTombstonesByPaneKey = { + pane: { worktreeId } as never + } + }, + (state, worktreeId) => { + state.workspaceSession.closedTerminalTabTombstonesByTabId = { + tab: { worktreeId, closedAt: 1 } + } + }, + (state, worktreeId) => { + state.workspaceSession.clientHostedBrowserCloseIntentsByEnvironment = { + runtime: [{ worktreeId, browserPageId: 'page', closedAt: 1 }] + } + }, + (state, worktreeId) => { + state.workspaceSession.browserPagesByWorkspace = { + browser: [{ worktreeId } as never] + } + }, + (state, worktreeId) => { + state.workspaceSessionsByHostId = { + 'ssh:builder': { + ...getDefaultWorkspaceSession(), + lastVisitedAtByWorktreeId: { [worktreeId]: 1 } + } + } + }, + (state, worktreeId) => { + state.mobileClientTabSelectionsByDeviceId = { + phone: { [worktreeId]: {} as never } + } + }, + (state, worktreeId) => { + state.sshRemotePtyLeases = [ + { + targetId: 'builder', + ptyId: 'pty', + worktreeId, + state: 'detached', + createdAt: 1, + updatedAt: 1 + } + ] + }, + (state, worktreeId) => { + state.migrationUnsupportedPtyEntries = [ + { + ptyId: 'pty', + worktreeId, + reason: 'legacy-numeric-pane-key', + source: 'local', + updatedAt: 1 + } + ] + }, + (state, worktreeId) => { + state.automations = [ + { + enabled: true, + workspaceMode: 'existing', + workspaceId: worktreeId + } as never + ] + }, + (state, worktreeId) => { + state.automationRuns = [{ status: 'dispatching', workspaceId: worktreeId } as never] + } + ] + + for (const [index, mutate] of ownerMutations.entries()) { + const state = makeState() + const worktreeId = `${REPO_ID}::/workspace/owner-${index}` + state.worktreeMeta[worktreeId] = makeMeta(worktreeId) + const scan = capture(state) + mutate(state, worktreeId) + expect(pruneCaptured(state, scan, [worktreeId])).toEqual([]) + } + }) + + it('preserves canonically equivalent session and top-level owners', () => { + const candidateId = `${REPO_ID}::/workspace/Café`.normalize('NFC') + const ownerId = candidateId.normalize('NFD') + const ownerMutations: ((state: PersistedState) => void)[] = [ + (state) => { + state.workspaceSession.activeWorktreeId = ownerId + }, + (state) => { + state.ui.lastActiveWorktreeId = ownerId + } + ] + + for (const mutate of ownerMutations) { + const state = makeState() + state.worktreeMeta[candidateId] = makeMeta(candidateId) + const scan = capture(state) + mutate(state) + + expect(pruneCaptured(state, scan, [candidateId])).toEqual([]) + expect(state.worktreeMeta[candidateId]).toBeDefined() + } + }) + + it('removes a row on a later pass after its persisted owner is cleared', () => { + const state = makeState() + const worktreeId = `${REPO_ID}::/workspace/two-phase` + state.worktreeMeta[worktreeId] = makeMeta(worktreeId) + state.workspaceSession.lastVisitedAtByWorktreeId = { [worktreeId]: 1 } + const scan = capture(state) + + expect(pruneCaptured(state, scan, [worktreeId])).toEqual([]) + delete state.workspaceSession.lastVisitedAtByWorktreeId![worktreeId] + expect(pruneCaptured(state, scan, [worktreeId])).toEqual([worktreeId]) + }) + + it('treats dotfile visibility and cleanup dismissals as non-owning preferences', () => { + const state = makeState() + const worktreeId = `${REPO_ID}::/workspace/non-owning-preferences` + state.worktreeMeta[worktreeId] = makeMeta(worktreeId) + const showDotfilesByWorktree = { [worktreeId]: true } + const workspaceCleanup = { + dismissals: { + [worktreeId]: { + worktreeId, + dismissedAt: 1, + fingerprint: 'fingerprint', + classifierVersion: 1 + } + } + } + state.ui.showDotfilesByWorktree = showDotfilesByWorktree + state.ui.workspaceCleanup = workspaceCleanup + const scan = capture(state) + + expect(pruneCaptured(state, scan, [worktreeId])).toEqual([worktreeId]) + expect(state.ui.showDotfilesByWorktree).toBe(showDotfilesByWorktree) + expect(state.ui.workspaceCleanup).toBe(workspaceCleanup) + }) + + it('preserves foreign aliases, ambiguous local aliases, and divergent projections', () => { + const foreignState = makeState() + const foreignId = `${REPO_ID}::/workspace/foreign-alias` + const localMeta = makeMeta(foreignId) + const remoteMeta = { ...localMeta, hostId: 'ssh:builder' as const } + foreignState.worktreeMeta[foreignId] = localMeta + foreignState.worktreeMetaByIdentity = { local: localMeta, remote: remoteMeta } + foreignState.worktreeIdentityAliases = { + [`local|${foreignId}`]: ['local'], + [`ssh:builder|${foreignId}`]: ['remote'] + } + const foreignScan = capture(foreignState) + + expect(pruneCaptured(foreignState, foreignScan, [foreignId])).toEqual([]) + expect(foreignState.worktreeMetaByIdentity).toEqual({ local: localMeta, remote: remoteMeta }) + + const ambiguousState = makeState() + const ambiguousId = `${REPO_ID}::/workspace/ambiguous` + const first = makeMeta(ambiguousId) + ambiguousState.worktreeMeta[ambiguousId] = first + ambiguousState.worktreeMetaByIdentity = { first, second: { ...first } } + ambiguousState.worktreeIdentityAliases = { [`local|${ambiguousId}`]: ['first', 'second'] } + expect(pruneCaptured(ambiguousState, capture(ambiguousState), [ambiguousId])).toEqual([]) + + const divergentState = makeState() + const divergentId = `${REPO_ID}::/workspace/divergent` + const legacy = makeMeta(divergentId) + const canonical = { ...legacy, comment: 'newer canonical comment' } + divergentState.worktreeMeta[divergentId] = legacy + divergentState.worktreeMetaByIdentity = { canonical } + divergentState.worktreeIdentityAliases = { [`local|${divergentId}`]: ['canonical'] } + const divergentScan = capture(divergentState) + expect(pruneCaptured(divergentState, divergentScan, [divergentId])).toEqual([]) + }) + + it('fails closed for repo replacement, host collisions, remote, runtime, and folder repos', () => { + const mutations: ((state: PersistedState) => void)[] = [ + (state) => { + state.repos[0] = makeRepo({ path: '/workspace/replacement' }) + }, + (state) => { + state.repos.push(makeRepo({ connectionId: 'builder', executionHostId: 'ssh:builder' })) + }, + (state) => { + state.repos[0] = makeRepo({ connectionId: 'builder', executionHostId: 'ssh:builder' }) + }, + (state) => { + state.repos[0] = makeRepo({ executionHostId: 'runtime:environment' }) + }, + (state) => { + state.repos[0] = makeRepo({ kind: 'folder' }) + } + ] + for (const mutate of mutations) { + const state = makeState() + const worktreeId = `${REPO_ID}::/workspace/stale` + state.worktreeMeta[worktreeId] = makeMeta(worktreeId) + const scan = capture(state) + mutate(state) + expect(pruneCaptured(state, scan, [worktreeId])).toEqual([]) + } + }) + + it('fails closed when project runtime routing changes away and back during the scan', () => { + const state = makeState() + const worktreeId = `${REPO_ID}::/workspace/project-routing-race` + const project: Project = { + id: 'project-1', + displayName: 'project', + badgeColor: '#000', + localWindowsRuntimePreference: { kind: 'windows-host' }, + sourceRepoIds: [REPO_ID], + createdAt: 1, + updatedAt: 1 + } + state.projects = [project] + state.worktreeMeta[worktreeId] = makeMeta(worktreeId) + const scan = capture(state) + project.localWindowsRuntimePreference = { kind: 'wsl', distro: 'Ubuntu' } + project.updatedAt += 1 + project.localWindowsRuntimePreference = { kind: 'windows-host' } + project.updatedAt += 1 + + expect(pruneCaptured(state, scan, [worktreeId])).toEqual([]) + expect(state.worktreeMeta[worktreeId]).toBeDefined() + }) + + it('fails closed when global runtime routing changes away and back during the scan', () => { + const state = makeState() + const worktreeId = `${REPO_ID}::/workspace/settings-routing-race` + const originalDefault = state.settings.localWindowsRuntimeDefault + state.worktreeMeta[worktreeId] = makeMeta(worktreeId) + const scan = capture(state) + state.settings = { + ...state.settings, + localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' } + } + state.settings = { + ...state.settings, + localWindowsRuntimeDefault: originalDefault + } + + expect(state.settings.localWindowsRuntimeDefault).toEqual(originalDefault) + expect(pruneCaptured(state, scan, [worktreeId])).toEqual([]) + expect(state.worktreeMeta[worktreeId]).toBeDefined() + }) + + it('rejects malformed, folder-instance, WSL, and non-native Windows candidate paths', () => { + const state = makeState() + const foreignWindowsId = `${REPO_ID}::C:\\workspace\\stale` + const ids = [ + `${REPO_ID}::relative/path`, + `${REPO_ID}::`, + `wrong::/workspace/stale`, + `${REPO_ID}::/workspace/folder::workspace:11111111-1111-4111-8111-111111111111`, + `${REPO_ID}::\\\\wsl.localhost\\Ubuntu\\home\\gone`, + `${REPO_ID}::/home/user/wsl-legacy`, + foreignWindowsId + ] + for (const worktreeId of ids) { + state.worktreeMeta[worktreeId] = makeMeta(worktreeId) + } + const scan = capture(state) + + expect(pruneCaptured(state, scan, ids.slice(0, -2))).toEqual([]) + expect(pruneCaptured(state, scan, [ids.at(-2)!], 'win32')).toEqual([]) + expect(pruneCaptured(state, scan, [foreignWindowsId], 'linux')).toEqual([]) + }) +}) diff --git a/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.ts b/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.ts new file mode 100644 index 00000000000..3ab380bdd8e --- /dev/null +++ b/src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.ts @@ -0,0 +1,153 @@ +import { isWindowsAbsolutePathLike } from '../../../shared/cross-platform-path' +import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host' +import type { PersistedState } from '../../../shared/persisted-state-types' +import { getRepoKind } from '../../../shared/repo-kind' +import { worktreeWorkspaceKey } from '../../../shared/workspace-scope' +import { FOLDER_WORKSPACE_INSTANCE_SEPARATOR, splitWorktreeId } from '../../../shared/worktree/id' +import { isWslUncPath } from '../../../shared/wsl-paths' +import { isFinalAutomationRunStatus } from '../../../shared/automations-types' +import { pruneUnreferencedWorktreeIdentityMeta } from '../loading-store/worktree-identity-metadata' +import { + addPersistedSessionWorktreeOwners, + createWorktreeOwnerCandidateCollector +} from '../restoring-sessions/session-worktree-ownership' +import { + indexMetadataAliasesForWorktreeIds, + removeRevalidatedLocalWorktreeMetadata, + type LocalWorktreeMetadataPruneExpectation, + type NativeLocalWorktreeMetadataScanExpectation +} from './local-worktree-metadata-scan-expectation' + +export { captureNativeLocalWorktreeMetadataScanExpectation } from './local-worktree-metadata-scan-expectation' +export type { + LocalWorktreeMetadataPruneExpectation, + NativeLocalWorktreeMetadataScanExpectation +} from './local-worktree-metadata-scan-expectation' + +function collectPersistedWorkspaceOwners( + state: PersistedState, + candidateIds: ReadonlySet, + platform: NodeJS.Platform +): Set { + const collector = createWorktreeOwnerCandidateCollector(candidateIds, platform) + addPersistedSessionWorktreeOwners(state, collector) + const add = collector.addOwner + add(state.ui.lastActiveWorktreeId) + for (const selections of Object.values(state.mobileClientTabSelectionsByDeviceId ?? {})) { + for (const worktreeId of Object.keys(selections)) { + add(worktreeId) + } + } + for (const lease of state.sshRemotePtyLeases) { + add(lease.worktreeId) + } + for (const entry of state.migrationUnsupportedPtyEntries) { + add(entry.worktreeId) + } + for (const automation of state.automations) { + if (automation.enabled && automation.workspaceMode === 'existing') { + add(automation.workspaceId) + } + } + for (const run of state.automationRuns) { + if (!isFinalAutomationRunStatus(run.status)) { + add(run.workspaceId) + } + } + return collector.owners +} + +function repoStillMatches( + state: PersistedState, + expected: NativeLocalWorktreeMetadataScanExpectation['repo'] +): boolean { + const owners = state.repos.filter((repo) => repo.id === expected.id) + const current = owners[0] + return Boolean( + owners.length === 1 && + current && + current === expected.expectedRepo && + current.path === expected.path && + current.connectionId === expected.connectionId && + current.executionHostId === expected.executionHostId && + getRepoKind(current) === expected.kind && + expected.kind === 'git' && + !current.connectionId && + getRepoExecutionHostId(current) === LOCAL_EXECUTION_HOST_ID + ) +} + +function routingStillMatches( + state: PersistedState, + expected: NativeLocalWorktreeMetadataScanExpectation +): boolean { + const currentProject = state.projects.find((project) => + project.sourceRepoIds.includes(expected.repo.id) + ) + return ( + currentProject === expected.routing.expectedProject && + currentProject?.updatedAt === expected.routing.expectedProjectUpdatedAt && + state.settings === expected.routing.expectedSettings + ) +} + +function isValidCandidateId( + repoId: string, + worktreeId: string, + platform: NodeJS.Platform +): boolean { + const parsed = splitWorktreeId(worktreeId) + return Boolean( + parsed?.repoId === repoId && + parsed.worktreePath.length > 0 && + !isWslUncPath(parsed.worktreePath) && + (platform === 'win32' + ? isWindowsAbsolutePathLike(parsed.worktreePath) + : parsed.worktreePath.startsWith('/')) && + !worktreeId.includes(FOLDER_WORKSPACE_INSTANCE_SEPARATOR) + ) +} + +export function pruneSessionlessMissingLocalWorktreeMetadataForRepo( + state: PersistedState, + scan: NativeLocalWorktreeMetadataScanExpectation, + missingMetadata: readonly LocalWorktreeMetadataPruneExpectation[], + platform = process.platform +): string[] { + if ( + missingMetadata.length === 0 || + !repoStillMatches(state, scan.repo) || + !routingStillMatches(state, scan) + ) { + return [] + } + + const candidateIds = new Set(missingMetadata.map(({ worktreeId }) => worktreeId)) + const sessionOwners = collectPersistedWorkspaceOwners(state, candidateIds, platform) + const aliasesByWorktreeId = indexMetadataAliasesForWorktreeIds(state, candidateIds) + const removedIdentityKeys = new Set() + const removedIds: string[] = [] + for (const expectation of missingMetadata) { + const { worktreeId } = expectation + if ( + !isValidCandidateId(scan.repo.id, worktreeId, platform) || + sessionOwners.has(worktreeId) || + !removeRevalidatedLocalWorktreeMetadata( + state, + expectation, + aliasesByWorktreeId.get(worktreeId) ?? [], + removedIdentityKeys + ) + ) { + continue + } + delete state.worktreeLineageById[worktreeId] + delete state.workspaceLineageByChildKey[worktreeWorkspaceKey(worktreeId)] + removedIds.push(worktreeId) + } + if (removedIds.length > 0) { + // Why: a global sweep could delete unrelated unaliased rows from another repo or host. + pruneUnreferencedWorktreeIdentityMeta(state, removedIdentityKeys) + } + return removedIds +} diff --git a/src/main/persistence/tracking-repos/project-host-operations.ts b/src/main/persistence/tracking-repos/project-host-operations.ts index c6a8441f58a..605c258f4f7 100644 --- a/src/main/persistence/tracking-repos/project-host-operations.ts +++ b/src/main/persistence/tracking-repos/project-host-operations.ts @@ -19,6 +19,7 @@ import { repoGitUsernameCacheKey } from './repo-hydration' export type ProjectHostMutationOperations = { state: PersistedState gitUsernameCache: Map + bumpLocalWorktreeScanGeneration: (repoId: string) => void hydrateRepo: (repo: Repo) => Repo updateRepoBackedProjectHostSetup: ( setup: ProjectHostSetup, @@ -87,6 +88,9 @@ export class ProjectHostPersistenceOperations { return null } if ('localWindowsRuntimePreference' in updates) { + for (const repoId of project.sourceRepoIds) { + this.operations.bumpLocalWorktreeScanGeneration(repoId) + } if (updates.localWindowsRuntimePreference === undefined) { delete project.localWindowsRuntimePreference } else { diff --git a/src/main/persistence/tracking-repos/repo-update-operations.ts b/src/main/persistence/tracking-repos/repo-update-operations.ts index f39e0e1e26f..6ad6f23903b 100644 --- a/src/main/persistence/tracking-repos/repo-update-operations.ts +++ b/src/main/persistence/tracking-repos/repo-update-operations.ts @@ -9,6 +9,7 @@ import { sanitizeRepoUpdatesForPersistence } from './repo-sanitization' export type RepoUpdateMutationOperations = { state: PersistedState + bumpLocalWorktreeScanGeneration: (repoId: string) => void syncProjectHostSetupCompatibilityState: () => void scheduleSave: () => void hydrateRepo: (repo: Repo) => Repo @@ -21,6 +22,10 @@ export class RepoUpdatePersistenceOperations { return this.operations.state } + private bumpLocalWorktreeScanGeneration(repoId: string): void { + this.operations.bumpLocalWorktreeScanGeneration(repoId) + } + private syncProjectHostSetupCompatibilityState(): void { this.operations.syncProjectHostSetupCompatibilityState() } @@ -177,6 +182,7 @@ export class RepoUpdatePersistenceOperations { } } Object.assign(repo, sanitizedUpdates) + this.bumpLocalWorktreeScanGeneration(id) this.syncProjectHostSetupCompatibilityState() this.scheduleSave() return this.hydrateRepo(repo) diff --git a/src/main/worktree-lineage-pruning.test.ts b/src/main/worktree-lineage-pruning.test.ts index db393e864d5..22b34f70104 100644 --- a/src/main/worktree-lineage-pruning.test.ts +++ b/src/main/worktree-lineage-pruning.test.ts @@ -57,6 +57,82 @@ function createStore( } describe('pruneLineageForMissingRepoWorktrees', () => { + it('keeps lineage stored under another spelling of a live worktree path', () => { + const parentId = 'repo-1::/repo' + const childId = 'repo-1::/repo/./child' + const edge = lineage(childId, parentId) + const worktreeLineageById = { [childId]: edge } + const workspaceLineageByChildKey = { + [worktreeWorkspaceKey(childId)]: workspaceLineage(childId, parentId) + } + const metaById = { + [parentId]: { instanceId: edge.parentWorktreeInstanceId } as WorktreeMeta + } + const store = createStore(worktreeLineageById, workspaceLineageByChildKey, metaById) + + pruneLineageForMissingRepoWorktrees( + store as never, + repo, + [ + { path: '/repo', head: 'a', branch: 'main', isBare: false, isMainWorktree: true }, + { path: '/repo/child/', head: 'b', branch: 'child', isBare: false, isMainWorktree: false } + ], + { platform: 'linux' } + ) + + expect(store.removeWorktreeLineage).not.toHaveBeenCalled() + expect(store.removeWorkspaceLineage).not.toHaveBeenCalled() + expect(store.setWorktreeMeta).not.toHaveBeenCalled() + expect(worktreeLineageById[childId]).toBe(edge) + }) + + it('keeps captured metadata lineage not removed after Git canonicalizes paths', () => { + const parentId = 'repo-1::/alias/parent' + const childId = 'repo-1::/alias/child' + const edge = lineage(childId, parentId) + const worktreeLineageById = { [childId]: edge } + const workspaceLineageByChildKey = { + [worktreeWorkspaceKey(childId)]: workspaceLineage(childId, parentId) + } + const metaById = { + [childId]: { instanceId: edge.worktreeInstanceId } as WorktreeMeta, + [parentId]: { instanceId: edge.parentWorktreeInstanceId } as WorktreeMeta + } + const store = createStore(worktreeLineageById, workspaceLineageByChildKey, metaById) + + pruneLineageForMissingRepoWorktrees( + store as never, + repo, + [ + { + path: '/canonical/parent', + head: 'a', + branch: 'parent', + isBare: false, + isMainWorktree: false + }, + { + path: '/canonical/child', + head: 'b', + branch: 'child', + isBare: false, + isMainWorktree: false + } + ], + { + platform: 'linux', + preservedMetadataCandidateIds: new Set([parentId, childId]) + } + ) + + expect(store.removeWorktreeLineage).not.toHaveBeenCalled() + expect(store.removeWorkspaceLineage).not.toHaveBeenCalled() + expect(store.setWorktreeMeta).not.toHaveBeenCalled() + expect(worktreeLineageById[childId]).toBe(edge) + expect(workspaceLineageByChildKey[worktreeWorkspaceKey(childId)]).toBeDefined() + expect(metaById[parentId].instanceId).toBe(edge.parentWorktreeInstanceId) + }) + it('refuses an empty scan when the repo still has registered lineage', () => { const parentId = 'repo-1::/repo/parent' const childId = 'repo-1::/repo/child' @@ -78,7 +154,7 @@ describe('pruneLineageForMissingRepoWorktrees', () => { expect(workspaceLineageByChildKey[worktreeWorkspaceKey(childId)]).toBe(workspaceEdge) }) - it('prunes missing children and rotates missing parents after a trusted non-empty scan', () => { + it('prunes and rotates missing IDs absent from preserved metadata candidates', () => { const liveParentId = 'repo-1::/repo/live-parent' const missingChildId = 'repo-1::/repo/missing-child' const liveChildId = 'repo-1::/repo/live-child' @@ -99,25 +175,31 @@ describe('pruneLineageForMissingRepoWorktrees', () => { } const store = createStore(worktreeLineageById, workspaceLineageByChildKey, metaById) - pruneLineageForMissingRepoWorktrees(store as never, repo, [ - { - path: '/repo/live-parent', - head: 'a', - branch: 'main', - isBare: false, - isMainWorktree: false - }, - { - path: '/repo/live-child', - head: 'b', - branch: 'child', - isBare: false, - isMainWorktree: false - } - ]) + pruneLineageForMissingRepoWorktrees( + store as never, + repo, + [ + { + path: '/repo/live-parent', + head: 'a', + branch: 'main', + isBare: false, + isMainWorktree: false + }, + { + path: '/repo/live-child', + head: 'b', + branch: 'child', + isBare: false, + isMainWorktree: false + } + ], + { preservedMetadataCandidateIds: new Set([liveParentId, liveChildId]) } + ) expect(store.removeWorktreeLineage).toHaveBeenCalledWith(missingChildId) expect(store.removeWorktreeLineage).not.toHaveBeenCalledWith(liveChildId) + expect(store.removeWorkspaceLineage).toHaveBeenCalledWith(worktreeWorkspaceKey(missingChildId)) expect(store.setWorktreeMeta).toHaveBeenCalledWith(missingParentId, { instanceId: expect.any(String) }) @@ -126,4 +208,25 @@ describe('pruneLineageForMissingRepoWorktrees', () => { missingParentEdge.parentWorktreeInstanceId ) }) + + it('does not rematerialize metadata already removed for a missing parent', () => { + const liveChildId = 'repo-1::/repo/live-child' + const removedParentId = 'repo-1::/repo/removed-parent' + const edge = lineage(liveChildId, removedParentId) + const worktreeLineageById = { [liveChildId]: edge } + const store = createStore(worktreeLineageById, {}, {}) + + pruneLineageForMissingRepoWorktrees(store as never, repo, [ + { + path: '/repo/live-child', + head: 'child-head', + branch: 'child', + isBare: false, + isMainWorktree: false + } + ]) + + expect(store.setWorktreeMeta).not.toHaveBeenCalled() + expect(worktreeLineageById[liveChildId]).toBe(edge) + }) }) diff --git a/src/main/worktree-lineage-pruning.ts b/src/main/worktree-lineage-pruning.ts index 34a99ef6e95..6040f25c03d 100644 --- a/src/main/worktree-lineage-pruning.ts +++ b/src/main/worktree-lineage-pruning.ts @@ -4,6 +4,8 @@ import type { WorkspaceLineage, WorktreeLineage } from '../shared/worktree/linea import type { GitWorktreeInfo } from '../shared/worktree/types' import { getRepoExecutionHostId } from '../shared/execution-host' import { isWorkspaceKey, parseWorkspaceKey, worktreeWorkspaceKey } from '../shared/workspace-scope' +import { splitWorktreeId } from '../shared/worktree/id' +import { worktreeRetentionPathComparisonKey } from './worktree-retention-path-comparison' import type { Store } from './persistence' function worktreeIdBelongsToRepo(worktreeId: string, repoPrefix: string): boolean { @@ -37,8 +39,13 @@ function hasStoredRepoLineage( export function pruneLineageForMissingRepoWorktrees( store: Store, repo: Repo, - gitWorktrees: GitWorktreeInfo[] + gitWorktrees: GitWorktreeInfo[], + options: { + platform?: NodeJS.Platform + preservedMetadataCandidateIds?: ReadonlySet + } = {} ): void { + const { platform = process.platform, preservedMetadataCandidateIds } = options if ( typeof store.getAllWorktreeLineage !== 'function' || typeof store.removeWorktreeLineage !== 'function' @@ -56,6 +63,24 @@ export function pruneLineageForMissingRepoWorktrees( return } const liveIds = new Set(gitWorktrees.map((worktree) => `${repo.id}::${worktree.path}`)) + const livePathKeys = new Set( + [repo.path, ...gitWorktrees.map(({ path }) => path)].map((pathValue) => + worktreeRetentionPathComparisonKey(pathValue, platform) + ) + ) + const isLive = (worktreeId: string): boolean => { + if (liveIds.has(worktreeId) || preservedMetadataCandidateIds?.has(worktreeId)) { + return true + } + const worktreePath = splitWorktreeId(worktreeId)?.worktreePath + if (!worktreePath) { + return false + } + if (livePathKeys.has(worktreeRetentionPathComparisonKey(worktreePath, platform))) { + return true + } + return false + } const expectedHostId = getRepoExecutionHostId(repo) const repoOwners = store.getRepos().filter((candidate) => candidate.id === repo.id) const canMutateWorktree = (worktreeId: string): boolean => { @@ -68,7 +93,7 @@ export function pruneLineageForMissingRepoWorktrees( childScope?.type === 'worktree' && worktreeIdBelongsToRepo(childScope.worktreeId, repoPrefix) && canMutateWorktree(childScope.worktreeId) && - !liveIds.has(childScope.worktreeId) && + !isLive(childScope.worktreeId) && isWorkspaceKey(childWorkspaceKey) ) { store.removeWorkspaceLineage?.(childWorkspaceKey) @@ -78,7 +103,7 @@ export function pruneLineageForMissingRepoWorktrees( if ( worktreeIdBelongsToRepo(childId, repoPrefix) && canMutateWorktree(childId) && - !liveIds.has(childId) + !isLive(childId) ) { // Why: a proven-missing path must not transfer its lineage to a future checkout at that path. store.removeWorktreeLineage(childId) @@ -87,10 +112,10 @@ export function pruneLineageForMissingRepoWorktrees( if ( worktreeIdBelongsToRepo(lineage.parentWorktreeId, repoPrefix) && canMutateWorktree(lineage.parentWorktreeId) && - !liveIds.has(lineage.parentWorktreeId) + !isLive(lineage.parentWorktreeId) ) { const parentMeta = store.getWorktreeMeta(lineage.parentWorktreeId) - if (!parentMeta || parentMeta.instanceId === lineage.parentWorktreeInstanceId) { + if (parentMeta?.instanceId === lineage.parentWorktreeInstanceId) { // Why: rotate a proven-missing parent's identity once so path reuse cannot validate old lineage. store.setWorktreeMeta(lineage.parentWorktreeId, { instanceId: randomUUID() }) } diff --git a/src/main/worktree-retention-path-comparison.ts b/src/main/worktree-retention-path-comparison.ts new file mode 100644 index 00000000000..2ff3fd02990 --- /dev/null +++ b/src/main/worktree-retention-path-comparison.ts @@ -0,0 +1,24 @@ +import { splitWorktreeId } from '../shared/worktree/id' +import { worktreePathComparisonKey } from './ipc/worktree-path-comparison' + +/** Comparison keys used only to retain state; canonical equivalence must never select a delete target. */ +export function worktreeRetentionPathComparisonKey( + pathValue: string, + platform: NodeJS.Platform +): string { + return worktreePathComparisonKey(pathValue.normalize('NFC'), platform) +} + +export function worktreeRetentionIdComparisonKey( + worktreeId: string, + platform: NodeJS.Platform +): string | null { + const parsed = splitWorktreeId(worktreeId) + if (!parsed?.repoId || !parsed.worktreePath) { + return null + } + return JSON.stringify([ + parsed.repoId, + worktreeRetentionPathComparisonKey(parsed.worktreePath, platform) + ]) +} diff --git a/src/shared/priority-semaphore.test.ts b/src/shared/priority-semaphore.test.ts index 035e3ca920d..aeeacda94ef 100644 --- a/src/shared/priority-semaphore.test.ts +++ b/src/shared/priority-semaphore.test.ts @@ -145,4 +145,19 @@ describe('PrioritySemaphore', () => { const r2 = await sem.acquire(0) r2() }) + + it('removes an aborted waiter without consuming the next permit', async () => { + const sem = new PrioritySemaphore(1) + const release = await sem.acquire(0) + const controller = new AbortController() + const reason = new Error('canceled') + const waiting = sem.acquire(0, controller.signal) + + controller.abort(reason) + await expect(waiting).rejects.toBe(reason) + release() + + const nextRelease = await sem.acquire(0) + nextRelease() + }) }) diff --git a/src/shared/priority-semaphore.ts b/src/shared/priority-semaphore.ts index 4d31726937d..b7a0a50e06c 100644 --- a/src/shared/priority-semaphore.ts +++ b/src/shared/priority-semaphore.ts @@ -1,6 +1,8 @@ type Waiter = { priority: number resolve: (release: () => void) => void + signal?: AbortSignal + onAbort?: () => void } export class PrioritySemaphore { @@ -11,7 +13,10 @@ export class PrioritySemaphore { this.available = concurrency } - acquire(priority: number): Promise<() => void> { + acquire(priority: number, signal?: AbortSignal): Promise<() => void> { + if (signal?.aborted) { + return Promise.reject(signal.reason) + } if (this.available > 0) { this.available-- let released = false @@ -24,8 +29,20 @@ export class PrioritySemaphore { }) } - return new Promise<() => void>((resolve) => { - this.waiters.push({ priority, resolve }) + return new Promise<() => void>((resolve, reject) => { + const waiter: Waiter = { priority, resolve, signal } + if (signal) { + waiter.onAbort = () => { + const index = this.waiters.indexOf(waiter) + if (index === -1) { + return + } + this.waiters.splice(index, 1) + reject(signal.reason) + } + signal.addEventListener('abort', waiter.onAbort, { once: true }) + } + this.waiters.push(waiter) }) } @@ -45,6 +62,9 @@ export class PrioritySemaphore { } const waiter = this.waiters.splice(bestIdx, 1)[0] + if (waiter.signal && waiter.onAbort) { + waiter.signal.removeEventListener('abort', waiter.onAbort) + } let released = false waiter.resolve(() => { if (released) { From de851d5ca7b01c0a9bd8d8e2caaea9d1e89635bc Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:25:30 -0700 Subject: [PATCH 30/80] perf(terminals): drop disposed queued inspections (#17187) --- ...dinator-queued-inspection-disposal.test.ts | 81 +++++++++++++++++++ .../agent-completion-process-monitor.ts | 1 + .../agent-process-inspection-queue.ts | 11 +++ 3 files changed, 93 insertions(+) create mode 100644 src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts diff --git a/src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts new file mode 100644 index 00000000000..9d83b1d6a93 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it, vi } from 'vitest' +import { createAgentCompletionCoordinator } from './agent-completion-coordinator' +import { + createDeferred, + flushAsyncTicks, + processResult, + useAgentCompletionCoordinatorLifecycle +} from './agent-completion-coordinator-test-harness' +import type { RuntimeTerminalProcessInspection } from '@/runtime/runtime-terminal-inspection' + +describe('agent completion coordinator queued inspections', () => { + useAgentCompletionCoordinatorLifecycle() + + it('drops inspections queued by a disposed coordinator before starting live work', async () => { + const blockers = Array.from({ length: 4 }, () => + createDeferred() + ) + const blockerInspectors = blockers.map((inspection) => vi.fn(() => inspection.promise)) + const blockerCoordinators = blockerInspectors.map((inspectProcess, index) => + createAgentCompletionCoordinator({ + paneKey: `tab-1:blocked-${index}`, + getPtyId: () => `pty-blocked-${index}`, + getSettings: () => null, + inspectProcess, + dispatchCompletion: vi.fn(), + isLive: () => true + }) + ) + + blockerCoordinators.forEach((coordinator) => coordinator.startProcessTracking()) + await vi.advanceTimersByTimeAsync(2_000) + expect( + blockerInspectors.every((inspectProcess) => inspectProcess.mock.calls.length === 1) + ).toBe(true) + + const staleInspectProcesses = Array.from({ length: 8 }, () => + vi.fn(async () => processResult(null, false)) + ) + const staleCoordinators = staleInspectProcesses.map((inspectProcess, index) => + createAgentCompletionCoordinator({ + paneKey: `tab-1:stale-${index}`, + getPtyId: () => `pty-stale-${index}`, + getSettings: () => null, + inspectProcess, + dispatchCompletion: vi.fn(), + isLive: () => true + }) + ) + const liveInspectProcess = vi.fn(async () => processResult(null, false)) + const liveCoordinator = createAgentCompletionCoordinator({ + paneKey: 'tab-1:live', + getPtyId: () => 'pty-live', + getSettings: () => null, + inspectProcess: liveInspectProcess, + dispatchCompletion: vi.fn(), + isLive: () => true + }) + + for (const [index, coordinator] of staleCoordinators.entries()) { + coordinator.observeTitle(`Codex working ${index}`) + coordinator.observeTitle(`~/stale-${index}`) + } + liveCoordinator.observeTitle('Codex working') + liveCoordinator.observeTitle('~/live') + staleCoordinators.forEach((coordinator) => coordinator.dispose()) + + blockers.forEach((inspection) => inspection.resolve(processResult(null, false))) + await flushAsyncTicks() + // Existing 100ms pump admits live work after blockers release. + await vi.advanceTimersByTimeAsync(100) + await flushAsyncTicks() + + expect( + staleInspectProcesses.every((inspectProcess) => inspectProcess.mock.calls.length === 0) + ).toBe(true) + expect(liveInspectProcess).toHaveBeenCalledTimes(1) + + blockerCoordinators.forEach((coordinator) => coordinator.dispose()) + liveCoordinator.dispose() + }) +}) diff --git a/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts b/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts index 39ca68e1ad4..f0a0492fdfe 100644 --- a/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts +++ b/src/renderer/src/components/terminal-pane/agent-completion-process-monitor.ts @@ -146,6 +146,7 @@ export function createAgentCompletionProcessMonitor({ const pendingTitleIdAtRequest = priority === 'pending-title' ? pendingTitle.get()?.id : null enqueueAgentProcessInspection({ priority, + canRun: () => !state.disposed, run: async () => { let inspectedRecognizedAgent = false let inspectionSucceeded = false diff --git a/src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts b/src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts index 5c581ddc652..f7512ad8c60 100644 --- a/src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts +++ b/src/renderer/src/components/terminal-pane/agent-process-inspection-queue.ts @@ -2,6 +2,7 @@ export type InspectionPriority = 'cadence' | 'pending-title' type InspectionTask = { priority: InspectionPriority + canRun: () => boolean run: () => Promise } @@ -37,6 +38,16 @@ function scheduleInspectionPump(delayMs = 0): void { } function pumpInspectionQueue(): void { + // Drop disposed tasks before slot/rate accounting. + for (let index = inspectionQueue.length - 1; index >= 0; index -= 1) { + const task = inspectionQueue[index] + if (task && !task.canRun()) { + inspectionQueue.splice(index, 1) + } + } + if (inspectionQueue.length === 0) { + return + } const now = Date.now() if (!canStartInspection(now)) { scheduleInspectionPump(100) From 0a995cc30d14e980cf738204e49d64dcb8467bd6 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:14:03 -0700 Subject: [PATCH 31/80] Fix md file rendering 2 (#17295) * Improve markdown rich mode: distinguish HTML tags from placeholders - Consolidate size limit and unsupported content checks into single function - Refine HTML/JSX detection to validate against rehype-sanitize's known tag names - Allow bare placeholders like `` and `` in rich mode rendering * Add decision explainer for markdown rich-mode rendering fix * rm html explainer --- ...orContent.markdown-classification.test.tsx | 15 ++++-- .../editor/editor-panel-render-model.ts | 17 +++---- .../editor/markdown-rich-mode.test.ts | 44 ++++++++++++++++- .../components/editor/markdown-rich-mode.ts | 49 ++++++++++++++++++- 4 files changed, 109 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx b/src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx index 7d8767071be..4a5600192d3 100644 --- a/src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx +++ b/src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx @@ -10,11 +10,16 @@ const classifiers = vi.hoisted(() => ({ })) vi.mock('./markdown-rich-mode', () => ({ - getMarkdownRichModeUnsupportedMessage: classifiers.getUnsupportedMessage -})) - -vi.mock('./markdown-rich-size-limit', () => ({ - exceedsMarkdownRichModeSizeLimit: classifiers.exceedsSizeLimit + getMarkdownRichModeEligibility: ({ + content, + sizeOverridden + }: { + content: string + sizeOverridden: boolean + }) => ({ + exceedsSizeLimit: !sizeOverridden && classifiers.exceedsSizeLimit(content), + unsupportedMessage: classifiers.getUnsupportedMessage(content) + }) })) vi.mock('./editor-lazy-views', () => { diff --git a/src/renderer/src/components/editor/editor-panel-render-model.ts b/src/renderer/src/components/editor/editor-panel-render-model.ts index 8a479794f83..db03e65efe1 100644 --- a/src/renderer/src/components/editor/editor-panel-render-model.ts +++ b/src/renderer/src/components/editor/editor-panel-render-model.ts @@ -13,8 +13,7 @@ import type { EditorToggleValue } from './EditorViewToggle' import type { FileContent } from './editor-panel-content-types' import { canUseChangesModeForFile } from './editor-panel-file-mode' import { getMarkdownRenderMode, type MarkdownRenderState } from './markdown-render-mode' -import { getMarkdownRichModeUnsupportedMessage } from './markdown-rich-mode' -import { exceedsMarkdownRichModeSizeLimit } from './markdown-rich-size-limit' +import { getMarkdownRichModeEligibility } from './markdown-rich-mode' type StoreState = ReturnType @@ -143,16 +142,16 @@ export function getEditorPanelRenderModel({ let inlineMarkdownRenderState: MarkdownRenderState | null = null if (canRenderInlineMarkdown) { const shouldClassifyRichMode = mdViewMode === 'rich' - const exceedsRichModeSizeLimit = - shouldClassifyRichMode && - markdownRichModeSizeOverride[activeFile.id] !== true && - exceedsMarkdownRichModeSizeLimit(inlineMarkdownContent) - const richModeUnsupportedMessage = shouldClassifyRichMode - ? getMarkdownRichModeUnsupportedMessage(inlineMarkdownContent) + const richModeEligibility = shouldClassifyRichMode + ? getMarkdownRichModeEligibility({ + content: inlineMarkdownContent, + sizeOverridden: markdownRichModeSizeOverride[activeFile.id] === true + }) : null + const richModeUnsupportedMessage = richModeEligibility?.unsupportedMessage ?? null inlineMarkdownRenderState = { renderMode: getMarkdownRenderMode({ - exceedsRichModeSizeLimit, + exceedsRichModeSizeLimit: richModeEligibility?.exceedsSizeLimit ?? false, hasRichModeUnsupportedContent: richModeUnsupportedMessage !== null, viewMode: mdViewMode }), diff --git a/src/renderer/src/components/editor/markdown-rich-mode.test.ts b/src/renderer/src/components/editor/markdown-rich-mode.test.ts index e95b6f49c81..25f2e3bd869 100644 --- a/src/renderer/src/components/editor/markdown-rich-mode.test.ts +++ b/src/renderer/src/components/editor/markdown-rich-mode.test.ts @@ -1,5 +1,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { getMarkdownRichModeUnsupportedMessage } from './markdown-rich-mode' +import { RICH_MARKDOWN_MAX_SIZE_BYTES } from '../../../../shared/constants' +import { + getMarkdownRichModeEligibility, + getMarkdownRichModeUnsupportedMessage +} from './markdown-rich-mode' afterEach(() => { vi.restoreAllMocks() @@ -46,6 +50,31 @@ describe('getMarkdownRichModeUnsupportedMessage', () => { ).toBeNull() }) + it('allows bare lowercase placeholders in prose', () => { + expect( + getMarkdownRichModeUnsupportedMessage( + 'Use --host runtime:, --output , and --project .\n' + ) + ).toBeNull() + }) + + it('allows bare lowercase placeholders in large documents', () => { + const content = `${'a'.repeat(50_001)}\nUse --host runtime: and --project .\n` + + expect(getMarkdownRichModeUnsupportedMessage(content)).toBeNull() + }) + + it('still blocks large documents with actual html or jsx', () => { + const prefix = 'a'.repeat(50_001) + + expect(getMarkdownRichModeUnsupportedMessage(`${prefix}\ntext\n`)).not.toBeNull() + expect(getMarkdownRichModeUnsupportedMessage(`${prefix}\ntext\n`)).not.toBeNull() + expect(getMarkdownRichModeUnsupportedMessage(`${prefix}\n\n`)).not.toBeNull() + expect( + getMarkdownRichModeUnsupportedMessage(`${prefix}\n\n`) + ).not.toBeNull() + }) + it('allows block html and mdx-like tags by preserving them as passthrough nodes', () => { expect(getMarkdownRichModeUnsupportedMessage('\n')).toBeNull() expect(getMarkdownRichModeUnsupportedMessage('
block
\n')).toBeNull() @@ -90,4 +119,17 @@ describe('getMarkdownRichModeUnsupportedMessage', () => { ) expect(usedGlobalHtmlFragmentMatch).toBe(false) }) + + it('keeps unsupported content blocked when it also exceeds the size limit', () => { + const content = `${'a'.repeat(RICH_MARKDOWN_MAX_SIZE_BYTES + 1)}` + + expect(getMarkdownRichModeEligibility({ content, sizeOverridden: false })).toEqual({ + exceedsSizeLimit: true, + unsupportedMessage: expect.any(String) + }) + expect(getMarkdownRichModeEligibility({ content, sizeOverridden: true })).toEqual({ + exceedsSizeLimit: false, + unsupportedMessage: expect.any(String) + }) + }) }) diff --git a/src/renderer/src/components/editor/markdown-rich-mode.ts b/src/renderer/src/components/editor/markdown-rich-mode.ts index 694a8a7ee5f..4764892a4ea 100644 --- a/src/renderer/src/components/editor/markdown-rich-mode.ts +++ b/src/renderer/src/components/editor/markdown-rich-mode.ts @@ -1,5 +1,7 @@ +import { defaultSchema } from 'rehype-sanitize' import { getRichMarkdownRoundTripOutput } from './markdown-round-trip' import { extractFrontMatter } from './markdown-frontmatter' +import { exceedsMarkdownRichModeSizeLimit } from './markdown-rich-size-limit' import { translate } from '@/i18n/i18n' export type MarkdownRichModeUnsupportedReason = @@ -14,6 +16,13 @@ type UnsupportedMatch = { pattern: RegExp } +export type MarkdownRichModeEligibility = { + exceedsSizeLimit: boolean + unsupportedMessage: string | null +} + +const KNOWN_MARKDOWN_HTML_TAG_NAMES = new Set(defaultSchema.tagNames ?? []) + const UNSUPPORTED_PATTERNS: UnsupportedMatch[] = [ { reason: 'html-or-jsx', @@ -66,7 +75,7 @@ export function getMarkdownRichModeUnsupportedMessage(content: string): string | // opinion when HTML is detected, to verify the HTML survives the round-trip // before blocking the user from rich mode. const htmlMatcher = UNSUPPORTED_PATTERNS.find((m) => m.reason === 'html-or-jsx') - const hasHtml = htmlMatcher && htmlMatcher.pattern.test(contentWithoutCode) + const hasHtml = htmlMatcher && hasHtmlOrJsx(contentWithoutCode, htmlMatcher.pattern) for (const matcher of UNSUPPORTED_PATTERNS) { if (matcher.reason === 'html-or-jsx') { @@ -91,6 +100,44 @@ export function getMarkdownRichModeUnsupportedMessage(content: string): string | return null } +export function getMarkdownRichModeEligibility({ + content, + sizeOverridden +}: { + content: string + sizeOverridden: boolean +}): MarkdownRichModeEligibility { + const exceedsSizeLimit = !sizeOverridden && exceedsMarkdownRichModeSizeLimit(content) + return { + exceedsSizeLimit, + unsupportedMessage: getMarkdownRichModeUnsupportedMessage(content) + } +} + +function hasHtmlOrJsx(content: string, pattern: RegExp): boolean { + for (const match of content.matchAll(new RegExp(pattern, 'g'))) { + if (isHtmlOrJsxFragment(match[0])) { + return true + } + } + return false +} + +function isHtmlOrJsxFragment(fragment: string): boolean { + if (fragment.startsWith('