Compare commits

..
19 Commits
Author SHA1 Message Date
hh 43d1e1a4a3 feat(settings,docs): vault subpath becomes the chats folder, panel screenshots in the readme 2026-09-05 04:56:03 +02:00
hh 223560c8da fix(view): follow the keyboard through the navbar hiding behind it
Same two misses as the calendar's: the four-frame watch stopped before
Obsidian hid its navbar - a second move of the ground, after the keyboard's
own - and a trigger that arrived while a watch was running was dropped rather
than restarting it. The watch now needs twelve steady frames, restarts on
every trigger, and listens to Obsidian's Capacitor keyboard events too.
2026-09-05 01:15:11 +02:00
hh 70cf985aa1 refactor(inset): the bottom-inset arithmetic becomes testable, and is tested
The measurement lived inside the view, where the only way to check it was to
run Obsidian on a phone. It moves to a pure `insetFor(leaf, bars, visible)`
against the numbers a phone reports - including the case the events hid: while
the keyboard is up the navbar is behind it, so the deeper of the two is the
inset and adding them would push the composer twice as far as it must go.
2026-09-05 00:49:54 +02:00
hh 413d68fc87 fix(view): the inset chases the keyboard by frame, and comes back when it goes
Two fixed delays into the keyboard's animation is not where it ends: the
composer read the viewport mid-slide, and on the way out - when the navbar
comes back - nothing re-measured at all, so the panel stayed pushed up or
sat back under the bar. In a sidedock drawer no resize event fires either,
so a phone's keyboard moved nothing there.

The inset is now re-measured every frame until the reading holds still, and
blur is a trigger as much as focus. `inset()` returns what it wrote so the
loop can tell it has settled rather than counting milliseconds.
2026-09-05 00:47:44 +02:00
hh a04ad0094f fix(css): an icon-only grid button centres its icon again
Obsidian styles every `button` with `justify-content: center`, so the reset
answers with `flex-start` - which also lands on a grid button and overrides
the inline half of its `place-items: center`. The rail's connection dot came
out pinned 12px left of centre inside its own hit area.
2026-09-05 00:28:50 +02:00
hh cc46abc34f fix(view,panel): the composer clears the navbar and the keyboard, panel opens on the master
The inset only knew the desktop status bar, so on a phone the composer sat
under the navbar - which paints over a leaf instead of shortening it - and
the software keyboard hid it outright. Every bottom bar is measured now, and
the keyboard is read off `visualViewport`: it shrinks the visible area
without resizing the layout, so no workspace event fires and the panel had
no way to know. Focus re-measures once iOS has finished animating the keys in.

The panel asks the shell to fall back to the latest master, so opening it on
a phone lands in the day's thread rather than on a prompt.

The preview fixtures anchor at midday instead of the wall clock: spread over
hours, a run just after midnight pushed half of them into yesterday and
emptied the "Today" group the switcher smoke checks.
2026-09-05 00:20:33 +02:00
hh a76ee29255 test(smoke): subagent tool calls read as Agent 2026-09-04 20:24:02 +02:00
hh 68870864a6 build: d3-force for the shared graph 2026-09-02 04:35:13 +02:00
hh 0887728a8c feat(preview): stamped history for message times 2026-09-02 04:24:20 +02:00
hh cec5aef98c feat(panel): island, rail by day, companion mode beside a deep note, cached index and threads, obsidian skin for strips 2026-09-02 03:48:42 +02:00
hh 126e8b0451 fix(css): scope the subject compound so theme-dark rules keep matching 2026-08-29 19:50:03 +02:00
hh b619c75119 fix(theme): switch thumb travels inside the track, toggle scene for the preview 2026-08-29 19:42:59 +02:00
hh 353ee143f6 fix(css): leave tailwind theme vars on :root so obsidian's accent wins inside the panel 2026-08-29 19:42:55 +02:00
hh ab22efff7b fix(css): scope utilities under the view root so tailwind plugins stop outranking each other 2026-08-29 19:39:21 +02:00
hh b46dab194b fix(theme): switches take obsidian's toggle look, thumb visible on dark themes 2026-08-29 19:25:25 +02:00
hh 8cefc54552 fix(view): pad the panel by the status bar overlap so the composer stays clear 2026-08-29 19:21:18 +02:00
hh 8e74439b4d feat(main): ribbon opens the panel as a tab, tab and sidedock leaves reused separately 2026-08-29 19:08:50 +02:00
hh 04400c5076 feat(*): svelte 5 panel view sharing the gateway ui, obsidian theme, preview and tests 2026-08-29 18:30:31 +02:00
hh a1bf4db905 feat(activity,api,settings): activity panel v0 over conversation events sse 2026-08-28 17:01:02 +02:00
39 changed files with 4378 additions and 988 deletions

No files matched your search

+5
View File
@@ -3,7 +3,11 @@ node_modules/
# build output (regenerated by `make install` / `make zip`) # build output (regenerated by `make install` / `make zip`)
main.js main.js
styles.css
.tailwind.css
*.zip *.zip
preview/bundle.js
tests/.entry.ts
# plugin runtime — Obsidian writes settings here when the symlinked dir # plugin runtime — Obsidian writes settings here when the symlinked dir
# is used as the live plugin; never want it in the repo # is used as the live plugin; never want it in the repo
@@ -14,3 +18,4 @@ data.json
.DS_Store .DS_Store
.vscode/ .vscode/
.idea/ .idea/
preview/shots/
+22 -9
View File
@@ -1,15 +1,25 @@
PLUGIN_ID := beaver PLUGIN_ID := beaver
ASSETS := manifest.json main.js versions.json ASSETS := manifest.json main.js styles.css versions.json
PM := $(shell command -v pnpm 2>/dev/null || command -v npm 2>/dev/null)
.PHONY: install zip _build .PHONY: sync check fix test build install zip preview
_build: sync:
@test -n "$(PM)" || (echo "no pnpm or npm in PATH" >&2; exit 1) bun install
$(PM) install --silent
$(PM) run build
install: _build check:
bun lint
bun run check
fix:
bun fix
test:
bun run test
build:
bun run build
install: build
@test -n "$(VAULT)" || (echo "set VAULT=<path-to-vault>" >&2; exit 1) @test -n "$(VAULT)" || (echo "set VAULT=<path-to-vault>" >&2; exit 1)
@test -d "$(VAULT)/.obsidian" || (echo "no .obsidian dir at $(VAULT)" >&2; exit 1) @test -d "$(VAULT)/.obsidian" || (echo "no .obsidian dir at $(VAULT)" >&2; exit 1)
@target="$(VAULT)/.obsidian/plugins/$(PLUGIN_ID)"; \ @target="$(VAULT)/.obsidian/plugins/$(PLUGIN_ID)"; \
@@ -17,9 +27,12 @@ install: _build
cp $(ASSETS) "$$target/"; \ cp $(ASSETS) "$$target/"; \
echo "installed → $$target" echo "installed → $$target"
zip: _build zip: build
@version=$$(node -p "require('./manifest.json').version"); \ @version=$$(node -p "require('./manifest.json').version"); \
out="beaver-plugin-$$version.zip"; \ out="beaver-plugin-$$version.zip"; \
rm -f "$$out"; \ rm -f "$$out"; \
zip -q -j "$$out" $(ASSETS); \ zip -q -j "$$out" $(ASSETS); \
echo "wrote $$out" echo "wrote $$out"
preview:
bun run preview
+43 -36
View File
@@ -1,56 +1,63 @@
# Beaver — Obsidian plugin # Beaver - Obsidian plugin
[![AI Slop Inside](https://sladge.net/badge.svg)](https://sladge.net) [![AI Slop Inside](https://sladge.net/badge.svg)](https://sladge.net)
Sends the current note to Beaver's `markdown frontend` (`POST /chat` on `beaver-gateway`) and writes the agent's reply back into the file. Skips the Obsidian Sync round-trip by POSTing the buffer contents directly. Two things:
- **The panel**: Beaver's conversations next to your notes - the master thread, branches, deep chats - with the live tree of tool calls and subagents, the composer, and the same actions the admin has (branch with a seed, return a branch to Telegram or hide it, the memory flag, close with a digest). A narrow sidedock column, a full tab, or the phone screen. It follows the active note's `conversation_id` unless you pin it.
- **Send the current note** to Beaver's markdown frontend (`POST /chat/stream`) and stream the agent's reply back into the buffer.
![The panel as a tab: the conversation rail on the left, the master thread with its tool calls on the right](docs/panel-tab.webp)
![The panel as a column in the right sidedock, next to the note](docs/panel-column.webp)
## Commands ## Commands
Both commands appear in the command palette only when the active note's YAML frontmatter has an `agent:` field: - **Beaver: Open panel in a tab** - the wide layout with the conversation rail on the left (also the ribbon icon).
- **Beaver: Open panel** - the narrow column in the right sidedock; a tab and a column can be open at once, each command reuses its own.
- **Beaver: Open chat in panel** - the active note has `conversation_id` in its frontmatter; the panel shows that conversation.
- **Beaver: Send using selected agent** / **Send using different agent** - the note has `agent:` in its frontmatter.
- **Beaver: Send using selected agent** — uses `frontmatter.agent`. ## The panel
- **Beaver: Send using different agent** — fetches the agent list from the gateway and opens a fuzzy picker.
The UI is `beaver-gateway/ui/src/lib/panel`, compiled into this plugin by `esbuild.config.mjs` (`$lib/*` points at the gateway's `ui/src/lib`; every bare import resolves from this plugin's `node_modules`, so there is one Svelte runtime; `svelte-sonner` becomes Obsidian notices). The same components render the browser admin and `/admin/panel`.
Theme: `src/tailwind.css` maps the panel's tokens onto Obsidian's variables on `.beaver-root`, exactly as beaver-calendar does with `.bcal-root` - the app's font, accent, radii and the current theme, light or dark. Menus and dialogs portal to a `.beaver-root.beaver-portal` layer on `body`, above the sidedocks.
Markdown in the thread goes through Obsidian's `MarkdownRenderer`: `[[links]]` open the note, external links open the browser. A deep chat's row offers **Open note** (its markdown binding, under the chats folder).
Layout: below 48rem of panel width the conversation switcher sits in the header (kind, title, live dot, follow toggle); wider than that the grouped rail (master, branches, deep chats, jobs) sits on the left. Right-click a row for its actions; on touch the `…` button is always visible. `↑`/`↓` walk the rows, `/` jumps to the search, `⌘↩` sends.
## Settings ## Settings
- **Base URL** — markdown-frontend root, e.g. `http://localhost:62993`. - **Base URL** - markdown frontend root, e.g. `https://host/md`.
- **Bearer token** — a token with the `messages` scope (mint via the admin frontend or `BOOTSTRAP_TOKENS`). - **API origin** - where `/api/…` lives; empty derives it from the base URL (`…/md` → `…`).
- **Test connection** — calls `GET /agents`. - **Bearer token** - `messages` scope for sending, `api` scope for the panel (or a `*` bootstrap token).
- **Chats folder** - the folder in this vault where the deep chats live, the one the gateway's markdown frontend writes to. Paths the gateway hands out are relative to it.
## Note format Changing settings remounts open panels.
A minimal note that the plugin can send: ## Build
```markdown ```
--- bun install
agent: beaver-opus-medium bun run dev # css + esbuild watch
--- bun run build # svelte-check + css (minified, scoped) + bundle
bun run test # vitest (theme rules) + tests/smoke.mjs (jsdom, fake gateway)
hi there bun run preview # the panel in a browser, no Obsidian: http://localhost:4174/#<scene>
make check # ultracite + svelte-check
make install VAULT=~/Obsidian/my-vault
make zip
``` ```
The gateway parses `### User:` / `### Assistant:` markers; a note with no markers is treated as a single user turn. After a successful send, the gateway returns the new file content (including an `### Assistant:` block and a fresh `### User:` scaffold), and the plugin writes it back to the buffer. Preview scenes (`#thread` default, `#switcher`, `#closed`, `#menu`, `#actions`, `#activity`, `#branch`, `#question`, `#deep`, `#empty`, `#offline`, `#stream`; suffix `-light` for the light theme). Width is the window: run headless Chrome with `--window-size=340,720` for the sidedock, `1100,720` for a tab, `390,800` for a phone. Set `VAULT=` or `THEME=` to preview against a community theme; `app.css` is read from the Obsidian install.
`styles.css` is Tailwind output run through `scripts/scope-css.mjs`: every selector gets `:is(.beaver-root, .beaver-root *)` in front. Other Tailwind plugins (beaver-calendar for one) put their utilities into the same `utilities` cascade layer, and without the extra specificity whichever plugin loaded last would win - its `.hidden` over ours and the other way round.
## Install ## Install
``` `make install VAULT=…` copies `manifest.json`, `main.js`, `styles.css`, `versions.json` into `<vault>/.obsidian/plugins/beaver`; then enable **Beaver** in Settings → Community plugins. On a phone, sync the four files with Obsidian Sync ("Installed community plugins") or any file sync, and point the settings at a URL the phone can reach.
make install VAULT=~/Obsidian/my-vault # build + copy manifest.json + main.js into <vault>/.obsidian/plugins/beaver
make zip # build → beaver-plugin-<version>.zip you can carry to any vault
```
Then enable **Beaver** in Settings → Community plugins. ## Version
### Mobile Edit `manifest.json`, `package.json` and `versions.json` by hand.
The plugin is desktop-and-mobile (`isDesktopOnly: false`). Two things to get right on a phone:
- The base URL in plugin settings has to be reachable from the phone — `http://localhost:62993` won't work; use the gateway's LAN IP, a tunnel, or a public hostname.
- Get the plugin files onto the phone vault via Obsidian Sync (toggle "Installed community plugins" in your sync settings) or any file-sync tool you already use (Working Copy, Syncthing, etc.). `make install` only knows how to write to a local path.
### Hand-rolled
Three files are all Obsidian needs — `manifest.json`, `main.js`, `versions.json`. Drop them in `<vault>/.obsidian/plugins/beaver/` however you like (unzip, `scp`, `rsync`, your own git, …).
## Bumping the version
Edit the version field in `manifest.json`, `package.json`, and `versions.json` by hand. Three files, one change each.
+67
View File
@@ -0,0 +1,67 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["ultracite/biome/core", "ultracite/biome/svelte"],
"javascript": {
"globals": ["Bun"]
},
"files": {
"includes": ["**/*", "!main.js", "!styles.css", "!preview/bundle.js"]
},
"linter": {
"rules": {
"performance": {
"noNamespaceImport": "off"
},
"suspicious": {
"noConsole": "off"
}
}
},
"overrides": [
{
"includes": ["**/*.svelte"],
"linter": {
"rules": {
"correctness": {
"noUndeclaredVariables": "off",
"noUnusedFunctionParameters": "off",
"noUnusedVariables": "off"
},
"style": {
"useFilenamingConvention": "off"
},
"a11y": {
"noSvgWithoutTitle": "warn",
"useButtonType": "warn"
}
}
}
},
{
"includes": ["src/**"],
"linter": {
"rules": {
"correctness": {
"noUnusedInstantiation": "off"
}
}
}
},
{
"includes": ["tests/**", "preview/**", "scripts/**"],
"linter": {
"rules": {
"performance": {
"useTopLevelRegex": "off"
},
"style": {
"noMagicNumbers": "off"
},
"suspicious": {
"noEmptyBlockStatements": "off"
}
}
}
}
]
}
+721
View File
@@ -0,0 +1,721 @@
{
"lockfileVersion": 2,
"configVersion": 1,
"workspaces": {
"": {
"name": "beaver-plugin-obsidian",
"devDependencies": {
"@biomejs/biome": "^2.5.11",
"@electron/asar": "^4.3.0",
"@lucide/svelte": "^1.37.0",
"@tailwindcss/cli": "^4.3.3",
"@types/node": "^26.4.0",
"bits-ui": "^2.19.0",
"builtin-modules": "^5.3.0",
"clsx": "^2.1.1",
"d3-force": "^3.0.0",
"dompurify": "^3.4.14",
"esbuild": "^0.28.2",
"esbuild-svelte": "^0.9.5",
"jsdom": "^30.0.1",
"marked": "^18.0.11",
"obsidian": "^1.13.1",
"svelte": "^5.57.0",
"svelte-check": "^4.7.6",
"tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.3.1",
"tailwindcss": "^4.3.3",
"typescript": "^6",
"ultracite": "^7.10.7",
"vitest": "^4.1.11",
},
},
},
"packages": {
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@6.0.7", "", { "dependencies": { "@csstools/css-calc": "^3.3.0", "@csstools/css-color-parser": "^4.1.10", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.5.2" } }, "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw=="],
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@8.3.2", "", { "dependencies": { "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2" } }, "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
"@biomejs/biome": ["@biomejs/biome@2.5.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.11", "@biomejs/cli-darwin-x64": "2.5.11", "@biomejs/cli-linux-arm64": "2.5.11", "@biomejs/cli-linux-arm64-musl": "2.5.11", "@biomejs/cli-linux-x64": "2.5.11", "@biomejs/cli-linux-x64-musl": "2.5.11", "@biomejs/cli-win32-arm64": "2.5.11", "@biomejs/cli-win32-x64": "2.5.11" }, "bin": { "biome": "bin/biome" } }, "sha512-Tj0dnkLPdW0ASjHfj2D/ZkkvPU2wrFmnE1jWTD2xzV1ycapV1DutbYXk4NDnR3rYTi1ZCbNFD4G2gRMEY65WaA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6SGZxoKbXvUjMn1t6A98HqWISPnGNbYs0R/Rt2JarmXBSev+lva4QxUMWEBX9lX1Wo1XTJ78uk5xVDtG58SRZg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-nYkXY7tLBEgnGbYapDKAyKzgt44ZEyG+AKalvTXtCWKYgepI9dw327q+cVgedxm+Udi1ZzHKUyZrIusHi/KQbw=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-3PVLSTD9RR73rvVPt5G3T1gc+ycggWEGfTD7RvzzbtcDPD27NxgxBbAFfpm7DXJKW6VLHWE1lLMGvFt2Qxjcow=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-qhyZUMyCbWYFV2bAwRNVvfMVZ+hv7WYl6mossGrxC+uiQQXhvsuWWU8zz6jYX0mChZd9MgQZbm4vozTmG/5iGw=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-JOytptlsgM33B2MMFUg8iBrb4IKpbD5JnJrSeYiaFEeAj4vuXx0iQSQZ4qK7sqyMtfjZxxPdNdMZZVL4y/mFyA=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-oRRlrchG5EfrEL/EmtT1qUjSNHk3/5LGeZhQqADBBAJF1b1ET6964xEKe7aGlGARzDfza8H/seEsFJl7S6Ql9w=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-e49E6K9hzH/ohJNx8Y26mY8HaV4I4ZViIeoqhKsmoXLKHhQnMeBAVqCgsGf2Wa3lXlS7RkporDXMHHWkzvZzFw=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.11", "", { "os": "win32", "cpu": "x64" }, "sha512-QSQr/KjOgXA7OzXJUWS+oguKyAZ3Q0l/lnlDGbu397eKo83atuWUjBPJrsqbKNF6CARGw8XXJLGzpHC8Ryhd4Q=="],
"@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
"@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="],
"@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
"@codemirror/state": ["@codemirror/state@6.5.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw=="],
"@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="],
"@csstools/color-helpers": ["@csstools/color-helpers@6.1.1", "", {}, "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w=="],
"@csstools/css-calc": ["@csstools/css-calc@3.3.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ=="],
"@csstools/css-color-parser": ["@csstools/css-color-parser@4.2.1", "", { "dependencies": { "@csstools/color-helpers": "^6.1.1", "@csstools/css-calc": "^3.3.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-YpAJZhaHplYQkG8ib+/Fx5Y0eF2lVWi3tIvMJA6i39TLyUNp2439cifzW8VMjhlqrBjHzK5hVGugRRm2zTKI/A=="],
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.9", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A=="],
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
"@electron/asar": ["@electron/asar@4.3.0", "", { "dependencies": { "glob": "^13.0.2", "minimatch": "^10.0.1" }, "bin": { "asar": "bin/asar.mjs" } }, "sha512-k/FFC/NQoTykGBi/Ga4L4KorsAKDdkK0LfNVG90eUG6vPVpJaL3iR9V1ZLbzBAF3QpojK5DcaGOdQOheKZv4JQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
"@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="],
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
"@internationalized/date": ["@internationalized/date@3.12.3", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
"@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=="],
"@lucide/svelte": ["@lucide/svelte@1.37.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-AxaiSxeuErlt4fNmmN28oaL5R7yVoQLsfSeiAONxCEK+l0/SrLAjRktQ0DkTQpomv1oRzOA3hohWY0ZHTZCMmw=="],
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.4", "", {}, "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@oxc-project/types": ["@oxc-project/types@0.147.0", "", {}, "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg=="],
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="],
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="],
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="],
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="],
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="],
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="],
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="],
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="],
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="],
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="],
"@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="],
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.6", "", { "os": "android", "cpu": "arm" }, "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.6", "", { "os": "android", "cpu": "arm64" }, "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.6", "", { "os": "linux", "cpu": "arm" }, "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.6", "", { "os": "none", "cpu": "arm64" }, "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="],
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
"@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
"@tailwindcss/cli": ["@tailwindcss/cli@4.3.3", "", { "dependencies": { "@parcel/watcher": "2.5.1", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "enhanced-resolve": "^5.24.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.3.3" }, "bin": { "tailwindcss": "./dist/index.mjs" } }, "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw=="],
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
"@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="],
"@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="],
"@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="],
"@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="],
"@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="],
"@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="],
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
"bits-ui": ["bits-ui@2.19.0", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-dHDm5Jw2NZJgLRvHAwPeuR4BnZij+AqTIq33OqVfo+qiMXontoC/yIvGT5TZaviv7OuBdJ2QUy2JPQ2yKBvF6w=="],
"brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"builtin-modules": ["builtin-modules@5.3.0", "", {}, "sha512-hMQUl2bUFG339QygPM97E+mc8OY1IAchORZxm4a/frcYwKzozMzRVDBwHW0NjOqGElLm2O37AVQE8ikxlZHrMQ=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
"cli-truncate": ["cli-truncate@6.1.1", "", { "dependencies": { "slice-ansi": "^9.0.0", "string-width": "^8.2.0" } }, "sha512-06p9vyLahLa4zkGcgsGxU6iEkSOiuI4fhCH6Emhe2lPAcoUv73n72DnODsnHA+5wwXGnV0n9M9/qOQJSjYhFhw=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"crelt": ["crelt@1.0.7", "", {}, "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA=="],
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
"d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
"d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
"d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"devalue": ["devalue@5.9.2", "", {}, "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w=="],
"dompurify": ["dompurify@3.4.14", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg=="],
"empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="],
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
"entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
"es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
"esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
"esbuild-svelte": ["esbuild-svelte@0.9.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.19" }, "peerDependencies": { "esbuild": ">=0.17.0", "svelte": ">=4.2.1 <6" } }, "sha512-16FUSj64aiS28CCxYeK3hVxCyU4PwGBSkeArawAtnWSV7l0Gc+frIOP88MNj8Q7tDAGyEJAHT6OpOvM24BG46w=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"esrap": ["esrap@2.3.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"execa": ["execa@10.0.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "figures": "^6.1.0", "get-stream": "^9.0.1", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.3.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "which-command": "^0.1.0", "yoctocolors": "^2.1.2" } }, "sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw=="],
"expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
"fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-workspaces": ["find-workspaces@0.3.1", "", { "dependencies": { "fast-glob": "^3.3.2", "pkg-types": "^1.0.3", "yaml": "^2.3.4" } }, "sha512-UDkGILGJSA1LN5Aa7McxCid4sqW3/e+UYsVwyxki3dDT0F8+ym0rAfnCkEfkL0rO7M+8/mvkim4t/s3IPHmg+w=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"jsdom": ["jsdom@30.0.1", "", { "dependencies": { "@asamuzakjp/css-color": "^6.0.5", "@asamuzakjp/dom-selector": "^8.3.0", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.7", "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.2", "undici": "^8.9.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^17.1.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.2.3" }, "optionalPeers": ["canvas"] }, "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA=="],
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"log-update": ["log-update@8.0.0", "", { "dependencies": { "ansi-escapes": "^7.3.0", "cli-cursor": "^5.0.0", "slice-ansi": "^9.0.0", "string-width": "^8.2.0", "strip-ansi": "^7.2.0", "wrap-ansi": "^10.0.0" } }, "sha512-lddSgOt3bPASrylL54ZSpy8nBHns+vBVSoILlVOx+dei300pnLRN958rj/EdlVLKuWlSESU3qdnDZdAI7FXYGg=="],
"lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"magicast": ["magicast@0.5.4", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w=="],
"marked": ["marked@18.0.11", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw=="],
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
"moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
"nypm": ["nypm@0.6.9", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.2.4" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w=="],
"obsidian": ["obsidian@1.13.1", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-qtTEA2pmhJzhuhJqzbBFRYhpIOqvW+krDYjtFynv66KbxBbumHBlsJfWw3I4jtnK/6fZwbQhCrmmDdRwXmX56w=="],
"obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
"parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
"path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
"pretty-ms": ["pretty-ms@9.3.1", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rolldown": ["rolldown@1.2.6", "", { "dependencies": { "@oxc-project/types": "=0.147.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.6", "@rolldown/binding-android-arm64": "1.2.6", "@rolldown/binding-darwin-arm64": "1.2.6", "@rolldown/binding-darwin-x64": "1.2.6", "@rolldown/binding-freebsd-x64": "1.2.6", "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", "@rolldown/binding-linux-arm64-gnu": "1.2.6", "@rolldown/binding-linux-arm64-musl": "1.2.6", "@rolldown/binding-linux-ppc64-gnu": "1.2.6", "@rolldown/binding-linux-s390x-gnu": "1.2.6", "@rolldown/binding-linux-x64-gnu": "1.2.6", "@rolldown/binding-linux-x64-musl": "1.2.6", "@rolldown/binding-openharmony-arm64": "1.2.6", "@rolldown/binding-win32-arm64-msvc": "1.2.6", "@rolldown/binding-win32-x64-msvc": "1.2.6" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"slice-ansi": ["slice-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
"string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
"svelte": ["svelte@5.57.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg=="],
"svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
"svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="],
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
"tailwind-variants": ["tailwind-variants@3.3.1", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge", "tailwindcss"] }, "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw=="],
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="],
"tldts": ["tldts@7.4.11", "", { "dependencies": { "tldts-core": "^7.4.11" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw=="],
"tldts-core": ["tldts-core@7.4.11", "", {}, "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="],
"tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
"ultracite": ["ultracite@7.10.7", "", { "dependencies": { "@clack/prompts": "^1.5.1", "cli-truncate": "^6.1.1", "commander": "^15.0.0", "deepmerge": "^4.3.1", "empathic": "^2.0.1", "execa": "^10.0.1", "fast-glob": "^3.3.3", "find-workspaces": "^0.3.1", "jsonc-parser": "^3.3.1", "log-update": "^8.0.0", "magicast": "^0.5.4", "nypm": "^0.6.9", "resolve.exports": "^2.0.3", "string-width": "^8.2.2", "yaml": "^2.9.0", "zod": "^4.4.3" }, "peerDependencies": { "oxfmt": ">=0.1.0", "oxlint": "^1.79.0" }, "optionalPeers": ["oxfmt", "oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-rc+TG/zLCV+Uk1PL4XPmsq7CW8GtlaV4g7Q53Bt5o7UNGSKKNRoPY4syz1BDWwSoNBcvWKpSnfSVflsecok4EA=="],
"undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
"vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.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-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="],
"vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
"webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
"whatwg-url": ["whatwg-url@17.1.0", "", { "dependencies": { "@exodus/bytes": "^1.15.1", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw=="],
"which-command": ["which-command@0.1.0", "", { "bin": { "which-command": "cli.js" } }, "sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"wrap-ansi": ["wrap-ansi@10.0.1", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0" } }, "sha512-M0N4xzyzosiIok3svYlEo1sdLZts/8FPgYH/GPC3wvlmPoRvnoManGMrE54waYj3tISA8w6lsdesfVv67qSr8Q=="],
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"zod": ["zod@4.5.2", "", {}, "sha512-XkYXCol10+ba/6F/cueWV+TezUeOqXW0hdeJt5CdXjTYeAgAQg5N03RQdJ80mhfFE72+pblvYMW4wy2Qp4Qbrg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"data-urls/whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
"vite/lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
"vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
"vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
"vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
"vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
"vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
"vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
"vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
"vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
"vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
"vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+115 -34
View File
@@ -1,40 +1,121 @@
import esbuild from "esbuild"; import path from "node:path";
import process from "node:process"; import process from "node:process";
import { fileURLToPath } from "node:url";
import builtins from "builtin-modules"; import builtins from "builtin-modules";
import esbuild from "esbuild";
import sveltePlugin from "esbuild-svelte";
export const root = path.dirname(fileURLToPath(import.meta.url));
export const LIB = path.resolve(root, "../beaver-gateway/ui/src/lib");
const EXTERNAL = [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
];
const LIB_IMPORT = /^\$lib\//;
const APP_IMPORT = /^\$app\//;
const SONNER_IMPORT = /^svelte-sonner$/;
const OBSIDIAN_IMPORT = /^obsidian$/;
const BARE_IMPORT = /^[^./#]/;
function isExternal(specifier) {
return (
specifier.startsWith("node:") ||
EXTERNAL.some(
(name) => specifier === name || specifier.startsWith(`${name}/`)
)
);
}
// The panel is beaver-gateway's ``ui/src/lib``, compiled into this plugin.
// Two rules keep it one bundle: ``$lib/*`` points at that directory, and
// every bare import - svelte, bits-ui, the icons - resolves from this
// plugin's node_modules, never from the gateway's, so there is exactly one
// Svelte runtime. ``svelte-sonner`` becomes Obsidian notices; ``$app/*``
// is SvelteKit-only and refused.
export function sharedLib({ obsidian = "external" } = {}) {
return {
name: "beaver-shared-lib",
setup(build) {
build.onResolve({ filter: LIB_IMPORT }, (args) =>
build.resolve(`./${args.path.slice("$lib/".length)}`, {
kind: args.kind,
pluginData: { pinned: true },
resolveDir: LIB,
})
);
build.onResolve({ filter: APP_IMPORT }, (args) => ({
errors: [
{
text: `${args.path} is SvelteKit-only; the panel must not import it`,
},
],
}));
build.onResolve({ filter: SONNER_IMPORT }, () => ({
path: path.join(root, "src/shims/sonner.ts"),
}));
build.onResolve({ filter: OBSIDIAN_IMPORT }, () =>
obsidian === "external"
? { external: true, path: "obsidian" }
: { path: obsidian }
);
build.onResolve({ filter: BARE_IMPORT }, (args) => {
if (args.pluginData?.pinned) {
return;
}
if (isExternal(args.path)) {
return { external: true, path: args.path };
}
return build.resolve(args.path, {
kind: args.kind,
pluginData: { pinned: true },
resolveDir: root,
});
});
},
};
}
export const svelte = () =>
sveltePlugin({
compilerOptions: { css: "injected", runes: true },
filterWarnings: (warning) => !warning.filename?.includes("node_modules"),
});
const prod = process.argv[2] === "production"; const prod = process.argv[2] === "production";
const ctx = await esbuild.context({ if (process.argv[1] === fileURLToPath(import.meta.url)) {
entryPoints: ["src/main.ts"], const ctx = await esbuild.context({
bundle: true, bundle: true,
external: [ conditions: ["svelte", "browser"],
"obsidian", entryPoints: ["src/main.ts"],
"electron", format: "cjs",
"@codemirror/autocomplete", logLevel: "info",
"@codemirror/collab", mainFields: ["svelte", "browser", "module", "main"],
"@codemirror/commands", minify: prod,
"@codemirror/language", outfile: "main.js",
"@codemirror/lint", plugins: [sharedLib(), svelte()],
"@codemirror/search", sourcemap: prod ? false : "inline",
"@codemirror/state", target: "es2022",
"@codemirror/view", treeShaking: true,
"@lezer/common", });
"@lezer/highlight", if (prod) {
"@lezer/lr", await ctx.rebuild();
...builtins, await ctx.dispose();
], } else {
format: "cjs", await ctx.watch();
target: "es2022", }
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await ctx.rebuild();
await ctx.dispose();
} else {
await ctx.watch();
} }
+2 -2
View File
@@ -1,9 +1,9 @@
{ {
"id": "beaver", "id": "beaver",
"name": "Beaver", "name": "Beaver",
"version": "0.1.0", "version": "0.3.0",
"minAppVersion": "1.5.0", "minAppVersion": "1.5.0",
"description": "Send the current note to a Beaver agent via the markdown frontend.", "description": "Beaver's panel: conversations, live tool calls and branches next to your notes; sends notes to the markdown frontend.",
"author": "h", "author": "h",
"isDesktopOnly": false "isDesktopOnly": false
} }
-602
View File
@@ -1,602 +0,0 @@
{
"name": "beaver-plugin-obsidian",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beaver-plugin-obsidian",
"version": "0.1.0",
"devDependencies": {
"@types/node": "^20.11.0",
"builtin-modules": "^4.0.0",
"esbuild": "^0.21.5",
"obsidian": "^1.5.7",
"typescript": "^5.4.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.38.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@types/codemirror": {
"version": "5.60.8",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/tern": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/builtin-modules": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/obsidian": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/codemirror": "5.60.8",
"moment": "2.29.4"
},
"peerDependencies": {
"@codemirror/state": "6.5.0",
"@codemirror/view": "6.38.6"
}
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"dev": true,
"license": "MIT",
"peer": true
}
}
}
+36 -10
View File
@@ -1,18 +1,44 @@
{ {
"name": "beaver-plugin-obsidian", "name": "beaver-plugin-obsidian",
"version": "0.1.0", "version": "0.3.0",
"description": "Obsidian plugin for Beaver's markdown frontend.",
"main": "main.js",
"private": true, "private": true,
"description": "Beaver's panel and markdown frontend for Obsidian.",
"type": "module",
"main": "main.js",
"scripts": { "scripts": {
"dev": "node esbuild.config.mjs", "build": "bun run check && node scripts/css.mjs --minify && node esbuild.config.mjs production",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production" "check": "svelte-check --tsconfig ./tsconfig.json --threshold error",
"css": "node scripts/css.mjs",
"css:watch": "node scripts/css.mjs --watch",
"dev": "bun run css && node esbuild.config.mjs",
"fix": "ultracite fix",
"lint": "ultracite check",
"preview": "node preview/serve.mjs",
"test": "vitest run && node tests/smoke.mjs"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.11.0", "@biomejs/biome": "^2.5.11",
"builtin-modules": "^4.0.0", "@electron/asar": "^4.3.0",
"esbuild": "^0.21.5", "@lucide/svelte": "^1.37.0",
"obsidian": "^1.5.7", "@tailwindcss/cli": "^4.3.3",
"typescript": "^5.4.5" "@types/node": "^26.4.0",
"bits-ui": "^2.19.0",
"builtin-modules": "^5.3.0",
"clsx": "^2.1.1",
"d3-force": "^3.0.0",
"dompurify": "^3.4.14",
"esbuild": "^0.28.2",
"esbuild-svelte": "^0.9.5",
"jsdom": "^30.0.1",
"marked": "^18.0.11",
"obsidian": "^1.13.1",
"svelte": "^5.57.0",
"svelte-check": "^4.7.6",
"tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.3.1",
"tailwindcss": "^4.3.3",
"typescript": "^6",
"ultracite": "^7.10.7",
"vitest": "^4.1.11"
} }
} }
+140
View File
@@ -0,0 +1,140 @@
import { Component } from "obsidian";
import { mount } from "svelte";
import { PanelState } from "../src/state.svelte";
import App from "../src/ui/app.svelte";
import { BRANCH_BLASTER, DEEP_PANEL, FakeClient, MASTER } from "./fake-client";
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// bits-ui opens menus on pointerdown and tabs on pointer or click: send both.
function press(element: Element | null | undefined): void {
if (!element) {
return;
}
element.dispatchEvent(
new PointerEvent("pointerdown", {
bubbles: true,
button: 0,
cancelable: true,
pointerType: "mouse",
})
);
(element as HTMLElement).focus();
(element as HTMLElement).click();
}
// Scenes for screenshots: open the page with a hash to land in a state.
// Width comes from the window (``--window-size``), the state from here.
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one flat scene list
async function main(): Promise<void> {
const client = new FakeClient();
const state = new PanelState();
const scene = location.hash.slice(1) || "thread";
state.selected = MASTER;
if (scene === "empty") {
state.selected = null;
}
if (scene === "question" || scene === "toggle") {
state.selected = BRANCH_BLASTER;
}
if (scene === "deep") {
state.selected = DEEP_PANEL;
}
if (scene === "offline") {
state.selected = MASTER;
}
document.body.classList.toggle("theme-light", scene.endsWith("-light"));
document.body.classList.toggle("theme-dark", !scene.endsWith("-light"));
const app = {
workspace: {
openLinkText: (target: string) => console.log("open", target),
},
};
mount(App, {
props: {
app: app as never,
chatsFolder: () => "💬 чаты",
client: scene === "offline" ? null : client,
component: new Component() as never,
onOpenSettings: () => console.log("settings"),
state,
},
target: document.getElementById("app") as HTMLElement,
});
await wait(80);
if (scene.startsWith("switcher")) {
document
.querySelector<HTMLButtonElement>('[aria-label="Switch conversation"]')
?.click();
}
if (scene.startsWith("menu")) {
const row =
document.querySelector<HTMLElement>(`[data-row="${BRANCH_BLASTER}"]`) ??
(await (async () => {
document
.querySelector<HTMLButtonElement>(
'[aria-label="Switch conversation"]'
)
?.click();
await wait(60);
return document.querySelector<HTMLElement>(
`[data-row="${BRANCH_BLASTER}"]`
);
})());
row?.dispatchEvent(
new MouseEvent("contextmenu", {
bubbles: true,
clientX: row.getBoundingClientRect().left + 120,
clientY: row.getBoundingClientRect().top + 16,
})
);
}
if (scene.startsWith("toggle")) {
press(document.querySelector('[data-slot="switch"]'));
}
if (scene.startsWith("actions")) {
press(document.querySelector('[aria-label="Conversation actions"]'));
}
if (scene.startsWith("activity")) {
for (const trigger of document.querySelectorAll<HTMLButtonElement>(
".rule-word"
)) {
if (trigger.textContent?.trim() === "Activity") {
press(trigger);
}
}
}
if (scene.startsWith("context")) {
for (const trigger of document.querySelectorAll<HTMLButtonElement>(
".rule-word"
)) {
if (trigger.textContent?.trim() === "Context") {
press(trigger);
}
}
}
if (scene.startsWith("branch")) {
document
.querySelector<HTMLButtonElement>(
'[aria-label="Branch off this conversation"]'
)
?.click();
}
await wait(60);
if (scene.startsWith("stream")) {
// A tool lands while you watch: the tree grows without a reload.
client.emit({
conversation_id: MASTER,
input: { command: "curl -sI https://b.hhhhh.dev/healthz" },
name: "Bash",
parent_tool_use_id: null,
tool_use_id: "tu_live",
turn_id: "t-run",
type: "tool",
});
}
}
main().catch(console.error);
+611
View File
@@ -0,0 +1,611 @@
import { ApiClient, ApiError } from "$lib/api/client";
import type {
BusEvent,
ConversationInfo,
ConversationSummary,
HistoryMessage,
} from "$lib/api/types";
const MINUTE = 60_000;
const HOUR = 60 * MINUTE;
// Anchored inside the day rather than at the wall clock: the fixtures are
// spread over hours, and a run just after midnight would push half of them
// into yesterday and empty the "Today" group the rail is checked against.
const wall = new Date();
const now =
wall.getHours() < 12 ? new Date(wall).setHours(12, 0, 0, 0) : wall.getTime();
const ago = (ms: number) => new Date(now - ms).toISOString();
export const MASTER = "c1master0000";
export const BRANCH_UFW = "c2ufw0000000";
export const BRANCH_BLASTER = "c3blaster000";
export const DEEP_PANEL = "c4panel00000";
export const JOB_TRIAGE = "c5triage0000";
export const FORK_MERGE = "c6fork000000";
function summary(
over: Partial<ConversationSummary> & Pick<ConversationSummary, "id" | "kind">
): ConversationSummary {
return {
agent: "beaver-dispatcher",
context_tokens: 84_000,
created_at: ago(9 * HOUR),
flags: {},
last_activity_at: ago(4 * MINUTE),
last_item: null,
last_user_activity_at: ago(12 * MINUTE),
origin: "telegram",
parent_row: null,
pending_question: false,
running_turn: null,
session_id: "9c0c3fb2-2ac7-4d6b-8d4e-1a3f0c1a2b3c",
status: "open",
title: null,
...over,
};
}
const ROWS: ConversationSummary[] = [
summary({
id: MASTER,
kind: "master",
last_item: {
created_at: ago(4 * MINUTE),
id: 41,
origin: "telegram",
priority: "user",
status: "running",
text: "посмотри, что с ufw на dell - порт 62990 снаружи не отвечает",
},
running_turn: "t-run",
title: "Master · 29 Aug",
}),
summary({
id: BRANCH_UFW,
kind: "branch",
last_activity_at: ago(35 * MINUTE),
last_item: {
created_at: ago(35 * MINUTE),
id: 40,
origin: "telegram",
priority: "user",
status: "done",
text: "правило для 62990 добавлено, проверь снаружи",
},
parent: MASTER,
parent_row: 1,
title: "ufw на dell",
}),
summary({
id: BRANCH_BLASTER,
kind: "branch",
last_activity_at: ago(3 * HOUR),
parent: MASTER,
parent_row: 1,
pending_question: true,
title: "бластер - что заказать",
}),
summary({
agent: "beaver-opus-high",
id: DEEP_PANEL,
kind: "deep",
last_activity_at: ago(50 * MINUTE),
origin: "markdown",
title: "2026-08-29 - панель в Obsidian",
}),
summary({
agent: "beaver-triage",
id: JOB_TRIAGE,
kind: "job",
last_activity_at: ago(2 * HOUR),
origin: "cron",
status: "closed",
title: "триаж 14:00",
}),
summary({
id: FORK_MERGE,
kind: "fork",
last_activity_at: ago(6 * HOUR),
origin: "api",
status: "merged",
title: "слив: утренняя ветка",
}),
];
const INFO: Record<string, Partial<ConversationInfo>> = {
[MASTER]: {
bindings: [
{ external_id: "-1002233:general", frontend: "telegram", visible: true },
],
busy: true,
live: true,
queue: [
{
created_at: ago(2 * MINUTE),
id: 42,
origin: "watch",
priority: "normal",
status: "queued",
text: "vault: 📅 дни/2026-08-29.md +3 строки",
},
],
turn: {
id: "t-run",
origin: "user",
started_at: ago(48_000),
text: "посмотри, что с ufw на dell - порт 62990 снаружи не отвечает",
tools: [
{
content: null,
ended_at: null,
input: {
description: "проверить ufw на dell",
prompt:
"ssh dell 'sudo ufw status numbered' и сравнить с site.caddy",
},
is_error: null,
name: "Task",
parent_tool_use_id: null,
started_at: ago(40_000),
tool_use_id: "tu_task",
},
{
content:
"Status: active\n[ 1] 22/tcp ALLOW IN Anywhere\n[ 2] 443/tcp ALLOW IN Anywhere",
ended_at: ago(30_000),
input: { command: "ssh dell 'sudo ufw status numbered'" },
is_error: false,
name: "Bash",
parent_tool_use_id: "tu_task",
started_at: ago(38_000),
tool_use_id: "tu_bash",
},
{
content: null,
ended_at: null,
input: { file_path: "/vault/мета/бобер/скиллы/ops/komodo/SKILL.md" },
is_error: null,
name: "Read",
parent_tool_use_id: "tu_task",
started_at: ago(8000),
tool_use_id: "tu_read",
},
],
},
},
[BRANCH_UFW]: {
bindings: [
{ external_id: "-1002233:517", frontend: "telegram", visible: true },
],
parent: MASTER,
},
[BRANCH_BLASTER]: {
bindings: [
{ external_id: "-1002233:498", frontend: "telegram", visible: false },
],
parent: MASTER,
question: {
id: "q1",
questions: [
{
options: [
{ description: "Ozon, 3 дня", label: "Xiaomi Mi Blaster" },
{ description: "AliExpress, 2 недели", label: "Broadlink RM4" },
],
question: "Какой ИК-бластер заказать?",
},
],
},
},
[DEEP_PANEL]: {
bindings: [
{
external_id: "2026-08-29 - панель в Obsidian.md",
frontend: "markdown",
visible: true,
},
],
flags: { memory: true },
},
[JOB_TRIAGE]: { flags: { critical: false } },
[FORK_MERGE]: { parent: MASTER },
};
// Ten minutes apart, ending now: enough to show a time on every message.
const STEP_MS = 600_000;
function stamped(messages: HistoryMessage[]): HistoryMessage[] {
const end = Date.now();
return messages.map((message, index) => ({
...message,
ts: new Date(end - (messages.length - index) * STEP_MS).toISOString(),
}));
}
const HISTORY: Record<string, HistoryMessage[]> = {
[MASTER]: [
{ content: "с утра: дневник приехал? что в хендауте", role: "user" },
{
content:
"Приехал. В [[2026-08-28|хендауте]] два хвоста: **бластер** ждёт выбора модели, и **ufw на dell** - порт 62990 снаружи молчит. Ветка по ufw открыта, см. [[ufw на dell]].",
role: "assistant",
},
{ content: "ок, гони ufw", role: "user" },
{
content: [
{
text: "Смотрю правила через `ssh dell` и сверяю с `site.caddy`.",
type: "text",
},
{
id: "tu_prev",
input: { command: "cat /root/beaver-agent/caddy/site.caddy" },
name: "Bash",
type: "tool_use",
},
],
role: "assistant",
},
{
content: [
{
content: "beaver.hhhhh.dev {\n reverse_proxy localhost:62990\n}",
tool_use_id: "tu_prev",
type: "tool_result",
},
],
role: "user",
},
{
content:
"Caddy проксирует на `localhost:62990`, значит снаружи должен отвечать 443, а не 62990 - проверю, открыт ли он вообще.",
role: "assistant",
},
],
[BRANCH_UFW]: [
{ content: "правило для 62990 добавлено, проверь снаружи", role: "user" },
{
content:
"Снаружи `curl -sI https://b.hhhhh.dev/healthz` → 200. Порт 62990 наружу не нужен: его слушает только Caddy. Предлагаю правило убрать и слить ветку.",
role: "assistant",
},
],
[BRANCH_BLASTER]: [
{ content: "нужен ИК-бластер для кондиционера, выбери", role: "user" },
{ content: "Два варианта, спрошу кнопками.", role: "assistant" },
],
[DEEP_PANEL]: [
{
content: "как панель в Obsidian должна вести себя в узкой колонке?",
role: "user",
},
{
content:
"Тред всегда на виду; переключатель разговоров - в шапке. Список как отдельный экран - только на телефоне.\n\n- узкая колонка: шапка + тред + композер\n- вкладка: рейл слева\n\nСм. [[архитектура#2. Окна]].",
role: "assistant",
},
],
};
interface Route {
handle: (match: RegExpMatchArray, body: unknown) => unknown;
method: string;
pattern: RegExp;
}
// The gateway as the panel sees it, in memory: enough of ``/api`` for the
// preview scenes and the smoke test, with a scripted event stream.
export class FakeClient extends ApiClient {
rows = ROWS.map((row) => ({ ...row }));
readonly sent: { id: string; text: string; origin: string }[] = [];
private readonly listeners = new Set<(event: BusEvent) => void>();
private seq = 1;
constructor() {
super("http://fake", "token");
}
private readonly routes: Route[] = [
{
handle: () => ({ agents: [], frontends: [], mcps: [] }),
method: "GET",
pattern: /^\/api\/agents$/,
},
{
handle: () => ({ conversations: [], files: [], query: "" }),
method: "GET",
pattern: /^\/api\/search/,
},
{
handle: () => ({ edges: [], nodes: [], root: "" }),
method: "GET",
pattern: /^\/api\/vault\/graph/,
},
{
handle: ([, id]) => ({
agent: {
effort: "high",
model: "claude-opus-5",
name: "beaver-dispatcher",
},
context_tokens: 84_000,
files: [
{
last_at: ago(MINUTE),
other: 0,
path: "📅 дни/2026-08-29.md",
reads: 1,
writes: 0,
},
{
last_at: ago(5 * MINUTE),
other: 0,
path: "👤 люди/Марта Ветрова.md",
reads: 2,
writes: 1,
},
],
history_tokens_est: 60_000,
id,
kind: "master",
prompt: {
bytes: 24_000,
granules: [
{
bytes: 9000,
path: "мета/бобер/промпты/гранулы/голос/role.md",
tag: "role",
tokens_est: 2800,
},
{
bytes: 15_000,
path: "мета/бобер/промпты/гранулы/диспетчер.md",
tag: "dispatcher",
tokens_est: 4700,
},
],
tokens_est: 7500,
},
skills: [
{
path: "мета/бобер/скиллы/общие",
set: "общие",
skills: [
{
description: "серверы и стеки",
name: "ops",
path: "мета/бобер/скиллы/общие/ops",
},
],
},
],
skills_tokens_est: 20,
tool_counts: { Bash: 1, Read: 3 },
tools: {
allowed: null,
disallowed: [],
gateway: ["say", "spawn"],
mcps: ["komodo"],
},
turns: 4,
}),
method: "GET",
pattern: /^\/api\/conversations\/([^/]+)\/context/,
},
{
handle: () => ({ conversations: this.rows }),
method: "GET",
pattern: /^\/api\/conversations$/,
},
{
handle: ([, id]) => this.info(id),
method: "GET",
pattern: /^\/api\/conversations\/([^/]+)$/,
},
{
handle: ([, id]) => ({ id, messages: stamped(HISTORY[id] ?? []) }),
method: "GET",
pattern: /^\/api\/conversations\/([^/]+)\/history$/,
},
{
handle: ([, id]) => ({
entries: [],
id,
offset: 0,
subpath: "",
subpaths: [],
total: 0,
}),
method: "GET",
pattern: /^\/api\/conversations\/([^/]+)\/entries$/,
},
{
handle: ([, id], body) => {
const { text, origin } = body as { text: string; origin: string };
this.sent.push({ id, origin, text });
return { id, item: this.sent.length, status: "queued" };
},
method: "POST",
pattern: /^\/api\/conversations\/([^/]+)\/messages$/,
},
{
handle: ([, id], body) => {
const { text, origin } = body as { text: string; origin: string };
this.sent.push({ id, origin: `inject:${origin}`, text });
return { id, item: this.sent.length, priority: "normal" };
},
method: "POST",
pattern: /^\/api\/conversations\/([^/]+)\/inject$/,
},
{
handle: ([, id], body) => {
const child = summary({
id: `child${this.rows.length}`,
kind: "branch",
parent_row: 1,
title: (body as { title?: string }).title ?? "новая ветка",
});
this.rows.unshift(child);
return this.info(child.id, id);
},
method: "POST",
pattern: /^\/api\/conversations\/([^/]+)\/branch$/,
},
{
handle: ([, id], body) => {
const {
frontend,
external_id: external,
visible,
} = body as {
frontend: string;
external_id?: string;
visible: boolean;
};
if (!INFO[id]) {
INFO[id] = {};
}
const info = INFO[id];
const bindings = (info.bindings ?? []).filter(
(b) => b.frontend !== frontend
);
bindings.push({
external_id: external ?? `${frontend}:new`,
frontend,
visible,
});
info.bindings = bindings;
return this.info(id);
},
method: "POST",
pattern: /^\/api\/conversations\/([^/]+)\/bind$/,
},
{
handle: ([, id], body) => {
const row = this.row(id);
row.flags = { ...row.flags, ...(body as Record<string, unknown>) };
return row;
},
method: "PATCH",
pattern: /^\/api\/conversations\/([^/]+)\/flags$/,
},
{
handle: ([, id], body) => {
const row = this.row(id);
const patch = body as {
status?: ConversationSummary["status"];
title?: string;
};
if (patch.status) {
row.status = patch.status;
}
if (typeof patch.title === "string") {
row.title = patch.title;
}
return row;
},
method: "PATCH",
pattern: /^\/api\/conversations\/([^/]+)$/,
},
{
handle: ([, id]) => {
this.row(id).status = "merged";
return { fork: "f", id, status: "merged", text: "слито" };
},
method: "POST",
pattern: /^\/api\/conversations\/([^/]+)\/merge$/,
},
{
handle: ([, id]) => {
this.row(id).status = "closed";
return {
digest: "мета/бобер/выжимки/2026-08-29 - панель.md",
id,
status: "closed",
};
},
method: "POST",
pattern: /^\/api\/conversations\/([^/]+)\/close$/,
},
{
handle: ([, id], body) => ({
id,
question_id: (body as { question_id: string }).question_id,
}),
method: "POST",
pattern: /^\/api\/conversations\/([^/]+)\/answer$/,
},
];
private row(id: string): ConversationSummary {
const row = this.rows.find((r) => r.id === id);
if (!row) {
throw new ApiError(404, `unknown conversation ${id}`, null);
}
return row;
}
private info(id: string, parent?: string): ConversationInfo {
const row = this.row(id);
const extra = INFO[id] ?? {};
return {
...row,
bindings: [],
busy: false,
live: Boolean(row.session_id),
parent: parent ?? null,
question: null,
queue: [],
turn: null,
...extra,
};
}
private route(method: string, path: string, body?: unknown): unknown {
for (const route of this.routes) {
const match = route.method === method ? path.match(route.pattern) : null;
if (match) {
return route.handle(match, body);
}
}
throw new ApiError(404, `no route for ${method} ${path}`, null);
}
override get<T>(path: string): Promise<T> {
return Promise.resolve(this.route("GET", path) as T);
}
override post<T>(path: string, body?: unknown): Promise<T> {
return Promise.resolve(this.route("POST", path, body ?? {}) as T);
}
override patch<T>(path: string, body?: unknown): Promise<T> {
return Promise.resolve(this.route("PATCH", path, body ?? {}) as T);
}
override stream(
_path: string,
onEvent: (event: BusEvent) => void,
signal: AbortSignal
): Promise<void> {
return new Promise((resolve) => {
onEvent({ seq: 0, ts: new Date().toISOString(), type: "hello" });
this.listeners.add(onEvent);
signal.addEventListener("abort", () => {
this.listeners.delete(onEvent);
resolve();
});
});
}
emit(event: Omit<BusEvent, "seq" | "ts"> & { ts?: string }): void {
this.seq += 1;
const full = {
seq: this.seq,
ts: new Date().toISOString(),
...event,
} as BusEvent;
for (const listener of this.listeners) {
listener(full);
}
}
}
+48
View File
@@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>beaver panel preview</title>
<link href="/app.css" rel="stylesheet">
<link href="/theme.css" rel="stylesheet">
<link href="/styles.css" rel="stylesheet">
<style>
body.theme-dark,
body.theme-light {
--accent-h: 254;
--accent-s: 80%;
--accent-l: 68%;
--font-interface:
-apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto,
sans-serif;
}
body {
height: 100vh;
margin: 0;
overflow: hidden;
background: var(--background-primary);
}
#app {
height: 100vh;
}
</style>
</head>
<body class="theme-dark">
<!--
impeccable direction contract (mode operate, brief-pinned world: beaver-calendar's Obsidian skin)
THESIS: the agent's terminal, not a chat app; the thread is always on screen, picking a
conversation is a header switch. OWN-WORLD: Obsidian's own variables through .beaver-root;
hairline rows, tabular numerals, uppercase 12px section labels; nothing pink.
STORY: follow the note, watch tools stream, answer, branch with a seed, send back to Telegram.
FIRST VIEWPORT: switcher over the thread header, tabs, live thread, composer pinned below;
wide: the grouped rail on the left. FORM: two-pane operator console, pinned by the brief.
FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review,
the verdict, and DESIGN.md
-->
<div id="app"></div>
<script defer src="/preview/bundle.js"></script>
</body>
</html>
+126
View File
@@ -0,0 +1,126 @@
import {
createReadStream,
existsSync,
mkdtempSync,
readdirSync,
statSync,
writeFileSync,
} from "node:fs";
import { createServer } from "node:http";
import { homedir, tmpdir } from "node:os";
import { extname, join, normalize } from "node:path";
import { extractFile } from "@electron/asar";
import esbuild from "esbuild";
import { root, sharedLib, svelte } from "../esbuild.config.mjs";
const port = Number(process.env.PORT ?? 4174);
const ASAR_CANDIDATES = [
process.env.OBSIDIAN_ASAR,
"/Applications/Obsidian.app/Contents/Resources/obsidian.asar",
join(homedir(), "Applications/Obsidian.app/Contents/Resources/obsidian.asar"),
"/opt/Obsidian/resources/obsidian.asar",
"/usr/lib/obsidian/resources/obsidian.asar",
].filter(Boolean);
function findAppCss() {
for (const candidate of ASAR_CANDIDATES) {
if (!existsSync(candidate)) {
continue;
}
try {
const path = join(mkdtempSync(join(tmpdir(), "beaver-app-")), "app.css");
writeFileSync(path, extractFile(candidate, "app.css"));
return path;
} catch {
/* try the next candidate */
}
}
console.warn(
"preview: could not read app.css from an Obsidian install. Set OBSIDIAN_ASAR to point at obsidian.asar; without it the chrome will not match the real app."
);
return null;
}
function findTheme() {
if (process.env.THEME) {
return process.env.THEME;
}
const vault = process.env.VAULT;
if (!vault) {
return null;
}
const themes = join(vault, ".obsidian/themes");
if (!existsSync(themes)) {
return null;
}
for (const name of readdirSync(themes)) {
const css = join(themes, name, "theme.css");
if (existsSync(css)) {
return css;
}
}
return null;
}
const appCss = findAppCss();
const theme = findTheme();
const ctx = await esbuild.context({
bundle: true,
conditions: ["svelte", "browser"],
entryPoints: [join(root, "preview/entry.ts")],
format: "iife",
logLevel: "info",
mainFields: ["svelte", "browser", "module", "main"],
outfile: join(root, "preview/bundle.js"),
plugins: [
sharedLib({ obsidian: join(root, "tests/obsidian-stub.ts") }),
svelte(),
],
sourcemap: "inline",
target: "es2022",
});
await ctx.rebuild();
await ctx.watch();
const TYPES = {
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
};
createServer((request, response) => {
const url = decodeURIComponent((request.url ?? "/").split("?")[0]);
const external = { "/app.css": appCss, "/theme.css": theme };
if (url in external) {
const file = external[url];
if (!file) {
response.writeHead(200, { "content-type": TYPES[".css"] }).end("");
return;
}
response.writeHead(200, {
"cache-control": "no-store",
"content-type": TYPES[".css"],
});
createReadStream(file).pipe(response);
return;
}
const file = join(root, normalize(url === "/" ? "/preview/index.html" : url));
if (
!(file.startsWith(root) && existsSync(file)) ||
statSync(file).isDirectory()
) {
response.writeHead(404).end("not found");
return;
}
response.writeHead(200, {
"cache-control": "no-store",
"content-type": TYPES[extname(file)] ?? "application/octet-stream",
});
createReadStream(file).pipe(response);
}).listen(port, () => {
console.log(`preview: http://localhost:${port}/`);
});
+81
View File
@@ -0,0 +1,81 @@
// bun preview/shot.ts <out.png> <url> [width] [height]
// Headless Chrome over CDP: the preview server must be running.
const [out, url, w = "340", h = "720"] = process.argv.slice(2);
const port = 9334;
const proc = Bun.spawn(
[
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"--headless=new",
"--disable-gpu",
"--hide-scrollbars",
`--remote-debugging-port=${port}`,
`--window-size=${w},${h}`,
"--user-data-dir=/tmp/beaver-preview-chrome",
"about:blank",
],
{ stderr: "ignore", stdout: "ignore" }
);
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
let targets: { type: string; webSocketDebuggerUrl: string }[] = [];
for (let i = 0; i < 40; i += 1) {
try {
// biome-ignore lint/performance/noAwaitInLoops: polling until Chrome is up
targets = await (await fetch(`http://127.0.0.1:${port}/json`)).json();
if (targets.length) {
break;
}
} catch {
/* not up yet */
}
await wait(250);
}
const page = targets.find((t) => t.type === "page");
if (!page) {
proc.kill();
throw new Error("no page target");
}
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((resolve) => {
ws.onopen = resolve;
});
let id = 0;
const pending = new Map<number, (v: { result?: { data?: string } }) => void>();
ws.onmessage = (m) => {
const d = JSON.parse(String(m.data));
if (d.id && pending.has(d.id)) {
pending.get(d.id)?.(d);
pending.delete(d.id);
}
};
const send = (method: string, params: Record<string, unknown> = {}) =>
new Promise<{ result?: { data?: string } }>((resolve) => {
id += 1;
pending.set(id, resolve);
ws.send(JSON.stringify({ id, method, params }));
});
await send("Emulation.setDeviceMetricsOverride", {
deviceScaleFactor: 2,
height: Number(h),
mobile: Number(w) < 500,
width: Number(w),
});
await send("Page.enable");
await send("Page.navigate", { url });
await wait(2500);
if (process.env.EVAL) {
const evaluated = await send("Runtime.evaluate", {
expression: process.env.EVAL,
returnByValue: true,
});
console.log(
JSON.stringify(
(evaluated.result as { result?: { value?: unknown } }).result?.value
)
);
await wait(400);
}
const shot = await send("Page.captureScreenshot", { format: "png" });
await Bun.write(out, Buffer.from(shot.result?.data ?? "", "base64"));
console.log(`wrote ${out}`);
ws.close();
proc.kill();
+54
View File
@@ -0,0 +1,54 @@
// node scripts/css.mjs [--minify] [--watch]
//
// Tailwind writes to a scratch file; the scoped, optionally minified result
// is what lands in styles.css. See scope-css.mjs for why the extra step.
import { spawn, spawnSync } from "node:child_process";
import { readFileSync, watch, writeFileSync } from "node:fs";
import { join } from "node:path";
import { scopeCss } from "./scope-css.mjs";
const root = new URL("..", import.meta.url).pathname;
const args = new Set(process.argv.slice(2));
const minify = args.has("--minify");
const watching = args.has("--watch");
const input = join(root, "src/tailwind.css");
const scratch = join(root, ".tailwind.css");
const output = join(root, "styles.css");
const cli = join(root, "node_modules/.bin/tailwindcss");
function finish() {
const built = readFileSync(scratch, "utf8");
writeFileSync(output, scopeCss(built, { minify }));
console.log(`css: wrote styles.css (${minify ? "minified, " : ""}scoped)`);
}
if (watching) {
writeFileSync(scratch, "");
const child = spawn(cli, ["-i", input, "-o", scratch, "--watch"], {
stdio: "inherit",
});
let timer = null;
watch(scratch, () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
timer = null;
try {
finish();
} catch (error) {
console.error("css:", error);
}
}, 60);
});
child.on("exit", (code) => process.exit(code ?? 0));
} else {
const run = spawnSync(cli, ["-i", input, "-o", scratch], {
stdio: "inherit",
});
if (run.status !== 0) {
process.exit(run.status ?? 1);
}
finish();
}
+95
View File
@@ -0,0 +1,95 @@
import { transform } from "lightningcss";
/**
* Obsidian loads every plugin's stylesheet into one document, and Tailwind
* puts its utilities into a cascade layer called `utilities` in each of them.
* Layers with the same name merge, so another Tailwind plugin's
* `.hidden{display:none!important}` and this plugin's
* `@container … .\@min-\[30rem\]\:inline-block{…!important}` end up in the
* same layer with the same specificity, and whichever plugin loaded last
* wins. Prefixing every selector with `:is(.beaver-root, .beaver-root *)` adds a
* class of specificity that no unscoped utility can match, whatever the load
* order, while still reaching the view root itself and the portal layer,
* both of which carry `.beaver-root`.
*
* Tailwind's own theme variables stay on `:root, :host`: moved onto the view
* root they would sit above Obsidian's `body` values, and `--color-accent`
* is a name both sides use — the accent would turn into the hover grey.
*/
const ROOT = "beaver-root";
const SCOPE = {
kind: "is",
selectors: [
[{ name: ROOT, type: "class" }],
[
{ name: ROOT, type: "class" },
{ type: "combinator", value: "descendant" },
{ type: "universal" },
],
],
type: "pseudo-class",
};
function isPseudo(selector, kind) {
return (
selector.length === 1 &&
selector[0].type === "pseudo-class" &&
selector[0].kind === kind
);
}
function mentions(selector, name) {
for (const part of selector) {
if (part.type === "class" && part.name === name) {
return true;
}
if (part.selectors?.some((inner) => mentions(inner, name))) {
return true;
}
}
return false;
}
function scopeSelector(selector) {
if (isPseudo(selector, "root") || isPseudo(selector, "host")) {
return selector;
}
if (mentions(selector, ROOT)) {
return selector;
}
// The scope goes on the subject — the last compound — not the first:
// `.theme-dark .hue-chip` must keep matching with `.theme-dark` on body,
// outside the root.
let at = 0;
for (let i = 0; i < selector.length; i += 1) {
if (selector[i].type === "combinator") {
at = i + 1;
}
}
const head = selector.slice(0, at);
const subject = selector.slice(at);
// `*` and type selectors must open a compound; :is() goes in their place
// or right after them.
if (subject[0]?.type === "universal") {
return [...head, SCOPE, ...subject.slice(1)];
}
if (subject[0]?.type === "type") {
return [...head, subject[0], SCOPE, ...subject.slice(1)];
}
return [...head, SCOPE, ...subject];
}
/** Rewrites a built stylesheet so its rules only reach the plugin's own DOM. */
export function scopeCss(code, { minify = false } = {}) {
// The selector visitor, not the rule one: handing a whole rule back makes
// lightningcss re-read its declarations, and some of Tailwind's it cannot.
const result = transform({
code: Buffer.from(code),
filename: "styles.css",
minify,
visitor: { Selector: scopeSelector },
});
return result.code.toString();
}
+4 -2
View File
@@ -1,10 +1,12 @@
import { App, FuzzySuggestModal } from "obsidian"; import { type App, FuzzySuggestModal } from "obsidian";
export function pickAgent(app: App, agents: string[]): Promise<string | null> { export function pickAgent(app: App, agents: string[]): Promise<string | null> {
return new Promise((resolve) => { return new Promise((resolve) => {
let resolved = false; let resolved = false;
const settle = (value: string | null) => { const settle = (value: string | null) => {
if (resolved) return; if (resolved) {
return;
}
resolved = true; resolved = true;
resolve(value); resolve(value);
}; };
+141 -140
View File
@@ -1,18 +1,20 @@
import { requestUrl, RequestUrlParam } from "obsidian"; import type { RequestUrlParam } from "obsidian";
import { requestUrl } from "obsidian";
import type { BeaverSettings } from "./settings"; import type { BeaverSettings } from "./settings";
import { readSse } from "./sse";
export interface ChatRequest { export interface ChatRequest {
filename: string;
content: string;
agent?: string; agent?: string;
content: string;
filename: string;
} }
export interface ChatResponse { export interface ChatResponse {
status: "ok" | "nothing_to_do" | "in_progress";
reason?: string;
agent?: string; agent?: string;
turns_appended?: number;
new_content?: string; new_content?: string;
reason?: string;
status: "ok" | "nothing_to_do" | "in_progress";
turns_appended?: number;
} }
export class BeaverApiError extends Error { export class BeaverApiError extends Error {
@@ -25,53 +27,106 @@ export class BeaverApiError extends Error {
} }
} }
function baseUrl(settings: BeaverSettings): string { // Two hosts, one token. ``mdBase`` is the markdown frontend (``/chat``,
const url = settings.baseUrl.trim().replace(/\/+$/, ""); // ``/agents``); ``apiBase`` is the origin under which the conversations
if (!url) throw new Error("Beaver: base URL is not configured"); // API lives (``<apiBase>/api/conversations/…``). Behind the gateway's
// Caddy the markdown root is ``https://host/md`` and the API root is the
// bare ``https://host``, so when the API origin isn't set explicitly we
// derive it: strip a trailing ``/md``, or bump the default local port.
const TRAILING_SLASHES = /\/+$/;
const LOCAL_MD_PORT = /:62993$/;
export function mdBase(settings: BeaverSettings): string {
const url = settings.baseUrl.trim().replace(TRAILING_SLASHES, "");
if (!url) {
throw new Error("Beaver: base URL is not configured");
}
return url; return url;
} }
export function apiBase(settings: BeaverSettings): string {
const explicit = settings.apiBaseUrl.trim().replace(TRAILING_SLASHES, "");
if (explicit) {
return explicit;
}
const md = mdBase(settings);
if (md.endsWith("/md")) {
return md.slice(0, -"/md".length);
}
if (LOCAL_MD_PORT.test(md)) {
return md.replace(LOCAL_MD_PORT, ":62994");
}
throw new Error(
`Beaver: API origin is not configured and can't be derived from ${md}`
);
}
function authHeader(settings: BeaverSettings): Record<string, string> { function authHeader(settings: BeaverSettings): Record<string, string> {
const token = settings.token.trim(); const token = settings.token.trim();
if (!token) throw new Error("Beaver: bearer token is not configured"); if (!token) {
throw new Error("Beaver: bearer token is not configured");
}
return { Authorization: `Bearer ${token}` }; return { Authorization: `Bearer ${token}` };
} }
// FastAPI's default error body is ``{detail: …}``; the API frontend
// rewrites it to ``{error: …}``. Accept both, fall back to the raw text.
function errorMessage(status: number, body: unknown): string {
if (body && typeof body === "object") {
const obj = body as Record<string, unknown>;
for (const key of ["detail", "error"]) {
if (typeof obj[key] === "string") {
return obj[key] as string;
}
}
}
if (typeof body === "string" && body) {
return body;
}
return `HTTP ${status}`;
}
async function call( async function call(
settings: BeaverSettings, settings: BeaverSettings,
init: Omit<RequestUrlParam, "url"> & { path: string }, init: Omit<RequestUrlParam, "url"> & { url: string }
): Promise<unknown> { ): Promise<unknown> {
const { path, ...rest } = init;
const url = `${baseUrl(settings)}${path}`;
const headers = { const headers = {
...authHeader(settings), ...authHeader(settings),
...(rest.headers ?? {}), ...(init.headers ?? {}),
}; };
// throw=false → we handle non-2xx ourselves so we can extract FastAPI's // throw=false → we handle non-2xx ourselves so we can extract the
// {detail: "..."} body and surface a useful message. // error body and surface a useful message.
const res = await requestUrl({ url, throw: false, ...rest, headers }); const res = await requestUrl({ throw: false, ...init, headers });
if (res.status < 200 || res.status >= 300) { if (res.status < 200 || res.status >= 300) {
let detail: unknown; let body: unknown;
try { try {
detail = res.json; body = res.json;
} catch { } catch {
detail = res.text; body = res.text;
} }
const detailMsg = throw new BeaverApiError(res.status, errorMessage(res.status, body), body);
detail && typeof detail === "object" && "detail" in detail
? String((detail as { detail: unknown }).detail)
: typeof detail === "string"
? detail
: `HTTP ${res.status}`;
throw new BeaverApiError(res.status, detailMsg, detail);
} }
return res.json; return res.json;
} }
async function errorFromResponse(res: Response): Promise<BeaverApiError> {
// Drain the body so we can surface a useful detail. 409 in particular
// returns JSON; the rest may be JSON or plain text.
const text = await res.text().catch(() => "");
let body: unknown = text;
try {
body = JSON.parse(text);
} catch {
// not JSON; leave body as the raw text
}
return new BeaverApiError(res.status, errorMessage(res.status, body), body);
}
export async function listAgents(settings: BeaverSettings): Promise<string[]> { export async function listAgents(settings: BeaverSettings): Promise<string[]> {
const body = (await call(settings, { path: "/agents", method: "GET" })) as { const body = (await call(settings, {
agents?: Array<{ name?: unknown }>; method: "GET",
}; url: `${mdBase(settings)}/agents`,
})) as { agents?: Array<{ name?: unknown }> };
const list = body.agents ?? []; const list = body.agents ?? [];
return list return list
.map((a) => (typeof a?.name === "string" ? a.name : null)) .map((a) => (typeof a?.name === "string" ? a.name : null))
@@ -80,13 +135,13 @@ export async function listAgents(settings: BeaverSettings): Promise<string[]> {
export async function sendChat( export async function sendChat(
settings: BeaverSettings, settings: BeaverSettings,
req: ChatRequest, req: ChatRequest
): Promise<ChatResponse> { ): Promise<ChatResponse> {
return (await call(settings, { return (await call(settings, {
path: "/chat",
method: "POST",
contentType: "application/json",
body: JSON.stringify(req), body: JSON.stringify(req),
contentType: "application/json",
method: "POST",
url: `${mdBase(settings)}/chat`,
})) as ChatResponse; })) as ChatResponse;
} }
@@ -96,128 +151,74 @@ export interface StreamChatCallbacks {
// responsible for splicing it into the editor — we don't ship the // responsible for splicing it into the editor — we don't ship the
// diff because the gateway already renders the canonical view and we // diff because the gateway already renders the canonical view and we
// don't want two slightly-different renderers to drift. // don't want two slightly-different renderers to drift.
onDelta(newContent: string): void; onDelta: (newContent: string) => void;
// Fires exactly once at end-of-turn (success or nothing_to_do). // Fires exactly once at end-of-turn (success or nothing_to_do).
onDone(response: ChatResponse): void; onDone: (response: ChatResponse) => void;
} }
// Server-Sent Events arrive as ``event: <name>\ndata: <json>\n\n`` // CORS is allowed by the gateway's ``CORSMiddleware``; auth is the same
// frames. We can't use ``requestUrl`` (it buffers the whole body), so // bearer token as the other endpoints.
// SSE is the one place in the plugin that goes through native
// ``fetch``. CORS is allowed by the gateway's ``CORSMiddleware``; auth
// is the same bearer token as the other endpoints.
export async function sendChatStream( export async function sendChatStream(
settings: BeaverSettings, settings: BeaverSettings,
req: ChatRequest, req: ChatRequest,
cb: StreamChatCallbacks, cb: StreamChatCallbacks,
signal?: AbortSignal, signal?: AbortSignal
): Promise<void> { ): Promise<void> {
const url = `${baseUrl(settings)}/chat/stream`; const res = await fetch(`${mdBase(settings)}/chat/stream`, {
const headers: Record<string, string> = {
...authHeader(settings),
"Content-Type": "application/json",
Accept: "text/event-stream",
};
const res = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(req), body: JSON.stringify(req),
headers: {
...authHeader(settings),
Accept: "text/event-stream",
"Content-Type": "application/json",
},
method: "POST",
signal, signal,
}); });
if (!res.ok || !res.body) { if (!(res.ok && res.body)) {
// Drain the body so we can surface a useful detail. 409 in throw await errorFromResponse(res);
// particular returns JSON; the rest may be JSON or plain text. }
const text = await res.text().catch(() => "");
let detail: unknown = text; await readSse(res.body, (frame) => {
let data: unknown;
try { try {
const parsed = JSON.parse(text) as { detail?: unknown }; data = JSON.parse(frame.data);
detail =
parsed && typeof parsed === "object" && "detail" in parsed
? parsed.detail
: parsed;
} catch { } catch {
// not JSON; leave detail as the raw text return "continue";
} }
const msg = const obj = data as Record<string, unknown>;
typeof detail === "string" && detail if (frame.event === "delta") {
? detail const content = obj.new_content;
: `HTTP ${res.status}`; if (typeof content === "string") {
throw new BeaverApiError(res.status, msg, detail); cb.onDelta(content);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
// SSE frames are separated by a blank line (``\n\n``). We buffer
// partial frames across reads, then flush full ones in order.
let buf = "";
let done = false;
while (!done) {
const chunk = await reader.read();
done = chunk.done;
if (chunk.value) buf += decoder.decode(chunk.value, { stream: !done });
let sep = buf.indexOf("\n\n");
while (sep >= 0) {
const frame = buf.slice(0, sep);
buf = buf.slice(sep + 2);
const handled = handleSseFrame(frame, cb);
if (handled === "stop") {
// ``done``/``error`` is terminal — stop reading even if the
// server sends extra padding before closing.
try {
await reader.cancel();
} catch {
// best-effort cleanup
}
return;
} }
sep = buf.indexOf("\n\n"); return "continue";
} }
} if (frame.event === "done") {
cb.onDone(obj as unknown as ChatResponse);
// ``done``/``error`` is terminal — stop reading even if the
// server sends extra padding before closing.
return "stop";
}
if (frame.event === "error") {
const code = typeof obj.status_code === "number" ? obj.status_code : 500;
const detail =
typeof obj.detail === "string" ? obj.detail : `HTTP ${code}`;
throw new BeaverApiError(code, detail, obj);
}
return "continue";
});
} }
function handleSseFrame( // ---- conversations API (``/api``, bearer scope ``api``) ----
frame: string,
cb: StreamChatCallbacks, export async function listApiAgents(
): "continue" | "stop" { settings: BeaverSettings
// SSE lines: ``event: <name>`` / ``data: <json>``. ``data`` may span ): Promise<string[]> {
// multiple lines (concatenated with ``\n``) per the spec; we honour const body = (await call(settings, {
// that even though the gateway emits single-line ``data:`` today. method: "GET",
let event = "message"; url: `${apiBase(settings)}/api/agents`,
const dataLines: string[] = []; })) as { agents?: Array<{ name?: unknown }> };
for (const rawLine of frame.split("\n")) { return (body.agents ?? [])
const line = rawLine.replace(/\r$/, ""); .map((a) => (typeof a?.name === "string" ? a.name : null))
if (!line || line.startsWith(":")) continue; .filter((n): n is string => !!n);
const colon = line.indexOf(":");
if (colon < 0) continue;
const field = line.slice(0, colon);
let value = line.slice(colon + 1);
if (value.startsWith(" ")) value = value.slice(1);
if (field === "event") event = value;
else if (field === "data") dataLines.push(value);
}
if (dataLines.length === 0) return "continue";
let data: unknown;
try {
data = JSON.parse(dataLines.join("\n"));
} catch {
return "continue";
}
const obj = data as Record<string, unknown>;
if (event === "delta") {
const content = obj.new_content;
if (typeof content === "string") cb.onDelta(content);
return "continue";
}
if (event === "done") {
cb.onDone(obj as unknown as ChatResponse);
return "stop";
}
if (event === "error") {
const code =
typeof obj.status_code === "number" ? obj.status_code : 500;
const detail =
typeof obj.detail === "string" ? obj.detail : `HTTP ${code}`;
throw new BeaverApiError(code, detail, obj);
}
return "continue";
} }
+47
View File
@@ -0,0 +1,47 @@
/**
* How much of a leaf's bottom is covered by something the operator cannot
* move: Obsidian's desktop status bar, a phone's navbar (which paints over a
* leaf rather than shortening it), and the software keyboard, which shrinks
* the visual viewport while the layout keeps its full height.
*
* Pure on purpose - the view feeds it live rectangles, the tests feed it the
* numbers a phone actually reports.
*/
export interface Rect {
bottom: number;
height: number;
left: number;
right: number;
top: number;
}
export interface Visible {
/** `visualViewport.height`: what is left once the keyboard is up. */
height: number;
/** `visualViewport.offsetTop`: where the visible area starts. */
offsetTop: number;
}
export function insetFor(
leaf: Rect,
bars: Rect[],
visible: Visible | null
): number {
if (leaf.height <= 0) {
return 0;
}
let overlap = 0;
for (const bar of bars) {
// A bar that does not sit over this leaf horizontally covers nothing of
// it - the desktop status bar next to a left-hand sidedock, say.
if (bar.height > 0 && bar.left < leaf.right && bar.right > leaf.left) {
overlap = Math.max(overlap, leaf.bottom - bar.top);
}
}
if (visible) {
const floor = visible.offsetTop + visible.height;
overlap = Math.max(overlap, leaf.bottom - floor);
}
return Math.max(0, Math.round(overlap));
}
+245 -126
View File
@@ -1,69 +1,66 @@
import type { WorkspaceLeaf } from "obsidian";
import { import {
Editor, type Editor,
MarkdownView, type MarkdownView,
Notice, Notice,
Plugin, Plugin,
TFile, type TFile,
} from "obsidian"; } from "obsidian";
import { import { ApiClient } from "$lib/api/client";
BeaverApiError, import { pickAgent } from "./agent-picker";
ChatResponse, import type { ChatResponse } from "./api";
listAgents, import { apiBase, BeaverApiError, listAgents, sendChatStream } from "./api";
sendChatStream, import type { BeaverSettings } from "./settings";
} from "./api"; import { BeaverSettingsTab, DEFAULT_SETTINGS, errorText } from "./settings";
import { pickAgent } from "./agentPicker"; import { PanelView, VIEW_TYPE_PANEL } from "./view";
import {
BeaverSettings,
BeaverSettingsTab,
DEFAULT_SETTINGS,
} from "./settings";
const AGENT_CACHE_TTL_MS = 5 * 60 * 1000; const AGENT_CACHE_TTL_MS = 5 * 60 * 1000;
// Obsidian's ``Editor`` is a thin shim over a CodeMirror 6 // Obsidian's ``Editor`` is a thin shim over a CodeMirror 6 ``EditorView``;
// ``EditorView``; reaching for it directly is the only way to grab // reaching for it directly is the only way to grab the real scrollable DOM
// the real scrollable DOM node (``.cm-scroller``). Property name is // node (``.cm-scroller``). Property name is unofficial but stable across
// unofficial but stable across recent Obsidian releases. // recent Obsidian releases.
interface CMHandle { interface CMHandle {
scrollDOM: HTMLElement; scrollDOM: HTMLElement;
} }
function cmOf(editor: Editor): CMHandle | null { function cmOf(editor: Editor): CMHandle | null {
const cm = (editor as unknown as { cm?: CMHandle }).cm; const { cm } = editor as unknown as { cm?: CMHandle };
return cm && cm.scrollDOM instanceof HTMLElement ? cm : null; return cm && cm.scrollDOM instanceof HTMLElement ? cm : null;
} }
// Boundary of the YAML frontmatter block. ``---\n…---\n`` at the very // Boundary of the YAML frontmatter block: ``---\n…---\n`` at the very start
// start of the file; anything else is a no-frontmatter file and we // of the file; anything else is a no-frontmatter file and we return 0.
// return 0.
function frontmatterEnd(s: string): number { function frontmatterEnd(s: string): number {
if (!s.startsWith("---\n")) return 0; if (!s.startsWith("---\n")) {
return 0;
}
const idx = s.indexOf("\n---\n", 4); const idx = s.indexOf("\n---\n", 4);
return idx >= 0 ? idx + 5 : 0; return idx >= 0 ? idx + 5 : 0;
} }
// Replace only the regions that actually changed instead of // Replace only the regions that actually changed instead of
// ``editor.setValue`` (which resets cursor + scroll and is expensive // ``editor.setValue`` (which resets cursor + scroll and is expensive on
// on every snapshot for long files). // every snapshot for long files).
// //
// Fast path: when the body after the frontmatter is identical except // Fast path: when the body after the frontmatter is identical except for a
// for a tail append (the common case for the final write — gateway // tail append (the common case for the final write - the gateway refreshes
// refreshes frontmatter at the top and appends USER_SCAFFOLD at the // frontmatter at the top and appends USER_SCAFFOLD at the bottom, body in
// bottom, body in between is unchanged), we apply TWO small edits // between unchanged), we apply TWO small edits instead of one huge one. A
// instead of one huge one. The frontmatter rewrite stays a small, // single big ``replaceRange`` spanning the frontmatter triggers Obsidian's
// localised change; the tail append touches only the very last bytes. // Properties-widget rebuild + an asynchronous scroll-to-start-of-content
// A single big ``replaceRange`` spanning the frontmatter triggers // that is hard to override.
// Obsidian's Properties-widget rebuild + an asynchronous scroll-to-
// start-of-content that's hard to override.
// //
// Slow path: prefix/suffix-trimmed single splice for any other shape. // Slow path: prefix/suffix-trimmed single splice for any other shape.
// //
// After every edit we restore scroll on the scrollDOM directly, // After every edit we restore scroll on the scrollDOM directly, repeatedly
// repeatedly across several frames, because the widget rebuild keeps // across several frames, because the widget rebuild keeps resetting scroll
// resetting scroll asynchronously and a single restore loses the // asynchronously and a single restore loses the race.
// race. // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: three splice shapes belong together
function spliceIntoEditor(editor: Editor, newContent: string): void { function spliceIntoEditor(editor: Editor, newContent: string): void {
const current = editor.getValue(); const current = editor.getValue();
if (current === newContent) return; if (current === newContent) {
return;
}
const cm = cmOf(editor); const cm = cmOf(editor);
const savedScrollTop = cm?.scrollDOM.scrollTop ?? null; const savedScrollTop = cm?.scrollDOM.scrollTop ?? null;
@@ -74,11 +71,13 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
const fmsDiffer = const fmsDiffer =
current.slice(0, oldFmEnd) !== newContent.slice(0, newFmEnd); current.slice(0, oldFmEnd) !== newContent.slice(0, newFmEnd);
if (newBody.startsWith(oldBody) && (fmsDiffer || newBody.length > oldBody.length)) { if (
// Tail append first, then (maybe) frontmatter rewrite. Order newBody.startsWith(oldBody) &&
// matters: appending at the current end keeps the frontmatter (fmsDiffer || newBody.length > oldBody.length)
// splice positions valid. Reversing the order would invalidate ) {
// the append offset after the frontmatter grew. // Tail append first, then (maybe) frontmatter rewrite. Order matters:
// appending at the current end keeps the frontmatter splice positions
// valid; the reverse would invalidate the append offset.
if (newBody.length > oldBody.length) { if (newBody.length > oldBody.length) {
const appendStart = editor.offsetToPos(current.length); const appendStart = editor.offsetToPos(current.length);
editor.replaceRange(newBody.slice(oldBody.length), appendStart); editor.replaceRange(newBody.slice(oldBody.length), appendStart);
@@ -88,9 +87,12 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
const fmEnd = editor.offsetToPos(oldFmEnd); const fmEnd = editor.offsetToPos(oldFmEnd);
editor.replaceRange(newContent.slice(0, newFmEnd), fmStart, fmEnd); editor.replaceRange(newContent.slice(0, newFmEnd), fmStart, fmEnd);
} }
} else if (oldBody.startsWith(newBody) && (fmsDiffer || oldBody.length > newBody.length)) { } else if (
// Symmetric case: tail truncation. Unlikely on the streaming oldBody.startsWith(newBody) &&
// path but cheap to handle. (fmsDiffer || oldBody.length > newBody.length)
) {
// Symmetric case: tail truncation. Unlikely on the streaming path but
// cheap to handle.
if (oldBody.length > newBody.length) { if (oldBody.length > newBody.length) {
const truncStart = editor.offsetToPos(oldFmEnd + newBody.length); const truncStart = editor.offsetToPos(oldFmEnd + newBody.length);
const truncEnd = editor.offsetToPos(current.length); const truncEnd = editor.offsetToPos(current.length);
@@ -109,7 +111,7 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
prefix < cap && prefix < cap &&
current.charCodeAt(prefix) === newContent.charCodeAt(prefix) current.charCodeAt(prefix) === newContent.charCodeAt(prefix)
) { ) {
prefix++; prefix += 1;
} }
let suffix = 0; let suffix = 0;
const maxSuffix = cap - prefix; const maxSuffix = cap - prefix;
@@ -118,22 +120,21 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
current.charCodeAt(current.length - 1 - suffix) === current.charCodeAt(current.length - 1 - suffix) ===
newContent.charCodeAt(newContent.length - 1 - suffix) newContent.charCodeAt(newContent.length - 1 - suffix)
) { ) {
suffix++; suffix += 1;
} }
const start = editor.offsetToPos(prefix); const start = editor.offsetToPos(prefix);
const end = editor.offsetToPos(current.length - suffix); const end = editor.offsetToPos(current.length - suffix);
editor.replaceRange( editor.replaceRange(
newContent.slice(prefix, newContent.length - suffix), newContent.slice(prefix, newContent.length - suffix),
start, start,
end, end
); );
} }
if (cm != null && savedScrollTop != null) { if (cm !== null && savedScrollTop !== null) {
// Race against Obsidian's deferred scroll-reset. Sync restore + // Race against Obsidian's deferred scroll-reset: sync restore + two
// two animation frames + two timeouts; whichever fires after // animation frames + two timeouts; whichever fires after Obsidian's own
// Obsidian's own scroll write wins. Cheap noop on the // scroll write wins. Cheap noop on the already-correct frames.
// already-correct frames.
const restore = () => { const restore = () => {
if (cm.scrollDOM.scrollTop !== savedScrollTop) { if (cm.scrollDOM.scrollTop !== savedScrollTop) {
cm.scrollDOM.scrollTop = savedScrollTop; cm.scrollDOM.scrollTop = savedScrollTop;
@@ -151,6 +152,7 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
export default class BeaverPlugin extends Plugin { export default class BeaverPlugin extends Plugin {
settings: BeaverSettings = { ...DEFAULT_SETTINGS }; settings: BeaverSettings = { ...DEFAULT_SETTINGS };
readonly views = new Set<PanelView>();
private cachedAgents: string[] | null = null; private cachedAgents: string[] | null = null;
private cachedAt = 0; private cachedAt = 0;
@@ -159,40 +161,112 @@ export default class BeaverPlugin extends Plugin {
async onload(): Promise<void> { async onload(): Promise<void> {
await this.loadSettings(); await this.loadSettings();
this.addSettingTab(new BeaverSettingsTab(this.app, this)); this.addSettingTab(new BeaverSettingsTab(this.app, this));
this.registerView(VIEW_TYPE_PANEL, (leaf) => new PanelView(leaf, this));
this.addRibbonIcon("bot", "Beaver", () => {
this.openPanel("tab").catch(() => undefined);
});
this.addCommand({ this.addCommand({
id: "send-selected", callback: () => {
name: "Send using selected agent", this.openPanel("side").catch(() => undefined);
checkCallback: (checking) => {
const ctx = this.getFrontmatterAgentContext();
if (!ctx) return false;
if (!checking) void this.dispatch(ctx.file, ctx.agent);
return true;
}, },
id: "open-panel",
name: "Open panel",
});
this.addCommand({
callback: () => {
this.openPanel("tab").catch(() => undefined);
},
id: "open-panel-tab",
name: "Open panel in a tab",
}); });
this.addCommand({ this.addCommand({
id: "send-different",
name: "Send using different agent",
checkCallback: (checking) => { checkCallback: (checking) => {
const ctx = this.getFrontmatterAgentContext(); const file = this.app.workspace.getActiveFile();
if (!ctx) return false; const id = this.conversationIdOf(file);
if (!checking) void this.runDifferent(ctx.file); if (!id) {
return false;
}
if (!checking) {
this.openPanel("side", id).catch(() => undefined);
}
return true; return true;
}, },
id: "open-in-panel",
name: "Open chat in panel",
});
this.addCommand({
checkCallback: (checking) => {
const ctx = this.getFrontmatterAgentContext();
if (!ctx) {
return false;
}
if (!checking) {
this.dispatch(ctx.file, ctx.agent).catch(() => undefined);
}
return true;
},
id: "send-selected",
name: "Send using selected agent",
});
this.addCommand({
checkCallback: (checking) => {
const ctx = this.getFrontmatterAgentContext();
if (!ctx) {
return false;
}
if (!checking) {
this.runDifferent(ctx.file).catch(() => undefined);
}
return true;
},
id: "send-different",
name: "Send using different agent",
}); });
} }
async loadSettings(): Promise<void> { async loadSettings(): Promise<void> {
this.settings = Object.assign( const stored = ((await this.loadData()) ??
{}, {}) as Partial<BeaverSettings> & {
DEFAULT_SETTINGS, vaultSubpath?: string;
await this.loadData(), };
); // The setting used to be called "vault subpath"; carry the value over.
const { vaultSubpath, ...rest } = stored;
this.settings = { ...DEFAULT_SETTINGS, ...rest };
if (vaultSubpath && !rest.chatsFolder) {
this.settings.chatsFolder = vaultSubpath;
}
} }
async saveSettings(): Promise<void> { async saveSettings(): Promise<void> {
await this.saveData(this.settings); await this.saveData(this.settings);
await Promise.all([...this.views].map((view) => view.remount()));
}
// The conversations API client for the panel, or null until the settings
// name a reachable origin and a token.
apiClient(): ApiClient | null {
const token = this.settings.token.trim();
if (!token) {
return null;
}
try {
return new ApiClient(apiBase(this.settings), token);
} catch {
return null;
}
}
openSettings(): void {
const { setting } = this.app as unknown as {
setting: { open: () => void; openTabById: (id: string) => void };
};
setting.open();
setting.openTabById(this.manifest.id);
} }
cacheAgents(agents: string[]): void { cacheAgents(agents: string[]): void {
@@ -201,15 +275,57 @@ export default class BeaverPlugin extends Plugin {
this.lastListFailed = false; this.lastListFailed = false;
} }
private getFrontmatterAgentContext(): // ``conversation_id`` from the note's frontmatter, or null. The panel and
| { file: TFile; agent: string } // the command both key off this; the gateway writes it after the first
| null { // turn (§3.10: frontmatter is only ``agent`` + ``conversation_id``).
conversationIdOf(file: TFile | null): string | null {
if (file?.extension !== "md") {
return null;
}
const cache = this.app.metadataCache.getFileCache(file);
const raw = cache?.frontmatter?.conversation_id;
return typeof raw === "string" && raw.trim() ? raw.trim() : null;
}
// Reveal a panel (creating one on first use) and, given an id, show it.
// A tab and a sidedock column can coexist; each placement reuses its own.
async openPanel(placement: "side" | "tab", id?: string): Promise<void> {
const { workspace } = this.app;
const inTabs = (candidate: WorkspaceLeaf) =>
candidate.getRoot() === workspace.rootSplit;
let leaf: WorkspaceLeaf | null =
workspace
.getLeavesOfType(VIEW_TYPE_PANEL)
.find((candidate) => inTabs(candidate) === (placement === "tab")) ??
null;
if (!leaf) {
leaf =
placement === "tab"
? workspace.getLeaf("tab")
: workspace.getRightLeaf(false);
if (!leaf) {
new Notice("Beaver: couldn't open the panel");
return;
}
await leaf.setViewState({ active: true, type: VIEW_TYPE_PANEL });
}
await workspace.revealLeaf(leaf);
if (id && leaf.view instanceof PanelView) {
leaf.view.show(id);
}
}
private getFrontmatterAgentContext(): { file: TFile; agent: string } | null {
const file = this.app.workspace.getActiveFile(); const file = this.app.workspace.getActiveFile();
if (!file || file.extension !== "md") return null; if (file?.extension !== "md") {
return null;
}
const cache = this.app.metadataCache.getFileCache(file); const cache = this.app.metadataCache.getFileCache(file);
const raw = cache?.frontmatter?.agent; const raw = cache?.frontmatter?.agent;
if (typeof raw !== "string" || !raw.trim()) return null; if (typeof raw !== "string" || !raw.trim()) {
return { file, agent: raw.trim() }; return null;
}
return { agent: raw.trim(), file };
} }
private async runDifferent(file: TFile): Promise<void> { private async runDifferent(file: TFile): Promise<void> {
@@ -224,7 +340,9 @@ export default class BeaverPlugin extends Plugin {
return; return;
} }
const picked = await pickAgent(this.app, agents); const picked = await pickAgent(this.app, agents);
if (!picked) return; if (!picked) {
return;
}
await this.dispatch(file, picked); await this.dispatch(file, picked);
} catch (err) { } catch (err) {
this.notifyError("send-different", err); this.notifyError("send-different", err);
@@ -236,7 +354,9 @@ export default class BeaverPlugin extends Plugin {
this.cachedAgents && this.cachedAgents &&
!this.lastListFailed && !this.lastListFailed &&
Date.now() - this.cachedAt < AGENT_CACHE_TTL_MS; Date.now() - this.cachedAt < AGENT_CACHE_TTL_MS;
if (fresh) return this.cachedAgents; if (fresh) {
return this.cachedAgents;
}
try { try {
const agents = await listAgents(this.settings); const agents = await listAgents(this.settings);
this.cacheAgents(agents); this.cacheAgents(agents);
@@ -250,13 +370,15 @@ export default class BeaverPlugin extends Plugin {
private async dispatch(file: TFile, agent: string): Promise<void> { private async dispatch(file: TFile, agent: string): Promise<void> {
const filename = this.gatewayFilename(file); const filename = this.gatewayFilename(file);
if (!filename) return; if (!filename) {
return;
}
// Snapshot the buffer if the file is open, otherwise read disk. We // Snapshot the buffer if the file is open, otherwise read disk. We don't
// don't keep the editor: an ``Editor`` belongs to a leaf, not a // keep the editor: an ``Editor`` belongs to a leaf, not a file, so if the
// file, so if the user opens another note in the same tab mid- // user opens another note in the same tab mid-stream the old handle now
// stream the old handle now points at the wrong document. Every // points at the wrong document. Every delta and the final write
// delta and the final write re-resolve the leaf by ``file.path``. // re-resolve the leaf by ``file.path``.
const initialEditor = this.findEditorFor(file); const initialEditor = this.findEditorFor(file);
const content = initialEditor const content = initialEditor
? initialEditor.getValue() ? initialEditor.getValue()
@@ -267,23 +389,24 @@ export default class BeaverPlugin extends Plugin {
try { try {
await sendChatStream( await sendChatStream(
this.settings, this.settings,
{ filename, content, agent }, { agent, content, filename },
{ {
onDelta: (newContent) => { onDelta: (newContent) => {
// Live splice into the editor only — we deliberately don't // Live splice into the editor only - the partial never touches
// write the partial to disk. The gateway also skips its own // disk. The gateway also skips its own intermediate writes on
// intermediate file writes on this endpoint, so the local // this endpoint, so the file (and Obsidian Sync's copy) sees the
// file (and Obsidian Sync's copy) only sees the final state // final state once. Re-resolved per delta: if ``file`` is no
// once. Re-resolved per delta: if ``file`` is no longer // longer open anywhere, the delta is dropped and the final
// open anywhere, the delta is dropped and the final
// ``vault.modify`` catches the buffer up. // ``vault.modify`` catches the buffer up.
const editor = this.findEditorFor(file); const editor = this.findEditorFor(file);
if (editor) spliceIntoEditor(editor, newContent); if (editor) {
spliceIntoEditor(editor, newContent);
}
}, },
onDone: (resp) => { onDone: (response) => {
finalResponse = resp; finalResponse = response;
}, },
}, }
); );
} catch (err) { } catch (err) {
if (err instanceof BeaverApiError && err.status === 409) { if (err instanceof BeaverApiError && err.status === 409) {
@@ -297,11 +420,9 @@ export default class BeaverPlugin extends Plugin {
} }
if (!finalResponse) { if (!finalResponse) {
// Stream ended without a ``done`` event — shouldn't happen but
// we surface it instead of pretending nothing was wrong.
this.notifyError( this.notifyError(
`sending to ${agent}`, `sending to ${agent}`,
new Error("stream ended without a done event"), new Error("stream ended without a done event")
); );
return; return;
} }
@@ -319,29 +440,33 @@ export default class BeaverPlugin extends Plugin {
} }
private gatewayFilename(file: TFile): string | null { private gatewayFilename(file: TFile): string | null {
const subpath = this.settings.vaultSubpath; const folder = this.settings.chatsFolder;
if (!subpath) return file.path; if (!folder) {
const prefix = subpath + "/"; return file.path;
if (file.path === subpath || file.path.startsWith(prefix)) { }
const prefix = `${folder}/`;
if (file.path === folder || file.path.startsWith(prefix)) {
return file.path.slice(prefix.length); return file.path.slice(prefix.length);
} }
new Notice( new Notice(
`Beaver: file ${file.path} is outside the configured vault subpath (${subpath})`, `Beaver: file ${file.path} is outside the chats folder (${folder})`,
8000, 8000
); );
return null; return null;
} }
private findEditorFor(file: TFile): Editor | null { private findEditorFor(file: TFile): Editor | null {
// Walk open markdown views — we want the editor instance that owns // Walk open markdown views: we want the editor instance that owns
// ``file`` *right now* so we can splice (keeps the buffer's edit // ``file`` *right now* so we can splice (keeps the buffer's edit history
// history intact) instead of falling back to vault.modify. Never // intact) instead of falling back to vault.modify. Never cached by
// cached by callers: ``view.file`` changes when the user navigates // callers: ``view.file`` changes when the user navigates within the same
// within the same leaf, and is ``null`` between switches. // leaf, and is ``null`` between switches.
const leaves = this.app.workspace.getLeavesOfType("markdown"); const leaves = this.app.workspace.getLeavesOfType("markdown");
for (const leaf of leaves) { for (const leaf of leaves) {
const view = leaf.view as MarkdownView; const view = leaf.view as MarkdownView;
if (view?.file?.path === file.path) return view.editor; if (view?.file?.path === file.path) {
return view.editor;
}
} }
return null; return null;
} }
@@ -349,26 +474,20 @@ export default class BeaverPlugin extends Plugin {
private async writeBack(file: TFile, newContent: string): Promise<void> { private async writeBack(file: TFile, newContent: string): Promise<void> {
const editor = this.findEditorFor(file); const editor = this.findEditorFor(file);
if (editor) { if (editor) {
// Reuse the splice path so the final write (frontmatter refresh // Reuse the splice path so the final write (frontmatter refresh at the
// at the top + USER_SCAFFOLD appended at the bottom) preserves // top + USER_SCAFFOLD appended at the bottom) preserves scroll just
// scroll just like the streaming deltas did. // like the streaming deltas did.
spliceIntoEditor(editor, newContent); spliceIntoEditor(editor, newContent);
return; return;
} }
// File not open (anymore): write to disk. Obsidian refreshes any // File not open (anymore): write to disk. Obsidian refreshes any buffer
// buffer that opens it later, and a leaf that shows it in another // that opens it later, and a leaf that shows it in another pane gets the
// pane gets the update through the vault event. // update through the vault event.
await this.app.vault.modify(file, newContent); await this.app.vault.modify(file, newContent);
} }
private notifyError(action: string, err: unknown): void { private notifyError(action: string, err: unknown): void {
const msg = new Notice(`Beaver (${action}): ${errorText(err)}`, 8000);
err instanceof BeaverApiError
? `${err.status}: ${err.message}`
: err instanceof Error
? err.message
: String(err);
new Notice(`Beaver (${action}): ${msg}`, 8000);
console.error("Beaver:", action, err); console.error("Beaver:", action, err);
} }
} }
+75 -23
View File
@@ -1,23 +1,36 @@
import { App, Notice, PluginSettingTab, Setting } from "obsidian"; import { type App, Notice, PluginSettingTab, Setting } from "obsidian";
import { listAgents, BeaverApiError } from "./api"; import { apiBase, BeaverApiError, listAgents, listApiAgents } from "./api";
import type BeaverPlugin from "./main"; import type BeaverPlugin from "./main";
export interface BeaverSettings { export interface BeaverSettings {
apiBaseUrl: string;
baseUrl: string; baseUrl: string;
// Folder in this vault where the deep chats live: the gateway's markdown
// frontend reads and writes there, so paths it hands out are relative to it.
chatsFolder: string;
token: string; token: string;
vaultSubpath: string;
} }
export const DEFAULT_SETTINGS: BeaverSettings = { export const DEFAULT_SETTINGS: BeaverSettings = {
apiBaseUrl: "",
baseUrl: "http://localhost:62993", baseUrl: "http://localhost:62993",
chatsFolder: "",
token: "", token: "",
vaultSubpath: "",
}; };
export function normalizeSubpath(raw: string): string { export function normalizeFolder(raw: string): string {
return raw.trim().replace(/^\/+|\/+$/g, ""); return raw.trim().replace(/^\/+|\/+$/g, "");
} }
const TRAILING_SLASHES = /\/+$/;
export function errorText(err: unknown): string {
if (err instanceof BeaverApiError) {
return `${err.status}: ${err.message}`;
}
return err instanceof Error ? err.message : String(err);
}
export class BeaverSettingsTab extends PluginSettingTab { export class BeaverSettingsTab extends PluginSettingTab {
plugin: BeaverPlugin; plugin: BeaverPlugin;
@@ -38,14 +51,38 @@ export class BeaverSettingsTab extends PluginSettingTab {
.setPlaceholder("http://localhost:62993") .setPlaceholder("http://localhost:62993")
.setValue(this.plugin.settings.baseUrl) .setValue(this.plugin.settings.baseUrl)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.baseUrl = value.trim().replace(/\/+$/, ""); this.plugin.settings.baseUrl = value
.trim()
.replace(TRAILING_SLASHES, "");
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}), })
);
new Setting(containerEl)
.setName("API origin")
.setDesc(
"Origin of the conversations API; `/api/…` is appended to it. " +
"Leave empty to derive from Base URL (`…/md` → `…`, port 62993 → 62994). " +
"Example: https://beaver.example.com or http://localhost:62994."
)
.addText((text) =>
text
.setPlaceholder("(derived)")
.setValue(this.plugin.settings.apiBaseUrl)
.onChange(async (value) => {
this.plugin.settings.apiBaseUrl = value
.trim()
.replace(TRAILING_SLASHES, "");
await this.plugin.saveSettings();
})
); );
new Setting(containerEl) new Setting(containerEl)
.setName("Bearer token") .setName("Bearer token")
.setDesc("Token with the `messages` scope.") .setDesc(
"Token with the `messages` scope for sending and the `api` scope " +
"for the panel (or a `*` bootstrap token)."
)
.addText((text) => { .addText((text) => {
text.inputEl.type = "password"; text.inputEl.type = "password";
text text
@@ -58,24 +95,27 @@ export class BeaverSettingsTab extends PluginSettingTab {
}); });
new Setting(containerEl) new Setting(containerEl)
.setName("Vault subpath") .setName("Chats folder")
.setDesc( .setDesc(
"Folder in this Obsidian vault that maps to the gateway's vault root. " + "Folder in this vault where the deep chats live - the one the gateway's " +
"Leave empty if the two vault roots match. Example: `💬 чаты`.", "markdown frontend writes to. Leave empty if the chats sit at the vault root. " +
"Example: `💬 чаты`."
) )
.addText((text) => .addText((text) =>
text text
.setPlaceholder("") .setPlaceholder("")
.setValue(this.plugin.settings.vaultSubpath) .setValue(this.plugin.settings.chatsFolder)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.vaultSubpath = normalizeSubpath(value); this.plugin.settings.chatsFolder = normalizeFolder(value);
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}), })
); );
new Setting(containerEl) new Setting(containerEl)
.setName("Test connection") .setName("Test connection")
.setDesc("Calls GET /agents and reports the count.") .setDesc(
"Calls GET /agents on the markdown frontend and reports the count."
)
.addButton((btn) => .addButton((btn) =>
btn.setButtonText("Test").onClick(async () => { btn.setButtonText("Test").onClick(async () => {
try { try {
@@ -83,15 +123,27 @@ export class BeaverSettingsTab extends PluginSettingTab {
this.plugin.cacheAgents(agents); this.plugin.cacheAgents(agents);
new Notice(`Beaver: found ${agents.length} agents`); new Notice(`Beaver: found ${agents.length} agents`);
} catch (err) { } catch (err) {
const msg = new Notice(`Beaver: ${errorText(err)}`, 8000);
err instanceof BeaverApiError
? `${err.status}: ${err.message}`
: err instanceof Error
? err.message
: String(err);
new Notice(`Beaver: ${msg}`, 8000);
} }
}), })
);
new Setting(containerEl)
.setName("Test API")
.setDesc(
"Calls GET /api/agents on the API origin (needs the `api` scope)."
)
.addButton((btn) =>
btn.setButtonText("Test").onClick(async () => {
try {
const agents = await listApiAgents(this.plugin.settings);
new Notice(
`Beaver: API at ${apiBase(this.plugin.settings)} - ${agents.length} agents`
);
} catch (err) {
new Notice(`Beaver: ${errorText(err)}`, 8000);
}
})
); );
} }
} }
+18
View File
@@ -0,0 +1,18 @@
import { Notice } from "obsidian";
const SHORT_MS = 4000;
const LONG_MS = 8000;
function show(message: string, duration: number): Notice {
return new Notice(message, duration);
}
// ``svelte-sonner`` as the shared panel code imports it, backed by Obsidian
// notices; the esbuild resolver points the bare import here.
export const toast = {
error: (message: string) => show(`Beaver: ${message}`, LONG_MS),
info: (message: string) => show(message, SHORT_MS),
message: (message: string) => show(message, SHORT_MS),
success: (message: string) => show(message, SHORT_MS),
warning: (message: string) => show(message, LONG_MS),
};
+81
View File
@@ -0,0 +1,81 @@
// Server-Sent Events over a ``fetch`` body. ``EventSource`` can't send a
// bearer header and ``requestUrl`` buffers the whole response, so every
// SSE consumer in the plugin (chat streaming, the activity panel) goes
// through this reader. Frames are ``event: <name>\ndata: <json>\n\n``;
// ``data`` may span several lines per the spec, comment lines (``:
// keepalive``) are dropped.
const CARRIAGE_RETURN = /\r$/;
export interface SseFrame {
data: string;
event: string;
}
export function parseSseFrame(frame: string): SseFrame | null {
let event = "message";
const dataLines: string[] = [];
for (const rawLine of frame.split("\n")) {
const line = rawLine.replace(CARRIAGE_RETURN, "");
if (!line || line.startsWith(":")) {
continue;
}
const colon = line.indexOf(":");
if (colon < 0) {
continue;
}
const field = line.slice(0, colon);
let value = line.slice(colon + 1);
if (value.startsWith(" ")) {
value = value.slice(1);
}
if (field === "event") {
event = value;
} else if (field === "data") {
dataLines.push(value);
}
}
if (dataLines.length === 0) {
return null;
}
return { data: dataLines.join("\n"), event };
}
// Reads ``body`` to the end (or until ``onFrame`` returns ``"stop"``),
// delivering complete frames in order. Partial frames are buffered
// across reads. Aborting the underlying fetch rejects ``reader.read()``
// with an ``AbortError`` that propagates to the caller.
export async function readSse(
body: ReadableStream<Uint8Array>,
onFrame: (frame: SseFrame) => "continue" | "stop"
): Promise<void> {
const reader = body.getReader();
const decoder = new TextDecoder();
let buf = "";
let done = false;
try {
while (!done) {
// biome-ignore lint/performance/noAwaitInLoops: a stream is read chunk by chunk
const { done: finished, value } = await reader.read();
done = finished;
if (value) {
buf += decoder.decode(value, { stream: !done });
}
let sep = buf.indexOf("\n\n");
while (sep >= 0) {
const frame = parseSseFrame(buf.slice(0, sep));
buf = buf.slice(sep + 2);
if (frame && onFrame(frame) === "stop") {
return;
}
sep = buf.indexOf("\n\n");
}
}
} finally {
try {
await reader.cancel();
} catch {
// best-effort cleanup
}
}
}
+10
View File
@@ -0,0 +1,10 @@
// What a panel leaf remembers: the conversation on screen, whether it follows
// the active note, and which note that is. Reactive so the view and the
// Svelte tree share one truth.
export class PanelState {
selected = $state<string | null>(null);
follow = $state(true);
sourcePath = $state("");
// The selected conversation is the open note's own: show activity, not a copy.
companion = $state(false);
}
+488
View File
@@ -0,0 +1,488 @@
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities) important;
@import "../../beaver-gateway/ui/src/lib/panel/panel.css";
@source "./";
@source "../../beaver-gateway/ui/src/lib";
@custom-variant dark (&:where(.theme-dark, .theme-dark *));
/* The panel's tokens are Obsidian's own variables, exactly as beaver-calendar
maps them onto .bcal-root: the theme, the accent, the font and the radii
come from the app and the user's theme. The msos palette never enters. */
.beaver-root {
--background: var(--background-primary);
--foreground: var(--text-normal);
--card: var(--background-primary-alt);
--card-foreground: var(--text-normal);
--popover: var(--background-secondary);
--popover-foreground: var(--text-normal);
--primary: var(--color-accent);
--primary-foreground: var(--text-on-accent);
--secondary: var(--background-modifier-hover);
--secondary-foreground: var(--text-normal);
--muted: var(--background-modifier-hover);
--muted-foreground: var(--text-muted);
--accent: var(--background-modifier-hover);
--accent-foreground: var(--text-normal);
--destructive: var(--text-error);
--destructive-foreground: var(--text-on-accent);
--border: var(--background-modifier-border);
--input: var(--background-modifier-border);
--ring: var(--color-accent);
--icon: var(--text-muted);
--signal: var(--color-accent);
--link: var(--link-color, var(--text-accent));
--note: oklch(0.52 0.13 78);
--warn: oklch(0.55 0.13 75);
--status-new: var(--color-accent);
--status-done: oklch(0.55 0.14 155);
--status-skip: var(--text-faint);
--status-reply: oklch(0.55 0.14 235);
--status-snooze: oklch(0.66 0.13 75);
--status-meeting: oklch(0.5 0.2 300);
--status-work: oklch(0.55 0.13 195);
--sidebar: var(--background-secondary);
--sidebar-accent: var(--background-modifier-hover);
--rack: var(--background-secondary);
--strip: var(--background-primary);
--attention: oklch(0.72 0.16 72);
--attention-foreground: oklch(0.5 0.14 70);
--shadow-lift:
0 6px 16px -10px rgb(0 0 0 / 0.35), 0 1px 2px rgb(0 0 0 / 0.08);
--shadow-float:
0 20px 50px -20px rgb(0 0 0 / 0.5), 0 2px 6px rgb(0 0 0 / 0.1);
--radius: 0.45rem;
}
.theme-dark .beaver-root {
--attention: oklch(0.8 0.15 75);
--attention-foreground: oklch(0.85 0.14 78);
--strip: var(--background-primary-alt);
--note: oklch(0.84 0.14 88);
--warn: oklch(0.8 0.13 75);
--status-done: oklch(0.74 0.14 155);
--status-reply: oklch(0.72 0.13 235);
--status-snooze: oklch(0.8 0.13 75);
--status-meeting: oklch(0.72 0.16 300);
--status-work: oklch(0.75 0.12 195);
}
@theme inline {
--font-sans: var(--font-interface);
--text-xs: 0.75rem;
--text-sm: 0.8125rem;
--text-base: 0.875rem;
--text-lg: 1rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--color-icon: var(--icon);
--color-signal: var(--signal);
--color-note: var(--note);
--color-sidebar: var(--sidebar);
--color-sidebar-accent: var(--sidebar-accent);
--color-rack: var(--rack);
--color-strip: var(--strip);
--color-attention: var(--attention);
--color-attention-foreground: var(--attention-foreground);
--shadow-lift: var(--shadow-lift);
--shadow-float: var(--shadow-float);
--color-status-new: var(--status-new);
--color-status-done: var(--status-done);
--color-status-skip: var(--status-skip);
--color-status-reply: var(--status-reply);
--color-status-snooze: var(--status-snooze);
--color-status-meeting: var(--status-meeting);
--color-status-work: var(--status-work);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive-foreground: var(--destructive-foreground);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
}
.beaver-root {
box-sizing: border-box;
height: 100%;
padding-bottom: var(--beaver-inset-bottom, 0px);
font-family: var(--font-interface);
font-size: var(--text-sm);
color: var(--foreground);
background: var(--background);
}
/* Portal layer for menus and dialogs: a zero-sized anchor at body level, above
the sidedocks, carrying the theme aliases so popovers stay on theme. */
.beaver-portal {
position: fixed;
top: 0;
left: 0;
z-index: var(--layer-menu, 65);
width: 0;
height: 0;
background: transparent;
}
.beaver-root.beaver-root *,
.beaver-root.beaver-root *::before,
.beaver-root.beaver-root *::after {
box-sizing: border-box;
border: 0 solid var(--border);
}
.beaver-root.beaver-root
:is(h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd) {
margin: 0;
font-size: inherit;
font-weight: inherit;
line-height: inherit;
}
.beaver-root.beaver-root :is(ul, ol) {
padding: 0;
margin: 0;
list-style: none;
}
.beaver-root.beaver-root :is(button, input, select, textarea) {
height: auto;
min-height: 0;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
letter-spacing: inherit;
-webkit-appearance: none;
appearance: none;
outline: none;
background: transparent;
border: 0 solid var(--border);
border-radius: 0;
box-shadow: none;
}
.beaver-root.beaver-root :is(button, input, select, textarea):hover,
.beaver-root.beaver-root :is(button, input, select, textarea):focus,
.beaver-root.beaver-root :is(button, input, select, textarea):active {
box-shadow: none;
}
.beaver-root.beaver-root button {
justify-content: flex-start;
width: auto;
text-align: left;
cursor: pointer;
}
/* A grid button centres by `place-items`, which the rule above would override
for its inline axis: an icon-only button ends up pinned to its left edge. */
.beaver-root.beaver-root button:is(.grid, .inline-grid) {
justify-content: initial;
}
.beaver-root.beaver-root button:hover {
background: transparent;
}
.beaver-root.beaver-root button:disabled {
cursor: default;
}
.beaver-root.beaver-root input::placeholder,
.beaver-root.beaver-root textarea::placeholder {
color: var(--muted-foreground);
}
.beaver-root.beaver-root :is(input, textarea) {
caret-color: var(--primary);
}
.beaver-root.beaver-root :focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
.beaver-root.beaver-root svg {
display: block;
flex-shrink: 0;
}
.beaver-root.beaver-root a,
.beaver-root.beaver-root a:hover {
color: inherit;
text-decoration: inherit;
}
.beaver-root.beaver-root img {
display: block;
max-width: 100%;
height: auto;
}
.beaver-root.beaver-root :is(code, pre, kbd) {
font-family: var(--font-monospace);
font-size: 0.8125rem;
}
/* Chat text rendered by Obsidian itself, kept compact for a sidedock. */
.beaver-root.beaver-root .beaver-md {
font-size: var(--text-sm);
line-height: 1.5;
}
.beaver-root.beaver-root .beaver-md :is(p, ul, ol, pre, blockquote, table) {
margin-block: 0.4em;
}
.beaver-root.beaver-root .beaver-md :is(h1, h2, h3, h4, h5, h6) {
margin-block: 0.8em 0.4em;
font-size: 1em;
font-weight: 600;
}
.beaver-root.beaver-root .beaver-md :is(ul, ol) {
padding-left: 1.25em;
list-style: disc;
}
.beaver-root.beaver-root .beaver-md ol {
list-style: decimal;
}
.beaver-root.beaver-root .beaver-md li + li {
margin-top: 0.15em;
}
.beaver-root.beaver-root .beaver-md a {
color: var(--link);
text-decoration: none;
}
.beaver-root.beaver-root .beaver-md a:hover {
text-decoration: underline;
}
.beaver-root.beaver-root .beaver-md a.is-unresolved {
color: var(--link-unresolved-color, var(--muted-foreground));
opacity: 0.75;
}
.beaver-root.beaver-root .beaver-md code {
padding: 0.1em 0.3em;
font-size: 0.9em;
font-weight: 500;
background: color-mix(in oklch, var(--muted) 70%, transparent);
border-radius: var(--radius-sm);
}
.beaver-root.beaver-root .beaver-md pre {
padding: 0.6em 0.8em;
overflow: auto;
background: var(--muted);
border-radius: var(--radius-md);
}
.beaver-root.beaver-root .beaver-md pre code {
padding: 0;
font-weight: 400;
background: transparent;
}
.beaver-root.beaver-root .beaver-md blockquote {
padding-left: 0.75em;
color: var(--muted-foreground);
border-left: 1px solid var(--border);
}
.beaver-root.beaver-root .beaver-md > :first-child {
margin-top: 0;
}
.beaver-root.beaver-root .beaver-md > :last-child {
margin-bottom: 0;
}
/* The luma primitives are pill-shaped for the browser; inside Obsidian every
control takes the app's own corner language, like beaver-calendar's. */
@layer utilities {
/* Utilities are !important here, so the hidden attribute needs its own weight. */
.beaver-root [hidden] {
display: none !important;
}
.beaver-root [data-slot="button"],
.beaver-root [data-slot="select-trigger"] {
border-radius: var(--button-radius, var(--radius-s)) !important;
}
.beaver-root [data-slot="input"],
.beaver-root [data-slot="tabs-trigger"],
.beaver-root [data-slot$="-item"] {
border-radius: var(--input-radius, var(--radius-s)) !important;
}
.beaver-root [data-slot="tabs-list"],
.beaver-root [data-slot="skeleton"] {
border-radius: var(--radius-m, 8px) !important;
}
/* The luma tabs mark the active trigger with the page colour, which on a
dark Obsidian theme is the same as the surface behind the list. */
.beaver-root [data-slot="tabs-list"] {
background: color-mix(
in oklab,
var(--text-normal) 6%,
transparent
) !important;
}
.beaver-root [data-slot="tabs-trigger"][data-state="active"] {
color: var(--text-normal) !important;
background: color-mix(
in oklab,
var(--text-normal) 14%,
transparent
) !important;
}
.beaver-root [data-slot="dialog-content"],
.beaver-root [data-slot="popover-content"],
.beaver-root [data-slot="dropdown-menu-content"],
.beaver-root [data-slot="context-menu-content"],
.beaver-root [data-slot="select-content"] {
border: 1px solid var(--border) !important;
border-radius: var(--radius-m, 8px) !important;
box-shadow: var(--shadow-s) !important;
}
.beaver-root [data-slot="dropdown-menu-content"],
.beaver-root [data-slot="context-menu-content"],
.beaver-root [data-slot="select-content"] {
background: var(--popover) !important;
}
.beaver-root [data-slot="dropdown-menu-content"]::before,
.beaver-root [data-slot="context-menu-content"]::before,
.beaver-root [data-slot="select-content"]::before {
display: none !important;
}
/* Obsidian's own toggle: border-hover track, accent when on, a white thumb.
Tripled class: luma's group/data variants carry three classes' worth of
specificity, and the utilities are all !important. */
.beaver-root.beaver-root.beaver-root [data-slot="switch"] {
justify-content: flex-start !important;
width: 40px !important;
height: 20px !important;
padding: 0 !important;
background: var(--background-modifier-border-hover) !important;
border: 0 !important;
border-radius: var(--toggle-radius, 18px) !important;
}
.beaver-root.beaver-root.beaver-root
[data-slot="switch"][data-state="checked"] {
background: var(--interactive-accent) !important;
}
.beaver-root.beaver-root.beaver-root [data-slot="switch-thumb"] {
display: block !important;
width: 16px !important;
height: 16px !important;
margin-left: 2px !important;
background: var(--toggle-thumb-color, white) !important;
border-radius: 50% !important;
box-shadow: var(--shadow-s) !important;
}
.beaver-root.beaver-root.beaver-root [data-slot="switch-thumb"] {
translate: 0 !important;
}
.beaver-root.beaver-root.beaver-root
[data-slot="switch-thumb"][data-state="checked"] {
translate: 20px 0 !important;
}
.beaver-root [data-slot="button"][data-variant="default"],
.beaver-root [data-slot="button"]:not([class*="bg-"]) {
font-weight: var(--font-medium, 500) !important;
}
}
/* Strips and the island are buttons here; the reset above flattens buttons,
so the vocabulary is restated at the reset's own specificity. */
.beaver-root.beaver-root .strip {
display: grid;
grid-template-columns: 1.5rem minmax(0, 1fr) auto;
align-items: center;
justify-content: stretch;
justify-items: stretch;
width: 100%;
padding: 0.375rem 0.875rem 0.375rem 0.625rem;
text-align: left;
background: var(--strip);
border-bottom: 1px solid var(--border);
border-radius: 0;
}
.beaver-root.beaver-root .strip:hover,
.beaver-root.beaver-root .strip:focus-visible {
background: var(--strip);
box-shadow: var(--shadow-lift);
}
.beaver-root.beaver-root .strip:first-child {
border-top-left-radius: var(--radius-md);
border-top-right-radius: var(--radius-md);
}
.beaver-root.beaver-root .strip:last-child {
border-bottom: 0;
border-bottom-right-radius: var(--radius-md);
border-bottom-left-radius: var(--radius-md);
}
.beaver-root.beaver-root .strip[aria-current="true"] {
background: color-mix(in oklab, var(--strip) 85%, var(--primary) 15%);
}
.beaver-root.beaver-root .strip[data-state="waiting"] {
box-shadow: var(--shadow-lift);
transform: translateX(0.5rem);
}
.beaver-root.beaver-root .strip.strip-child {
padding-left: 1.75rem;
}
.beaver-root.beaver-root button.island {
display: inline-flex;
gap: 0.625rem;
align-items: center;
height: 2.125rem;
padding: 0 0.875rem 0 0.75rem;
white-space: nowrap;
background: var(--strip);
border: 1px solid var(--border);
border-radius: 999px;
}
.beaver-root.beaver-root button.island:hover {
background: var(--strip);
}
.beaver-root.beaver-root button.island:disabled {
opacity: 1;
}
.beaver-root.beaver-root .bay-rack {
border: 1px solid var(--border);
border-radius: calc(var(--radius-md) + 1px);
}
.beaver-root.beaver-root .strip-child::before {
left: 1.125rem;
}
+82
View File
@@ -0,0 +1,82 @@
<script lang="ts">
import SettingsIcon from "@lucide/svelte/icons/settings";
import { BitsConfig } from "bits-ui";
import type { Component, App as ObsidianApp } from "obsidian";
import type { ApiClient } from "$lib/api/client";
import EmptyState from "$lib/components/empty-state.svelte";
import { Button } from "$lib/components/ui/button";
import { panelCache } from "$lib/panel/cache";
import { providePanelHost } from "$lib/panel/host";
import { ConversationIndex } from "$lib/panel/index.svelte";
import PanelShell from "$lib/panel/panel-shell.svelte";
import type { PanelState } from "../state.svelte";
import { obsidianHost } from "./host";
interface Props {
app: ObsidianApp;
chatsFolder: () => string;
client: ApiClient | null;
component: Component;
onOpenSettings: () => void;
state: PanelState;
}
let { app, client, component, onOpenSettings, state, chatsFolder }: Props =
$props();
// Menus hang off body, not off the view: Obsidian's sidedocks paint above
// the leaf, so anything portalled inside it is covered once it crosses a
// pane edge. The layer carries .beaver-root so the theme reaches popovers.
const layer = document.createElement("div");
layer.className = "beaver-root beaver-portal";
$effect(() => {
document.body.append(layer);
return () => layer.remove();
});
// Props are read once: the view remounts the tree when settings change.
// svelte-ignore state_referenced_locally
const cache = panelCache(`beaver.panel:${client?.base ?? "none"}`);
const host = providePanelHost({
...obsidianHost({
app,
chatsFolder,
component,
sourcePath: () => state.sourcePath,
}),
cache,
});
// svelte-ignore state_referenced_locally
const index = client ? new ConversationIndex(() => client, cache) : null;
</script>
<BitsConfig defaultPortalTo={layer}>
<div class="beaver-root">
{#if client && index}
<PanelShell
{client}
companion={state.companion}
fallbackToMaster
{index}
onOpenFile={(path) => host.openNote?.(`мета/бобер/${path}`)}
showFollow
bind:follow={state.follow}
bind:selected={state.selected}
/>
{:else}
<div class="p-4">
<EmptyState
hint="Set the gateway URL and a token with the api scope in the plugin settings."
title="Beaver is not connected"
>
<Button onclick={onOpenSettings} size="sm" variant="outline">
<SettingsIcon class="size-4" />
Open settings
</Button>
</EmptyState>
</div>
{/if}
</div>
</BitsConfig>
+57
View File
@@ -0,0 +1,57 @@
import type { App, Component } from "obsidian";
import { MarkdownRenderer, Platform } from "obsidian";
import type { PanelHost } from "$lib/panel/host";
export interface HostOptions {
app: App;
// Folder in this vault that maps to the gateway's vault root.
// Where the deep chats live in this vault; gateway paths are relative to it.
chatsFolder: () => string;
component: Component;
// The note the panel currently follows; link resolution starts from it.
sourcePath: () => string;
}
// The Obsidian side of the panel: markdown through the app's own renderer,
// [[links]] opened in place, a conversation's note opened by its binding.
export function obsidianHost(options: HostOptions): PanelHost {
const { app, component } = options;
return {
markdown(node, text) {
let alive = true;
const staging = document.createElement("div");
MarkdownRenderer.render(
app,
text,
staging,
options.sourcePath(),
component
)
.then(() => {
if (alive) {
node.replaceChildren(...staging.childNodes);
}
})
.catch(() => undefined);
return () => {
alive = false;
};
},
name: "obsidian",
openLink(target, kind) {
if (kind === "internal") {
app.workspace
.openLinkText(target, options.sourcePath(), false)
.catch(() => undefined);
return;
}
window.open(target);
},
openNote(path) {
const folder = options.chatsFolder();
const full = folder ? `${folder}/${path}` : path;
app.workspace.openLinkText(full, "", false).catch(() => undefined);
},
touch: Platform.isMobile,
};
}
+250
View File
@@ -0,0 +1,250 @@
import type { ViewStateResult, WorkspaceLeaf } from "obsidian";
import { ItemView, TFile } from "obsidian";
import { mount, unmount } from "svelte";
import type { Rect } from "./inset";
import { insetFor } from "./inset";
import type BeaverPlugin from "./main";
import { PanelState } from "./state.svelte";
import App from "./ui/app.svelte";
export const VIEW_TYPE_PANEL = "beaver-panel";
// Bars that paint over a leaf instead of shortening it. On a phone the navbar
// sits under every view, including the one filling the screen.
const BOTTOM_BARS = [".status-bar", ".mobile-navbar", ".mobile-toolbar"];
// The keyboard and the navbar both animate, and Obsidian hides the navbar
// *after* the keyboard is up: the inset is re-measured every frame until the
// reading has held still long enough to outlast both moves.
const STEADY_FRAMES = 12;
/** Obsidian's Capacitor keyboard events on mobile; absent on the desktop. */
const KEYBOARD_EVENTS = [
"keyboardWillShow",
"keyboardDidShow",
"keyboardWillHide",
"keyboardDidHide",
] as const;
interface SavedState {
follow?: unknown;
selected?: unknown;
}
// One panel leaf: a sidedock column, a tab, or the phone screen. Follows
// the active note's ``conversation_id`` unless pinned, and remembers both
// across restarts through the workspace layout.
export class PanelView extends ItemView {
readonly state = new PanelState();
private readonly plugin: BeaverPlugin;
private root: Record<string, unknown> | null = null;
private settling: number | null = null;
constructor(leaf: WorkspaceLeaf, plugin: BeaverPlugin) {
super(leaf);
this.plugin = plugin;
this.navigation = false;
}
getViewType(): string {
return VIEW_TYPE_PANEL;
}
getDisplayText(): string {
return "Beaver";
}
getIcon(): string {
return "bot";
}
getState(): Record<string, unknown> {
return { follow: this.state.follow, selected: this.state.selected };
}
async setState(state: unknown, result: ViewStateResult): Promise<void> {
const saved = (state ?? {}) as SavedState;
if (typeof saved.follow === "boolean") {
this.state.follow = saved.follow;
}
if (typeof saved.selected === "string" || saved.selected === null) {
this.state.selected = saved.selected;
}
await super.setState(state, result);
}
onOpen(): Promise<void> {
this.contentEl.empty();
this.contentEl.style.padding = "0";
this.contentEl.style.overflow = "hidden";
this.registerEvent(
this.app.workspace.on("file-open", (file) => {
// ``null`` means the active leaf is not a file (this panel got
// focus, say): keep following the last note rather than drop it.
if (file) {
this.follow(file);
}
})
);
this.registerEvent(
this.app.metadataCache.on("changed", (file) => {
// The note we follow just got its conversation_id - typically
// right after the first send. Re-resolve.
if (file.path === this.state.sourcePath) {
this.follow(file);
}
})
);
this.registerEvent(this.app.workspace.on("resize", () => this.settle()));
this.registerEvent(
this.app.workspace.on("layout-change", () => this.settle())
);
// The keyboard resizes the visual viewport only: no workspace event
// fires, so the composer would stay behind it without these two.
const vv = window.visualViewport;
if (vv) {
const onViewport = () => this.settle();
vv.addEventListener("resize", onViewport);
vv.addEventListener("scroll", onViewport);
this.register(() => {
vv.removeEventListener("resize", onViewport);
vv.removeEventListener("scroll", onViewport);
});
}
// Focus opens the keyboard, blur closes it; both animate, and on the way
// out the navbar comes back, so the leaf has to be re-measured after.
this.registerDomEvent(this.contentEl, "focusin", () => this.settle());
this.registerDomEvent(this.contentEl, "focusout", () => this.settle());
// Obsidian's own keyboard events (Capacitor, mobile only) fire before the
// animation and again as the navbar returns, which visualViewport alone
// does not always report.
for (const name of KEYBOARD_EVENTS) {
const onKeyboard = () => this.settle();
window.addEventListener(name, onKeyboard);
this.register(() => window.removeEventListener(name, onKeyboard));
}
this.follow(this.app.workspace.getActiveFile());
this.plugin.views.add(this);
this.mountApp();
this.settle();
return Promise.resolve();
}
/**
* Re-measure until the viewport stops moving. The keyboard animates both
* ways and Obsidian hides its navbar while it is up, so a fixed delay lands
* mid-animation; a frame loop that stops once two readings agree does not
* care how long the animation takes, or whether any event fires at all -
* which is the case in a sidedock drawer.
*/
private settle(): void {
let last = Number.NaN;
let steady = 0;
// A fresh trigger restarts the watch rather than being dropped: the navbar
// coming back is a second move, after the first one has settled.
if (this.settling !== null) {
cancelAnimationFrame(this.settling);
}
const tick = () => {
const floor = this.inset();
steady = floor === last ? steady + 1 : 0;
last = floor;
if (steady >= STEADY_FRAMES) {
this.settling = null;
return;
}
this.settling = requestAnimationFrame(tick);
};
this.settling = requestAnimationFrame(tick);
this.register(() => {
if (this.settling !== null) {
cancelAnimationFrame(this.settling);
this.settling = null;
}
});
}
// Whatever floats over the bottom of this leaf, measured rather than
// guessed. Returns the inset it wrote, so the settle loop can tell when the
// reading stops changing; the arithmetic itself lives in ``inset.ts``.
private inset(): number {
const box = this.contentEl.getBoundingClientRect();
if (box.height <= 0) {
return Number.NaN;
}
const bars: Rect[] = [];
for (const selector of BOTTOM_BARS) {
const node = document.querySelector<HTMLElement>(selector);
if (node && node.offsetParent !== null) {
bars.push(node.getBoundingClientRect());
}
}
const vv = window.visualViewport;
const inset = insetFor(
box,
bars,
vv ? { height: vv.height, offsetTop: vv.offsetTop } : null
);
this.contentEl.style.setProperty("--beaver-inset-bottom", `${inset}px`);
return inset;
}
async onClose(): Promise<void> {
this.plugin.views.delete(this);
await this.unmountApp();
this.contentEl.empty();
}
// The active note changed: remember it, and switch to its conversation
// when following and it has one.
follow(file: TFile | null): void {
if (!(file instanceof TFile)) {
return;
}
this.state.sourcePath = file.path;
if (!this.state.follow) {
return;
}
const id = this.plugin.conversationIdOf(file);
if (id) {
this.state.selected = id;
this.state.companion = !this.inTabs();
}
}
show(id: string): void {
this.state.selected = id;
this.state.companion = false;
}
private inTabs(): boolean {
return this.leaf.getRoot() === this.app.workspace.rootSplit;
}
// Settings changed: the client is built from them, so rebuild the tree.
async remount(): Promise<void> {
await this.unmountApp();
this.mountApp();
}
private mountApp(): void {
this.root = mount(App, {
props: {
app: this.app,
chatsFolder: () => this.plugin.settings.chatsFolder,
client: this.plugin.apiClient(),
component: this,
onOpenSettings: () => this.plugin.openSettings(),
state: this.state,
},
target: this.contentEl,
});
}
private async unmountApp(): Promise<void> {
if (this.root) {
await unmount(this.root);
this.root = null;
}
}
}
+35
View File
@@ -0,0 +1,35 @@
// Evaluate an expression inside a running Obsidian started with
// --remote-debugging-port. Usage: bun t/cdp-eval.ts <port> "<js expression>"
const [port, expression] = process.argv.slice(2);
const targets = (await (
await fetch(`http://localhost:${port}/json`)
).json()) as {
type: string;
title: string;
webSocketDebuggerUrl: string;
}[];
const page = targets.find((t) => t.type === "page");
if (!page) {
throw new Error("no page target");
}
const ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((resolve) => ws.addEventListener("open", resolve));
const reply = new Promise<string>((resolve) => {
ws.addEventListener("message", (event) => {
const data = JSON.parse(String(event.data));
if (data.id === 1) {
resolve(
JSON.stringify(data.result?.result?.value ?? data.result ?? data)
);
}
});
});
ws.send(
JSON.stringify({
id: 1,
method: "Runtime.evaluate",
params: { awaitPromise: true, expression, returnByValue: true },
})
);
console.log(await reply);
ws.close();
+67
View File
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";
import type { Rect } from "../src/inset";
import { insetFor } from "../src/inset";
const rect = (over: Partial<Rect> = {}): Rect => ({
bottom: 780,
height: 780,
left: 0,
right: 390,
top: 0,
...over,
});
// A phone: 780 tall, a 56px navbar over the bottom, 340px of keyboard.
const NAVBAR = rect({ bottom: 780, height: 56, top: 724 });
describe("insetFor", () => {
it("clears nothing when the leaf stands alone", () => {
expect(insetFor(rect(), [], null)).toBe(0);
});
it("clears the navbar that paints over the leaf", () => {
expect(insetFor(rect(), [NAVBAR], null)).toBe(56);
});
it("clears the keyboard, which shortens no layout", () => {
expect(insetFor(rect(), [], { height: 440, offsetTop: 0 })).toBe(340);
});
it("takes the deeper of the two rather than their sum", () => {
// While the keyboard is up the navbar is behind it: adding them would
// push the composer twice as far as it has to go.
expect(insetFor(rect(), [NAVBAR], { height: 440, offsetTop: 0 })).toBe(340);
});
it("comes back to the navbar once the keyboard goes", () => {
expect(insetFor(rect(), [NAVBAR], { height: 780, offsetTop: 0 })).toBe(56);
});
it("ignores a bar that sits beside the leaf, not over it", () => {
// The desktop status bar floats bottom-right; a left-hand sidedock never
// reaches it.
const sidedock = rect({ right: 300 });
const statusBar = rect({
bottom: 780,
height: 20,
left: 700,
right: 980,
top: 760,
});
expect(insetFor(sidedock, [statusBar], null)).toBe(0);
});
it("measures from where the visible area starts, not from zero", () => {
// iOS scrolls the page up under the keyboard instead of resizing it.
expect(insetFor(rect(), [], { height: 440, offsetTop: 100 })).toBe(240);
});
it("says nothing about a leaf with no height yet", () => {
expect(insetFor(rect({ bottom: 0, height: 0 }), [NAVBAR], null)).toBe(0);
});
it("never returns a negative inset", () => {
const short = rect({ bottom: 300, height: 300 });
expect(insetFor(short, [], { height: 780, offsetTop: 0 })).toBe(0);
});
});
+81
View File
@@ -0,0 +1,81 @@
export class TFile {
path: string;
extension: string;
constructor(path: string, extension = "md") {
this.path = path;
this.extension = extension;
}
}
export class TFolder {
path: string;
constructor(path: string) {
this.path = path;
}
}
export const notices: string[] = [];
export class Notice {
constructor(message: string) {
notices.push(message);
}
hide(): void {}
}
export class Plugin {}
export class ItemView {}
export class PluginSettingTab {}
export class Setting {}
export class Component {}
export class FuzzySuggestModal {}
export const Platform = { isMobile: false };
export const moment = () => undefined;
export const requestUrl = () =>
Promise.reject(new Error("no network in the stub"));
export const setIcon = () => undefined;
function escapeHtml(value: string): string {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
// Enough of Obsidian's renderer for the panel: paragraphs, bold, code,
// wikilinks as ``a.internal-link`` with ``data-href``, bare URLs.
export const MarkdownRenderer = {
render(
_app: unknown,
markdown: string,
el: HTMLElement,
_sourcePath: string,
_component: unknown
): Promise<void> {
const blocks = markdown.split(/\n{2,}/).map((block) => {
const html = escapeHtml(block)
.replace(
/!?\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
(_full, target: string, alias?: string) =>
`<a class="internal-link" data-href="${target}" href="${target}">${alias ?? target}</a>`
)
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
.replace(/`([^`]+)`/g, "<code>$1</code>")
.replace(
/(^|\s)(https?:\/\/[^\s]+)/g,
(_full, lead: string, url: string) =>
`${lead}<a class="external-link" href="${url}">${url}</a>`
)
.replace(/\n/g, "<br>");
if (block.startsWith("- ")) {
const items = html
.split("<br>")
.map((line) => `<li>${line.replace(/^- /, "")}</li>`);
return `<ul>${items.join("")}</ul>`;
}
return `<p>${html}</p>`;
});
el.innerHTML = blocks.join("");
return Promise.resolve();
},
};
+79
View File
@@ -0,0 +1,79 @@
import { describe, expect, it } from "vitest";
import { scopeCss } from "../scripts/scope-css.mjs";
const one = (css: string) => scopeCss(css).replace(/\s+/gu, " ").trim();
describe("scoping the built stylesheet", () => {
it("prefixes a utility with the view root, keeping the root itself in reach", () => {
expect(one(".flex{display:flex!important}")).toBe(
":is(.beaver-root, .beaver-root *).flex { display: flex !important; }"
);
});
it("outranks the same utility from another plugin's Tailwind", () => {
// Class specificity: theirs is (0,1,0); ours must be (0,2,0).
const out = one(".hidden{display:none!important}");
expect(out.startsWith(":is(.beaver-root, .beaver-root *).hidden")).toBe(
true
);
});
it("keeps pseudo-elements at the end where they are valid", () => {
expect(one(".x::placeholder{color:red}")).toBe(
":is(.beaver-root, .beaver-root *).x::placeholder { color: red; }"
);
expect(one(".y::first-letter{t:1}")).toContain(".y:first-letter");
});
it("scopes rules nested in container queries and layers", () => {
const out = one(
"@layer utilities{@container (min-width:30rem){.a{display:inline-block!important}}}"
);
// lightningcss rewrites the query into range syntax; Obsidian's Chromium
// has understood that inside @container since container queries landed.
expect(out).toContain("@container (width >= 30rem)");
expect(out).toContain(":is(.beaver-root, .beaver-root *).a");
});
it("drops a leading universal selector instead of producing `:is()*`", () => {
const out = one("*,:before,::backdrop{--tw-a:initial}");
expect(out).toContain(":is(.beaver-root, .beaver-root *),");
expect(out).toContain(":is(.beaver-root, .beaver-root *):before");
expect(out).not.toContain("*)*");
});
it("leaves tailwind's theme variables on :root, under obsidian's body values", () => {
expect(one(":root,:host{--spacing:.25rem}")).toBe(
":root, :host { --spacing: .25rem; }"
);
});
it("scopes the subject, so an ancestor outside the root still matches", () => {
expect(one(".theme-dark .hue-chip{c:1}")).toBe(
".theme-dark :is(.beaver-root, .beaver-root *).hue-chip { c: 1; }"
);
expect(one(".a > button{c:2}")).toBe(
".a > button:is(.beaver-root, .beaver-root *) { c: 2; }"
);
expect(one(".a *{c:3}")).toBe(
".a :is(.beaver-root, .beaver-root *) { c: 3; }"
);
});
it("leaves rules that already name the root alone", () => {
const reset = ".beaver-root.beaver-root button{cursor:pointer}";
expect(one(reset)).toBe(
".beaver-root.beaver-root button { cursor: pointer; }"
);
expect(one(".theme-dark .beaver-root{--note:red}")).toBe(
".theme-dark .beaver-root { --note: red; }"
);
});
it("scopes variant selectors without breaking their inner :where()", () => {
const out = one(".dark\\:bg-x:where(.theme-dark,.theme-dark *){b:1}");
expect(out).toBe(
":is(.beaver-root, .beaver-root *).dark\\:bg-x:where(.theme-dark, .theme-dark *) { b: 1; }"
);
});
});
+320
View File
@@ -0,0 +1,320 @@
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import esbuild from "esbuild";
import { JSDOM } from "jsdom";
import { root, sharedLib, svelte } from "../esbuild.config.mjs";
const out = join(mkdtempSync(join(tmpdir(), "beaver-")), "bundle.mjs");
writeFileSync(
join(root, "tests/.entry.ts"),
`export { default as App } from "../src/ui/app.svelte";
export { PanelState } from "../src/state.svelte";
export { FakeClient, MASTER, BRANCH_UFW, BRANCH_BLASTER, DEEP_PANEL } from "../preview/fake-client";
export { mount, unmount, flushSync } from "svelte";
export { Component } from "obsidian";
export { conversationActions } from "$lib/panel/actions.svelte";
`
);
await esbuild.build({
bundle: true,
conditions: ["svelte", "browser"],
entryPoints: [join(root, "tests/.entry.ts")],
format: "esm",
logLevel: "error",
mainFields: ["svelte", "browser", "module", "main"],
outfile: out,
plugins: [
sharedLib({ obsidian: join(root, "tests/obsidian-stub.ts") }),
svelte(),
],
target: "es2022",
});
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
pretendToBeVisual: true,
url: "http://localhost/",
});
for (const key of Object.getOwnPropertyNames(dom.window)) {
if (key.startsWith("_") || globalThis[key] !== undefined) {
continue;
}
try {
globalThis[key] = dom.window[key];
} catch {
/* getter-only window properties are fine to skip */
}
}
globalThis.window = dom.window;
globalThis.document = dom.window.document;
globalThis.Event = dom.window.Event;
globalThis.CustomEvent = dom.window.CustomEvent;
dom.window.Element.prototype.getBoundingClientRect = () => ({
bottom: 0,
height: 0,
left: 0,
right: 0,
toJSON: () => ({}),
top: 0,
width: 0,
x: 0,
y: 0,
});
dom.window.Element.prototype.setPointerCapture = () => {};
dom.window.Element.prototype.releasePointerCapture = () => {};
dom.window.Element.prototype.hasPointerCapture = () => false;
dom.window.Element.prototype.scrollIntoView = () => {};
dom.window.Element.prototype.animate = () => {
const animation = {
addEventListener() {},
cancel() {},
commitStyles() {},
currentTime: 0,
effect: { getComputedTiming: () => ({ delay: 0, duration: 0 }) },
finish() {},
finished: Promise.resolve(),
onfinish: null,
pause() {},
play() {},
playState: "finished",
removeEventListener() {},
reverse() {},
startTime: 0,
};
queueMicrotask(() => animation.onfinish?.());
return animation;
};
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
globalThis.matchMedia = () => ({
addEventListener() {},
matches: false,
removeEventListener() {},
});
dom.window.matchMedia = globalThis.matchMedia;
const {
App,
PanelState,
FakeClient,
MASTER,
BRANCH_UFW,
BRANCH_BLASTER,
DEEP_PANEL,
mount,
unmount,
flushSync,
Component,
conversationActions,
} = await import(pathToFileURL(out).href);
const failures = [];
function check(name, condition, detail = "") {
if (condition) {
console.log(` ok ${name}`);
} else {
failures.push(name);
console.log(` FAIL ${name}${detail ? ` -- ${detail}` : ""}`);
}
}
const settle = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms));
const client = new FakeClient();
const state = new PanelState();
state.selected = MASTER;
const opened = [];
const app = {
workspace: {
openLinkText: (link, from) => opened.push({ from, target: link }),
},
};
const target = dom.window.document.body;
const instance = mount(App, {
props: {
app,
chatsFolder: () => "💬 чаты",
client,
component: new Component(),
onOpenSettings: () => {},
state,
},
target,
});
flushSync();
await settle();
flushSync();
const text = () => target.textContent ?? "";
check(
"root carries the theme class",
target.querySelector(".beaver-root") !== null
);
check(
"portal layer is on body",
target.querySelector(".beaver-portal") !== null
);
check("thread header shows the master", text().includes("Master · 29 Aug"));
check(
"the running turn's tools are on screen",
text().includes("Agent") && text().includes("Bash")
);
check(
"history rendered through the host renderer",
target.querySelector(".beaver-md") !== null
);
const wikilink = target.querySelector("a.internal-link");
check("wikilink rendered as an internal link", wikilink !== null);
if (wikilink) {
wikilink.dispatchEvent(
new dom.window.MouseEvent("click", { bubbles: true, cancelable: true })
);
check(
"clicking it opens the note in Obsidian",
opened.some((o) => o.target === "2026-08-28"),
JSON.stringify(opened)
);
}
state.selected = BRANCH_BLASTER;
flushSync();
await settle();
flushSync();
check("switching follows the state", text().includes("бластер"));
check(
"a pending question shows its buttons",
text().includes("Xiaomi Mi Blaster")
);
// Nothing picked - the panel just opened on a phone - lands on the master
// rather than on a prompt to pick something.
state.selected = null;
flushSync();
await settle();
flushSync();
check(
"with nothing picked it falls back to the master",
state.selected === MASTER,
String(state.selected)
);
state.selected = BRANCH_BLASTER;
flushSync();
await settle();
flushSync();
check("an explicit pick still wins", state.selected === BRANCH_BLASTER);
const composer = target.querySelector("textarea");
check("composer is there", composer !== null);
if (composer) {
composer.value = "закажи Broadlink";
composer.dispatchEvent(new dom.window.Event("input", { bubbles: true }));
flushSync();
composer
.closest("form")
?.dispatchEvent(
new dom.window.Event("submit", { bubbles: true, cancelable: true })
);
await settle();
check(
"sending goes out as source=panel",
client.sent.some(
(m) => m.origin === "panel" && m.text === "закажи Broadlink"
),
JSON.stringify(client.sent)
);
}
const switcher = target.querySelector('[aria-label="Switch conversation"]');
check("narrow layout shows the switcher", switcher !== null);
switcher?.dispatchEvent(
new dom.window.PointerEvent("pointerdown", {
bubbles: true,
button: 0,
pointerType: "mouse",
})
);
switcher?.click();
flushSync();
await settle();
const rows = [...dom.window.document.querySelectorAll("[data-row]")];
check(
"switcher lists open conversations grouped by day",
rows.length >= 4 && text().includes("Today"),
`${rows.length}`
);
check(
"closed ones stay out until asked",
!rows.some((row) => row.dataset.row === "c5triage0000")
);
dom.window.document.dispatchEvent(
new dom.window.KeyboardEvent("keydown", {
bubbles: true,
code: "Escape",
key: "Escape",
})
);
flushSync();
await settle();
state.selected = DEEP_PANEL;
flushSync();
await settle();
flushSync();
check(
"header carries the actions menu",
target.querySelector('[aria-label="Conversation actions"]') !== null
);
const scope = (current) => ({
client,
current,
host: { name: "obsidian", openNote: () => {}, touch: false },
onBranch: () => {},
onChanged: () => {},
onOpen: () => {},
onRename: () => {},
});
const labels = async (id, current = true) =>
conversationActions(await client.conversation(id), scope(current)).map(
(a) => a.label
);
const deep = await labels(DEEP_PANEL);
check(
"deep chat offers its note, memory and digest",
deep.includes("Open note") &&
deep.includes("Remember on close") &&
deep.includes("Close with digest"),
deep.join(", ")
);
check(
"a deep chat has no Telegram toggle",
!deep.some((l) => l.includes("Telegram"))
);
const hidden = await labels(BRANCH_BLASTER, false);
check(
"a hidden branch can return to Telegram and be opened",
hidden.includes("Return to Telegram") && hidden.includes("Open"),
hidden.join(", ")
);
const shown = await labels(BRANCH_UFW);
check(
"a visible branch can hide from Telegram and merge",
shown.includes("Hide from Telegram") &&
shown.includes("Merge into parent") &&
!shown.includes("Open"),
shown.join(", ")
);
await unmount(instance);
console.log(
failures.length === 0
? "\nsmoke: all good"
: `\nsmoke: ${failures.length} FAILED`
);
process.exit(failures.length === 0 ? 0 : 1);
+100
View File
@@ -0,0 +1,100 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const source = readFileSync(
join(import.meta.dirname, "../src/tailwind.css"),
"utf8"
);
function block(selector: string): string {
const at = source.indexOf(`\n${selector} {`);
if (at === -1) {
return "";
}
const open = source.indexOf("{", at);
const close = source.indexOf("\n}", open);
return source.slice(open, close);
}
describe("theme aliases", () => {
const OBSIDIAN_VARS = [
"--background-primary",
"--background-secondary",
"--background-modifier-border",
"--text-normal",
"--text-muted",
"--color-accent",
"--text-on-accent",
"--text-error",
];
it("are declared on .beaver-root, not :root", () => {
const root = block(".beaver-root");
expect(root).not.toBe("");
for (const name of OBSIDIAN_VARS) {
expect(root, `${name} must be aliased on .beaver-root`).toContain(name);
}
});
it("never reference Obsidian variables from :root", () => {
const rootBlocks = [...source.matchAll(/(^|\n):root\s*\{([^}]*)\}/g)];
for (const [, , body] of rootBlocks) {
for (const name of OBSIDIAN_VARS) {
expect(body, `:root must not reference ${name}`).not.toContain(name);
}
}
});
it("scopes the dark override under the view root", () => {
expect(source).toContain(".theme-dark .beaver-root");
expect(source).not.toMatch(/\n\.theme-dark\s*\{/);
});
it("keeps the msos palette out: the live accent is Obsidian's", () => {
const root = block(".beaver-root");
expect(root).toContain("--signal: var(--color-accent)");
expect(root).toContain("--primary: var(--color-accent)");
expect(root).not.toMatch(/oklch\([^)]*\s34[0-2]\)/);
});
});
describe("reset specificity", () => {
it("uses a doubled class to outrank app and theme styles", () => {
expect(source).toContain(".beaver-root.beaver-root");
});
it("gives every control Obsidian's corner language", () => {
expect(source).toContain('[data-slot="button"]');
expect(source).toContain("var(--button-radius");
expect(source).toContain('[data-slot="input"]');
});
});
describe("cascade against Obsidian", () => {
it("marks utilities important so they outrank app.css", () => {
expect(source).toMatch(
/@import "tailwindcss\/utilities\.css"[^;]*\bimportant\b/
);
});
it("scans the shared panel sources", () => {
expect(source).toContain('@source "../../beaver-gateway/ui/src/lib"');
expect(source).toContain("panel/panel.css");
});
it("imports only the theme and utility layers", () => {
expect(source).toContain('@import "tailwindcss/theme.css"');
expect(source).toContain('@import "tailwindcss/utilities.css"');
expect(source).not.toMatch(/@import "tailwindcss"\s*[;l]/);
expect(source).not.toMatch(/@import\s+"tailwindcss\/preflight/);
});
// Obsidian's own `button` rules are what the reset answers, but a grid
// button must keep centring itself or an icon-only one drifts left.
it("leaves grid buttons to their own placement", () => {
expect(source).toMatch(
/button:is\(\.grid, \.inline-grid\) \{\s*justify-content: initial/
);
});
});
+16 -3
View File
@@ -10,9 +10,22 @@
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"importHelpers": true, "importHelpers": true,
"isolatedModules": true, "isolatedModules": true,
"strictNullChecks": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"lib": ["DOM", "ES2022"] "skipLibCheck": true,
"verbatimModuleSyntax": true,
"types": ["node", "svelte"],
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"paths": {
"$lib/*": ["../beaver-gateway/ui/src/lib/*"],
"svelte-sonner": ["src/shims/sonner.ts"]
}
}, },
"include": ["src/**/*.ts"] "include": [
"src/**/*.ts",
"src/**/*.svelte",
"preview/**/*.ts",
"tests/**/*.ts"
],
"exclude": ["preview/shot.ts"]
} }
+3 -1
View File
@@ -1,3 +1,5 @@
{ {
"0.1.0": "1.5.0" "0.1.0": "1.5.0",
"0.2.0": "1.5.0",
"0.3.0": "1.5.0"
} }
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
resolve: {
alias: {
obsidian: new URL("./tests/obsidian-stub.ts", import.meta.url).pathname,
},
},
test: {
environment: "node",
include: ["tests/**/*.test.ts"],
},
});