feat(*): svelte 5 panel view sharing the gateway ui, obsidian theme, preview and tests

This commit is contained in:
hh
2026-08-29 18:30:31 +02:00
parent a1bf4db905
commit 04400c5076
33 changed files with 3378 additions and 1803 deletions
+3
View File
@@ -3,7 +3,10 @@ node_modules/
# build output (regenerated by `make install` / `make zip`) # build output (regenerated by `make install` / `make zip`)
main.js main.js
styles.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
+21 -9
View File
@@ -1,16 +1,25 @@
PLUGIN_ID := beaver PLUGIN_ID := beaver
ASSETS := manifest.json main.js styles.css versions.json ASSETS := manifest.json main.js styles.css versions.json
# package-lock.json → npm; otherwise whichever of pnpm/npm is around.
PM := $(if $(wildcard package-lock.json),$(shell command -v npm 2>/dev/null),$(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)"; \
@@ -18,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
+31 -47
View File
@@ -1,73 +1,57 @@
# 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)
Two things: Two things:
- **Send the current note** to Beaver's `markdown frontend` (`POST /chat/stream` on `beaver-gateway`) and stream the agent's reply back into the buffer. Skips the Obsidian Sync round-trip by POSTing the buffer contents directly. - **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.
- **Activity panel** (v0): a side view that follows the active note's `conversation_id` and shows, live over SSE, what the agent is doing — tool calls, subagents as a tree, turn status, usage of each turn. - **Send the current note** to Beaver's markdown frontend (`POST /chat/stream`) and stream the agent's reply back into the buffer.
## Commands ## Commands
- **Beaver: Send using selected agent** — uses `frontmatter.agent`. Only when the active note has an `agent:` field. - **Beaver: Open panel** - the right sidedock (also the ribbon icon).
- **Beaver: Send using different agent** — fetches the agent list from the gateway and opens a fuzzy picker. Same condition. - **Beaver: Open panel in a tab** - the wide layout with the conversation rail on the left.
- **Beaver: Open chat in panel** — reveals the activity panel in the right sidebar (creates it on first use) and points it at the active note. - **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.
## Activity panel ## The panel
Subscribes to `GET /api/conversations/{id}/events` (SSE, bearer) where `{id}` is the `conversation_id` from the active note's frontmatter. Switching notes switches the subscription; a note without `conversation_id` shows a hint instead. On connect it fetches `GET /api/conversations/{id}` for the snapshot (title, agent, kind, status, whether a turn is running), then renders bus events: 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`.
- `turn.start` / `turn.end` — a turn block with status (running / done / interrupted / error), origin (`user` / `inject`), start time, duration and usage (`in · out · cache read/write · $ · time`). 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.
- `tool` / `tool.result` — a tool row (name, input summary, result status); clicking it expands the raw input and the result preview. Rows with `parent_tool_use_id` nest under the `Task`/`Agent` tool that spawned the subagent.
- `stream` `content_block_start` of type `tool_use` — pre-creates the row so a long-running tool shows up before its input is complete.
- `say` — text the dispatcher sent out of an inject turn.
The stream reconnects with exponential backoff (1 s → 30 s, jittered) and resets on `hello`. `401`/`403`/`404` are not retried: the panel shows the error and waits for you to fix the token or the id. When it reconnects and the snapshot says nothing is running, turns still shown as running are marked interrupted — their `turn.end` was lost with the connection. 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 vault subpath).
Plain DOM and CSS on Obsidian's theme variables; no framework. It is the v0 of the panel from `архитектура.md` §2 — the full Svelte UI (M5) replaces it. 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` or `https://host/md`. - **Base URL** - markdown frontend root, e.g. `https://host/md`.
- **API origin** where `/api/…` lives, e.g. `http://localhost:62994` or `https://host`. Empty = derived from Base URL (`…/md``…`, port `62993``62994`). - **API origin** - where `/api/…` lives; empty derives it from the base URL (`…/md``…`).
- **Bearer token** — needs the `messages` scope for sending and the `api` scope for the panel; a `*` bootstrap token covers both. - **Bearer token** - `messages` scope for sending, `api` scope for the panel (or a `*` bootstrap token).
- **Vault subpath** folder in this vault that maps to the gateway's vault root. - **Vault subpath** - the folder in this vault that maps to the gateway's vault root.
- **Test connection** / **Test API**`GET /agents` on the markdown frontend, `GET /api/agents` on the API origin.
## 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) + 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 the first turn the gateway adds `conversation_id` to the frontmatter — that is what the activity panel keys off. 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.
## 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, styles.css, versions.json 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 (or reload it if it was already enabled). ## 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 / API origin in plugin settings have to be reachable from the phone — `http://localhost:…` 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
Four files are all Obsidian needs — `manifest.json`, `main.js`, `styles.css`, `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/**"],
"linter": {
"rules": {
"performance": {
"useTopLevelRegex": "off"
},
"style": {
"noMagicNumbers": "off"
},
"suspicious": {
"noEmptyBlockStatements": "off"
}
}
}
}
]
}
+712
View File
@@ -0,0 +1,712 @@
{
"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",
"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=="],
"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=="],
}
}
+94 -13
View File
@@ -1,13 +1,14 @@
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";
const prod = process.argv[2] === "production"; export const root = path.dirname(fileURLToPath(import.meta.url));
export const LIB = path.resolve(root, "../beaver-gateway/ui/src/lib");
const ctx = await esbuild.context({ const EXTERNAL = [
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian", "obsidian",
"electron", "electron",
"@codemirror/autocomplete", "@codemirror/autocomplete",
@@ -22,19 +23,99 @@ const ctx = await esbuild.context({
"@lezer/highlight", "@lezer/highlight",
"@lezer/lr", "@lezer/lr",
...builtins, ...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`,
},
], ],
format: "cjs", }));
target: "es2022", build.onResolve({ filter: SONNER_IMPORT }, () => ({
logLevel: "info", path: path.join(root, "src/shims/sonner.ts"),
sourcemap: prod ? false : "inline", }));
treeShaking: true, build.onResolve({ filter: OBSIDIAN_IMPORT }, () =>
outfile: "main.js", obsidian === "external"
minify: prod, ? { 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";
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const ctx = await esbuild.context({
bundle: true,
conditions: ["svelte", "browser"],
entryPoints: ["src/main.ts"],
format: "cjs",
logLevel: "info",
mainFields: ["svelte", "browser", "module", "main"],
minify: prod,
outfile: "main.js",
plugins: [sharedLib(), svelte()],
sourcemap: prod ? false : "inline",
target: "es2022",
treeShaking: true,
});
if (prod) { if (prod) {
await ctx.rebuild(); await ctx.rebuild();
await ctx.dispose(); await ctx.dispose();
} else { } else {
await ctx.watch(); await ctx.watch();
} }
}
+2 -2
View File
@@ -1,9 +1,9 @@
{ {
"id": "beaver", "id": "beaver",
"name": "Beaver", "name": "Beaver",
"version": "0.2.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.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beaver-plugin-obsidian",
"version": "0.2.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
}
}
}
+35 -10
View File
@@ -1,18 +1,43 @@
{ {
"name": "beaver-plugin-obsidian", "name": "beaver-plugin-obsidian",
"version": "0.2.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 && bun run css -- --minify && node esbuild.config.mjs production",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production" "check": "svelte-check --tsconfig ./tsconfig.json --threshold error",
"css": "tailwindcss -i src/tailwind.css -o styles.css",
"css:watch": "tailwindcss -i src/tailwind.css -o styles.css --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",
"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"
} }
} }
+132
View File
@@ -0,0 +1,132 @@
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.
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") {
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,
client: scene === "offline" ? null : client,
component: new Component() as never,
onOpenSettings: () => console.log("settings"),
state,
vaultSubpath: () => "💬 чаты",
},
target: document.getElementById("app") as HTMLElement,
});
await wait(80);
if (scene.startsWith("closed")) {
press(document.querySelector('[aria-label="Switch conversation"]'));
await wait(60);
press(document.querySelector('[title="Show closed conversations"]'));
}
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("actions")) {
press(document.querySelector('[aria-label="Conversation actions"]'));
}
if (scene.startsWith("activity")) {
for (const trigger of document.querySelectorAll<HTMLButtonElement>(
'[data-slot="tabs-trigger"]'
)) {
if (trigger.textContent?.trim() === "Activity") {
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);
+512
View File
@@ -0,0 +1,512 @@
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;
const now = Date.now();
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",
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_row: 1,
title: "ufw на dell",
}),
summary({
id: BRANCH_BLASTER,
kind: "branch",
last_activity_at: ago(3 * HOUR),
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 },
};
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: this.rows }),
method: "GET",
pattern: /^\/api\/conversations$/,
},
{
handle: ([, id]) => this.info(id),
method: "GET",
pattern: /^\/api\/conversations\/([^/]+)$/,
},
{
handle: ([, id]) => ({ id, messages: 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();
-657
View File
@@ -1,657 +0,0 @@
import { ItemView, TFile, WorkspaceLeaf, setIcon } from "obsidian";
import {
BeaverApiError,
BusEvent,
ConversationInfo,
getConversation,
streamConversationEvents,
} from "./api";
import type BeaverPlugin from "./main";
export const ACTIVITY_VIEW_TYPE = "beaver-activity";
// ---- model ----
type ToolStatus = "running" | "done" | "error" | "aborted";
export interface ToolNode {
id: string;
name: string;
input: unknown;
parent: string | null;
status: ToolStatus;
startedAt: string;
endedAt: string | null;
result: string;
children: ToolNode[];
expanded: boolean;
}
type TurnStatus = "running" | "done" | "interrupted" | "error";
export interface TurnUsage {
input?: number;
output?: number;
cache_read?: number;
cache_creation?: number;
cost_usd?: number | null;
duration_ms?: number | null;
}
export interface Turn {
id: string;
origin: string;
status: TurnStatus;
startedAt: string;
endedAt: string | null;
usage: TurnUsage | null;
resultSubtype: string | null;
roots: ToolNode[];
nodes: Map<string, ToolNode>;
says: string[];
}
export interface Connection {
state: "off" | "connecting" | "open" | "retrying" | "failed";
detail?: string;
}
const MAX_TURNS = 20;
// Pure state: bus events in, tree of turns → tool calls → subagent tool
// calls out. ``apply`` returns whether anything the panel shows changed,
// so the chatty ``stream`` deltas don't trigger re-renders.
export class ActivityModel {
conversation: ConversationInfo | null = null;
turns: Turn[] = []; // newest first
connection: Connection = { state: "off" };
setConversation(info: ConversationInfo): void {
this.conversation = info;
const now = new Date().toISOString();
// The snapshot is the truth about what runs *now*. A turn we still
// show as running but the server doesn't (we were disconnected when
// it ended, or the gateway restarted) is over; its ``turn.end`` is
// not coming.
for (const turn of this.turns) {
if (turn.status !== "running" || turn.id === info.running_turn) continue;
turn.status = "interrupted";
turn.endedAt = now;
for (const node of turn.nodes.values()) {
if (node.status === "running") {
node.status = "aborted";
node.endedAt = now;
}
}
}
// Connected mid-turn: the events for this turn started before we
// subscribed. Open the turn so its tools have somewhere to land.
if (info.running_turn) this.ensureTurn(info.running_turn, now, null);
}
apply(ev: BusEvent): boolean {
const ts = typeof ev.ts === "string" ? ev.ts : new Date().toISOString();
const turnId = typeof ev.turn_id === "string" ? ev.turn_id : null;
const parent =
typeof ev.parent_tool_use_id === "string" ? ev.parent_tool_use_id : null;
switch (ev.type) {
case "turn.start": {
if (!turnId) return false;
const turn = this.ensureTurn(turnId, ts, str(ev.origin));
turn.status = "running";
turn.startedAt = ts;
return true;
}
case "stream": {
// Only the opening of a tool_use block is interesting here: it
// gives us id + name before the full ``tool`` event arrives with
// the input, so long-running tools show up as soon as they start.
if (!turnId) return false;
const raw = ev.event;
if (!raw || typeof raw !== "object") return false;
const sdk = raw as Record<string, unknown>;
if (sdk.type !== "content_block_start") return false;
const block = sdk.content_block as Record<string, unknown> | undefined;
if (!block || block.type !== "tool_use") return false;
if (typeof block.id !== "string") return false;
const turn = this.ensureTurn(turnId, ts, null);
const existed = turn.nodes.has(block.id);
this.ensureNode(turn, block.id, str(block.name) ?? "?", parent, ts);
return !existed;
}
case "tool": {
if (!turnId || typeof ev.tool_use_id !== "string") return false;
const turn = this.ensureTurn(turnId, ts, null);
const node = this.ensureNode(
turn,
ev.tool_use_id,
str(ev.name) ?? "?",
parent,
ts,
);
node.input = ev.input ?? null;
return true;
}
case "tool.result": {
if (!turnId || typeof ev.tool_use_id !== "string") return false;
const turn = this.ensureTurn(turnId, ts, null);
const node = this.ensureNode(turn, ev.tool_use_id, "?", parent, ts);
node.status = ev.is_error ? "error" : "done";
node.endedAt = ts;
node.result = str(ev.content) ?? "";
return true;
}
case "result": {
if (!turnId) return false;
const turn = this.ensureTurn(turnId, ts, null);
turn.resultSubtype = str(ev.subtype);
if (ev.is_error) turn.status = "error";
return true;
}
case "turn.end": {
if (!turnId) return false;
const turn = this.ensureTurn(turnId, ts, str(ev.origin));
const stop = str(ev.stop);
turn.status =
stop === "end_turn"
? turn.status === "error"
? "error"
: "done"
: stop === "interrupted"
? "interrupted"
: "error";
turn.endedAt = ts;
turn.usage =
ev.usage && typeof ev.usage === "object"
? (ev.usage as TurnUsage)
: null;
for (const node of turn.nodes.values()) {
if (node.status === "running") {
node.status = "aborted";
node.endedAt = ts;
}
}
return true;
}
case "say": {
if (typeof ev.text !== "string") return false;
const turn = turnId
? this.ensureTurn(turnId, ts, null)
: this.turns[0];
if (!turn) return false;
turn.says.push(ev.text);
return true;
}
case "conversation.updated":
case "conversation.created": {
if (!this.conversation || ev.id !== this.conversation.id) return false;
this.conversation = {
...this.conversation,
...(ev as unknown as Partial<ConversationInfo>),
};
return true;
}
default:
return false;
}
}
private ensureTurn(id: string, ts: string, origin: string | null): Turn {
let turn = this.turns.find((t) => t.id === id);
if (!turn) {
turn = {
id,
origin: origin ?? "?",
status: "running",
startedAt: ts,
endedAt: null,
usage: null,
resultSubtype: null,
roots: [],
nodes: new Map(),
says: [],
};
this.turns.unshift(turn);
if (this.turns.length > MAX_TURNS) this.turns.length = MAX_TURNS;
} else if (origin && turn.origin === "?") {
turn.origin = origin;
}
return turn;
}
private ensureNode(
turn: Turn,
id: string,
name: string,
parent: string | null,
ts: string,
): ToolNode {
let node = turn.nodes.get(id);
if (node) {
if (node.name === "?" && name !== "?") node.name = name;
return node;
}
node = {
id,
name,
input: null,
parent,
status: "running",
startedAt: ts,
endedAt: null,
result: "",
children: [],
expanded: false,
};
turn.nodes.set(id, node);
if (parent) {
// A subagent's tool call: hang it under the Task/Agent tool that
// spawned it. Events can arrive before that tool's own ``tool``
// event (the SDK streams the child first), so create a placeholder.
this.ensureNode(turn, parent, "?", null, ts).children.push(node);
} else {
turn.roots.push(node);
}
return node;
}
}
function str(v: unknown): string | null {
return typeof v === "string" ? v : null;
}
// ---- presentation helpers ----
function pick(input: unknown, ...keys: string[]): string | null {
if (!input || typeof input !== "object") return null;
const obj = input as Record<string, unknown>;
for (const key of keys) {
const v = obj[key];
if (typeof v === "string" && v.trim()) return v;
}
return null;
}
function clip(s: string, max: number): string {
const one = s.replace(/\s+/g, " ").trim();
return one.length > max ? one.slice(0, max - 1) + "…" : one;
}
export function summarizeInput(name: string, input: unknown): string {
if (input == null) return "";
let s: string | null = null;
switch (name) {
case "Bash":
s = pick(input, "command", "description");
break;
case "Read":
case "Edit":
case "Write":
case "MultiEdit":
case "NotebookEdit":
s = pick(input, "file_path", "notebook_path");
break;
case "Grep":
case "Glob":
s = pick(input, "pattern");
break;
case "Task":
case "Agent":
s = pick(input, "description", "prompt");
break;
case "WebFetch":
s = pick(input, "url");
break;
case "WebSearch":
s = pick(input, "query");
break;
case "Skill":
s = pick(input, "skill");
break;
default:
s = null;
}
if (s === null) {
if (typeof input === "object") {
const first = Object.values(input as Record<string, unknown>).find(
(v) => typeof v === "string" && v.trim(),
);
s = typeof first === "string" ? first : JSON.stringify(input);
} else {
s = String(input);
}
}
return clip(s, 140);
}
function fmtTime(iso: string): string {
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? iso : d.toLocaleTimeString();
}
function fmtDuration(ms: number): string {
if (ms < 1000) return `${Math.round(ms)}ms`;
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
const m = Math.floor(ms / 60_000);
const s = Math.round((ms % 60_000) / 1000);
return `${m}m${s.toString().padStart(2, "0")}s`;
}
function fmtTokens(n: number | undefined): string {
if (n == null) return "-";
return n >= 1000 ? `${(n / 1000).toFixed(n >= 10_000 ? 0 : 1)}k` : String(n);
}
function fmtUsage(u: TurnUsage): string {
const parts = [
`in ${fmtTokens(u.input)}`,
`out ${fmtTokens(u.output)}`,
`cache ${fmtTokens(u.cache_read)}/${fmtTokens(u.cache_creation)}`,
];
if (typeof u.cost_usd === "number") parts.push(`$${u.cost_usd.toFixed(4)}`);
if (typeof u.duration_ms === "number") parts.push(fmtDuration(u.duration_ms));
return parts.join(" · ");
}
function elapsed(from: string, to: string | null): number {
const a = new Date(from).getTime();
const b = to ? new Date(to).getTime() : Date.now();
return Number.isNaN(a) || Number.isNaN(b) ? 0 : Math.max(0, b - a);
}
function sleep(ms: number, signal: AbortSignal): Promise<void> {
return new Promise((resolve) => {
if (signal.aborted) return resolve();
const timer = window.setTimeout(done, ms);
function done() {
window.clearTimeout(timer);
signal.removeEventListener("abort", done);
resolve();
}
signal.addEventListener("abort", done, { once: true });
});
}
const BACKOFF_MIN_MS = 1000;
const BACKOFF_MAX_MS = 30_000;
// ---- view ----
export class ActivityView extends ItemView {
private readonly plugin: BeaverPlugin;
private model = new ActivityModel();
private abort: AbortController | null = null;
private conversationId: string | null = null;
private file: TFile | null = null;
private root: HTMLElement | null = null;
private renderQueued = false;
private ticker: number | null = null;
constructor(leaf: WorkspaceLeaf, plugin: BeaverPlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType(): string {
return ACTIVITY_VIEW_TYPE;
}
getDisplayText(): string {
return "Beaver: activity";
}
getIcon(): string {
return "activity";
}
async onOpen(): Promise<void> {
this.contentEl.empty();
this.root = this.contentEl.createDiv({ cls: "beaver-activity" });
this.registerEvent(
this.app.workspace.on("file-open", (file) => {
// ``null`` means the active leaf isn't a file (e.g. this panel
// got focus) — 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 (or lost) its conversation_id —
// typically right after the first send. Re-resolve.
if (this.file && file.path === this.file.path) this.follow(file);
}),
);
// Once a second while a turn runs, so "running for 12s" ticks.
this.ticker = window.setInterval(() => {
if (this.model.turns.some((t) => t.status === "running")) this.render();
}, 1000);
this.registerInterval(this.ticker);
this.follow(this.app.workspace.getActiveFile());
}
async onClose(): Promise<void> {
this.unsubscribe();
this.root = null;
}
// Follow ``file``: subscribe to the conversation its frontmatter names,
// or show why we can't. Cheap when nothing changed.
follow(file: TFile | null): void {
this.file = file;
const id = this.plugin.conversationIdOf(file);
if (id === this.conversationId) {
this.scheduleRender();
return;
}
this.unsubscribe();
this.conversationId = id;
this.model = new ActivityModel();
if (id) this.subscribe(id);
this.scheduleRender();
}
private subscribe(id: string): void {
const controller = new AbortController();
this.abort = controller;
void this.loop(id, controller.signal).catch((err) => {
console.error("Beaver activity: subscription loop died", err);
});
}
private unsubscribe(): void {
this.abort?.abort();
this.abort = null;
this.model.connection = { state: "off" };
}
private async loop(id: string, signal: AbortSignal): Promise<void> {
let delay = BACKOFF_MIN_MS;
while (!signal.aborted) {
this.model.connection = {
state: delay === BACKOFF_MIN_MS ? "connecting" : "retrying",
detail: this.model.connection.detail,
};
this.scheduleRender();
try {
// Snapshot first: status/title and, if a turn is already
// running, its id, so events that follow land in the right place.
const info = await getConversation(this.plugin.settings, id);
if (signal.aborted) return;
this.model.setConversation(info);
this.scheduleRender();
await streamConversationEvents(
this.plugin.settings,
id,
(ev) => {
if (ev.type === "hello") {
delay = BACKOFF_MIN_MS;
this.model.connection = { state: "open" };
this.scheduleRender();
return;
}
if (this.model.apply(ev)) this.scheduleRender();
},
signal,
);
if (signal.aborted) return;
this.model.connection = { state: "retrying", detail: "stream closed" };
} catch (err) {
if (signal.aborted) return;
if (
err instanceof BeaverApiError &&
(err.status === 401 || err.status === 403 || err.status === 404)
) {
// Not transient: wrong token/scope or unknown conversation.
// Retrying would only spam the gateway; the user has to act.
this.model.connection = {
state: "failed",
detail: `${err.status}: ${err.message}`,
};
this.scheduleRender();
return;
}
this.model.connection = {
state: "retrying",
detail: err instanceof Error ? err.message : String(err),
};
}
this.scheduleRender();
await sleep(delay + Math.random() * 250, signal);
delay = Math.min(delay * 2, BACKOFF_MAX_MS);
}
}
private scheduleRender(): void {
if (this.renderQueued) return;
this.renderQueued = true;
requestAnimationFrame(() => {
this.renderQueued = false;
this.render();
});
}
private render(): void {
const root = this.root;
if (!root) return;
root.empty();
this.renderHeader(root);
const body = root.createDiv({ cls: "beaver-activity-body" });
if (!this.file) {
body.createDiv({
cls: "beaver-activity-empty",
text: "Open a note with conversation_id in its frontmatter.",
});
return;
}
if (!this.conversationId) {
body.createDiv({
cls: "beaver-activity-empty",
text: `«${this.file.basename}» has no conversation_id in its frontmatter.`,
});
return;
}
if (this.model.turns.length === 0) {
body.createDiv({
cls: "beaver-activity-empty",
text:
this.model.connection.state === "open"
? "No activity yet - waiting for the next turn."
: "Connecting…",
});
return;
}
for (const turn of this.model.turns) this.renderTurn(body, turn);
}
private renderHeader(root: HTMLElement): void {
const head = root.createDiv({ cls: "beaver-activity-head" });
const conv = this.model.conversation;
const title = head.createDiv({ cls: "beaver-activity-title" });
title.createSpan({
text:
conv?.title ||
this.file?.basename ||
this.conversationId ||
"Beaver: activity",
});
const conn = this.model.connection;
const dot = title.createSpan({
cls: `beaver-activity-conn is-${conn.state}`,
attr: { "aria-label": conn.detail ?? conn.state, title: conn.detail ?? conn.state },
});
dot.setText(conn.state);
const meta = head.createDiv({ cls: "beaver-activity-meta" });
if (conv) {
meta.createSpan({ text: conv.agent });
meta.createSpan({ text: conv.kind });
meta.createSpan({ text: conv.status });
if (conv.running_turn) meta.createSpan({ cls: "is-running", text: "turn running" });
if (conv.pending_question) meta.createSpan({ cls: "is-pending", text: "question pending" });
} else if (this.conversationId) {
meta.createSpan({ text: this.conversationId });
}
if (conn.state === "failed" || conn.state === "retrying") {
head.createDiv({
cls: `beaver-activity-note is-${conn.state}`,
text: conn.detail ?? conn.state,
});
}
}
private renderTurn(parent: HTMLElement, turn: Turn): void {
const el = parent.createDiv({ cls: `beaver-activity-turn is-${turn.status}` });
const line = el.createDiv({ cls: "beaver-activity-turn-line" });
line.createSpan({ cls: "beaver-activity-status", text: turn.status });
line.createSpan({ cls: "beaver-activity-origin", text: turn.origin });
line.createSpan({ cls: "beaver-activity-time", text: fmtTime(turn.startedAt) });
const dur = turn.usage?.duration_ms ?? elapsed(turn.startedAt, turn.endedAt);
line.createSpan({ cls: "beaver-activity-duration", text: fmtDuration(dur) });
if (turn.usage) {
el.createDiv({ cls: "beaver-activity-usage", text: fmtUsage(turn.usage) });
}
for (const text of turn.says) {
const say = el.createDiv({ cls: "beaver-activity-say" });
const icon = say.createSpan({ cls: "beaver-activity-icon" });
setIcon(icon, "message-square");
say.createSpan({ text: clip(text, 300) });
}
if (turn.roots.length > 0) {
const tree = el.createDiv({ cls: "beaver-activity-tree" });
for (const node of turn.roots) this.renderNode(tree, node);
}
}
private renderNode(parent: HTMLElement, node: ToolNode): void {
const el = parent.createDiv({ cls: `beaver-activity-tool is-${node.status}` });
const row = el.createDiv({ cls: "beaver-activity-tool-row" });
row.createSpan({ cls: "beaver-activity-dot" });
row.createSpan({ cls: "beaver-activity-tool-name", text: node.name });
const summary = summarizeInput(node.name, node.input);
if (summary) row.createSpan({ cls: "beaver-activity-tool-summary", text: summary });
const dur = elapsed(node.startedAt, node.endedAt);
if (node.status !== "running" || dur >= 2000) {
row.createSpan({ cls: "beaver-activity-duration", text: fmtDuration(dur) });
}
row.addEventListener("click", (evt) => {
evt.stopPropagation();
node.expanded = !node.expanded;
this.render();
});
if (node.expanded) {
const details = el.createDiv({ cls: "beaver-activity-tool-details" });
if (node.input != null) {
details.createEl("pre", {
text: JSON.stringify(node.input, null, 2),
});
}
if (node.result) {
details.createEl("pre", { cls: "is-result", text: node.result });
}
}
if (node.children.length > 0) {
const kids = el.createDiv({ cls: "beaver-activity-children" });
for (const child of node.children) this.renderNode(kids, child);
}
}
}
+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);
}; };
+55 -97
View File
@@ -1,19 +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"; 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 {
@@ -32,26 +33,39 @@ export class BeaverApiError extends Error {
// Caddy the markdown root is ``https://host/md`` and the API root is the // 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 // 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. // 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 { export function mdBase(settings: BeaverSettings): string {
const url = settings.baseUrl.trim().replace(/\/+$/, ""); const url = settings.baseUrl.trim().replace(TRAILING_SLASHES, "");
if (!url) throw new Error("Beaver: base URL is not configured"); if (!url) {
throw new Error("Beaver: base URL is not configured");
}
return url; return url;
} }
export function apiBase(settings: BeaverSettings): string { export function apiBase(settings: BeaverSettings): string {
const explicit = settings.apiBaseUrl.trim().replace(/\/+$/, ""); const explicit = settings.apiBaseUrl.trim().replace(TRAILING_SLASHES, "");
if (explicit) return explicit; if (explicit) {
return explicit;
}
const md = mdBase(settings); const md = mdBase(settings);
if (md.endsWith("/md")) return md.slice(0, -"/md".length); if (md.endsWith("/md")) {
if (/:62993$/.test(md)) return md.replace(/:62993$/, ":62994"); return md.slice(0, -"/md".length);
}
if (LOCAL_MD_PORT.test(md)) {
return md.replace(LOCAL_MD_PORT, ":62994");
}
throw new Error( throw new Error(
`Beaver: API origin is not configured and can't be derived from ${md}`, `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}` };
} }
@@ -61,16 +75,20 @@ function errorMessage(status: number, body: unknown): string {
if (body && typeof body === "object") { if (body && typeof body === "object") {
const obj = body as Record<string, unknown>; const obj = body as Record<string, unknown>;
for (const key of ["detail", "error"]) { for (const key of ["detail", "error"]) {
if (typeof obj[key] === "string") return obj[key] as string; if (typeof obj[key] === "string") {
return obj[key] as string;
} }
} }
if (typeof body === "string" && body) return body; }
if (typeof body === "string" && body) {
return body;
}
return `HTTP ${status}`; return `HTTP ${status}`;
} }
async function call( async function call(
settings: BeaverSettings, settings: BeaverSettings,
init: Omit<RequestUrlParam, "url"> & { url: string }, init: Omit<RequestUrlParam, "url"> & { url: string }
): Promise<unknown> { ): Promise<unknown> {
const headers = { const headers = {
...authHeader(settings), ...authHeader(settings),
@@ -106,8 +124,8 @@ async function errorFromResponse(res: Response): Promise<BeaverApiError> {
export async function listAgents(settings: BeaverSettings): Promise<string[]> { export async function listAgents(settings: BeaverSettings): Promise<string[]> {
const body = (await call(settings, { const body = (await call(settings, {
url: `${mdBase(settings)}/agents`,
method: "GET", method: "GET",
url: `${mdBase(settings)}/agents`,
})) as { agents?: Array<{ name?: unknown }> }; })) as { agents?: Array<{ name?: unknown }> };
const list = body.agents ?? []; const list = body.agents ?? [];
return list return list
@@ -117,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, {
url: `${mdBase(settings)}/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;
} }
@@ -133,9 +151,9 @@ 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;
} }
// CORS is allowed by the gateway's ``CORSMiddleware``; auth is the same // CORS is allowed by the gateway's ``CORSMiddleware``; auth is the same
@@ -144,19 +162,21 @@ 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 res = await fetch(`${mdBase(settings)}/chat/stream`, { const res = await fetch(`${mdBase(settings)}/chat/stream`, {
method: "POST", body: JSON.stringify(req),
headers: { headers: {
...authHeader(settings), ...authHeader(settings),
"Content-Type": "application/json",
Accept: "text/event-stream", Accept: "text/event-stream",
"Content-Type": "application/json",
}, },
body: JSON.stringify(req), method: "POST",
signal, signal,
}); });
if (!res.ok || !res.body) throw await errorFromResponse(res); if (!(res.ok && res.body)) {
throw await errorFromResponse(res);
}
await readSse(res.body, (frame) => { await readSse(res.body, (frame) => {
let data: unknown; let data: unknown;
@@ -168,7 +188,9 @@ export async function sendChatStream(
const obj = data as Record<string, unknown>; const obj = data as Record<string, unknown>;
if (frame.event === "delta") { if (frame.event === "delta") {
const content = obj.new_content; const content = obj.new_content;
if (typeof content === "string") cb.onDelta(content); if (typeof content === "string") {
cb.onDelta(content);
}
return "continue"; return "continue";
} }
if (frame.event === "done") { if (frame.event === "done") {
@@ -178,8 +200,7 @@ export async function sendChatStream(
return "stop"; return "stop";
} }
if (frame.event === "error") { if (frame.event === "error") {
const code = const code = typeof obj.status_code === "number" ? obj.status_code : 500;
typeof obj.status_code === "number" ? obj.status_code : 500;
const detail = const detail =
typeof obj.detail === "string" ? obj.detail : `HTTP ${code}`; typeof obj.detail === "string" ? obj.detail : `HTTP ${code}`;
throw new BeaverApiError(code, detail, obj); throw new BeaverApiError(code, detail, obj);
@@ -190,77 +211,14 @@ export async function sendChatStream(
// ---- conversations API (``/api``, bearer scope ``api``) ---- // ---- conversations API (``/api``, bearer scope ``api``) ----
export interface ConversationInfo {
id: string;
kind: string;
agent: string;
title: string | null;
status: string;
running_turn: string | null;
pending_question: unknown;
parent?: string | null;
live?: boolean;
busy?: boolean;
}
// One record of the gateway bus as it arrives over SSE: ``type`` is the
// SSE event name (``turn.start``, ``stream``, ``tool``, ``tool.result``,
// ``result``, ``turn.end``, ``say``, ``conversation.*`` …), the rest is
// the event's own payload.
export type BusEvent = {
type: string;
seq?: number;
ts?: string;
conversation_id?: string | null;
} & Record<string, unknown>;
export async function listApiAgents( export async function listApiAgents(
settings: BeaverSettings, settings: BeaverSettings
): Promise<string[]> { ): Promise<string[]> {
const body = (await call(settings, { const body = (await call(settings, {
url: `${apiBase(settings)}/api/agents`,
method: "GET", method: "GET",
url: `${apiBase(settings)}/api/agents`,
})) as { agents?: Array<{ name?: unknown }> }; })) as { agents?: Array<{ name?: unknown }> };
return (body.agents ?? []) return (body.agents ?? [])
.map((a) => (typeof a?.name === "string" ? a.name : null)) .map((a) => (typeof a?.name === "string" ? a.name : null))
.filter((n): n is string => !!n); .filter((n): n is string => !!n);
} }
export async function getConversation(
settings: BeaverSettings,
id: string,
): Promise<ConversationInfo> {
return (await call(settings, {
url: `${apiBase(settings)}/api/conversations/${encodeURIComponent(id)}`,
method: "GET",
})) as ConversationInfo;
}
// Resolves when the server closes the stream; rejects on HTTP errors,
// network failures and abort. Reconnection is the caller's business.
export async function streamConversationEvents(
settings: BeaverSettings,
id: string,
onEvent: (event: BusEvent) => void,
signal: AbortSignal,
): Promise<void> {
const url = `${apiBase(settings)}/api/conversations/${encodeURIComponent(id)}/events`;
const res = await fetch(url, {
method: "GET",
headers: { ...authHeader(settings), Accept: "text/event-stream" },
signal,
});
if (!res.ok || !res.body) throw await errorFromResponse(res);
await readSse(res.body, (frame) => {
let data: unknown;
try {
data = JSON.parse(frame.data);
} catch {
return "continue";
}
if (data && typeof data === "object") {
onEvent({ ...(data as Record<string, unknown>), type: frame.event });
}
return "continue";
});
}
+202 -144
View File
@@ -1,71 +1,66 @@
import type { WorkspaceLeaf } from "obsidian";
import { import {
Editor, type Editor,
MarkdownView, type MarkdownView,
Notice, Notice,
Plugin, Plugin,
TFile, type TFile,
WorkspaceLeaf,
} 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 { ACTIVITY_VIEW_TYPE, ActivityView } from "./activity"; import { PanelView, VIEW_TYPE_PANEL } from "./view";
import { pickAgent } from "./agentPicker";
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;
@@ -76,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);
@@ -90,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);
@@ -111,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;
@@ -120,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;
@@ -153,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;
@@ -161,55 +161,103 @@ 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( this.registerView(VIEW_TYPE_PANEL, (leaf) => new PanelView(leaf, this));
ACTIVITY_VIEW_TYPE, this.addRibbonIcon("bot", "Beaver", () => {
(leaf) => new ActivityView(leaf, this), this.openPanel("side").catch(() => undefined);
); });
this.addCommand({
callback: () => {
this.openPanel("side").catch(() => undefined);
},
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: "open-in-panel",
name: "Open chat in panel",
checkCallback: (checking) => { checkCallback: (checking) => {
const file = this.app.workspace.getActiveFile(); const file = this.app.workspace.getActiveFile();
if (!file || file.extension !== "md") return false; const id = this.conversationIdOf(file);
if (!checking) void this.openActivityPanel(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({ 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", id: "send-selected",
name: "Send using selected agent", name: "Send using selected agent",
checkCallback: (checking) => {
const ctx = this.getFrontmatterAgentContext();
if (!ctx) return false;
if (!checking) void this.dispatch(ctx.file, ctx.agent);
return true;
},
}); });
this.addCommand({ this.addCommand({
id: "send-different",
name: "Send using different agent",
checkCallback: (checking) => { checkCallback: (checking) => {
const ctx = this.getFrontmatterAgentContext(); const ctx = this.getFrontmatterAgentContext();
if (!ctx) return false; if (!ctx) {
if (!checking) void this.runDifferent(ctx.file); return false;
}
if (!checking) {
this.runDifferent(ctx.file).catch(() => undefined);
}
return true; return true;
}, },
id: "send-different",
name: "Send using different agent",
}); });
} }
async loadSettings(): Promise<void> { async loadSettings(): Promise<void> {
this.settings = Object.assign( this.settings = { ...DEFAULT_SETTINGS, ...(await this.loadData()) };
{},
DEFAULT_SETTINGS,
await this.loadData(),
);
} }
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 {
@@ -218,44 +266,51 @@ export default class BeaverPlugin extends Plugin {
this.lastListFailed = false; this.lastListFailed = false;
} }
// ``conversation_id`` from the note's frontmatter, or null. The panel // ``conversation_id`` from the note's frontmatter, or null. The panel and
// and the command both key off this; the gateway writes it after the // the command both key off this; the gateway writes it after the first
// first turn (§3.10: frontmatter is only ``agent`` + ``conversation_id``). // turn (§3.10: frontmatter is only ``agent`` + ``conversation_id``).
conversationIdOf(file: TFile | null): string | null { conversationIdOf(file: TFile | null): string | null {
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?.conversation_id; const raw = cache?.frontmatter?.conversation_id;
return typeof raw === "string" && raw.trim() ? raw.trim() : null; return typeof raw === "string" && raw.trim() ? raw.trim() : null;
} }
// Reveal the activity panel (creating it in the right sidebar on first // Reveal a panel (creating one on first use) and, given an id, show it.
// use) and point it at ``file``. async openPanel(placement: "side" | "tab", id?: string): Promise<void> {
async openActivityPanel(file: TFile | null): Promise<void> {
const { workspace } = this.app; const { workspace } = this.app;
let leaf: WorkspaceLeaf | null = let leaf: WorkspaceLeaf | null =
workspace.getLeavesOfType(ACTIVITY_VIEW_TYPE)[0] ?? null; workspace.getLeavesOfType(VIEW_TYPE_PANEL)[0] ?? null;
if (!leaf) { if (!leaf) {
leaf = workspace.getRightLeaf(false); leaf =
placement === "tab"
? workspace.getLeaf("tab")
: workspace.getRightLeaf(false);
if (!leaf) { if (!leaf) {
new Notice("Beaver: couldn't open the side panel"); new Notice("Beaver: couldn't open the panel");
return; return;
} }
await leaf.setViewState({ type: ACTIVITY_VIEW_TYPE, active: true }); await leaf.setViewState({ active: true, type: VIEW_TYPE_PANEL });
} }
await workspace.revealLeaf(leaf); await workspace.revealLeaf(leaf);
const view = leaf.view; if (id && leaf.view instanceof PanelView) {
if (view instanceof ActivityView) view.follow(file); leaf.view.show(id);
}
} }
private getFrontmatterAgentContext(): private getFrontmatterAgentContext(): { file: TFile; agent: string } | null {
| { 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> {
@@ -270,7 +325,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);
@@ -282,7 +339,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);
@@ -296,13 +355,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()
@@ -313,23 +374,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) => { }
finalResponse = resp;
}, },
onDone: (response) => {
finalResponse = response;
}, },
}
); );
} catch (err) { } catch (err) {
if (err instanceof BeaverApiError && err.status === 409) { if (err instanceof BeaverApiError && err.status === 409) {
@@ -343,11 +405,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;
} }
@@ -366,28 +426,32 @@ export default class BeaverPlugin extends Plugin {
private gatewayFilename(file: TFile): string | null { private gatewayFilename(file: TFile): string | null {
const subpath = this.settings.vaultSubpath; const subpath = this.settings.vaultSubpath;
if (!subpath) return file.path; if (!subpath) {
const prefix = subpath + "/"; return file.path;
}
const prefix = `${subpath}/`;
if (file.path === subpath || file.path.startsWith(prefix)) { if (file.path === subpath || 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 configured vault subpath (${subpath})`,
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;
} }
@@ -395,26 +459,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);
} }
} }
+32 -23
View File
@@ -1,17 +1,17 @@
import { App, Notice, PluginSettingTab, Setting } from "obsidian"; import { type App, Notice, PluginSettingTab, Setting } from "obsidian";
import { BeaverApiError, apiBase, listAgents, listApiAgents } 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 {
baseUrl: string;
apiBaseUrl: string; apiBaseUrl: string;
baseUrl: string;
token: string; token: string;
vaultSubpath: string; vaultSubpath: string;
} }
export const DEFAULT_SETTINGS: BeaverSettings = { export const DEFAULT_SETTINGS: BeaverSettings = {
baseUrl: "http://localhost:62993",
apiBaseUrl: "", apiBaseUrl: "",
baseUrl: "http://localhost:62993",
token: "", token: "",
vaultSubpath: "", vaultSubpath: "",
}; };
@@ -20,12 +20,13 @@ export function normalizeSubpath(raw: string): string {
return raw.trim().replace(/^\/+|\/+$/g, ""); return raw.trim().replace(/^\/+|\/+$/g, "");
} }
function errorText(err: unknown): string { const TRAILING_SLASHES = /\/+$/;
return err instanceof BeaverApiError
? `${err.status}: ${err.message}` export function errorText(err: unknown): string {
: err instanceof Error if (err instanceof BeaverApiError) {
? err.message return `${err.status}: ${err.message}`;
: String(err); }
return err instanceof Error ? err.message : String(err);
} }
export class BeaverSettingsTab extends PluginSettingTab { export class BeaverSettingsTab extends PluginSettingTab {
@@ -48,9 +49,11 @@ 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) new Setting(containerEl)
@@ -58,23 +61,25 @@ export class BeaverSettingsTab extends PluginSettingTab {
.setDesc( .setDesc(
"Origin of the conversations API; `/api/…` is appended to it. " + "Origin of the conversations API; `/api/…` is appended to it. " +
"Leave empty to derive from Base URL (`…/md` → `…`, port 62993 → 62994). " + "Leave empty to derive from Base URL (`…/md` → `…`, port 62993 → 62994). " +
"Example: https://beaver.example.com or http://localhost:62994.", "Example: https://beaver.example.com or http://localhost:62994."
) )
.addText((text) => .addText((text) =>
text text
.setPlaceholder("(derived)") .setPlaceholder("(derived)")
.setValue(this.plugin.settings.apiBaseUrl) .setValue(this.plugin.settings.apiBaseUrl)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.apiBaseUrl = value.trim().replace(/\/+$/, ""); this.plugin.settings.apiBaseUrl = value
.trim()
.replace(TRAILING_SLASHES, "");
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}), })
); );
new Setting(containerEl) new Setting(containerEl)
.setName("Bearer token") .setName("Bearer token")
.setDesc( .setDesc(
"Token with the `messages` scope for sending and the `api` scope " + "Token with the `messages` scope for sending and the `api` scope " +
"for the activity panel (or a `*` bootstrap token).", "for the panel (or a `*` bootstrap token)."
) )
.addText((text) => { .addText((text) => {
text.inputEl.type = "password"; text.inputEl.type = "password";
@@ -91,7 +96,7 @@ export class BeaverSettingsTab extends PluginSettingTab {
.setName("Vault subpath") .setName("Vault subpath")
.setDesc( .setDesc(
"Folder in this Obsidian vault that maps to the gateway's vault root. " + "Folder in this Obsidian vault that maps to the gateway's vault root. " +
"Leave empty if the two vault roots match. Example: `💬 чаты`.", "Leave empty if the two vault roots match. Example: `💬 чаты`."
) )
.addText((text) => .addText((text) =>
text text
@@ -100,12 +105,14 @@ export class BeaverSettingsTab extends PluginSettingTab {
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.vaultSubpath = normalizeSubpath(value); this.plugin.settings.vaultSubpath = normalizeSubpath(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 on the markdown frontend 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 {
@@ -115,23 +122,25 @@ export class BeaverSettingsTab extends PluginSettingTab {
} catch (err) { } catch (err) {
new Notice(`Beaver: ${errorText(err)}`, 8000); new Notice(`Beaver: ${errorText(err)}`, 8000);
} }
}), })
); );
new Setting(containerEl) new Setting(containerEl)
.setName("Test API") .setName("Test API")
.setDesc("Calls GET /api/agents on the API origin (needs the `api` scope).") .setDesc(
"Calls GET /api/agents on the API origin (needs the `api` scope)."
)
.addButton((btn) => .addButton((btn) =>
btn.setButtonText("Test").onClick(async () => { btn.setButtonText("Test").onClick(async () => {
try { try {
const agents = await listApiAgents(this.plugin.settings); const agents = await listApiAgents(this.plugin.settings);
new Notice( new Notice(
`Beaver: API at ${apiBase(this.plugin.settings)} - ${agents.length} agents`, `Beaver: API at ${apiBase(this.plugin.settings)} - ${agents.length} agents`
); );
} catch (err) { } catch (err) {
new Notice(`Beaver: ${errorText(err)}`, 8000); 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),
};
+32 -14
View File
@@ -5,27 +5,40 @@
// ``data`` may span several lines per the spec, comment lines (``: // ``data`` may span several lines per the spec, comment lines (``:
// keepalive``) are dropped. // keepalive``) are dropped.
const CARRIAGE_RETURN = /\r$/;
export interface SseFrame { export interface SseFrame {
event: string;
data: string; data: string;
event: string;
} }
export function parseSseFrame(frame: string): SseFrame | null { export function parseSseFrame(frame: string): SseFrame | null {
let event = "message"; let event = "message";
const dataLines: string[] = []; const dataLines: string[] = [];
for (const rawLine of frame.split("\n")) { for (const rawLine of frame.split("\n")) {
const line = rawLine.replace(/\r$/, ""); const line = rawLine.replace(CARRIAGE_RETURN, "");
if (!line || line.startsWith(":")) continue; if (!line || line.startsWith(":")) {
continue;
}
const colon = line.indexOf(":"); const colon = line.indexOf(":");
if (colon < 0) continue; if (colon < 0) {
continue;
}
const field = line.slice(0, colon); const field = line.slice(0, colon);
let value = line.slice(colon + 1); let value = line.slice(colon + 1);
if (value.startsWith(" ")) value = value.slice(1); if (value.startsWith(" ")) {
if (field === "event") event = value; value = value.slice(1);
else if (field === "data") dataLines.push(value);
} }
if (dataLines.length === 0) return null; if (field === "event") {
return { event, data: dataLines.join("\n") }; 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"``), // Reads ``body`` to the end (or until ``onFrame`` returns ``"stop"``),
@@ -34,7 +47,7 @@ export function parseSseFrame(frame: string): SseFrame | null {
// with an ``AbortError`` that propagates to the caller. // with an ``AbortError`` that propagates to the caller.
export async function readSse( export async function readSse(
body: ReadableStream<Uint8Array>, body: ReadableStream<Uint8Array>,
onFrame: (frame: SseFrame) => "continue" | "stop", onFrame: (frame: SseFrame) => "continue" | "stop"
): Promise<void> { ): Promise<void> {
const reader = body.getReader(); const reader = body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
@@ -42,14 +55,19 @@ export async function readSse(
let done = false; let done = false;
try { try {
while (!done) { while (!done) {
const chunk = await reader.read(); // biome-ignore lint/performance/noAwaitInLoops: a stream is read chunk by chunk
done = chunk.done; const { done: finished, value } = await reader.read();
if (chunk.value) buf += decoder.decode(chunk.value, { stream: !done }); done = finished;
if (value) {
buf += decoder.decode(value, { stream: !done });
}
let sep = buf.indexOf("\n\n"); let sep = buf.indexOf("\n\n");
while (sep >= 0) { while (sep >= 0) {
const frame = parseSseFrame(buf.slice(0, sep)); const frame = parseSseFrame(buf.slice(0, sep));
buf = buf.slice(sep + 2); buf = buf.slice(sep + 2);
if (frame && onFrame(frame) === "stop") return; if (frame && onFrame(frame) === "stop") {
return;
}
sep = buf.indexOf("\n\n"); sep = buf.indexOf("\n\n");
} }
} }
+8
View File
@@ -0,0 +1,8 @@
// 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("");
}
+365
View File
@@ -0,0 +1,365 @@
@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);
--radius: 0.45rem;
}
.theme-dark .beaver-root {
--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-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 {
height: 100%;
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 {
text-align: inherit;
cursor: pointer;
}
.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;
}
.beaver-root [data-slot="button"][data-variant="default"],
.beaver-root [data-slot="button"]:not([class*="bg-"]) {
font-weight: var(--font-medium, 500) !important;
}
}
+76
View File
@@ -0,0 +1,76 @@
<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 { 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;
client: ApiClient | null;
component: Component;
onOpenSettings: () => void;
state: PanelState;
vaultSubpath: () => string;
}
let { app, client, component, onOpenSettings, state, vaultSubpath }: 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
providePanelHost(
obsidianHost({
app,
component,
sourcePath: () => state.sourcePath,
vaultSubpath,
})
);
// svelte-ignore state_referenced_locally
const index = client ? new ConversationIndex(() => client) : null;
</script>
<BitsConfig defaultPortalTo={layer}>
<div class="beaver-root">
{#if client && index}
<PanelShell
{client}
{index}
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>
+56
View File
@@ -0,0 +1,56 @@
import type { App, Component } from "obsidian";
import { MarkdownRenderer, Platform } from "obsidian";
import type { PanelHost } from "$lib/panel/host";
export interface HostOptions {
app: App;
component: Component;
// The note the panel currently follows; link resolution starts from it.
sourcePath: () => string;
// Folder in this vault that maps to the gateway's vault root.
vaultSubpath: () => 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 subpath = options.vaultSubpath();
const full = subpath ? `${subpath}/${path}` : path;
app.workspace.openLinkText(full, "", false).catch(() => undefined);
},
touch: Platform.isMobile,
};
}
+136
View File
@@ -0,0 +1,136 @@
import type { ViewStateResult, WorkspaceLeaf } from "obsidian";
import { ItemView, TFile } from "obsidian";
import { mount, unmount } from "svelte";
import type BeaverPlugin from "./main";
import { PanelState } from "./state.svelte";
import App from "./ui/app.svelte";
export const VIEW_TYPE_PANEL = "beaver-panel";
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;
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.follow(this.app.workspace.getActiveFile());
this.plugin.views.add(this);
this.mountApp();
return Promise.resolve();
}
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;
}
}
show(id: string): void {
this.state.selected = id;
}
// 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,
client: this.plugin.apiClient(),
component: this,
onOpenSettings: () => this.plugin.openSettings(),
state: this.state,
vaultSubpath: () => this.plugin.settings.vaultSubpath,
},
target: this.contentEl,
});
}
private async unmountApp(): Promise<void> {
if (this.root) {
await unmount(this.root);
this.root = null;
}
}
}
-158
View File
@@ -1,158 +0,0 @@
/* Beaver: activity panel (plain DOM, v0). Theme through Obsidian variables. */
.beaver-activity {
font-size: var(--font-ui-small);
line-height: 1.4;
user-select: text;
}
.beaver-activity-head {
position: sticky;
top: 0;
padding-bottom: var(--size-4-2);
margin-bottom: var(--size-4-2);
border-bottom: 1px solid var(--background-modifier-border);
background: var(--background-primary);
}
.beaver-activity-title {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--size-4-2);
font-weight: var(--font-semibold);
color: var(--text-normal);
}
.beaver-activity-conn {
font-size: var(--font-ui-smaller);
font-weight: var(--font-normal);
color: var(--text-faint);
white-space: nowrap;
}
.beaver-activity-conn::before {
content: "";
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
margin-right: 4px;
background: var(--text-faint);
}
.beaver-activity-conn.is-open::before { background: var(--color-green); }
.beaver-activity-conn.is-connecting::before,
.beaver-activity-conn.is-retrying::before { background: var(--color-yellow); }
.beaver-activity-conn.is-failed::before { background: var(--color-red); }
.beaver-activity-meta {
display: flex;
flex-wrap: wrap;
gap: var(--size-4-1) var(--size-4-2);
color: var(--text-muted);
font-size: var(--font-ui-smaller);
}
.beaver-activity-meta .is-running { color: var(--color-yellow); }
.beaver-activity-meta .is-pending { color: var(--color-orange); }
.beaver-activity-note {
margin-top: var(--size-4-1);
font-size: var(--font-ui-smaller);
color: var(--text-muted);
}
.beaver-activity-note.is-failed { color: var(--color-red); }
.beaver-activity-empty {
color: var(--text-muted);
padding: var(--size-4-2) 0;
}
.beaver-activity-turn {
padding: var(--size-4-2) 0;
border-bottom: 1px solid var(--background-modifier-border);
}
.beaver-activity-turn:last-child { border-bottom: none; }
.beaver-activity-turn-line {
display: flex;
align-items: baseline;
gap: var(--size-4-2);
color: var(--text-muted);
font-size: var(--font-ui-smaller);
}
.beaver-activity-status {
font-weight: var(--font-semibold);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.beaver-activity-turn.is-running .beaver-activity-status { color: var(--color-yellow); }
.beaver-activity-turn.is-done .beaver-activity-status { color: var(--color-green); }
.beaver-activity-turn.is-interrupted .beaver-activity-status { color: var(--color-orange); }
.beaver-activity-turn.is-error .beaver-activity-status { color: var(--color-red); }
.beaver-activity-duration { margin-left: auto; color: var(--text-faint); white-space: nowrap; }
.beaver-activity-usage {
color: var(--text-faint);
font-size: var(--font-ui-smaller);
font-variant-numeric: tabular-nums;
}
.beaver-activity-say {
display: flex;
gap: var(--size-4-1);
margin-top: var(--size-4-1);
color: var(--text-normal);
}
.beaver-activity-icon svg { width: 14px; height: 14px; vertical-align: -2px; }
.beaver-activity-tree { margin-top: var(--size-4-1); }
.beaver-activity-tool-row {
display: flex;
align-items: baseline;
gap: var(--size-4-1);
padding: 1px 0;
cursor: pointer;
min-width: 0;
}
.beaver-activity-tool-row:hover { background: var(--background-modifier-hover); }
.beaver-activity-dot {
flex: none;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--text-faint);
align-self: center;
}
.beaver-activity-tool.is-running > .beaver-activity-tool-row .beaver-activity-dot {
background: var(--color-yellow);
animation: beaver-activity-pulse 1.2s ease-in-out infinite;
}
.beaver-activity-tool.is-done > .beaver-activity-tool-row .beaver-activity-dot { background: var(--color-green); }
.beaver-activity-tool.is-error > .beaver-activity-tool-row .beaver-activity-dot { background: var(--color-red); }
.beaver-activity-tool.is-aborted > .beaver-activity-tool-row .beaver-activity-dot { background: var(--color-orange); }
@keyframes beaver-activity-pulse {
50% { opacity: 0.3; }
}
.beaver-activity-tool-name {
flex: none;
font-family: var(--font-monospace);
font-size: var(--font-ui-smaller);
color: var(--text-normal);
}
.beaver-activity-tool-summary {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-muted);
}
.beaver-activity-tool-details pre {
margin: var(--size-4-1) 0 var(--size-4-1) 12px;
padding: var(--size-4-1) var(--size-4-2);
max-height: 240px;
overflow: auto;
font-size: var(--font-ui-smaller);
white-space: pre-wrap;
word-break: break-word;
background: var(--background-secondary);
border-radius: var(--radius-s);
}
.beaver-activity-tool-details pre.is-result { border-left: 2px solid var(--color-green); }
.beaver-activity-tool.is-error > .beaver-activity-tool-details pre.is-result { border-left-color: var(--color-red); }
.beaver-activity-children {
margin-left: 12px;
padding-left: var(--size-4-2);
border-left: 1px solid var(--background-modifier-border);
}
+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();
},
};
+305
View File
@@ -0,0 +1,305 @@
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,
client,
component: new Component(),
onOpenSettings: () => {},
state,
vaultSubpath: () => "💬 чаты",
},
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("Task") && 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")
);
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",
rows.length >= 4 &&
text().includes("Branches") &&
text().includes("Deep chats"),
`${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);
+92
View File
@@ -0,0 +1,92 @@
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/);
});
});
+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"]
} }
+2 -1
View File
@@ -1,4 +1,5 @@
{ {
"0.1.0": "1.5.0", "0.1.0": "1.5.0",
"0.2.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"],
},
});