diff --git a/.github/actions/updater-flags/action.yml b/.github/actions/updater-flags/action.yml
new file mode 100644
index 0000000..65e35eb
--- /dev/null
+++ b/.github/actions/updater-flags/action.yml
@@ -0,0 +1,35 @@
+name: Resolve updater signing
+description: >-
+ Decides whether this build should produce signed in-app updater artifacts.
+ `bundle.createUpdaterArtifacts` is a hard error when TAURI_SIGNING_PRIVATE_KEY
+ is missing, so it lives in tauri.updater.conf.json and is only merged in when
+ the secret actually exists. Without it, builds are byte-identical to before and
+ the app reports the `unconfigured` update status instead of offering a download
+ it could never verify.
+
+outputs:
+ config:
+ description: >-
+ Either the `--config ` flag to append to `tauri build`, or empty.
+ value: ${{ steps.resolve.outputs.config }}
+ signed:
+ description: '"true" when updater artifacts will be produced.'
+ value: ${{ steps.resolve.outputs.signed }}
+
+runs:
+ using: composite
+ steps:
+ - id: resolve
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ if [[ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]]; then
+ echo 'config=--config src-tauri/tauri.updater.conf.json' >> "$GITHUB_OUTPUT"
+ echo 'signed=true' >> "$GITHUB_OUTPUT"
+ echo "Updater artifacts enabled for this build."
+ else
+ echo 'config=' >> "$GITHUB_OUTPUT"
+ echo 'signed=false' >> "$GITHUB_OUTPUT"
+ echo "::notice title=No updater signing key::TAURI_SIGNING_PRIVATE_KEY is not set, so this build ships without in-app update artifacts. Generate a keypair per docs/SIGNING.md to enable them."
+ fi
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3ea65ce..db5b493 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,6 +25,11 @@ env:
CARGO_HTTP_MULTIPLEXING: 'false'
CARGO_HTTP_TIMEOUT: '60'
CARGO_NET_RETRY: '10'
+ # Signs the in-app updater artifacts (minisign; unrelated to OS code signing).
+ # Absent until the keypair exists, and every job below degrades to a plain
+ # unsigned build when it is — see the "Resolve updater signing" steps.
+ TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
+ TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
jobs:
macos:
@@ -40,7 +45,16 @@ jobs:
workspaces: src-tauri
- run: bun install --frozen-lockfile
- - run: bun run build
+
+ - id: updater
+ uses: ./.github/actions/updater-flags
+
+ # These three mirror the `build` script in package.json. They are spelled out
+ # here only so the updater `--config` flag can be injected into the middle
+ # one; keep them in step with that script.
+ - run: bun run typecheck
+ - run: bun run tauri build --bundles app ${{ steps.updater.outputs.config }}
+ - run: bun run dmg
- uses: actions/upload-artifact@v4
with:
@@ -49,6 +63,18 @@ jobs:
path: src-tauri/target/release/bundle/dmg/*.dmg
if-no-files-found: error
+ # The in-app updater installs this tarball, not the DMG: it replaces the
+ # bundle in place, so it must not be wrapped in a disk image.
+ - if: steps.updater.outputs.signed == 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: aether-macos-updater
+ retention-days: 14
+ path: |
+ src-tauri/target/release/bundle/macos/*.app.tar.gz
+ src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
+ if-no-files-found: error
+
linux:
name: Linux (x86_64)
runs-on: ubuntu-latest
@@ -71,7 +97,13 @@ jobs:
- run: bun install --frozen-lockfile
- run: bun run typecheck:web
- - run: bun run tauri build --bundles deb,appimage
+ # Unit tests are platform-independent, so one runner is enough.
+ - run: bun run test
+
+ - id: updater
+ uses: ./.github/actions/updater-flags
+
+ - run: bun run tauri build --bundles deb,appimage ${{ steps.updater.outputs.config }}
- run: scripts/normalize-deb-package.sh src-tauri/target/release/bundle/deb/*.deb
- uses: actions/upload-artifact@v4
@@ -83,6 +115,17 @@ jobs:
src-tauri/target/release/bundle/appimage/*.AppImage
if-no-files-found: error
+ # AppImage only. A .deb install is owned by the package manager, so ÆTHER
+ # refuses to self-update there (see updater_install_support in lib.rs) and
+ # publishing a signature for one would be misleading.
+ - if: steps.updater.outputs.signed == 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: aether-linux-x86_64-updater
+ retention-days: 14
+ path: src-tauri/target/release/bundle/appimage/*.AppImage.sig
+ if-no-files-found: error
+
linux-arm64:
name: Linux (ARM64)
runs-on: ubuntu-24.04-arm
@@ -132,7 +175,11 @@ jobs:
- run: bun install --frozen-lockfile
- run: bun run typecheck:web
- - run: bun run tauri build --bundles nsis
+
+ - id: updater
+ uses: ./.github/actions/updater-flags
+
+ - run: bun run tauri build --bundles nsis ${{ steps.updater.outputs.config }}
- uses: actions/upload-artifact@v4
with:
@@ -143,6 +190,14 @@ jobs:
src-tauri/target/release/bundle/msi/*.msi
if-no-files-found: error
+ - if: steps.updater.outputs.signed == 'true'
+ uses: actions/upload-artifact@v4
+ with:
+ name: aether-windows-x86_64-updater
+ retention-days: 14
+ path: src-tauri/target/release/bundle/nsis/*.exe.sig
+ if-no-files-found: error
+
android:
name: Android (APK)
if: github.event_name == 'workflow_dispatch'
@@ -196,6 +251,10 @@ jobs:
permissions:
contents: write
steps:
+ # Needed for scripts/updater-manifest.sh; this job previously only consumed
+ # artifacts and so did not check out the repository.
+ - uses: actions/checkout@v4
+
- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
@@ -230,6 +289,24 @@ jobs:
copy_first_from_artifact "aether-linux-x86_64" "*.AppImage" "AETHER_amd64.AppImage"
copy_first_from_artifact "aether-linux-arm64" "*.deb" "AETHER_arm64.deb"
+ # The updater downloads this tarball rather than the DMG. Renamed off
+ # "ÆTHER.app.tar.gz" because a non-ASCII release asset name has to be
+ # percent-encoded in latest.json, and that is a needless way to fail.
+ copy_first_from_artifact "aether-macos-updater" "*.app.tar.gz" "AETHER_macOS.app.tar.gz"
+
+ # Writes nothing when no signing key produced signatures, which is the normal
+ # state before the keypair exists — the release then publishes installers
+ # only and the app reports the `unconfigured` update status.
+ - name: Build updater manifest
+ shell: bash
+ run: |
+ scripts/updater-manifest.sh \
+ "$GITHUB_REF_NAME" "$GITHUB_REPOSITORY" artifacts release-assets/latest.json
+
+ if [[ ! -f release-assets/latest.json ]]; then
+ echo "::warning title=No updater manifest::No updater signatures were produced, so latest.json is not published and in-app updates stay disabled for this release."
+ fi
+
- name: Create release and attach installers
uses: softprops/action-gh-release@v2
with:
diff --git a/.planning/debug/resolved/ice-interaction-crystallization.md b/.planning/debug/resolved/ice-interaction-crystallization.md
new file mode 100644
index 0000000..dc4b5df
--- /dev/null
+++ b/.planning/debug/resolved/ice-interaction-crystallization.md
@@ -0,0 +1,45 @@
+---
+status: resolved
+trigger: "iCE card clicks teleport cards to the top-left instead of centering with a slight zoom; Ordered Topics centers without zoom; crystallization intermittently fails for Quantum; percentage labels and Open in Library should be removed."
+created: 2026-07-25T20:55:39+0200
+updated: 2026-07-25T21:38:00+0200
+---
+
+## Symptoms
+
+- expected: Clicking a canvas card or Ordered Topics entry centers that card and slightly zooms the canvas.
+- actual: Canvas-card clicks teleport the raised card to the top-left. Ordered Topics centers without zoom.
+- errors: The app reports "Crystallization failed." Intermittently reproducible with "Quantum".
+- timeline: These behaviors worked on the main branch and regressed during the overhaul.
+- reproduction: Generate an iCE map, click canvas cards and Ordered Topics entries, then repeatedly crystallize "Quantum".
+- requested cleanup: Remove floating percentage labels and the "Open in Library" action.
+
+## Current Focus
+
+- hypothesis: confirmed
+- test: complete
+- expecting: fixed
+- next_action: user verification in the native app
+- reasoning_checkpoint: The first fix moved handlers onto the foreignObject content but left the unsupported transform path in place. It addressed neither the WebKit relocation trigger nor native SVG hit testing.
+- tdd_checkpoint: Added a regression test for recovering complete iceberg items from truncated model output.
+
+## Evidence
+
+- Both canvas cards and the canvas pan handler received events through an SVG `foreignObject`. WebKit did not reliably resolve the previous ancestor guard across that namespace boundary.
+- Ordered Topics already used the correct `selectItem` and `focusItem` path. Canvas cards now call the same path directly from their native HTML button.
+- The normalizer required a closing array bracket. A model response cut off during its final item discarded every complete item generated before it.
+- Generation had a single attempt and a 4200-token output ceiling despite requesting up to 45 richly annotated items.
+- The first interaction fix left selected, hover, and focus transforms on the `foreignObject`. That is the WebKit operation responsible for relocating cards to SVG origin.
+- Direct pointer handling inside the embedded HTML remained dependent on events crossing the HTML/SVG namespace boundary.
+
+## Eliminated
+
+- The pan/zoom centering formula was not the source of the top-left card jump.
+- Topic coverage lookup was not involved in canvas selection.
+
+## Resolution
+
+- root_cause: Canvas presses crossed an unreliable HTML-in-SVG event boundary, while selected and focused states transformed the `foreignObject` and triggered WebKit's top-left relocation bug. The 172% focus target was also too weak to communicate focus consistently. Truncated local-model JSON had no recovery or retry path.
+- fix: Made embedded HTML presentation-only, added a native SVG hit rectangle, restored SVG-native click and keyboard handling, removed every local transform from the `foreignObject`, and raised the shared canvas/sidebar focus target to 190%. Also added balanced-object recovery for truncated arrays, one compact low-temperature retry, a 5200-token generation allowance, concrete backend errors, and the requested UI removals.
+- verification: `bun run typecheck:web`, `bun run lint`, `bun run build:vite`, and `cargo test --lib` all pass. Rust result: 64 passed, 0 failed, 2 ignored. The new truncated-output regression test passes.
+- files_changed: src/renderer/src/components/Crystallizer.tsx, src/renderer/src/assets/styles/crystallizer.css, src/renderer/src/App.tsx, src-tauri/src/iceberg.rs, src-tauri/src/inference.rs, src-tauri/src/lib.rs
diff --git a/README.md b/README.md
index fdb5644..f34a586 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,56 @@
+## Install
+
+Download the latest build for your platform from
+**[Releases](https://github.com/CanPixel/aether/releases/latest)**:
+
+| Platform | File |
+|---|---|
+| macOS (Apple Silicon, 11+) | `AETHER_macOS.dmg` |
+| Windows (x86_64) | `AETHER_x64-setup.exe` |
+| Linux (x86_64) | `AETHER_amd64.deb` · `AETHER_amd64.AppImage` |
+| Linux (ARM64) | `AETHER_arm64.deb` |
+
+> [!NOTE]
+> **Intel Macs are not supported.** Releases are built `arm64` only, and Rosetta
+> translates Intel binaries to Apple Silicon — not the other way round — so there is
+> no way to run this DMG on an Intel Mac. Building `universal-apple-darwin` would fix
+> it at the cost of doubling an already slow llama.cpp compile.
+
+> [!IMPORTANT]
+> **Releases are not yet code-signed**, so your OS will block the first launch. This
+> is a signing status, not a warning about the app — but you should only work around
+> it for software you actually trust, and you can verify what ÆTHER does by reading
+> this repository. Signing and notarization are the top roadmap item; the setup guide
+> is in [docs/SIGNING.md](docs/SIGNING.md).
+
+**macOS.** The `.dmg` is unsigned and un-notarized, so macOS quarantines it and
+reports *"ÆTHER is damaged and can't be opened"*. It is not damaged. Drag the app to
+`/Applications`, then clear the quarantine flag:
+
+```bash
+xattr -dr com.apple.quarantine /Applications/ÆTHER.app
+```
+
+Then open it normally. (Right-click → *Open* alone does not work for un-notarized
+apps on current macOS.)
+
+**Windows.** SmartScreen shows *"Windows protected your PC"*. Click **More info**,
+then **Run anyway**.
+
+**Linux.** No workaround needed.
+
+```bash
+sudo dpkg -i AETHER_amd64.deb # or: chmod +x AETHER_amd64.AppImage
+```
+
+After first launch, ÆTHER asks you to install a local model. Only **AiON MiST**
+(639 MB) is required — capture, search, and cited passage retrieval all work with it
+alone. The chat models are optional and can be added later from the model picker.
+See [Local Wisdom Setup](#local-wisdom-setup).
+
## The Æther That Is
> **New here?** Read the plain-language introduction: [What is ÆTHER?](docs/WHAT-IS-AETHER.md)
@@ -87,15 +137,23 @@ Fresh installs use **AiON Launch**, the in-app setup flow for downloading local
| Model | Role | Official source | Size |
| ------------- | ------------------------------------------------------------- | ------------------------------------- | -------: |
| **AiON MiST** | Required embedding model for search, capture, and retrieval | `Qwen/Qwen3-Embedding-0.6B-GGUF` | ~0.64 GB |
-| **AiON LiTE** | Smaller, faster chat model for everyday answers and summaries | `google/gemma-4-E2B-it-qat-q4_0-gguf` | ~3.35 GB |
-| **AiON WiSE** | Larger chat model for richer synthesis and iCE maps | `google/gemma-4-E4B-it-qat-q4_0-gguf` | ~5.15 GB |
+| **AiON LiTE** | Optional chat model for everyday answers and summaries | `google/gemma-4-E2B-it-qat-q4_0-gguf` | ~3.35 GB |
+| **AiON WiSE** | Optional chat model for richer synthesis and iCE maps | `google/gemma-4-E4B-it-qat-q4_0-gguf` | ~5.15 GB |
Install choices:
-- **MiST + LiTE**: best default for laptops, mobile-class hardware, and quick grounded answers.
-- **MiST + WiSE**: better for deeper synthesis, longer answers, and iCE generation.
+- **MiST alone (~0.64 GB)**: the smallest complete install. Capture, semantic search
+ across hubs, Flow, and Ask all work — Ask returns the best-matching passages from
+ your sources with citations instead of a written answer. Chat models can be added
+ later from Settings without re-indexing anything.
+- **MiST + LiTE**: adds written answers. Best default for laptops and mobile-class
+ hardware.
+- **MiST + WiSE**: deeper synthesis, longer answers, and iCE map generation.
- **MiST + LiTE + WiSE**: lets the user switch between speed and depth.
+Only **iCE map generation** strictly requires a chat model; everything else degrades
+to retrieval rather than failing.
+
Manual development installs can also place models here:
```text
@@ -263,11 +321,21 @@ Current storage paths:
```text
/aether-library/library.json
/aether-realms/chunks.json
+/aether-realms/chunks.vec
/aether-settings/settings.json
/aether-icebergs/icebergs.json
+/aether-conversations/conversations.json
+/aether-session/session.json
+/aether-backups/aether-export-/
./aether-models/
```
+Every store is written temp-file-then-rename and keeps the previous good copy as
+`.bak`. On load, an unreadable store falls back to its backup, and the damaged
+file is preserved as `.corrupt-` rather than overwritten. Settings
+also offers **Export Library**, which snapshots every store into a timestamped folder
+under `aether-backups/`.
+
`library.json` stores:
- Knowledge hub summaries.
@@ -277,10 +345,11 @@ Current storage paths:
- Chunk counts.
- Legacy migration flags.
-`chunks.json` stores embedded chunk rows:
+`chunks.json` stores chunk **metadata** (store format v2):
- `id`
-- `vector`
+- `vectorSlot` — index into `chunks.vec`
+- `needsReembed` — set when the chunk's text is kept but its vector is unusable
- `text`
- `collectionId`
- `captureId`
@@ -290,6 +359,40 @@ Current storage paths:
- `capturedAt`
- `chunkIndex`
+`chunks.vec` stores the embeddings themselves as raw little-endian `f32`, at a fixed
+stride of `dim * 4` bytes per slot. Keeping vectors out of JSON matters at scale: as
+decimal text a 1024-dim vector costs roughly 12 KB versus 4 KB binary, and — more
+importantly — the sidecar is **append-only**, so capturing a page writes only the new
+vectors instead of re-serializing every vector in the library. Deleting sources leaves
+dead slots behind; once they exceed half the file, the store compacts and renumbers.
+
+A v1 `chunks.json` (vectors inline) is migrated automatically on first load. The
+untouched original is archived as `chunks.v1.json` — a name no ordinary save touches,
+unlike `.bak`, which is one generation deep and would be recycled by the next capture.
+
+### Changing the embedding model
+
+The store holds exactly one embedding width, because the sidecar has a fixed stride.
+A v1 store written across a model change therefore holds vectors that cannot all fit:
+the migration keeps the width the most chunks use and **parks** the rest — their text
+is retained and `needsReembed` is set, so they are simply invisible to semantic search
+rather than deleted.
+
+Vectors from different models cannot be compared at all (cosine distance is undefined
+across widths), so parked chunks — and any chunk embedded by a superseded model — are
+recovered by **Settings → Re-index Library**. Because chunk text lives in the store,
+re-indexing is local compute: no page is fetched again.
+
+`conversations.json` stores the AiON thread per knowledge hub (plus one thread for
+current-page-only asks): prompt, answer, model, citations, and metrics for each turn.
+The most recent turns are replayed into the prompt so follow-up questions work, and
+each thread keeps its last 40 turns.
+
+`session.json` stores the open browser tabs, the active tab, and the window size and
+position, so quitting no longer discards them. It is written after every tab change
+rather than at exit, because the app force-exits on quit (see the llama.cpp Metal note)
+and never reaches a shutdown hook.
+
`settings.json` stores app preferences such as the default search engine, Developer Mode, and selected local model paths.
`icebergs.json` stores manually saved iCE generations:
@@ -396,6 +499,30 @@ Renderer responsibilities:
---
+## Tests
+
+```bash
+bun run test
+```
+
+Runs the Rust unit suite (also run in CI on the Linux job). Alongside the usual unit
+tests it includes a **retrieval eval**: fixture documents are pushed through the real
+pipeline — `split_text` → `push_chunks` → the binary vector store → `rank_library_hits`
+— and each question must surface its expected source in the top three, with a stricter
+top-one assertion for distinctive terms.
+
+The eval substitutes a deterministic hashed bag-of-words embedder for the real model,
+because the embedding model is a ~640 MB download that CI cannot fetch. That still
+covers every model-independent way retrieval regresses — chunk boundaries and overlap,
+vector/slot alignment in the sidecar, per-capture grouping, ordering stability, scoping
+and limits — but it does **not** judge model quality.
+
+To check the real model is wired up locally:
+
+```bash
+AETHER_EMBEDDING_MODEL=/path/to/embedding.gguf cargo test --manifest-path src-tauri/Cargo.toml --lib real_model -- --ignored
+```
+
## Development Prerequisites
Required:
@@ -730,18 +857,19 @@ iCE depends on the local chat model returning parseable JSON. Try:
## Current Limitations
-- macOS packages are local unsigned/ad-hoc builds until Developer ID signing and notarization are configured.
+- Tab favicons are fetched directly from each site by the privileged window, so that one request per host is not local. See [docs/SECURITY.md](docs/SECURITY.md).
+- Releases are unsigned on macOS and Windows, so the first launch has to be unblocked manually — see [Install](#install) for the steps and [docs/SIGNING.md](docs/SIGNING.md) for the fix.
- Capture quality depends on page structure, active webview snapshots, and fallback HTTP extraction quality.
- App-like authenticated services can still have browser API or popup edge cases.
- iCE generation depends on local model quality and JSON compliance.
-- Update checks notify about newer app releases, but they do not download or install updates yet.
+- In-app updates are implemented but inert until an updater signing keypair exists; until then the Install button explains that it cannot verify a download. `.deb`/`.rpm` and Linux ARM64 installs never self-update by design. See [docs/SIGNING.md](docs/SIGNING.md#in-app-updates--minisign).
- Search and Ask currently use one selected hub plus optional current page, not arbitrary multi-hub selection.
## Roadmap Ideas
Likely next improvements:
-- Production signing and notarization flow.
+- Production signing and notarization flow ([setup guide](docs/SIGNING.md)), and generating the updater keypair that switches in-app updates on.
- Import/export for knowledge hubs.
- Full capture library view with filtering and bulk actions.
- Per-hub retrieval/model settings.
diff --git a/bun.lock b/bun.lock
index 1825352..430a666 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,121 +6,75 @@
"name": "aether-browser",
"dependencies": {
"ldrs": "^1.1.9",
- "lucide-react": "^1.17.0",
+ "lucide-react": "^1.26.0",
},
"devDependencies": {
- "@eslint/js": "^9.39.4",
- "@tauri-apps/api": "^2.11.0",
- "@tauri-apps/cli": "^2.11.2",
+ "@babel/core": "^8.0.1",
+ "@eslint/js": "^9.39.5",
+ "@tauri-apps/api": "^2.11.1",
+ "@tauri-apps/cli": "^2.11.4",
"@types/bun": "^1.3.14",
- "@types/node": "^25.9.1",
- "@types/react": "^19.2.15",
+ "@types/node": "^25.9.5",
+ "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
- "@vitejs/plugin-react": "^6.0.2",
+ "@vitejs/plugin-react": "^6.0.4",
"appdmg": "^0.6.6",
- "eslint": "^10.4.0",
+ "brace-expansion": "^5.0.8",
+ "eslint": "^10.8.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
- "eslint-plugin-react-refresh": "^0.5.2",
- "prettier": "^3.8.3",
- "react": "^19.2.6",
- "react-dom": "^19.2.6",
+ "eslint-plugin-react-refresh": "^0.5.3",
+ "prettier": "^3.9.6",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8",
"typescript": "^6.0.3",
- "typescript-eslint": "^8.59.2",
- "vite": "^8.0.14",
+ "typescript-eslint": "^8.65.0",
+ "vite": "^8.1.5",
},
},
},
+ "overrides": {
+ "@babel/core": "^7.29.1",
+ "brace-expansion": "^5.0.7",
+ },
"packages": {
- "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
-
- "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
-
- "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
-
- "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
-
- "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
-
- "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
-
- "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
-
- "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
-
- "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
-
- "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
-
- "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
-
- "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
-
- "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
-
- "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
-
- "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
-
- "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
-
- "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
-
- "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+ "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
- "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+ "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
- "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
+ "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
- "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
+ "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
- "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
- "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
+ "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
- "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
- "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
- "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
- "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
- "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
- "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
+ "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
- "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
+ "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
- "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
+ "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
- "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
+ "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
- "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
+ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
- "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
+ "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
- "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
+ "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
- "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
-
- "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
-
- "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
-
- "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
-
- "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
-
- "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
-
- "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
-
- "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
-
- "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
-
- "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
@@ -128,15 +82,15 @@
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
- "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="],
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="],
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
- "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
+ "@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="],
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
- "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="],
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
@@ -158,69 +112,69 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
- "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
+ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
- "@oxc-project/types": ["@oxc-project/types@0.132.0", "", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="],
+ "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
- "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="],
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="],
- "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w=="],
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="],
- "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA=="],
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="],
- "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA=="],
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="],
- "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w=="],
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="],
- "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig=="],
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="],
- "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw=="],
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="],
- "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA=="],
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="],
- "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ=="],
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="],
- "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ=="],
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="],
- "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw=="],
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="],
- "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w=="],
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="],
- "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.2", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ=="],
+ "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="],
- "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="],
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="],
- "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="],
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
- "@tauri-apps/api": ["@tauri-apps/api@2.11.0", "", {}, "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA=="],
+ "@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="],
- "@tauri-apps/cli": ["@tauri-apps/cli@2.11.2", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.2", "@tauri-apps/cli-darwin-x64": "2.11.2", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", "@tauri-apps/cli-linux-arm64-musl": "2.11.2", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-musl": "2.11.2", "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", "@tauri-apps/cli-win32-x64-msvc": "2.11.2" }, "bin": { "tauri": "tauri.js" } }, "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw=="],
+ "@tauri-apps/cli": ["@tauri-apps/cli@2.11.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.4", "@tauri-apps/cli-darwin-x64": "2.11.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", "@tauri-apps/cli-linux-arm64-musl": "2.11.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-musl": "2.11.4", "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", "@tauri-apps/cli-win32-x64-msvc": "2.11.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ=="],
- "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w=="],
+ "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ=="],
- "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg=="],
+ "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A=="],
- "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.2", "", { "os": "linux", "cpu": "arm" }, "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA=="],
+ "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.4", "", { "os": "linux", "cpu": "arm" }, "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ=="],
- "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw=="],
+ "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA=="],
- "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw=="],
+ "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw=="],
- "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.2", "", { "os": "linux", "cpu": "none" }, "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ=="],
+ "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.4", "", { "os": "linux", "cpu": "none" }, "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ=="],
- "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw=="],
+ "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ=="],
- "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.2", "", { "os": "linux", "cpu": "x64" }, "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw=="],
+ "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A=="],
- "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA=="],
+ "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ=="],
- "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA=="],
+ "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA=="],
- "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.2", "", { "os": "win32", "cpu": "x64" }, "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA=="],
+ "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw=="],
- "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
+ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
@@ -230,33 +184,33 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
- "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
+ "@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="],
- "@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
+ "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
- "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="],
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.65.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/type-utils": "8.65.0", "@typescript-eslint/utils": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.65.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA=="],
- "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="],
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.65.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA=="],
- "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="],
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.65.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.65.0", "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q=="],
- "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="],
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0" } }, "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg=="],
- "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="],
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.65.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg=="],
- "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="],
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g=="],
- "@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="],
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.65.0", "", {}, "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg=="],
- "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="],
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.65.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.65.0", "@typescript-eslint/tsconfig-utils": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg=="],
- "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="],
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.65.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.65.0", "@typescript-eslint/types": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA=="],
- "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.65.0", "", { "dependencies": { "@typescript-eslint/types": "8.65.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A=="],
- "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="],
+ "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg=="],
"acorn": ["acorn@8.16.0", "", { "bin": "bin/acorn" }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
@@ -276,7 +230,7 @@
"bplist-creator": ["bplist-creator@0.0.8", "", { "dependencies": { "stream-buffers": "~2.2.0" } }, "sha512-Za9JKzD6fjLC16oX2wsXfc+qBEhJBJB1YPInoAQpMLhDuj5aVOv1baGeIQSq1Fr3OCqzvsoQcSBSwGId/Ja2PA=="],
- "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
+ "brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="],
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": "cli.js" }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
@@ -306,19 +260,17 @@
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
- "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": "bin/esbuild" }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
-
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
- "eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="],
+ "eslint": ["eslint@10.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ=="],
"eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": "bin/cli.js" }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
- "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.2", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA=="],
+ "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.3", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA=="],
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
@@ -396,8 +348,6 @@
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
- "jiti": ["jiti@2.7.0", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
-
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -446,7 +396,7 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
- "lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="],
+ "lucide-react": ["lucide-react@1.26.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-raglYVR2+VkMfJL158krjVmE+rV5ST2lzA/KQm1FRSjMHT4MnWaegHxoVEpmc2So3nOEhp9oGejJwAPX8MoAjg=="],
"macos-alias": ["macos-alias@0.2.12", "", { "dependencies": { "nan": "^2.4.0" }, "os": "darwin" }, "sha512-yiLHa7cfJcGRFq4FrR4tMlpNHb4Vy4mWnpajlSSIFM5k4Lv8/7BbbDLzCAVogWNl0LlLhizRp1drXv0hK9h0Yw=="],
@@ -460,7 +410,7 @@
"nan": ["nan@2.27.0", "", {}, "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ=="],
- "nanoid": ["nanoid@3.3.12", "", { "bin": "bin/nanoid.cjs" }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
+ "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
@@ -488,13 +438,13 @@
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
- "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+ "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
- "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
+ "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
- "prettier": ["prettier@3.8.3", "", { "bin": "bin/prettier.cjs" }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
+ "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
@@ -502,13 +452,13 @@
"random-path": ["random-path@0.1.2", "", { "dependencies": { "base32-encode": "^0.1.0 || ^1.0.0", "murmur-32": "^0.1.0 || ^0.2.0" } }, "sha512-4jY0yoEaQ5v9StCl5kZbNIQlg1QheIDBrdkDn53EynpPb9FgO6//p3X/tgMnrC45XN6QZCzU1Xz/+pSSsJBpRw=="],
- "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
+ "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
- "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
+ "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
"repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="],
- "rolldown": ["rolldown@1.0.2", "", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="],
+ "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
@@ -526,7 +476,7 @@
"strip-eof": ["strip-eof@1.0.0", "", {}, "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q=="],
- "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"tn1150": ["tn1150@0.1.0", "", { "dependencies": { "unorm": "^1.4.1" } }, "sha512-DbplOfQFkqG5IHcDyyrs/lkvSr3mPUVsFf/RbDppOshs22yTPnSJWEe6FkYd1txAwU/zcnR905ar2fi4kwF29w=="],
@@ -540,7 +490,7 @@
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
- "typescript-eslint": ["typescript-eslint@8.59.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="],
+ "typescript-eslint": ["typescript-eslint@8.65.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.65.0", "@typescript-eslint/parser": "8.65.0", "@typescript-eslint/typescript-estree": "8.65.0", "@typescript-eslint/utils": "8.65.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
@@ -550,7 +500,7 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
- "vite": ["vite@8.0.14", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="],
+ "vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -562,8 +512,6 @@
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
- "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="],
-
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
@@ -576,10 +524,16 @@
"@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "", { "bin": "bin/semver.js" }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
+ "bun-types/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
+
+ "eslint-plugin-react-hooks/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
+
"execa/cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="],
"npm-run-path/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="],
+ "eslint-plugin-react-hooks/@babel/parser/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
"execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="],
"execa/cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
@@ -588,6 +542,10 @@
"execa/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
+ "eslint-plugin-react-hooks/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "eslint-plugin-react-hooks/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
"execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="],
}
}
diff --git a/docs/LICENSING.md b/docs/LICENSING.md
new file mode 100644
index 0000000..99b74f4
--- /dev/null
+++ b/docs/LICENSING.md
@@ -0,0 +1,123 @@
+# Licensing — options and open questions
+
+A decision record, not a decision. Nothing here has been applied: ÆTHER is still
+under **PolyForm Strict License 1.0.0**. This exists so the reasoning is on paper
+when the choice is actually made.
+
+Not legal advice. Anything with revenue attached to it is worth a lawyer's hour.
+
+## Three gaps in the current setup
+
+These are true today and worth fixing **whatever licence is chosen**.
+
+### 1. Even local modification is not permitted
+
+PolyForm Strict grants everything *"other than distributing the software **or making
+changes or new works based on the software**."*
+
+That second clause is stricter than it usually reads. It means:
+
+- A user cannot legally patch a bug for their own machine.
+- A contributor cannot legally fork to open a pull request — a fork is
+ redistribution, and a patch is a new work.
+
+So the project cannot accept outside contributions without granting permission
+out-of-band first, per contributor, before they have written anything.
+
+### 2. The README promises a CLA that does not exist
+
+> External contributions require a signed Contributor License Agreement (CLA) or
+> another written contributor agreement.
+
+There is no `CONTRIBUTING.md`, no CLA text, and no CLA bot. The stated route to
+contributing terminates in nothing. Either write it or stop referring to it.
+
+### 3. There is no way to buy a commercial licence
+
+Redistribution and commercial use "require separate written permission from
+CanPixel" — and no email, form, or contact appears anywhere in the repository. The
+single revenue path the licence exists to protect has no door in it.
+
+This one is worth fixing immediately and costs one line.
+
+## What constrains the choice
+
+**Nothing in the dependency tree.** Gemma 4 E2B/E4B and Qwen3-Embedding-0.6B are
+Apache-2.0; llama.cpp, Tauri, and the rest are Apache-2.0/MIT. No copyleft
+obligation reaches ÆTHER's own code.
+
+**The product shape does.** ÆTHER is a local desktop app with no server. That rules
+out the usual open-core playbook — there is no hosted tier to sell, and AGPL has no
+leverage because there is no network service to trigger the source obligation. The
+realistic revenue is a one-time purchase or a per-seat commercial licence, which is
+what PolyForm already reserves.
+
+**The audience does too.** Researchers, journalists, and people in regulated or
+air-gapped environments care about auditability and about not being cut off. Both
+argue for source-available with a durable guarantee, and against anything that
+makes moving a build onto an offline machine legally awkward.
+
+## Options
+
+### A. PolyForm Noncommercial 1.0.0
+
+Permits modification and redistribution; still bans commercial use.
+
+- Fixes all three frictions: local fixes, sneakernet to an air-gapped machine, PRs.
+- Gives up nothing sellable — noncommercial use was already permitted.
+- Roughly a one-word change in the licence family, plus `package.json` and README.
+
+The smallest change that removes real friction. Still not OSI open source, so it
+does not buy open-source goodwill or a place in distro repositories.
+
+### B. FSL-1.1-Apache-2.0 (Functional Source License)
+
+Same commercial protection now; each release converts to Apache-2.0 two years after
+it ships.
+
+- The strongest "this will not be taken away from you" signal short of going open,
+ which matters for the trust-sensitive audience.
+- Irrevocable per release — a promise that cannot be walked back.
+- Two years is a long time in this category; by the time a release converts, it is
+ unlikely to be competitive.
+
+### C. AGPL-3.0 + commercial dual licence
+
+Real OSI open source, with a commercial licence sold to anyone who cannot comply.
+
+- Weak here. The copyleft trigger is *conveying* or *network use*; a local desktop
+ app with no server rarely trips either, so the commercial pressure that makes
+ dual-licensing work mostly is not there.
+- A competitor could fork commercially provided they publish source.
+- Meaningful legal and administrative overhead for a solo project.
+
+### D. Keep PolyForm Strict, close the gaps
+
+Write the CLA and `CONTRIBUTING.md` the README already promises, add a commercial
+contact.
+
+- Zero licence risk, and the documentation is owed regardless.
+- Contributors still cannot legally fork to submit a PR, so the contribution path
+ stays theoretical.
+
+### E. Fully permissive (Apache-2.0 / MIT)
+
+- Maximum adoption and goodwill.
+- No revenue path at all for a local app with no hosted component, and no
+ protection against a rebranded fork.
+
+## A correction to the audit that prompted this
+
+The audit said the current setup has *"the costs of proprietary and the revenue of
+open source."* That is unfair as written. PolyForm Strict **does** establish the
+legal basis for a commercial story — every commercial right is retained. What is
+missing is everything on the other side of it: no price, no tier, no contact. Gap 3
+above is the real finding; the licence family is a secondary question.
+
+## Suggested order
+
+1. **Add a commercial-licence contact.** One line. Fixes a gap that exists under
+ every option, and is the only one currently costing money.
+2. **Resolve the CLA claim** — write it, or remove the sentence.
+3. **Then** decide between A and B, if either. That decision is about how much
+ openness is worth to the audience, not about unblocking anything technical.
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
new file mode 100644
index 0000000..ad61772
--- /dev/null
+++ b/docs/SECURITY.md
@@ -0,0 +1,93 @@
+# Security Notes
+
+Not a policy document — a record of the decisions that are easy to undo by accident.
+
+## Two kinds of webview
+
+ÆTHER runs visited pages in **child webviews**, separate from the window that hosts
+the app's own UI. That split is the main boundary:
+
+| | Privileged window (`main`) | Child webviews (tabs) |
+|---|---|---|
+| Content | ÆTHER's own bundled UI | arbitrary web pages |
+| IPC bridge | yes — all Tauri commands | no |
+| CSP | `app.security.csp` (below) | the site's own |
+
+A page cannot reach the command bridge, because it is not in the context that has
+one. This is why an aggressive CSP on the privileged window costs page
+compatibility nothing: the policy never applies to page content.
+
+## Content Security Policy
+
+Lives in `src-tauri/tauri.conf.json` under `app.security.csp`, with a looser
+`devCsp` for Vite's HMR (inline scripts, `eval`, and a WebSocket to
+`127.0.0.1:1420`). Tauri injects it at load.
+
+**Deliberately not also a `` tag in `index.html`.** It used to be. Two
+policies are *intersected* by the engine, so with both in place a tightening in
+either silently overrides the other and the pair drifts apart. One source of truth.
+
+Why each directive is what it is:
+
+| Directive | Value | Reason |
+|---|---|---|
+| `default-src` | `'self'` | Nothing loads from anywhere else unless listed below. |
+| `script-src` | `'self'` | One bundled module script. No inline, no `eval`. |
+| `style-src` | `'self' 'unsafe-inline'` | The UI uses React `style` attributes throughout. This permits inline *style*, not inline script. |
+| `img-src` | `'self' data: blob: https: http:` | See favicons below. `data:`/`blob:` are tab thumbnails. |
+| `connect-src` | `'self' ipc: http://ipc.localhost` | Tauri's IPC transport. Removing these breaks every command. |
+| `object-src` | `'none'` | No plugins, ever. |
+| `base-uri` | `'self'` | Stops injected markup repointing relative URLs. |
+| `form-action` | `'none'` | The UI has no server to post to. |
+| `frame-ancestors` | `'none'` | Nothing may embed the privileged window. |
+
+**`img-src` allows any host, and that is a real hole.** Tab favicons are fetched
+straight from `https:///favicon.ico` by an `` in the privileged window
+(`favicon_for_url` in `src-tauri/src/util.rs`). Narrowing this needs favicons
+proxied through Rust and cached locally, which would also stop the privileged
+window making any outbound request at all. Worth doing; not done.
+
+## What the app sends anywhere
+
+Outbound requests, all from Rust except where noted:
+
+- **Hugging Face** — only while downloading a model the user chose.
+- **GitHub Releases API** — the update check, if enabled in Settings.
+- **The update endpoint** — only when the user presses Install Update.
+- **Pages the user visits** — in child webviews, as any browser.
+- **Favicons** — from the privileged window, per the above.
+
+No analytics, no crash reporting, no phone-home. Captured text, embeddings,
+answers, and iCE atlases never leave the machine.
+
+## Diagnostics log
+
+`src-tauri/src/diagnostics.rs`. Replaces 25 `eprintln!` calls that went to a stderr
+nobody reads — which mattered because the Windows and Linux builds ship without ever
+being run, and there is no telemetry to notice a failure.
+
+- Written only to the app data directory, capped at 512 KiB, rolling over to the
+ newest half rather than emptying.
+- Visible in Settings → Diagnostics (most recent first).
+- Leaves the machine only via **Export Log**, which writes a copy and reveals it.
+
+**Deliberately never recorded:** page text, captured content, search queries, chat
+prompts, and answers. Entries are operational — what failed, and where. That is the
+constraint that lets the log be exportable at all; an exported log must not be able
+to become a transcript of what someone was reading.
+
+Paths *are* recorded, including model and store paths under the user's home
+directory. Worth knowing before attaching a log to a public issue.
+
+## Capabilities
+
+`src-tauri/capabilities/default.json` grants `core:default` and `opener:default` to
+the `main` window only. The opener permission is what `reveal_item_in_dir` and
+external-link opening need. Nothing else is granted, and child webviews appear in no
+capability.
+
+## Known gaps
+
+- Releases are unsigned; see [SIGNING.md](SIGNING.md).
+- `img-src` is open to any host, per above.
+- `style-src` needs `'unsafe-inline'` until the UI stops using `style` attributes.
diff --git a/docs/SIGNING.md b/docs/SIGNING.md
new file mode 100644
index 0000000..77d72c9
--- /dev/null
+++ b/docs/SIGNING.md
@@ -0,0 +1,195 @@
+# Code Signing and Notarization
+
+ÆTHER currently ships **unsigned** on every platform. This document is the setup
+guide for changing that.
+
+There are two independent signing schemes here, and they are easy to confuse:
+
+| | Purpose | Cost | Status |
+|---|---|---|---|
+| **OS code signing** (Apple Developer ID, Azure Trusted Signing) | Stops the OS blocking the *first install* | ~$220/year | Not wired up — see below |
+| **Updater signing** (minisign) | Lets the app verify an *update* it downloads itself | Free | Wired up, waiting on a keypair |
+
+They are unrelated: the updater's minisign key is generated locally by the Tauri CLI
+and has nothing to do with Apple or Microsoft. So [in-app
+updates](#in-app-updates--minisign) can be turned on today, without waiting for the
+paid certificates.
+
+Until OS signing is done, see [Install](../README.md#install) for what users have to
+do to open an unsigned build.
+
+## Why it matters
+
+An unsigned, un-notarized `.dmg` downloaded through a browser is quarantined by
+macOS. The user does not get a "are you sure?" prompt they can click through — they
+get **"ÆTHER is damaged and can't be opened. You should move it to the Bin."** That
+message is indistinguishable from a malware warning, and it is the first thing a new
+user sees.
+
+On Windows, an unsigned NSIS installer triggers SmartScreen's "Windows protected
+your PC — unknown publisher" interstitial, where the *Run anyway* button is behind a
+*More info* link.
+
+For a tool whose entire pitch is that it keeps your data on your machine, asking the
+user to override their OS's security check is the wrong first impression.
+
+## macOS — Developer ID + notarization
+
+**Cost:** Apple Developer Program, $99/year.
+
+1. **Enrol** at . Individual enrolment is
+ enough; it can take a day or two to be approved.
+2. **Create a Developer ID Application certificate** in Certificates, Identifiers &
+ Profiles. Note this is *Developer ID Application*, not *Mac App Distribution* —
+ the latter only works for the Mac App Store and will not help with direct
+ downloads.
+3. **Export it as a `.p12`** from Keychain Access (right-click the certificate →
+ Export), set a password, then base64 it for CI:
+ ```bash
+ base64 -i certificate.p12 | pbcopy
+ ```
+4. **Create an App Store Connect API key** (Users and Access → Integrations → Keys)
+ with the *Developer* role. Download the `.p8` — Apple only lets you download it
+ once. Record the Key ID and Issuer ID shown next to it.
+5. **Add GitHub repository secrets:**
+
+ | Secret | Value |
+ |---|---|
+ | `APPLE_CERTIFICATE` | base64 of the `.p12` |
+ | `APPLE_CERTIFICATE_PASSWORD` | the `.p12` export password |
+ | `APPLE_SIGNING_IDENTITY` | e.g. `Developer ID Application: Your Name (TEAMID)` |
+ | `APPLE_API_KEY_ID` | Key ID from step 4 |
+ | `APPLE_API_ISSUER` | Issuer ID from step 4 |
+ | `APPLE_API_KEY` | base64 of the `.p8` |
+
+6. **Config changes.** In `src-tauri/tauri.conf.json`, under `bundle.macOS`, add a
+ hardened-runtime entitlements file. Notarization rejects binaries without the
+ hardened runtime, and llama.cpp's Metal path needs the JIT entitlement:
+
+ ```xml
+
+ com.apple.security.cs.allow-jit
+ com.apple.security.cs.allow-unsigned-executable-memory
+ ```
+
+ Tauri's bundler reads `APPLE_SIGNING_IDENTITY` and the `APPLE_API_*` variables
+ directly, so the macOS CI job needs the secrets in `env:` and an import step for
+ the certificate — it does not need a separate `notarytool` invocation.
+
+7. **Verify** a built app before trusting the pipeline:
+ ```bash
+ codesign --verify --deep --strict --verbose=2 "ÆTHER.app"
+ spctl --assess --type execute --verbose "ÆTHER.app"
+ xcrun stapler validate "ÆTHER.app"
+ ```
+ `spctl` must report `accepted source=Notarized Developer ID`.
+
+**Note on the DMG.** The installer is built by `scripts/make-styled-dmg.sh` via
+`appdmg`, not by Tauri's bundler, so the `.dmg` itself needs signing and stapling as
+a separate step after it is assembled — signing the `.app` alone is not enough.
+
+## Windows — Azure Trusted Signing
+
+**Cost:** roughly $10/month. The older route is an OV or EV certificate from a CA
+(DigiCert, Sectigo) at a few hundred dollars a year; EV additionally requires a
+hardware token, which does not work in CI without a cloud HSM. Azure Trusted Signing
+is the cheaper and more CI-friendly option for a solo project, and it accrues
+SmartScreen reputation against Microsoft's root rather than from zero.
+
+1. Create an Azure account and a **Trusted Signing** account and certificate profile.
+ Identity validation takes a few days for an individual.
+2. Register an app in Entra ID, grant it the *Trusted Signing Certificate Profile
+ Signer* role, and record the tenant, client ID, and client secret.
+3. Add `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`,
+ `AZURE_ENDPOINT`, `AZURE_CODE_SIGNING_NAME`, `AZURE_CERT_PROFILE_NAME` as
+ repository secrets.
+4. Set `bundle.windows.signCommand` in `tauri.conf.json` to invoke the Trusted
+ Signing CLI over `%1`. Tauri passes each artifact path through it.
+
+Reputation is earned per-publisher over download volume, so expect SmartScreen to
+keep warning for the first weeks even once signing works.
+
+## Linux
+
+No signing is required for `.deb` or AppImage — neither apt nor the AppImage
+runtime enforces publisher identity by default. If ÆTHER is ever published through
+Flathub, that has its own review and build process, and signing is handled there.
+
+## In-app updates — minisign
+
+This part **is** wired up. Settings → Updates offers *Install Update* when a newer
+release exists, downloads it, verifies its signature, and installs it in place. All
+that is missing is a keypair, which is free and takes a minute.
+
+Until the key exists nothing changes: builds are byte-identical to before, and the
+Install button reports `unconfigured` — *"This build has no update signing key, so
+ÆTHER cannot verify a download"* — rather than fetching something it could not check.
+
+### Turning it on
+
+1. **Generate the keypair.** The password may be empty, but a real one is better —
+ anyone with the private key can push an update to every ÆTHER install.
+ ```bash
+ bun run tauri signer generate -w ~/.tauri/aether-updater.key
+ ```
+ This prints a public key and writes the private key to that path. **Back both up
+ somewhere durable.** Losing the private key means no existing install can ever be
+ updated again — every user has to reinstall by hand.
+
+2. **Commit the public key** into `src-tauri/tauri.conf.json` under
+ `plugins.updater.pubkey`, replacing the empty placeholder. It is a public key;
+ committing it is the intended usage.
+
+3. **Add two repository secrets:**
+
+ | Secret | Value |
+ |---|---|
+ | `TAURI_SIGNING_PRIVATE_KEY` | contents of `~/.tauri/aether-updater.key` |
+ | `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | the password from step 1 |
+
+ The moment `TAURI_SIGNING_PRIVATE_KEY` exists, the build jobs start producing
+ signed updater artifacts and the release job publishes `latest.json`. Nothing else
+ needs changing — see `.github/actions/updater-flags`.
+
+4. **Verify** after the next tagged release:
+ ```bash
+ curl -sL https://github.com/CanPixel/aether/releases/latest/download/latest.json | jq .
+ ```
+ It should list `darwin-aarch64`, `windows-x86_64`, and `linux-x86_64`, each with a
+ non-empty `signature` and a URL pinned to the release tag.
+
+### What updates, and what does not
+
+| Install | Self-updates | Why |
+|---|---|---|
+| macOS `.dmg` → `/Applications` | Yes | Replaces the `.app` bundle in place |
+| Windows `-setup.exe` | Yes | Re-runs the NSIS installer silently |
+| Linux AppImage | Yes | Rewrites the AppImage file |
+| Linux `.deb` / `.rpm` | **No** | Owned by the package manager; ÆTHER reports `unsupported` rather than corrupting it |
+| Linux ARM64 | **No** | Only a `.deb` is published, so there is no signed artifact |
+| Android | **No** | Store-managed |
+
+### Two things worth knowing
+
+**`createUpdaterArtifacts` is a hard build error without the private key.** That is
+why it lives in `src-tauri/tauri.updater.conf.json` and is merged in only when the
+secret exists, instead of sitting in the main config — otherwise every branch push
+would fail until the key was created.
+
+**Updates may sidestep the quarantine problem.** Tauri's updater downloads and
+extracts the bundle itself rather than going through a browser, so it should not
+attach `com.apple.quarantine` — meaning the `xattr` dance in the README would apply
+only to the *first* install, not to subsequent updates. This follows from how
+quarantine is applied and has **not been verified against a real signed release**;
+confirm it before relying on it.
+
+## Order of work
+
+1. **Updater keypair first.** It is free, takes a minute, and is the only item here
+ that improves anything today — without it, a security fix reaches only users who
+ happen to re-download by hand.
+2. macOS Developer ID second. It is the only platform where the current state is a
+ hard block rather than a bypassable warning, and it is the platform ÆTHER is
+ developed on.
+3. Windows last. SmartScreen is unpleasant but clickable, and reputation takes
+ weeks to build regardless of when you start.
diff --git a/package.json b/package.json
index 80c8aca..b7179ab 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck:tauri": "cargo check --manifest-path src-tauri/Cargo.toml",
"typecheck": "bun run typecheck:web && bun run typecheck:tauri",
+ "test": "cargo test --manifest-path src-tauri/Cargo.toml --lib",
"version:bump": "bun scripts/bump-version.ts",
"version:check": "bun scripts/bump-version.ts --check",
"release": "bun scripts/release.ts",
@@ -43,29 +44,35 @@
"build:desktop-local": "bun run build && bun run linux:arm64:build && bun run linux:x64:build",
"tauri": "tauri"
},
+ "overrides": {
+ "brace-expansion": "^5.0.7",
+ "@babel/core": "^7.29.1"
+ },
"dependencies": {
"ldrs": "^1.1.9",
- "lucide-react": "^1.17.0"
+ "lucide-react": "^1.26.0"
},
"devDependencies": {
- "@eslint/js": "^9.39.4",
- "@tauri-apps/api": "^2.11.0",
- "@tauri-apps/cli": "^2.11.2",
+ "@babel/core": "^8.0.1",
+ "brace-expansion": "^5.0.8",
+ "@eslint/js": "^9.39.5",
+ "@tauri-apps/api": "^2.11.1",
+ "@tauri-apps/cli": "^2.11.4",
"@types/bun": "^1.3.14",
- "@types/node": "^25.9.1",
- "@types/react": "^19.2.15",
+ "@types/node": "^25.9.5",
+ "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
- "@vitejs/plugin-react": "^6.0.2",
+ "@vitejs/plugin-react": "^6.0.4",
"appdmg": "^0.6.6",
- "eslint": "^10.4.0",
+ "eslint": "^10.8.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
- "eslint-plugin-react-refresh": "^0.5.2",
- "prettier": "^3.8.3",
- "react": "^19.2.6",
- "react-dom": "^19.2.6",
+ "eslint-plugin-react-refresh": "^0.5.3",
+ "prettier": "^3.9.6",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8",
"typescript": "^6.0.3",
- "typescript-eslint": "^8.59.2",
- "vite": "^8.0.14"
+ "typescript-eslint": "^8.65.0",
+ "vite": "^8.1.5"
}
}
diff --git a/scripts/updater-manifest.sh b/scripts/updater-manifest.sh
new file mode 100755
index 0000000..78390cd
--- /dev/null
+++ b/scripts/updater-manifest.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+# Builds the `latest.json` that the in-app updater reads (tauri-plugin-updater's
+# "static" manifest format: a version plus one entry per `{os}-{arch}` target).
+#
+# Signatures are embedded as strings, so the .sig files produced by the bundler are
+# read here and never published as release assets. Any platform whose signature is
+# missing is left out of the manifest entirely — the app then reports "no build for
+# this platform" rather than downloading something it cannot verify. If no platform
+# has a signature at all, nothing is written and the script exits 0: that is the
+# normal state before an updater signing key exists (see docs/SIGNING.md).
+#
+# The shape this produces is locked by `updater_manifest_matches_the_plugin_format`
+# in src-tauri/src/lib.rs. Change one and the other fails.
+#
+# Usage: scripts/updater-manifest.sh
+set -euo pipefail
+
+tag="${1:?tag required, e.g. v1.0.30}"
+repo="${2:?repo required, e.g. CanPixel/aether}"
+artifacts="${3:?artifacts directory required}"
+out="${4:?output file required}"
+
+version="${tag#v}"
+base="https://github.com/${repo}/releases/download/${tag}"
+
+read_sig() {
+ local source
+ source="$(find "$artifacts/$1" -type f -name "$2" 2>/dev/null | sort | head -n 1)"
+ [[ -n "$source" ]] && tr -d '\n' < "$source"
+}
+
+mac_sig="$(read_sig "aether-macos-updater" "*.app.tar.gz.sig" || true)"
+win_sig="$(read_sig "aether-windows-x86_64-updater" "*.exe.sig" || true)"
+linux_sig="$(read_sig "aether-linux-x86_64-updater" "*.AppImage.sig" || true)"
+
+if [[ -z "$mac_sig$win_sig$linux_sig" ]]; then
+ echo "No updater signatures found under $artifacts; not writing $out." >&2
+ exit 0
+fi
+
+# Both macOS arches map to the same tarball: the bundle is built on Apple Silicon,
+# and an Intel Mac running it under Rosetta still reports darwin-x86_64 to the
+# updater. Drop darwin-x86_64 here if a real Intel or universal build is ever
+# published as a separate asset.
+jq -n \
+ --arg version "$version" \
+ --arg pub_date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
+ --arg notes "See https://github.com/${repo}/releases/tag/${tag}" \
+ --arg mac_sig "$mac_sig" \
+ --arg mac_url "$base/AETHER_macOS.app.tar.gz" \
+ --arg win_sig "$win_sig" \
+ --arg win_url "$base/AETHER_x64-setup.exe" \
+ --arg linux_sig "$linux_sig" \
+ --arg linux_url "$base/AETHER_amd64.AppImage" \
+ '{
+ version: $version,
+ notes: $notes,
+ pub_date: $pub_date,
+ platforms: (
+ {}
+ + (if $mac_sig == "" then {} else {
+ "darwin-aarch64": { signature: $mac_sig, url: $mac_url },
+ "darwin-x86_64": { signature: $mac_sig, url: $mac_url }
+ } end)
+ + (if $win_sig == "" then {} else {
+ "windows-x86_64": { signature: $win_sig, url: $win_url }
+ } end)
+ + (if $linux_sig == "" then {} else {
+ "linux-x86_64": { signature: $linux_sig, url: $linux_url }
+ } end)
+ )
+ }' > "$out"
+
+echo "Wrote $out for $version with targets:" >&2
+jq -c '.platforms | keys' "$out" >&2
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index cecba8b..a5e27db 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -22,6 +22,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-opener",
+ "tauri-plugin-updater",
"tokio",
"url",
"uuid",
@@ -66,6 +67,15 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -762,6 +772,17 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -1067,6 +1088,16 @@ dependencies = [
"rustc_version",
]
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -1979,6 +2010,36 @@ dependencies = [
"windows-sys 0.45.0",
]
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys 0.4.1",
+ "log",
+ "simd_cesu8",
+ "thiserror 2.0.18",
+ "walkdir",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.117",
+]
+
[[package]]
name = "jni-sys"
version = "0.3.1"
@@ -2263,6 +2324,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+[[package]]
+name = "minisign-verify"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
+
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -2505,6 +2572,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.0",
"block2",
+ "libc",
"objc2",
"objc2-core-foundation",
]
@@ -2520,6 +2588,18 @@ dependencies = [
"objc2-core-foundation",
]
+[[package]]
+name = "objc2-osa-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
+dependencies = [
+ "bitflags 2.13.0",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+]
+
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -2595,6 +2675,12 @@ dependencies = [
"pathdiff",
]
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -2611,6 +2697,20 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "osakit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+ "objc2-osa-kit",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
[[package]]
name = "pango"
version = "0.18.3"
@@ -3216,15 +3316,20 @@ dependencies = [
"http-body",
"http-body-util",
"hyper",
+ "hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
+ "tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -3292,6 +3397,18 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
@@ -3302,6 +3419,33 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+dependencies = [
+ "core-foundation",
+ "core-foundation-sys",
+ "jni 0.22.4",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
[[package]]
name = "rustls-webpki"
version = "0.103.13"
@@ -3334,6 +3478,15 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "schemars"
version = "0.8.22"
@@ -3406,6 +3559,29 @@ dependencies = [
"tendril 0.4.3",
]
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.0",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
[[package]]
name = "selectors"
version = "0.31.0"
@@ -3663,6 +3839,22 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+[[package]]
+name = "simd_cesu8"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
[[package]]
name = "siphasher"
version = "1.0.3"
@@ -3889,7 +4081,7 @@ dependencies = [
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
- "jni",
+ "jni 0.21.1",
"libc",
"log",
"ndk",
@@ -3922,6 +4114,17 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
[[package]]
name = "target-lexicon"
version = "0.12.16"
@@ -3945,7 +4148,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
- "jni",
+ "jni 0.21.1",
"libc",
"log",
"mime",
@@ -4079,6 +4282,39 @@ dependencies = [
"zbus",
]
+[[package]]
+name = "tauri-plugin-updater"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
+dependencies = [
+ "base64 0.22.1",
+ "dirs",
+ "flate2",
+ "futures-util",
+ "http",
+ "infer",
+ "log",
+ "minisign-verify",
+ "osakit",
+ "percent-encoding",
+ "reqwest 0.13.4",
+ "rustls",
+ "semver",
+ "serde",
+ "serde_json",
+ "tar",
+ "tauri",
+ "tauri-plugin",
+ "tempfile",
+ "thiserror 2.0.18",
+ "time",
+ "tokio",
+ "url",
+ "windows-sys 0.60.2",
+ "zip",
+]
+
[[package]]
name = "tauri-runtime"
version = "2.11.2"
@@ -4089,7 +4325,7 @@ dependencies = [
"dpi",
"gtk",
"http",
- "jni",
+ "jni 0.21.1",
"objc2",
"objc2-ui-kit",
"objc2-web-kit",
@@ -4112,7 +4348,7 @@ checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9"
dependencies = [
"gtk",
"http",
- "jni",
+ "jni 0.21.1",
"log",
"objc2",
"objc2-app-kit",
@@ -4320,9 +4556,21 @@ dependencies = [
"mio",
"pin-project-lite",
"socket2",
+ "tokio-macros",
"windows-sys 0.61.2",
]
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "tokio-rustls"
version = "0.26.4"
@@ -4985,6 +5233,15 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
+dependencies = [
+ "rustls-pki-types",
+]
+
[[package]]
name = "webpki-roots"
version = "1.0.7"
@@ -5616,7 +5873,7 @@ dependencies = [
"gtk",
"http",
"javascriptcore-rs",
- "jni",
+ "jni 0.21.1",
"libc",
"ndk",
"objc2",
@@ -5663,6 +5920,16 @@ dependencies = [
"pkg-config",
]
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
[[package]]
name = "yoke"
version = "0.8.3"
@@ -5827,6 +6094,18 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "zip"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
+dependencies = [
+ "arbitrary",
+ "crc32fast",
+ "indexmap 2.14.0",
+ "memchr",
+]
+
[[package]]
name = "zmij"
version = "1.0.21"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index e0508e4..9e58109 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -30,6 +30,17 @@ tokio = { version = "1", features = ["fs", "io-util", "sync", "time"] }
url = "2"
uuid = { version = "1", features = ["v4", "serde"] }
+[dev-dependencies]
+# The store-durability tests drive async fs helpers, which needs a runtime. Dev
+# features are not unified into non-test builds, so this does not affect the app.
+tokio = { version = "1", features = ["fs", "io-util", "macros", "rt"] }
+
+# The updater replaces the installed bundle in place, which only means anything on
+# desktop: Android and iOS updates go through their stores. Keeping the plugin out
+# of the mobile dependency graph entirely also keeps it from breaking those builds.
+[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
+tauri-plugin-updater = "2"
+
[target.'cfg(target_os = "macos")'.dependencies]
# Cargo unions features across target tables, so on macOS llama.cpp gets metal + sampler.
llama-cpp-2 = { version = "0.1.146", default-features = false, features = ["metal"] }
diff --git a/src-tauri/src/air.rs b/src-tauri/src/air.rs
new file mode 100644
index 0000000..994f74d
--- /dev/null
+++ b/src-tauri/src/air.rs
@@ -0,0 +1,791 @@
+//! AiR: rendering a set of captures into a Markdown dossier on disk.
+
+use super::*;
+
+pub(crate) async fn air_prepare_dossier(
+ state: &State<'_, Backend>,
+ input: AirDossierInput,
+ synthesize: bool,
+) -> Cmd {
+ let lens_kind = input.lens_kind.unwrap_or_default();
+ let lens = input.lens.trim().to_string();
+ let visible_lens = if lens.is_empty() {
+ match lens_kind {
+ AirLensKind::Topic => "Local knowledge".to_string(),
+ AirLensKind::Flow => "Current Flow lens".to_string(),
+ AirLensKind::Hub => "Selected knowledge hub".to_string(),
+ AirLensKind::Answer => "Latest AiON answer".to_string(),
+ AirLensKind::Iceberg => "Saved iCE map".to_string(),
+ }
+ } else {
+ lens
+ };
+ let generated_at = now();
+ let limit = input.limit.unwrap_or(12).clamp(1, 24);
+ let (sources, seed_answer, ice_map, seed_model) =
+ air_gather_context(state, &input, lens_kind, &visible_lens, limit).await?;
+ let title = format!("AiR Dossier: {visible_lens}");
+ let output_dir = resolve_air_export_dir(&state.paths, false).await?;
+
+ let mut model = seed_model.unwrap_or_else(|| "deterministic-scaffold".to_string());
+ let synthesized_sections = if synthesize {
+ match air_synthesize_sections(
+ state,
+ &visible_lens,
+ lens_kind,
+ &sources,
+ seed_answer.as_deref(),
+ ice_map.as_deref(),
+ )
+ .await
+ {
+ Ok((synthesis_model, sections)) => {
+ model = synthesis_model;
+ Some(sections)
+ }
+ Err(_) => None,
+ }
+ } else {
+ None
+ };
+
+ let markdown_preview = build_air_markdown(AirMarkdownInput {
+ title: &title,
+ lens: &visible_lens,
+ lens_kind,
+ generated_at: &generated_at,
+ model: &model,
+ sources: &sources,
+ synthesized_sections: synthesized_sections.as_deref(),
+ seed_answer: seed_answer.as_deref(),
+ ice_map: ice_map.as_deref(),
+ });
+
+ Ok(AirPreparedDossier {
+ title,
+ lens: visible_lens,
+ lens_kind,
+ generated_at,
+ model: Some(model),
+ output_dir: output_dir.display().to_string(),
+ markdown_preview,
+ sources,
+ })
+}
+
+pub(crate) async fn air_gather_context(
+ state: &State<'_, Backend>,
+ input: &AirDossierInput,
+ lens_kind: AirLensKind,
+ lens: &str,
+ limit: usize,
+) -> Cmd<(
+ Vec,
+ Option,
+ Option,
+ Option,
+)> {
+ match lens_kind {
+ AirLensKind::Answer => {
+ let Some(answer) = input.answer.clone() else {
+ return Ok((Vec::new(), None, None, None));
+ };
+ let library = load_library(&state.paths.library_path).await?;
+ let collection_names = library
+ .collections
+ .iter()
+ .map(|collection| (collection.id.clone(), collection.name.clone()))
+ .collect::>();
+ let sources = search_results_to_air_sources(
+ dedupe_citations(answer.citations)
+ .into_iter()
+ .take(limit)
+ .collect(),
+ &collection_names,
+ );
+ Ok((sources, Some(answer.answer), None, Some(answer.model)))
+ }
+ AirLensKind::Iceberg => {
+ let icebergs = load_icebergs(&state.paths.icebergs_path).await?;
+ let selected = input
+ .saved_iceberg_id
+ .as_deref()
+ .and_then(|id| icebergs.icebergs.iter().find(|iceberg| iceberg.id == id))
+ .or_else(|| {
+ icebergs
+ .icebergs
+ .iter()
+ .find(|iceberg| iceberg.title.eq_ignore_ascii_case(lens))
+ })
+ .or_else(|| icebergs.icebergs.first());
+ let Some(iceberg) = selected else {
+ return Ok((Vec::new(), None, None, None));
+ };
+ let mut items = iceberg.iceberg.items.clone();
+ items.sort_by(|left, right| {
+ left.level
+ .cmp(&right.level)
+ .then_with(|| left.name.to_lowercase().cmp(&right.name.to_lowercase()))
+ });
+ let sources = items
+ .iter()
+ .take(limit)
+ .map(|item| AirDossierSource {
+ id: format!("iceberg-{}", item.id),
+ title: item.name.clone(),
+ excerpt: format!("Level {}: {}", item.level, item.description),
+ collection_name: Some(format!("iCE · {}", iceberg.title)),
+ url: None,
+ host: None,
+ captured_at: Some(iceberg.updated_at.clone()),
+ score: None,
+ })
+ .collect::>();
+ let map = items
+ .iter()
+ .map(|item| {
+ format!(
+ "- Level {} · {}: {}",
+ item.level, item.name, item.description
+ )
+ })
+ .collect::>()
+ .join("\n");
+ Ok((
+ sources,
+ None,
+ Some(map),
+ Some(iceberg.iceberg.model.clone()),
+ ))
+ }
+ AirLensKind::Hub => {
+ let sources = air_sources_for_hub(state, input.collection_id.as_deref(), limit).await?;
+ Ok((sources, None, None, None))
+ }
+ AirLensKind::Flow => {
+ if let Some(collection_id) = input.collection_id.as_deref() {
+ let sources = air_sources_for_hub(state, Some(collection_id), limit).await?;
+ return Ok((sources, None, None, None));
+ }
+ let mut sources = match input.capture_id.as_deref() {
+ Some(capture_id) => air_sources_for_capture(state, capture_id).await?,
+ None => Vec::new(),
+ };
+ let existing = sources
+ .iter()
+ .map(|source| source.id.clone())
+ .collect::>();
+ let related = air_sources_for_topic(state, lens, limit).await?;
+ sources.extend(
+ related
+ .into_iter()
+ .filter(|source| !existing.contains(&source.id))
+ .take(limit.saturating_sub(sources.len())),
+ );
+ Ok((sources, None, None, None))
+ }
+ AirLensKind::Topic => {
+ let sources = air_sources_for_topic(state, lens, limit).await?;
+ Ok((sources, None, None, None))
+ }
+ }
+}
+
+pub(crate) async fn air_sources_for_hub(
+ state: &State<'_, Backend>,
+ collection_id: Option<&str>,
+ limit: usize,
+) -> Cmd> {
+ let library = load_library(&state.paths.library_path).await?;
+ let collection_names = library
+ .collections
+ .iter()
+ .map(|collection| (collection.id.clone(), collection.name.clone()))
+ .collect::>();
+ let vectors = with_vectors_read(state, |vectors| vectors.chunks.clone()).await?;
+ let mut chunks_by_capture = HashMap::>::new();
+ for chunk in vectors {
+ chunks_by_capture
+ .entry(chunk.capture_id.clone())
+ .or_default()
+ .push(chunk);
+ }
+ let mut captures = library
+ .captures
+ .into_iter()
+ .filter(|capture| {
+ collection_id
+ .map(|id| capture.collection_id == id)
+ .unwrap_or(true)
+ })
+ .collect::>();
+ captures.sort_by(|left, right| right.captured_at.cmp(&left.captured_at));
+ Ok(captures
+ .into_iter()
+ .take(limit)
+ .map(|capture| {
+ let excerpt = chunks_by_capture
+ .get(&capture.id)
+ .and_then(|chunks| chunks.iter().max_by_key(|chunk| chunk.text.chars().count()))
+ .map(|chunk| semantic_trail_excerpt(&chunk.text, 700))
+ .or_else(|| {
+ capture
+ .metadata
+ .as_ref()
+ .and_then(|metadata| metadata.summary.clone())
+ })
+ .unwrap_or_default();
+ AirDossierSource {
+ id: capture.id.clone(),
+ title: capture.title,
+ excerpt,
+ collection_name: collection_names.get(&capture.collection_id).cloned(),
+ url: Some(capture.url.clone()),
+ host: Some(get_tab_host(&capture.url)),
+ captured_at: Some(capture.captured_at),
+ score: None,
+ }
+ })
+ .collect())
+}
+
+pub(crate) async fn air_sources_for_capture(
+ state: &State<'_, Backend>,
+ capture_id: &str,
+) -> Cmd> {
+ let library = load_library(&state.paths.library_path).await?;
+ let collection_names = library
+ .collections
+ .iter()
+ .map(|collection| (collection.id.clone(), collection.name.clone()))
+ .collect::>();
+ let Some(capture) = library
+ .captures
+ .iter()
+ .find(|capture| capture.id == capture_id)
+ .cloned()
+ else {
+ return Ok(Vec::new());
+ };
+ let mut chunks = with_vectors_read(state, |vectors| {
+ vectors
+ .chunks
+ .iter()
+ .filter(|chunk| chunk.capture_id == capture_id)
+ .cloned()
+ .collect::>()
+ })
+ .await?;
+ chunks.sort_by_key(|chunk| chunk.chunk_index);
+ let excerpt = semantic_trail_excerpt(
+ &chunks
+ .iter()
+ .map(|chunk| chunk.text.as_str())
+ .collect::>()
+ .join("\n\n"),
+ 1200,
+ );
+ Ok(vec![AirDossierSource {
+ id: capture.id.clone(),
+ title: capture.title,
+ excerpt,
+ collection_name: collection_names.get(&capture.collection_id).cloned(),
+ url: Some(capture.url.clone()),
+ host: Some(get_tab_host(&capture.url)),
+ captured_at: Some(capture.captured_at),
+ score: Some(100.0),
+ }])
+}
+
+pub(crate) async fn air_sources_for_topic(
+ state: &State<'_, Backend>,
+ lens: &str,
+ limit: usize,
+) -> Cmd> {
+ let library = load_library(&state.paths.library_path).await?;
+ let collection_names = library
+ .collections
+ .iter()
+ .map(|collection| (collection.id.clone(), collection.name.clone()))
+ .collect::>();
+ let vectors = with_vectors_read(state, |vectors| vectors.chunks.clone()).await?;
+ if vectors.is_empty() {
+ return Ok(Vec::new());
+ }
+
+ let results = if lens.trim().is_empty() || lens == "Current Flow lens" {
+ let mut chunks = vectors;
+ chunks.sort_by(|left, right| right.captured_at.cmp(&left.captured_at));
+ chunks
+ .into_iter()
+ .take(limit * 2)
+ .map(|chunk| SearchResult {
+ score: 0.0,
+ id: chunk.id,
+ collection_id: chunk.collection_id,
+ capture_id: chunk.capture_id,
+ app_id: chunk.app_id,
+ title: chunk.title,
+ url: chunk.url,
+ captured_at: chunk.captured_at,
+ chunk_index: chunk.chunk_index,
+ text: chunk.text,
+ })
+ .collect::>()
+ } else {
+ let settings = load_settings(&state.paths.settings_path).await?;
+ let query_vector = local_embed_query(state, &settings, lens.to_string()).await?;
+ let mut scored = vectors
+ .into_iter()
+ .filter_map(|chunk| {
+ let distance = cosine_distance(&query_vector, &chunk.vector);
+ if !distance.is_finite() {
+ return None;
+ }
+ Some((distance, chunk))
+ })
+ .collect::>();
+ scored.sort_by(|left, right| left.0.partial_cmp(&right.0).unwrap_or(Ordering::Equal));
+ scored
+ .into_iter()
+ .take(limit * 3)
+ .map(|(score, chunk)| SearchResult {
+ score,
+ id: chunk.id,
+ collection_id: chunk.collection_id,
+ capture_id: chunk.capture_id,
+ app_id: chunk.app_id,
+ title: chunk.title,
+ url: chunk.url,
+ captured_at: chunk.captured_at,
+ chunk_index: chunk.chunk_index,
+ text: chunk.text,
+ })
+ .collect::>()
+ };
+ Ok(search_results_to_air_sources(
+ dedupe_citations(results).into_iter().take(limit).collect(),
+ &collection_names,
+ ))
+}
+
+pub(crate) fn search_results_to_air_sources(
+ results: Vec,
+ collection_names: &HashMap,
+) -> Vec {
+ results
+ .into_iter()
+ .map(|result| {
+ let collection_name = collection_names.get(&result.collection_id).cloned();
+ AirDossierSource {
+ id: result.capture_id.clone(),
+ title: result.title,
+ excerpt: semantic_trail_excerpt(&result.text, 850),
+ collection_name,
+ url: Some(result.url.clone()),
+ host: Some(get_tab_host(&result.url)),
+ captured_at: Some(result.captured_at),
+ score: Some(round_score(semantic_score_from_distance(result.score))),
+ }
+ })
+ .collect()
+}
+
+pub(crate) async fn air_synthesize_sections(
+ state: &State<'_, Backend>,
+ lens: &str,
+ lens_kind: AirLensKind,
+ sources: &[AirDossierSource],
+ seed_answer: Option<&str>,
+ ice_map: Option<&str>,
+) -> Cmd<(String, String)> {
+ let settings = load_settings(&state.paths.settings_path).await?;
+ let citations = sources
+ .iter()
+ .enumerate()
+ .map(|(index, source)| SearchResult {
+ id: source.id.clone(),
+ collection_id: source.collection_name.clone().unwrap_or_default(),
+ capture_id: source.id.clone(),
+ app_id: "air".to_string(),
+ title: source.title.clone(),
+ url: source
+ .url
+ .clone()
+ .unwrap_or_else(|| format!("aether://air/source/{}", index + 1)),
+ captured_at: source.captured_at.clone().unwrap_or_else(now),
+ chunk_index: index,
+ text: source.excerpt.clone(),
+ score: source.score.unwrap_or(0.0),
+ })
+ .collect::>();
+ let source_rule = if citations.is_empty() {
+ "No local source excerpts matched. Say that coverage is currently sparse."
+ } else {
+ "Use only the numbered local source excerpts. Cite every concrete claim with bracket citations like [1] or [2]."
+ };
+ let prompt = format!(
+ "Render a concise Markdown dossier for Æther AiR.\nLens kind: {}\nLens: {lens}\n{source_rule}\n\nInclude exactly these sections:\n## Summary\n## Key Findings\n## Source-Backed Notes\n## Unresolved Questions\n\nDo not include YAML frontmatter, a title, or a source index. Keep prose dense and professional.\n\nSeed AiON answer:\n{}\n\nSaved iCE map:\n{}",
+ air_lens_label(lens_kind),
+ seed_answer.unwrap_or("None"),
+ ice_map.unwrap_or("None")
+ );
+ let result = local_chat(state, &settings, &prompt, citations, &[], None).await?;
+ Ok((
+ result.model,
+ normalize_model_markdown_citations(&result.answer, sources.len()),
+ ))
+}
+
+pub(crate) struct AirMarkdownInput<'a> {
+ pub(crate) title: &'a str,
+ pub(crate) lens: &'a str,
+ pub(crate) lens_kind: AirLensKind,
+ pub(crate) generated_at: &'a str,
+ pub(crate) model: &'a str,
+ pub(crate) sources: &'a [AirDossierSource],
+ pub(crate) synthesized_sections: Option<&'a str>,
+ pub(crate) seed_answer: Option<&'a str>,
+ pub(crate) ice_map: Option<&'a str>,
+}
+
+pub(crate) fn build_air_markdown(input: AirMarkdownInput<'_>) -> String {
+ let tags = air_tags(input.lens)
+ .into_iter()
+ .map(|tag| yaml_string(&tag))
+ .collect::>()
+ .join(", ");
+ let mut markdown = format!(
+ "---\ntitle: {}\ncreated: {}\naether_lens: {}\nsource_count: {}\ntags: [{}]\nmodel: {}\ntype: aether-dossier\n---\n\n# {}\n\n_Rendered by AiR from the {} lens on {}._\n\n",
+ yaml_string(input.title),
+ yaml_string(input.generated_at),
+ yaml_string(input.lens),
+ input.sources.len(),
+ tags,
+ yaml_string(input.model),
+ markdown_escape(input.title),
+ air_lens_label(input.lens_kind),
+ markdown_escape(input.generated_at)
+ );
+
+ if let Some(sections) = input
+ .synthesized_sections
+ .map(str::trim)
+ .filter(|sections| !sections.is_empty())
+ {
+ markdown.push_str(sections);
+ markdown.push_str("\n\n");
+ } else {
+ markdown.push_str("## Summary\n\n");
+ if input.sources.is_empty() {
+ markdown.push_str(&format!(
+ "No local sources matched [[{}]] yet. This dossier preserves the requested lens and can be regenerated after more captures are available.\n\n",
+ markdown_escape(input.lens)
+ ));
+ } else {
+ markdown.push_str(&format!(
+ "This dossier gathers {} local source{} around [[{}]]. It is generated as a deterministic citation scaffold from Æther's captured knowledge.\n\n",
+ input.sources.len(),
+ if input.sources.len() == 1 { "" } else { "s" },
+ markdown_escape(input.lens)
+ ));
+ }
+
+ markdown.push_str("## Key Findings\n\n");
+ if input.sources.is_empty() {
+ markdown.push_str("- Coverage is sparse; capture or connect more relevant material before treating this lens as complete.\n\n");
+ } else {
+ for (index, source) in input.sources.iter().take(6).enumerate() {
+ markdown.push_str(&format!(
+ "- **{}** anchors this lens with: {} [^{}]\n",
+ markdown_escape(&source.title),
+ markdown_escape(&semantic_trail_excerpt(&source.excerpt, 180)),
+ index + 1
+ ));
+ }
+ markdown.push('\n');
+ }
+
+ markdown.push_str("## Source-Backed Notes\n\n");
+ for (index, source) in input.sources.iter().enumerate() {
+ markdown.push_str(&format!(
+ "### {}. {}\n\n{}\n\n",
+ index + 1,
+ markdown_escape(&source.title),
+ markdown_escape(&source.excerpt)
+ ));
+ let mut meta = Vec::new();
+ if let Some(collection) = &source.collection_name {
+ meta.push(format!("Hub: {}", markdown_escape(collection)));
+ }
+ if let Some(host) = &source.host {
+ meta.push(format!("Host: {}", markdown_escape(host)));
+ }
+ if let Some(captured_at) = &source.captured_at {
+ meta.push(format!("Captured: {}", markdown_escape(captured_at)));
+ }
+ if let Some(score) = source.score {
+ meta.push(format!("Match: {score:.1}"));
+ }
+ if !meta.is_empty() {
+ markdown.push_str(&format!("{}\n\n", meta.join(" · ")));
+ }
+ }
+
+ if let Some(answer) = input
+ .seed_answer
+ .map(str::trim)
+ .filter(|answer| !answer.is_empty())
+ {
+ markdown.push_str("## AiON Seed\n\n");
+ markdown.push_str(&markdown_escape(answer));
+ markdown.push_str("\n\n");
+ }
+
+ if let Some(map) = input.ice_map.map(str::trim).filter(|map| !map.is_empty()) {
+ markdown.push_str("## iCE Map\n\n");
+ markdown.push_str(&markdown_escape(map));
+ markdown.push_str("\n\n");
+ }
+
+ markdown.push_str("## Unresolved Questions\n\n");
+ markdown.push_str("- Which claims need fresher or primary-source confirmation?\n");
+ markdown.push_str("- Which adjacent hubs should be captured next to improve coverage?\n\n");
+ }
+
+ markdown.push_str("## Source Index\n\n");
+ if input.sources.is_empty() {
+ markdown.push_str("No source index entries were available for this render.\n");
+ } else {
+ for (index, source) in input.sources.iter().enumerate() {
+ let title = markdown_escape(&source.title);
+ let collection = source
+ .collection_name
+ .as_deref()
+ .map(markdown_escape)
+ .unwrap_or_else(|| "Knowledge Hub".to_string());
+ let captured_at = source
+ .captured_at
+ .as_deref()
+ .map(markdown_escape)
+ .unwrap_or_else(|| "unknown capture date".to_string());
+ if let Some(url) = &source.url {
+ markdown.push_str(&format!(
+ "[^{}]: [{}]({}) — {} · {}\n",
+ index + 1,
+ title,
+ url.replace(')', "%29"),
+ collection,
+ captured_at
+ ));
+ } else {
+ markdown.push_str(&format!(
+ "[^{}]: {} — {} · {}\n",
+ index + 1,
+ title,
+ collection,
+ captured_at
+ ));
+ }
+ }
+ }
+ markdown
+}
+
+pub(crate) async fn resolve_air_export_dir(paths: &DataPaths, create: bool) -> Cmd {
+ let preferred = documents_air_dir();
+ let candidates = preferred
+ .into_iter()
+ .chain(std::iter::once(paths.air_exports_path.clone()))
+ .collect::>();
+ for candidate in candidates {
+ if !create {
+ if candidate.exists()
+ || candidate
+ .parent()
+ .is_some_and(|parent| parent.exists() && parent.is_dir())
+ {
+ return Ok(candidate);
+ }
+ continue;
+ }
+ if tokio::fs::create_dir_all(&candidate).await.is_ok() {
+ return Ok(candidate);
+ }
+ }
+ if create {
+ Err("Could not create an AiR export folder.".to_string())
+ } else {
+ Ok(paths.air_exports_path.clone())
+ }
+}
+
+pub(crate) fn documents_air_dir() -> Option {
+ env::var_os("HOME")
+ .or_else(|| env::var_os("USERPROFILE"))
+ .map(PathBuf::from)
+ .map(|home| home.join("Documents").join("Æther").join("AiR"))
+}
+
+pub(crate) async fn air_list_recent(paths: &DataPaths) -> Cmd> {
+ let mut files = Vec::::new();
+ let mut directories = Vec::new();
+ if let Some(documents) = documents_air_dir() {
+ directories.push(documents);
+ }
+ directories.push(paths.air_exports_path.clone());
+
+ for directory in directories {
+ let Ok(mut entries) = tokio::fs::read_dir(&directory).await else {
+ continue;
+ };
+ while let Some(entry) = entries
+ .next_entry()
+ .await
+ .map_err(|error| error.to_string())?
+ {
+ let path = entry.path();
+ if !path
+ .extension()
+ .and_then(|extension| extension.to_str())
+ .is_some_and(|extension| extension.eq_ignore_ascii_case("md"))
+ {
+ continue;
+ }
+ let filename = path
+ .file_name()
+ .and_then(|name| name.to_str())
+ .unwrap_or("aether-dossier.md")
+ .to_string();
+ let metadata = entry.metadata().await.ok();
+ let rendered_at = metadata
+ .and_then(|metadata| metadata.modified().ok())
+ .map(|modified| {
+ DateTime::::from(modified)
+ .to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
+ })
+ .unwrap_or_else(now);
+ let content = tokio::fs::read_to_string(&path).await.unwrap_or_default();
+ files.push(AirRecentFile {
+ title: air_frontmatter_value(&content, "title")
+ .unwrap_or_else(|| air_title_from_filename(&filename)),
+ lens: air_frontmatter_value(&content, "aether_lens").unwrap_or_default(),
+ source_count: air_frontmatter_value(&content, "source_count")
+ .and_then(|value| value.parse::().ok())
+ .unwrap_or(0),
+ path: path.display().to_string(),
+ filename,
+ rendered_at,
+ });
+ }
+ }
+ files.sort_by(|left, right| right.rendered_at.cmp(&left.rendered_at));
+ files.truncate(12);
+ Ok(files)
+}
+
+pub(crate) fn air_frontmatter_value(markdown: &str, key: &str) -> Option {
+ let mut lines = markdown.lines();
+ if lines.next()? != "---" {
+ return None;
+ }
+ let prefix = format!("{key}:");
+ for line in lines {
+ if line == "---" {
+ break;
+ }
+ if let Some(value) = line.strip_prefix(&prefix) {
+ return Some(value.trim().trim_matches('"').replace("\\\"", "\""));
+ }
+ }
+ None
+}
+
+pub(crate) fn air_dossier_filename(title: &str, generated_at: &str) -> String {
+ let _ = generated_at;
+ let filename = title
+ .trim()
+ .chars()
+ .map(|char| match char {
+ '/' | '\\' | '\0' => '-',
+ char if char.is_control() => ' ',
+ char => char,
+ })
+ .collect::()
+ .split_whitespace()
+ .collect::>()
+ .join(" ")
+ .trim_matches(['.', ' '])
+ .to_string();
+ if filename.is_empty() {
+ "AiR Dossier.md".to_string()
+ } else {
+ format!("{filename}.md")
+ }
+}
+
+pub(crate) fn air_title_from_filename(filename: &str) -> String {
+ filename
+ .trim_end_matches(".md")
+ .split('-')
+ .filter(|part| !part.chars().all(|char| char.is_ascii_digit()))
+ .take(8)
+ .map(|part| {
+ let mut chars = part.chars();
+ match chars.next() {
+ Some(first) => format!("{}{}", first.to_uppercase(), chars.as_str()),
+ None => String::new(),
+ }
+ })
+ .collect::>()
+ .join(" ")
+}
+
+pub(crate) fn yaml_string(value: &str) -> String {
+ format!(
+ "\"{}\"",
+ value
+ .replace('\\', "\\\\")
+ .replace('"', "\\\"")
+ .replace(['\n', '\r'], " ")
+ )
+}
+
+pub(crate) fn markdown_escape(value: &str) -> String {
+ strip_numeric_bracket_markers(value)
+ .replace('\r', "")
+ .trim()
+ .to_string()
+}
+
+pub(crate) fn air_tags(lens: &str) -> Vec {
+ let mut tags = vec![
+ "aether".to_string(),
+ "air".to_string(),
+ "dossier".to_string(),
+ ];
+ for word in lens
+ .split(|char: char| !char.is_alphanumeric())
+ .filter(|word| word.chars().count() >= 3)
+ .take(4)
+ {
+ tags.push(word.to_lowercase());
+ }
+ tags.sort();
+ tags.dedup();
+ tags
+}
+
+pub(crate) fn air_lens_label(kind: AirLensKind) -> &'static str {
+ match kind {
+ AirLensKind::Topic => "topic",
+ AirLensKind::Flow => "Flow",
+ AirLensKind::Hub => "hub",
+ AirLensKind::Answer => "AiON answer",
+ AirLensKind::Iceberg => "iCE",
+ }
+}
+
+pub(crate) fn normalize_model_markdown_citations(markdown: &str, source_count: usize) -> String {
+ normalize_answer_citations(&clean_model_output(markdown), source_count)
+}
diff --git a/src-tauri/src/chat.rs b/src-tauri/src/chat.rs
new file mode 100644
index 0000000..6642233
--- /dev/null
+++ b/src-tauri/src/chat.rs
@@ -0,0 +1,319 @@
+//! Chat prompt assembly and model-output cleanup.
+//!
+//! The cleanup half exists because a local model's raw output is not presentable:
+//! it leaks stop markers, invents citation numbers past the source count, and
+//! spaces brackets inconsistently.
+
+use super::*;
+
+pub(crate) fn chat_citation_limit() -> usize {
+ if cfg!(mobile) {
+ 5
+ } else {
+ 8
+ }
+}
+
+pub(crate) fn chat_snippet_char_limit() -> usize {
+ if cfg!(mobile) {
+ 1100
+ } else {
+ usize::MAX
+ }
+}
+
+pub(crate) fn build_chat_messages(
+ prompt: &str,
+ citations: &[SearchResult],
+ history: &[ConversationTurn],
+) -> Vec {
+ let snippet_limit = chat_snippet_char_limit();
+ let context_block = citations
+ .iter()
+ .enumerate()
+ .map(|(index, item)| {
+ let mut source_text = strip_numeric_bracket_markers(&item.text);
+ if source_text.chars().count() > snippet_limit {
+ source_text = source_text.chars().take(snippet_limit).collect();
+ }
+ format!(
+ "[{}] {}\nURL: {}\nCollection: {}\n{}",
+ index + 1,
+ item.title,
+ item.url,
+ item.collection_id,
+ source_text
+ )
+ })
+ .collect::>()
+ .join("\n\n");
+ let context = if context_block.is_empty() {
+ "No stored context was retrieved."
+ } else {
+ &context_block
+ };
+ // Phones decode on CPU, so every generated token is user-visible latency:
+ // steer the model toward tight answers instead of hard-truncating them.
+ let brevity = if cfg!(mobile) {
+ " Keep answers concise: a few sentences or a short list unless the question needs more."
+ } else {
+ ""
+ };
+ let mut messages = vec![ChatPromptMessage {
+ role: "system",
+ content: format!(
+ "You are Æther, a private local research assistant. Answer only from the supplied local collection context. If the context is insufficient, say what is missing. Cite sources only with Æther source numbers [1] through [{}]. Do not copy bracketed reference numbers from webpage text.{brevity}",
+ citations.len().max(1)
+ ),
+ }];
+
+ // Prior turns come first so "what about that?" resolves, but each answer is
+ // condensed: the citations for *this* question must stay the dominant context.
+ for turn in history {
+ messages.push(ChatPromptMessage {
+ role: "user",
+ content: turn.prompt.clone(),
+ });
+ messages.push(ChatPromptMessage {
+ role: "assistant",
+ content: condense_history_answer(&turn.answer),
+ });
+ }
+
+ messages.push(ChatPromptMessage {
+ role: "user",
+ content: format!("Local collection context:\n{context}\n\nQuestion: {prompt}"),
+ });
+ messages
+}
+
+// Strips citation markers and clips length. Replaying markers from an old answer
+// would let the model reuse source numbers that no longer refer to anything.
+pub(crate) fn condense_history_answer(answer: &str) -> String {
+ let cleaned = strip_numeric_bracket_markers(answer);
+ let trimmed = cleaned.trim();
+ if trimmed.chars().count() <= PROMPT_HISTORY_ANSWER_CHARS {
+ return trimmed.to_string();
+ }
+ let mut condensed = trimmed
+ .chars()
+ .take(PROMPT_HISTORY_ANSWER_CHARS)
+ .collect::();
+ condensed.push('…');
+ condensed
+}
+
+pub(crate) fn render_model_chat_prompt(
+ model: &LlamaModel,
+ messages: &[ChatPromptMessage],
+) -> Cmd {
+ let template = match model.chat_template(None) {
+ Ok(template) => template,
+ Err(_) => {
+ return Ok(RenderedChatPrompt {
+ prompt: fallback_chat_prompt(messages),
+ add_bos: AddBos::Never,
+ })
+ }
+ };
+ let chat = messages
+ .iter()
+ .map(|message| LlamaChatMessage::new(message.role.to_string(), message.content.clone()))
+ .collect::, _>>()
+ .map_err(|error| error.to_string())?;
+ match model.apply_chat_template(&template, &chat, true) {
+ Ok(prompt) => Ok(RenderedChatPrompt {
+ prompt,
+ add_bos: AddBos::Never,
+ }),
+ Err(_) => Ok(RenderedChatPrompt {
+ prompt: fallback_chat_prompt(messages),
+ add_bos: AddBos::Never,
+ }),
+ }
+}
+
+pub(crate) fn fallback_chat_prompt(messages: &[ChatPromptMessage]) -> String {
+ let mut prompt = String::from("");
+ let mut system_messages = Vec::new();
+
+ for message in messages {
+ match message.role {
+ "system" | "developer" => {
+ system_messages.push(message.content.trim().to_string());
+ }
+ "assistant" => {
+ prompt.push_str("<|turn>model\n");
+ prompt.push_str(message.content.trim());
+ prompt.push_str("\n");
+ }
+ "user" => {
+ if !system_messages.is_empty() {
+ prompt.push_str("<|turn>system\n");
+ prompt.push_str(&system_messages.join("\n\n"));
+ prompt.push_str("\n");
+ system_messages.clear();
+ }
+ prompt.push_str("<|turn>user\n");
+ prompt.push_str(message.content.trim());
+ prompt.push_str("\n");
+ }
+ role => {
+ prompt.push_str("<|turn>");
+ prompt.push_str(role);
+ prompt.push('\n');
+ prompt.push_str(message.content.trim());
+ prompt.push_str("\n");
+ }
+ }
+ }
+
+ if !system_messages.is_empty() {
+ prompt.push_str("<|turn>system\n");
+ prompt.push_str(&system_messages.join("\n\n"));
+ prompt.push_str("\n");
+ }
+
+ prompt.push_str("<|turn>model\n");
+ prompt
+}
+
+// Streaming deltas hold back a short tail starting at the most recent '<' so a
+// stop marker arriving across several tokens is never shown to the user.
+pub(crate) fn stream_safe_len(output: &str) -> usize {
+ let tail_start = output.len().saturating_sub(18);
+ let Some((boundary, _)) = output
+ .char_indices()
+ .find(|(index, _)| *index >= tail_start)
+ else {
+ return output.len();
+ };
+ match output[boundary..].rfind('<') {
+ Some(position) => boundary + position,
+ None => output.len(),
+ }
+}
+
+pub(crate) fn contains_stop_marker(output: &str) -> bool {
+ output.contains("")
+ || output.contains("")
+ || output.contains("")
+ || output.contains("<|turn>")
+ || output.contains("<|eot_id|>")
+ || output.contains("<|end|>")
+}
+
+pub(crate) fn clean_model_output(output: &str) -> String {
+ let mut cleaned = output.to_string();
+ for marker in [
+ "",
+ "model",
+ "assistant",
+ "",
+ "",
+ "<|turn>model",
+ "<|turn>assistant",
+ "<|turn>user",
+ "<|turn>system",
+ "<|turn>",
+ "<|eot_id|>",
+ "<|end|>",
+ ] {
+ cleaned = cleaned.replace(marker, "");
+ }
+ cleaned.trim().to_string()
+}
+
+pub(crate) fn normalize_answer_citations(answer: &str, citation_count: usize) -> String {
+ tidy_citation_spacing(&rewrite_numeric_bracket_markers(
+ answer,
+ citation_count,
+ true,
+ ))
+}
+
+pub(crate) fn strip_numeric_bracket_markers(text: &str) -> String {
+ rewrite_numeric_bracket_markers(text, 0, false)
+}
+
+pub(crate) fn rewrite_numeric_bracket_markers(
+ text: &str,
+ citation_count: usize,
+ keep_valid: bool,
+) -> String {
+ let mut rewritten = String::with_capacity(text.len());
+ let mut cursor = 0usize;
+
+ while let Some(relative_start) = text[cursor..].find('[') {
+ let start = cursor + relative_start;
+ let Some(relative_end) = text[start + 1..].find(']') else {
+ break;
+ };
+ let end = start + 1 + relative_end;
+ let inner = &text[start + 1..end];
+ let Some(numbers) = parse_numeric_citation_marker(inner) else {
+ rewritten.push_str(&text[cursor..=start]);
+ cursor = start + 1;
+ continue;
+ };
+
+ rewritten.push_str(&text[cursor..start]);
+ if keep_valid {
+ let valid = numbers
+ .into_iter()
+ .filter(|number| *number > 0 && *number <= citation_count)
+ .map(|number| number.to_string())
+ .collect::>();
+ if !valid.is_empty() {
+ rewritten.push('[');
+ rewritten.push_str(&valid.join(", "));
+ rewritten.push(']');
+ }
+ }
+ cursor = end + 1;
+ }
+
+ rewritten.push_str(&text[cursor..]);
+ rewritten
+}
+
+pub(crate) fn parse_numeric_citation_marker(value: &str) -> Option> {
+ if value.trim().is_empty()
+ || !value.chars().all(|character| {
+ character.is_ascii_digit() || character == ',' || character.is_whitespace()
+ })
+ {
+ return None;
+ }
+
+ let mut numbers = Vec::new();
+ for part in value.split(',') {
+ let part = part.trim();
+ if part.is_empty() {
+ return None;
+ }
+ let number = part.parse::().ok()?;
+ numbers.push(number);
+ }
+ (!numbers.is_empty()).then_some(numbers)
+}
+
+pub(crate) fn tidy_citation_spacing(value: &str) -> String {
+ let mut tidied = value.trim().to_string();
+ for (from, to) in [
+ (" .", "."),
+ (" ,", ","),
+ (" ;", ";"),
+ (" :", ":"),
+ (" !", "!"),
+ (" ?", "?"),
+ (" )", ")"),
+ ("( ", "("),
+ ] {
+ tidied = tidied.replace(from, to);
+ }
+ while tidied.contains(" ") {
+ tidied = tidied.replace(" ", " ");
+ }
+ tidied
+}
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
new file mode 100644
index 0000000..0df2f2d
--- /dev/null
+++ b/src-tauri/src/commands.rs
@@ -0,0 +1,2052 @@
+//! Every `#[tauri::command]` the renderer can invoke.
+//!
+//! Thin by design: each one validates its input, delegates to the domain function
+//! below, and maps the result into `Cmd`. Command names are the IPC contract
+//! with src/renderer/src/tauri-aether.ts and are also listed in `generate_handler!`
+//! in lib.rs — adding one means touching all three.
+
+use super::*;
+
+#[tauri::command]
+pub(crate) fn aether_state(state: State) -> Cmd {
+ Ok(lock_tabs(&state)?.state())
+}
+
+#[tauri::command]
+pub(crate) fn aether_apps_list(state: State) -> Cmd> {
+ Ok(lock_tabs(&state)?.apps())
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) fn aether_apps_activate(
+ app: AppHandle,
+ state: State,
+ app_id: String,
+) -> Cmd<()> {
+ if app_id != "browser" {
+ return Err(format!("Unknown app: {app_id}"));
+ }
+ {
+ let mut tabs = lock_tabs(&state)?;
+ tabs.dashboard_open = false;
+ }
+ let active_tab_id = lock_tabs(&state)?.active_tab_id.clone();
+ ensure_native_webview(&app, &state, &active_tab_id)?;
+ emit_state(&app, &state)
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) async fn aether_apps_navigate(
+ app: AppHandle,
+ state: State<'_, Backend>,
+ app_id: String,
+ url: String,
+) -> Cmd<()> {
+ if app_id != "browser" {
+ return Err(format!("Unknown app: {app_id}"));
+ }
+ navigate_active_tab(&app, &state, &url).await
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) fn aether_apps_go_back(
+ app: AppHandle,
+ state: State,
+ app_id: String,
+) -> Cmd<()> {
+ if app_id != "browser" {
+ return Err(format!("Unknown app: {app_id}"));
+ }
+ aether_tabs_go_back(app, state, String::new())
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) fn aether_apps_go_forward(
+ app: AppHandle,
+ state: State,
+ app_id: String,
+) -> Cmd<()> {
+ if app_id != "browser" {
+ return Err(format!("Unknown app: {app_id}"));
+ }
+ aether_tabs_go_forward(app, state, String::new())
+}
+
+#[tauri::command]
+pub(crate) fn aether_tabs_list(state: State) -> Cmd> {
+ Ok(lock_tabs(&state)?.tabs())
+}
+
+#[tauri::command]
+pub(crate) async fn aether_tabs_create(
+ app: AppHandle,
+ state: State<'_, Backend>,
+ input: Option,
+) -> Cmd {
+ let settings = load_settings(&state.paths.settings_path).await?;
+ // No URL → open a blank start-page tab (Portals + search) rather than a search engine.
+ let requested_url = input.and_then(|input| input.url);
+ let url = match requested_url {
+ Some(raw_url) => normalize_url(&raw_url, &settings.browser.default_search_engine),
+ None => START_PAGE_URL.to_string(),
+ };
+ let tab = ManagedTab::new("browser", &url);
+ let tab_id = tab.id.clone();
+ let summary = tab.summary(true);
+ {
+ let mut tabs = lock_tabs(&state)?;
+ tabs.active_tab_id = tab.id.clone();
+ tabs.active_app_id = tab.app_id.clone();
+ tabs.dashboard_open = false;
+ tabs.tabs.push(tab);
+ }
+ ensure_native_webview(&app, &state, &tab_id)?;
+ emit_state(&app, &state)?;
+ schedule_session_save(&app);
+ Ok(summary)
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) fn aether_tabs_activate(
+ app: AppHandle,
+ state: State,
+ tab_id: String,
+) -> Cmd<()> {
+ {
+ let mut tabs = lock_tabs(&state)?;
+ if !tabs.tabs.iter().any(|tab| tab.id == tab_id) {
+ return Err(format!("Unknown tab: {tab_id}"));
+ }
+ tabs.active_tab_id = tab_id.clone();
+ tabs.active_app_id = "browser".to_string();
+ tabs.dashboard_open = false;
+ }
+ ensure_native_webview(&app, &state, &tab_id)?;
+ emit_state(&app, &state)?;
+ schedule_session_save(&app);
+ Ok(())
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) fn aether_tabs_close(app: AppHandle, state: State, tab_id: String) -> Cmd<()> {
+ let mut next_active_tab_id = None;
+ {
+ let mut tabs = lock_tabs(&state)?;
+ if tabs.tabs.len() == 1 {
+ return Ok(());
+ }
+ let was_active = tabs.active_tab_id == tab_id;
+ tabs.tabs.retain(|tab| tab.id != tab_id);
+ if was_active {
+ if let Some(next_id) = tabs.tabs.last().map(|tab| tab.id.clone()) {
+ tabs.active_tab_id = next_id.clone();
+ next_active_tab_id = Some(next_id);
+ }
+ }
+ }
+ close_native_webview(&app, &state, &tab_id)?;
+ if let Some(active_tab_id) = next_active_tab_id {
+ ensure_native_webview(&app, &state, &active_tab_id)?;
+ } else {
+ sync_native_webview_visibility(&app, &state)?;
+ }
+ emit_state(&app, &state)?;
+ schedule_session_save(&app);
+ Ok(())
+}
+
+#[tauri::command]
+pub(crate) fn aether_tabs_reorder(
+ app: AppHandle,
+ state: State,
+ ids: Vec,
+) -> Cmd> {
+ let summaries = {
+ let mut tabs = lock_tabs(&state)?;
+ let has_every_tab_once = ids.len() == tabs.tabs.len()
+ && ids
+ .iter()
+ .all(|id| tabs.tabs.iter().any(|tab| tab.id == *id))
+ && !ids
+ .iter()
+ .enumerate()
+ .any(|(index, id)| ids[..index].iter().any(|previous| previous == id));
+ if !has_every_tab_once {
+ return Err("Tab order does not match the open tabs.".to_string());
+ }
+
+ let mut remaining = std::mem::take(&mut tabs.tabs);
+ tabs.tabs = ids
+ .iter()
+ .map(|id| {
+ let index = remaining
+ .iter()
+ .position(|tab| tab.id == *id)
+ .expect("tab order was validated");
+ remaining.remove(index)
+ })
+ .collect();
+ tabs.tabs()
+ };
+ emit_state(&app, &state)?;
+ schedule_session_save(&app);
+ Ok(summaries)
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) async fn aether_tabs_navigate(
+ app: AppHandle,
+ state: State<'_, Backend>,
+ tab_id: String,
+ url: String,
+) -> Cmd<()> {
+ let settings = load_settings(&state.paths.settings_path).await?;
+ let target_url = {
+ let mut tabs = lock_tabs(&state)?;
+ let tab = tabs
+ .tabs
+ .iter_mut()
+ .find(|tab| tab.id == tab_id)
+ .ok_or_else(|| format!("Unknown tab: {tab_id}"))?;
+ tab.navigate(&url, &settings.browser.default_search_engine);
+ let target_url = tab.url.clone();
+ tabs.active_tab_id = tab.id.clone();
+ tabs.dashboard_open = false;
+ target_url
+ };
+ navigate_native_webview(&app, &state, &tab_id, &target_url)?;
+ emit_state(&app, &state)?;
+ schedule_session_save(&app);
+ Ok(())
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) fn aether_tabs_scroll_to_text(
+ app: AppHandle,
+ state: State,
+ tab_id: String,
+ text: String,
+) -> Cmd<()> {
+ {
+ let tabs = lock_tabs(&state)?;
+ if !tabs.tabs.iter().any(|tab| tab.id == tab_id) {
+ return Err(format!("Unknown tab: {tab_id}"));
+ }
+ }
+ scroll_native_webview_to_text(&app, &state, &tab_id, &text)
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) fn aether_tabs_find(
+ app: AppHandle,
+ state: State,
+ tab_id: String,
+ query: Option,
+ action: Option,
+) -> Cmd<()> {
+ let target_tab_id = {
+ let tabs = lock_tabs(&state)?;
+ if tab_id.is_empty() {
+ tabs.active_tab_id.clone()
+ } else if tabs.tabs.iter().any(|tab| tab.id == tab_id) {
+ tab_id
+ } else {
+ return Err(format!("Unknown tab: {tab_id}"));
+ }
+ };
+ let action = action.as_deref().unwrap_or("find");
+ find_native_webview_text(&app, &state, &target_tab_id, query.as_deref(), action)
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) fn aether_tabs_go_back(
+ app: AppHandle,
+ state: State,
+ tab_id: String,
+) -> Cmd<()> {
+ let mut restore_start_page = false;
+ let target_tab_id = {
+ let mut tabs = lock_tabs(&state)?;
+ let tab = if tab_id.is_empty() {
+ tabs.active_tab_mut()
+ .ok_or_else(|| "No active browser tab.".to_string())?
+ } else {
+ tabs.tabs
+ .iter_mut()
+ .find(|tab| tab.id == tab_id)
+ .ok_or_else(|| format!("Unknown tab: {tab_id}"))?
+ };
+ // The start page is a renderer overlay, not a native page, so the webview can't
+ // navigate back to it. When the previous history entry is the start page, park
+ // the tab back on it (its webview is kept hidden for a later forward).
+ if tab.can_go_back()
+ && tab.history.get(tab.history_index - 1).map(String::as_str) == Some(START_PAGE_URL)
+ {
+ tab.history_index -= 1;
+ tab.url = START_PAGE_URL.to_string();
+ tab.title = "New tab".to_string();
+ tab.is_loading = false;
+ restore_start_page = true;
+ }
+ tab.id.clone()
+ };
+ if restore_start_page {
+ sync_native_webview_visibility(&app, &state)?;
+ } else {
+ navigate_native_webview_history(
+ &app,
+ &state,
+ &target_tab_id,
+ WebviewHistoryDirection::Back,
+ )?;
+ }
+ emit_state(&app, &state)
+}
+
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) fn aether_tabs_go_forward(
+ app: AppHandle,
+ state: State,
+ tab_id: String,
+) -> Cmd<()> {
+ let mut leave_start_page = false;
+ let target_tab_id = {
+ let mut tabs = lock_tabs(&state)?;
+ let tab = if tab_id.is_empty() {
+ tabs.active_tab_mut()
+ .ok_or_else(|| "No active browser tab.".to_string())?
+ } else {
+ tabs.tabs
+ .iter_mut()
+ .find(|tab| tab.id == tab_id)
+ .ok_or_else(|| format!("Unknown tab: {tab_id}"))?
+ };
+ // Forwarding off the start page: advance to the real page whose (hidden) webview
+ // we kept, then reveal it instead of issuing a native history.forward().
+ if tab.url == START_PAGE_URL && tab.can_go_forward() {
+ tab.history_index += 1;
+ tab.url = tab.history[tab.history_index].clone();
+ tab.title = title_from_url(&tab.url);
+ tab.is_loading = false;
+ leave_start_page = true;
+ }
+ tab.id.clone()
+ };
+ if leave_start_page {
+ ensure_native_webview(&app, &state, &target_tab_id)?;
+ } else {
+ navigate_native_webview_history(
+ &app,
+ &state,
+ &target_tab_id,
+ WebviewHistoryDirection::Forward,
+ )?;
+ }
+ emit_state(&app, &state)
+}
+
+// Mobile-only feedback channel: the Kotlin TabsPlugin evaluates
+// `window.__AETHER_TAB_EVENT__(...)` in the main webview and the renderer
+// forwards the payload here, mirroring how desktop child-webview callbacks
+// (on_navigation / on_page_load / on_document_title_changed) feed tab state.
+#[tauri::command]
+pub(crate) fn aether_tabs_report_native_event(
+ app: AppHandle,
+ state: State,
+ input: NativeTabEventInput,
+) -> Cmd<()> {
+ match input.kind.as_str() {
+ "navigation" => {
+ let parked_on_start_page = {
+ let tabs = lock_tabs(&state)?;
+ tabs.tabs
+ .iter()
+ .find(|tab| tab.id == input.tab_id)
+ .map(|tab| tab.url == START_PAGE_URL)
+ .unwrap_or(true)
+ };
+ // A tab parked on the start page keeps its (hidden) webview alive;
+ // ignore its stray events so the start-page sentinel survives.
+ if parked_on_start_page {
+ return Ok(());
+ }
+ if let Some(url) = input.url.as_deref() {
+ update_tab_navigation_state(
+ &state,
+ &input.tab_id,
+ url,
+ input.is_loading.unwrap_or(false),
+ );
+ }
+ {
+ let mut tabs = lock_tabs(&state)?;
+ if let Some(tab) = tabs.tabs.iter_mut().find(|tab| tab.id == input.tab_id) {
+ tab.native_can_go_back = input.can_go_back;
+ tab.native_can_go_forward = input.can_go_forward;
+ }
+ }
+ emit_state(&app, &state)
+ }
+ "title" => {
+ if let Some(title) = input.title.as_deref() {
+ update_tab_title(&state, &input.tab_id, title);
+ }
+ emit_state(&app, &state)
+ }
+ "find" => app
+ .emit(
+ AETHER_FIND_RESULT_EVENT,
+ FindResultPayload {
+ tab_id: input.tab_id,
+ current: input.current.unwrap_or(0),
+ total: input.total.unwrap_or(0),
+ },
+ )
+ .map_err(|error| error.to_string()),
+ _ => Ok(()),
+ }
+}
+
+// Preview image (data-URI JPEG) for the mobile tab-grid switcher. Desktop
+// renders live child webviews and never asks for one, so it returns None.
+// Async on purpose: the Kotlin side resolves on the Android UI thread, so the
+// blocking run_mobile_plugin call must sit on a tokio worker.
+#[tauri::command(rename_all = "camelCase")]
+pub(crate) async fn aether_tabs_thumbnail(app: AppHandle, tab_id: String) -> Cmd