Compare commits
19
Commits
755a475f27
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43d1e1a4a3 | ||
|
|
223560c8da | ||
|
|
70cf985aa1 | ||
|
|
413d68fc87 | ||
|
|
a04ad0094f | ||
|
|
cc46abc34f | ||
|
|
a76ee29255 | ||
|
|
68870864a6 | ||
|
|
0887728a8c | ||
|
|
cec5aef98c | ||
|
|
126e8b0451 | ||
|
|
b619c75119 | ||
|
|
353ee143f6 | ||
|
|
ab22efff7b | ||
|
|
b46dab194b | ||
|
|
8cefc54552 | ||
|
|
8e74439b4d | ||
|
|
04400c5076 | ||
|
|
a1bf4db905 |
No files matched your search
@@ -3,7 +3,11 @@ node_modules/
|
|||||||
|
|
||||||
# build output (regenerated by `make install` / `make zip`)
|
# build output (regenerated by `make install` / `make zip`)
|
||||||
main.js
|
main.js
|
||||||
|
styles.css
|
||||||
|
.tailwind.css
|
||||||
*.zip
|
*.zip
|
||||||
|
preview/bundle.js
|
||||||
|
tests/.entry.ts
|
||||||
|
|
||||||
# plugin runtime — Obsidian writes settings here when the symlinked dir
|
# plugin runtime — Obsidian writes settings here when the symlinked dir
|
||||||
# is used as the live plugin; never want it in the repo
|
# is used as the live plugin; never want it in the repo
|
||||||
@@ -14,3 +18,4 @@ data.json
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
preview/shots/
|
||||||
@@ -1,15 +1,25 @@
|
|||||||
PLUGIN_ID := beaver
|
PLUGIN_ID := beaver
|
||||||
ASSETS := manifest.json main.js versions.json
|
ASSETS := manifest.json main.js styles.css versions.json
|
||||||
PM := $(shell command -v pnpm 2>/dev/null || command -v npm 2>/dev/null)
|
|
||||||
|
|
||||||
.PHONY: install zip _build
|
.PHONY: sync check fix test build install zip preview
|
||||||
|
|
||||||
_build:
|
sync:
|
||||||
@test -n "$(PM)" || (echo "no pnpm or npm in PATH" >&2; exit 1)
|
bun install
|
||||||
$(PM) install --silent
|
|
||||||
$(PM) run build
|
|
||||||
|
|
||||||
install: _build
|
check:
|
||||||
|
bun lint
|
||||||
|
bun run check
|
||||||
|
|
||||||
|
fix:
|
||||||
|
bun fix
|
||||||
|
|
||||||
|
test:
|
||||||
|
bun run test
|
||||||
|
|
||||||
|
build:
|
||||||
|
bun run build
|
||||||
|
|
||||||
|
install: build
|
||||||
@test -n "$(VAULT)" || (echo "set VAULT=<path-to-vault>" >&2; exit 1)
|
@test -n "$(VAULT)" || (echo "set VAULT=<path-to-vault>" >&2; exit 1)
|
||||||
@test -d "$(VAULT)/.obsidian" || (echo "no .obsidian dir at $(VAULT)" >&2; exit 1)
|
@test -d "$(VAULT)/.obsidian" || (echo "no .obsidian dir at $(VAULT)" >&2; exit 1)
|
||||||
@target="$(VAULT)/.obsidian/plugins/$(PLUGIN_ID)"; \
|
@target="$(VAULT)/.obsidian/plugins/$(PLUGIN_ID)"; \
|
||||||
@@ -17,9 +27,12 @@ install: _build
|
|||||||
cp $(ASSETS) "$$target/"; \
|
cp $(ASSETS) "$$target/"; \
|
||||||
echo "installed → $$target"
|
echo "installed → $$target"
|
||||||
|
|
||||||
zip: _build
|
zip: build
|
||||||
@version=$$(node -p "require('./manifest.json').version"); \
|
@version=$$(node -p "require('./manifest.json').version"); \
|
||||||
out="beaver-plugin-$$version.zip"; \
|
out="beaver-plugin-$$version.zip"; \
|
||||||
rm -f "$$out"; \
|
rm -f "$$out"; \
|
||||||
zip -q -j "$$out" $(ASSETS); \
|
zip -q -j "$$out" $(ASSETS); \
|
||||||
echo "wrote $$out"
|
echo "wrote $$out"
|
||||||
|
|
||||||
|
preview:
|
||||||
|
bun run preview
|
||||||
@@ -1,56 +1,63 @@
|
|||||||
# Beaver — Obsidian plugin
|
# Beaver - Obsidian plugin
|
||||||
|
|
||||||
[](https://sladge.net)
|
[](https://sladge.net)
|
||||||
|
|
||||||
Sends the current note to Beaver's `markdown frontend` (`POST /chat` on `beaver-gateway`) and writes the agent's reply back into the file. Skips the Obsidian Sync round-trip by POSTing the buffer contents directly.
|
Two things:
|
||||||
|
|
||||||
|
- **The panel**: Beaver's conversations next to your notes - the master thread, branches, deep chats - with the live tree of tool calls and subagents, the composer, and the same actions the admin has (branch with a seed, return a branch to Telegram or hide it, the memory flag, close with a digest). A narrow sidedock column, a full tab, or the phone screen. It follows the active note's `conversation_id` unless you pin it.
|
||||||
|
- **Send the current note** to Beaver's markdown frontend (`POST /chat/stream`) and stream the agent's reply back into the buffer.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
Both commands appear in the command palette only when the active note's YAML frontmatter has an `agent:` field:
|
- **Beaver: Open panel in a tab** - the wide layout with the conversation rail on the left (also the ribbon icon).
|
||||||
|
- **Beaver: Open panel** - the narrow column in the right sidedock; a tab and a column can be open at once, each command reuses its own.
|
||||||
|
- **Beaver: Open chat in panel** - the active note has `conversation_id` in its frontmatter; the panel shows that conversation.
|
||||||
|
- **Beaver: Send using selected agent** / **Send using different agent** - the note has `agent:` in its frontmatter.
|
||||||
|
|
||||||
- **Beaver: Send using selected agent** — uses `frontmatter.agent`.
|
## The panel
|
||||||
- **Beaver: Send using different agent** — fetches the agent list from the gateway and opens a fuzzy picker.
|
|
||||||
|
The UI is `beaver-gateway/ui/src/lib/panel`, compiled into this plugin by `esbuild.config.mjs` (`$lib/*` points at the gateway's `ui/src/lib`; every bare import resolves from this plugin's `node_modules`, so there is one Svelte runtime; `svelte-sonner` becomes Obsidian notices). The same components render the browser admin and `/admin/panel`.
|
||||||
|
|
||||||
|
Theme: `src/tailwind.css` maps the panel's tokens onto Obsidian's variables on `.beaver-root`, exactly as beaver-calendar does with `.bcal-root` - the app's font, accent, radii and the current theme, light or dark. Menus and dialogs portal to a `.beaver-root.beaver-portal` layer on `body`, above the sidedocks.
|
||||||
|
|
||||||
|
Markdown in the thread goes through Obsidian's `MarkdownRenderer`: `[[links]]` open the note, external links open the browser. A deep chat's row offers **Open note** (its markdown binding, under the chats folder).
|
||||||
|
|
||||||
|
Layout: below 48rem of panel width the conversation switcher sits in the header (kind, title, live dot, follow toggle); wider than that the grouped rail (master, branches, deep chats, jobs) sits on the left. Right-click a row for its actions; on touch the `…` button is always visible. `↑`/`↓` walk the rows, `/` jumps to the search, `⌘↩` sends.
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
- **Base URL** — markdown-frontend root, e.g. `http://localhost:62993`.
|
- **Base URL** - markdown frontend root, e.g. `https://host/md`.
|
||||||
- **Bearer token** — a token with the `messages` scope (mint via the admin frontend or `BOOTSTRAP_TOKENS`).
|
- **API origin** - where `/api/…` lives; empty derives it from the base URL (`…/md` → `…`).
|
||||||
- **Test connection** — calls `GET /agents`.
|
- **Bearer token** - `messages` scope for sending, `api` scope for the panel (or a `*` bootstrap token).
|
||||||
|
- **Chats folder** - the folder in this vault where the deep chats live, the one the gateway's markdown frontend writes to. Paths the gateway hands out are relative to it.
|
||||||
|
|
||||||
## Note format
|
Changing settings remounts open panels.
|
||||||
|
|
||||||
A minimal note that the plugin can send:
|
## Build
|
||||||
|
|
||||||
```markdown
|
```
|
||||||
---
|
bun install
|
||||||
agent: beaver-opus-medium
|
bun run dev # css + esbuild watch
|
||||||
---
|
bun run build # svelte-check + css (minified, scoped) + bundle
|
||||||
|
bun run test # vitest (theme rules) + tests/smoke.mjs (jsdom, fake gateway)
|
||||||
hi there
|
bun run preview # the panel in a browser, no Obsidian: http://localhost:4174/#<scene>
|
||||||
|
make check # ultracite + svelte-check
|
||||||
|
make install VAULT=~/Obsidian/my-vault
|
||||||
|
make zip
|
||||||
```
|
```
|
||||||
|
|
||||||
The gateway parses `### User:` / `### Assistant:` markers; a note with no markers is treated as a single user turn. After a successful send, the gateway returns the new file content (including an `### Assistant:` block and a fresh `### User:` scaffold), and the plugin writes it back to the buffer.
|
Preview scenes (`#thread` default, `#switcher`, `#closed`, `#menu`, `#actions`, `#activity`, `#branch`, `#question`, `#deep`, `#empty`, `#offline`, `#stream`; suffix `-light` for the light theme). Width is the window: run headless Chrome with `--window-size=340,720` for the sidedock, `1100,720` for a tab, `390,800` for a phone. Set `VAULT=` or `THEME=` to preview against a community theme; `app.css` is read from the Obsidian install.
|
||||||
|
|
||||||
|
`styles.css` is Tailwind output run through `scripts/scope-css.mjs`: every selector gets `:is(.beaver-root, .beaver-root *)` in front. Other Tailwind plugins (beaver-calendar for one) put their utilities into the same `utilities` cascade layer, and without the extra specificity whichever plugin loaded last would win - its `.hidden` over ours and the other way round.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```
|
`make install VAULT=…` copies `manifest.json`, `main.js`, `styles.css`, `versions.json` into `<vault>/.obsidian/plugins/beaver`; then enable **Beaver** in Settings → Community plugins. On a phone, sync the four files with Obsidian Sync ("Installed community plugins") or any file sync, and point the settings at a URL the phone can reach.
|
||||||
make install VAULT=~/Obsidian/my-vault # build + copy manifest.json + main.js into <vault>/.obsidian/plugins/beaver
|
|
||||||
make zip # build → beaver-plugin-<version>.zip you can carry to any vault
|
|
||||||
```
|
|
||||||
|
|
||||||
Then enable **Beaver** in Settings → Community plugins.
|
## Version
|
||||||
|
|
||||||
### Mobile
|
Edit `manifest.json`, `package.json` and `versions.json` by hand.
|
||||||
|
|
||||||
The plugin is desktop-and-mobile (`isDesktopOnly: false`). Two things to get right on a phone:
|
|
||||||
|
|
||||||
- The base URL in plugin settings has to be reachable from the phone — `http://localhost:62993` won't work; use the gateway's LAN IP, a tunnel, or a public hostname.
|
|
||||||
- Get the plugin files onto the phone vault via Obsidian Sync (toggle "Installed community plugins" in your sync settings) or any file-sync tool you already use (Working Copy, Syncthing, etc.). `make install` only knows how to write to a local path.
|
|
||||||
|
|
||||||
### Hand-rolled
|
|
||||||
|
|
||||||
Three files are all Obsidian needs — `manifest.json`, `main.js`, `versions.json`. Drop them in `<vault>/.obsidian/plugins/beaver/` however you like (unzip, `scp`, `rsync`, your own git, …).
|
|
||||||
|
|
||||||
## Bumping the version
|
|
||||||
|
|
||||||
Edit the version field in `manifest.json`, `package.json`, and `versions.json` by hand. Three files, one change each.
|
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||||
|
"extends": ["ultracite/biome/core", "ultracite/biome/svelte"],
|
||||||
|
"javascript": {
|
||||||
|
"globals": ["Bun"]
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"includes": ["**/*", "!main.js", "!styles.css", "!preview/bundle.js"]
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"performance": {
|
||||||
|
"noNamespaceImport": "off"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noConsole": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"includes": ["**/*.svelte"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"correctness": {
|
||||||
|
"noUndeclaredVariables": "off",
|
||||||
|
"noUnusedFunctionParameters": "off",
|
||||||
|
"noUnusedVariables": "off"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"useFilenamingConvention": "off"
|
||||||
|
},
|
||||||
|
"a11y": {
|
||||||
|
"noSvgWithoutTitle": "warn",
|
||||||
|
"useButtonType": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"includes": ["src/**"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"correctness": {
|
||||||
|
"noUnusedInstantiation": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"includes": ["tests/**", "preview/**", "scripts/**"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"performance": {
|
||||||
|
"useTopLevelRegex": "off"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"noMagicNumbers": "off"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noEmptyBlockStatements": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,721 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "beaver-plugin-obsidian",
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^2.5.11",
|
||||||
|
"@electron/asar": "^4.3.0",
|
||||||
|
"@lucide/svelte": "^1.37.0",
|
||||||
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
|
"@types/node": "^26.4.0",
|
||||||
|
"bits-ui": "^2.19.0",
|
||||||
|
"builtin-modules": "^5.3.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"d3-force": "^3.0.0",
|
||||||
|
"dompurify": "^3.4.14",
|
||||||
|
"esbuild": "^0.28.2",
|
||||||
|
"esbuild-svelte": "^0.9.5",
|
||||||
|
"jsdom": "^30.0.1",
|
||||||
|
"marked": "^18.0.11",
|
||||||
|
"obsidian": "^1.13.1",
|
||||||
|
"svelte": "^5.57.0",
|
||||||
|
"svelte-check": "^4.7.6",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tailwind-variants": "^3.3.1",
|
||||||
|
"tailwindcss": "^4.3.3",
|
||||||
|
"typescript": "^6",
|
||||||
|
"ultracite": "^7.10.7",
|
||||||
|
"vitest": "^4.1.11",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@6.0.7", "", { "dependencies": { "@csstools/css-calc": "^3.3.0", "@csstools/css-color-parser": "^4.1.10", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.5.2" } }, "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw=="],
|
||||||
|
|
||||||
|
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@8.3.2", "", { "dependencies": { "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2" } }, "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||||
|
|
||||||
|
"@biomejs/biome": ["@biomejs/biome@2.5.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.11", "@biomejs/cli-darwin-x64": "2.5.11", "@biomejs/cli-linux-arm64": "2.5.11", "@biomejs/cli-linux-arm64-musl": "2.5.11", "@biomejs/cli-linux-x64": "2.5.11", "@biomejs/cli-linux-x64-musl": "2.5.11", "@biomejs/cli-win32-arm64": "2.5.11", "@biomejs/cli-win32-x64": "2.5.11" }, "bin": { "biome": "bin/biome" } }, "sha512-Tj0dnkLPdW0ASjHfj2D/ZkkvPU2wrFmnE1jWTD2xzV1ycapV1DutbYXk4NDnR3rYTi1ZCbNFD4G2gRMEY65WaA=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6SGZxoKbXvUjMn1t6A98HqWISPnGNbYs0R/Rt2JarmXBSev+lva4QxUMWEBX9lX1Wo1XTJ78uk5xVDtG58SRZg=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-nYkXY7tLBEgnGbYapDKAyKzgt44ZEyG+AKalvTXtCWKYgepI9dw327q+cVgedxm+Udi1ZzHKUyZrIusHi/KQbw=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-3PVLSTD9RR73rvVPt5G3T1gc+ycggWEGfTD7RvzzbtcDPD27NxgxBbAFfpm7DXJKW6VLHWE1lLMGvFt2Qxjcow=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-qhyZUMyCbWYFV2bAwRNVvfMVZ+hv7WYl6mossGrxC+uiQQXhvsuWWU8zz6jYX0mChZd9MgQZbm4vozTmG/5iGw=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-JOytptlsgM33B2MMFUg8iBrb4IKpbD5JnJrSeYiaFEeAj4vuXx0iQSQZ4qK7sqyMtfjZxxPdNdMZZVL4y/mFyA=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-oRRlrchG5EfrEL/EmtT1qUjSNHk3/5LGeZhQqADBBAJF1b1ET6964xEKe7aGlGARzDfza8H/seEsFJl7S6Ql9w=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-e49E6K9hzH/ohJNx8Y26mY8HaV4I4ZViIeoqhKsmoXLKHhQnMeBAVqCgsGf2Wa3lXlS7RkporDXMHHWkzvZzFw=="],
|
||||||
|
|
||||||
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.11", "", { "os": "win32", "cpu": "x64" }, "sha512-QSQr/KjOgXA7OzXJUWS+oguKyAZ3Q0l/lnlDGbu397eKo83atuWUjBPJrsqbKNF6CARGw8XXJLGzpHC8Ryhd4Q=="],
|
||||||
|
|
||||||
|
"@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
|
||||||
|
|
||||||
|
"@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="],
|
||||||
|
|
||||||
|
"@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
|
||||||
|
|
||||||
|
"@codemirror/state": ["@codemirror/state@6.5.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw=="],
|
||||||
|
|
||||||
|
"@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="],
|
||||||
|
|
||||||
|
"@csstools/color-helpers": ["@csstools/color-helpers@6.1.1", "", {}, "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w=="],
|
||||||
|
|
||||||
|
"@csstools/css-calc": ["@csstools/css-calc@3.3.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ=="],
|
||||||
|
|
||||||
|
"@csstools/css-color-parser": ["@csstools/css-color-parser@4.2.1", "", { "dependencies": { "@csstools/color-helpers": "^6.1.1", "@csstools/css-calc": "^3.3.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-YpAJZhaHplYQkG8ib+/Fx5Y0eF2lVWi3tIvMJA6i39TLyUNp2439cifzW8VMjhlqrBjHzK5hVGugRRm2zTKI/A=="],
|
||||||
|
|
||||||
|
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
|
||||||
|
|
||||||
|
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.9", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A=="],
|
||||||
|
|
||||||
|
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
|
||||||
|
|
||||||
|
"@electron/asar": ["@electron/asar@4.3.0", "", { "dependencies": { "glob": "^13.0.2", "minimatch": "^10.0.1" }, "bin": { "asar": "bin/asar.mjs" } }, "sha512-k/FFC/NQoTykGBi/Ga4L4KorsAKDdkK0LfNVG90eUG6vPVpJaL3iR9V1ZLbzBAF3QpojK5DcaGOdQOheKZv4JQ=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||||
|
|
||||||
|
"@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="],
|
||||||
|
|
||||||
|
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
|
||||||
|
|
||||||
|
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
|
||||||
|
|
||||||
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
|
||||||
|
|
||||||
|
"@internationalized/date": ["@internationalized/date@3.12.3", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@lucide/svelte": ["@lucide/svelte@1.37.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-AxaiSxeuErlt4fNmmN28oaL5R7yVoQLsfSeiAONxCEK+l0/SrLAjRktQ0DkTQpomv1oRzOA3hohWY0ZHTZCMmw=="],
|
||||||
|
|
||||||
|
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.4", "", {}, "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||||
|
|
||||||
|
"@oxc-project/types": ["@oxc-project/types@0.147.0", "", {}, "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg=="],
|
||||||
|
|
||||||
|
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="],
|
||||||
|
|
||||||
|
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.6", "", { "os": "android", "cpu": "arm" }, "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.6", "", { "os": "android", "cpu": "arm64" }, "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.6", "", { "os": "linux", "cpu": "arm" }, "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.6", "", { "os": "none", "cpu": "arm64" }, "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||||
|
|
||||||
|
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||||
|
|
||||||
|
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="],
|
||||||
|
|
||||||
|
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
|
||||||
|
|
||||||
|
"@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/cli": ["@tailwindcss/cli@4.3.3", "", { "dependencies": { "@parcel/watcher": "2.5.1", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "enhanced-resolve": "^5.24.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.3.3" }, "bin": { "tailwindcss": "./dist/index.mjs" } }, "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
||||||
|
|
||||||
|
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
|
|
||||||
|
"@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="],
|
||||||
|
|
||||||
|
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
|
||||||
|
|
||||||
|
"@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="],
|
||||||
|
|
||||||
|
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||||
|
|
||||||
|
"@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="],
|
||||||
|
|
||||||
|
"@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="],
|
||||||
|
|
||||||
|
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="],
|
||||||
|
|
||||||
|
"@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="],
|
||||||
|
|
||||||
|
"@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="],
|
||||||
|
|
||||||
|
"@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="],
|
||||||
|
|
||||||
|
"@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||||
|
|
||||||
|
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
|
|
||||||
|
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
||||||
|
|
||||||
|
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||||
|
|
||||||
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
|
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||||
|
|
||||||
|
"bits-ui": ["bits-ui@2.19.0", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-dHDm5Jw2NZJgLRvHAwPeuR4BnZij+AqTIq33OqVfo+qiMXontoC/yIvGT5TZaviv7OuBdJ2QUy2JPQ2yKBvF6w=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
|
||||||
|
|
||||||
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"builtin-modules": ["builtin-modules@5.3.0", "", {}, "sha512-hMQUl2bUFG339QygPM97E+mc8OY1IAchORZxm4a/frcYwKzozMzRVDBwHW0NjOqGElLm2O37AVQE8ikxlZHrMQ=="],
|
||||||
|
|
||||||
|
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
|
||||||
|
|
||||||
|
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||||
|
|
||||||
|
"cli-truncate": ["cli-truncate@6.1.1", "", { "dependencies": { "slice-ansi": "^9.0.0", "string-width": "^8.2.0" } }, "sha512-06p9vyLahLa4zkGcgsGxU6iEkSOiuI4fhCH6Emhe2lPAcoUv73n72DnODsnHA+5wwXGnV0n9M9/qOQJSjYhFhw=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
|
||||||
|
|
||||||
|
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||||
|
|
||||||
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
|
"crelt": ["crelt@1.0.7", "", {}, "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA=="],
|
||||||
|
|
||||||
|
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
|
||||||
|
|
||||||
|
"d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
|
||||||
|
|
||||||
|
"d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
|
||||||
|
|
||||||
|
"d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
|
||||||
|
|
||||||
|
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||||
|
|
||||||
|
"data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
|
||||||
|
|
||||||
|
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
|
||||||
|
|
||||||
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
|
|
||||||
|
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
||||||
|
|
||||||
|
"devalue": ["devalue@5.9.2", "", {}, "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w=="],
|
||||||
|
|
||||||
|
"dompurify": ["dompurify@3.4.14", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg=="],
|
||||||
|
|
||||||
|
"empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||||
|
|
||||||
|
"entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
|
||||||
|
|
||||||
|
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
|
||||||
|
|
||||||
|
"es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
||||||
|
|
||||||
|
"esbuild-svelte": ["esbuild-svelte@0.9.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.19" }, "peerDependencies": { "esbuild": ">=0.17.0", "svelte": ">=4.2.1 <6" } }, "sha512-16FUSj64aiS28CCxYeK3hVxCyU4PwGBSkeArawAtnWSV7l0Gc+frIOP88MNj8Q7tDAGyEJAHT6OpOvM24BG46w=="],
|
||||||
|
|
||||||
|
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||||
|
|
||||||
|
"esrap": ["esrap@2.3.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A=="],
|
||||||
|
|
||||||
|
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||||
|
|
||||||
|
"execa": ["execa@10.0.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "figures": "^6.1.0", "get-stream": "^9.0.1", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.3.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "which-command": "^0.1.0", "yoctocolors": "^2.1.2" } }, "sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw=="],
|
||||||
|
|
||||||
|
"expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="],
|
||||||
|
|
||||||
|
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
|
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
|
||||||
|
|
||||||
|
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
|
||||||
|
|
||||||
|
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
|
||||||
|
|
||||||
|
"fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||||
|
|
||||||
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
|
"find-workspaces": ["find-workspaces@0.3.1", "", { "dependencies": { "fast-glob": "^3.3.2", "pkg-types": "^1.0.3", "yaml": "^2.3.4" } }, "sha512-UDkGILGJSA1LN5Aa7McxCid4sqW3/e+UYsVwyxki3dDT0F8+ym0rAfnCkEfkL0rO7M+8/mvkim4t/s3IPHmg+w=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||||
|
|
||||||
|
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
||||||
|
|
||||||
|
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
|
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
||||||
|
|
||||||
|
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||||
|
|
||||||
|
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||||
|
|
||||||
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
||||||
|
|
||||||
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
|
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||||
|
|
||||||
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
|
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
|
||||||
|
|
||||||
|
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||||
|
|
||||||
|
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||||
|
|
||||||
|
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||||
|
|
||||||
|
"jsdom": ["jsdom@30.0.1", "", { "dependencies": { "@asamuzakjp/css-color": "^6.0.5", "@asamuzakjp/dom-selector": "^8.3.0", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.7", "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.2", "undici": "^8.9.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^17.1.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.2.3" }, "optionalPeers": ["canvas"] }, "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA=="],
|
||||||
|
|
||||||
|
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||||
|
|
||||||
|
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||||
|
|
||||||
|
"log-update": ["log-update@8.0.0", "", { "dependencies": { "ansi-escapes": "^7.3.0", "cli-cursor": "^5.0.0", "slice-ansi": "^9.0.0", "string-width": "^8.2.0", "strip-ansi": "^7.2.0", "wrap-ansi": "^10.0.0" } }, "sha512-lddSgOt3bPASrylL54ZSpy8nBHns+vBVSoILlVOx+dei300pnLRN958rj/EdlVLKuWlSESU3qdnDZdAI7FXYGg=="],
|
||||||
|
|
||||||
|
"lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
|
||||||
|
|
||||||
|
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"magicast": ["magicast@0.5.4", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w=="],
|
||||||
|
|
||||||
|
"marked": ["marked@18.0.11", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw=="],
|
||||||
|
|
||||||
|
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||||
|
|
||||||
|
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
|
|
||||||
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
|
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
|
||||||
|
|
||||||
|
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||||
|
|
||||||
|
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
|
||||||
|
|
||||||
|
"moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="],
|
||||||
|
|
||||||
|
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
||||||
|
|
||||||
|
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||||
|
|
||||||
|
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
|
||||||
|
|
||||||
|
"nypm": ["nypm@0.6.9", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.2.4" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w=="],
|
||||||
|
|
||||||
|
"obsidian": ["obsidian@1.13.1", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-qtTEA2pmhJzhuhJqzbBFRYhpIOqvW+krDYjtFynv66KbxBbumHBlsJfWw3I4jtnK/6fZwbQhCrmmDdRwXmX56w=="],
|
||||||
|
|
||||||
|
"obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
|
||||||
|
|
||||||
|
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||||
|
|
||||||
|
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
||||||
|
|
||||||
|
"parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||||
|
|
||||||
|
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
|
||||||
|
|
||||||
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
|
||||||
|
|
||||||
|
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
|
||||||
|
|
||||||
|
"pretty-ms": ["pretty-ms@9.3.1", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA=="],
|
||||||
|
|
||||||
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
|
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
|
||||||
|
|
||||||
|
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||||
|
|
||||||
|
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
|
|
||||||
|
"rolldown": ["rolldown@1.2.6", "", { "dependencies": { "@oxc-project/types": "=0.147.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.6", "@rolldown/binding-android-arm64": "1.2.6", "@rolldown/binding-darwin-arm64": "1.2.6", "@rolldown/binding-darwin-x64": "1.2.6", "@rolldown/binding-freebsd-x64": "1.2.6", "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", "@rolldown/binding-linux-arm64-gnu": "1.2.6", "@rolldown/binding-linux-arm64-musl": "1.2.6", "@rolldown/binding-linux-ppc64-gnu": "1.2.6", "@rolldown/binding-linux-s390x-gnu": "1.2.6", "@rolldown/binding-linux-x64-gnu": "1.2.6", "@rolldown/binding-linux-x64-musl": "1.2.6", "@rolldown/binding-openharmony-arm64": "1.2.6", "@rolldown/binding-win32-arm64-msvc": "1.2.6", "@rolldown/binding-win32-x64-msvc": "1.2.6" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA=="],
|
||||||
|
|
||||||
|
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||||
|
|
||||||
|
"runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
|
||||||
|
|
||||||
|
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||||
|
|
||||||
|
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
||||||
|
|
||||||
|
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||||
|
|
||||||
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
|
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||||
|
|
||||||
|
"slice-ansi": ["slice-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||||
|
|
||||||
|
"std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
|
||||||
|
|
||||||
|
"string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||||
|
|
||||||
|
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
||||||
|
|
||||||
|
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
||||||
|
|
||||||
|
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||||
|
|
||||||
|
"svelte": ["svelte@5.57.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg=="],
|
||||||
|
|
||||||
|
"svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
|
||||||
|
|
||||||
|
"svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="],
|
||||||
|
|
||||||
|
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
|
||||||
|
|
||||||
|
"tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
|
||||||
|
|
||||||
|
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
|
||||||
|
|
||||||
|
"tailwind-variants": ["tailwind-variants@3.3.1", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge", "tailwindcss"] }, "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||||
|
|
||||||
|
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||||
|
|
||||||
|
"tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||||
|
|
||||||
|
"tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="],
|
||||||
|
|
||||||
|
"tldts": ["tldts@7.4.11", "", { "dependencies": { "tldts-core": "^7.4.11" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw=="],
|
||||||
|
|
||||||
|
"tldts-core": ["tldts-core@7.4.11", "", {}, "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg=="],
|
||||||
|
|
||||||
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="],
|
||||||
|
|
||||||
|
"tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||||
|
|
||||||
|
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
|
||||||
|
|
||||||
|
"ultracite": ["ultracite@7.10.7", "", { "dependencies": { "@clack/prompts": "^1.5.1", "cli-truncate": "^6.1.1", "commander": "^15.0.0", "deepmerge": "^4.3.1", "empathic": "^2.0.1", "execa": "^10.0.1", "fast-glob": "^3.3.3", "find-workspaces": "^0.3.1", "jsonc-parser": "^3.3.1", "log-update": "^8.0.0", "magicast": "^0.5.4", "nypm": "^0.6.9", "resolve.exports": "^2.0.3", "string-width": "^8.2.2", "yaml": "^2.9.0", "zod": "^4.4.3" }, "peerDependencies": { "oxfmt": ">=0.1.0", "oxlint": "^1.79.0" }, "optionalPeers": ["oxfmt", "oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-rc+TG/zLCV+Uk1PL4XPmsq7CW8GtlaV4g7Q53Bt5o7UNGSKKNRoPY4syz1BDWwSoNBcvWKpSnfSVflsecok4EA=="],
|
||||||
|
|
||||||
|
"undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
|
||||||
|
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||||
|
|
||||||
|
"vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="],
|
||||||
|
|
||||||
|
"vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="],
|
||||||
|
|
||||||
|
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
||||||
|
|
||||||
|
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||||
|
|
||||||
|
"webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
|
||||||
|
|
||||||
|
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
|
||||||
|
|
||||||
|
"whatwg-url": ["whatwg-url@17.1.0", "", { "dependencies": { "@exodus/bytes": "^1.15.1", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw=="],
|
||||||
|
|
||||||
|
"which-command": ["which-command@0.1.0", "", { "bin": { "which-command": "cli.js" } }, "sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA=="],
|
||||||
|
|
||||||
|
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||||
|
|
||||||
|
"wrap-ansi": ["wrap-ansi@10.0.1", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0" } }, "sha512-M0N4xzyzosiIok3svYlEo1sdLZts/8FPgYH/GPC3wvlmPoRvnoManGMrE54waYj3tISA8w6lsdesfVv67qSr8Q=="],
|
||||||
|
|
||||||
|
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
|
||||||
|
|
||||||
|
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
|
||||||
|
|
||||||
|
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||||
|
|
||||||
|
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
|
||||||
|
|
||||||
|
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.5.2", "", {}, "sha512-XkYXCol10+ba/6F/cueWV+TezUeOqXW0hdeJt5CdXjTYeAgAQg5N03RQdJ80mhfFE72+pblvYMW4wy2Qp4Qbrg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"data-urls/whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
|
||||||
|
|
||||||
|
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
|
"vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
|
||||||
|
|
||||||
|
"vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
+115
-34
@@ -1,40 +1,121 @@
|
|||||||
import esbuild from "esbuild";
|
import path from "node:path";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import builtins from "builtin-modules";
|
import builtins from "builtin-modules";
|
||||||
|
import esbuild from "esbuild";
|
||||||
|
import sveltePlugin from "esbuild-svelte";
|
||||||
|
|
||||||
|
export const root = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
export const LIB = path.resolve(root, "../beaver-gateway/ui/src/lib");
|
||||||
|
|
||||||
|
const EXTERNAL = [
|
||||||
|
"obsidian",
|
||||||
|
"electron",
|
||||||
|
"@codemirror/autocomplete",
|
||||||
|
"@codemirror/collab",
|
||||||
|
"@codemirror/commands",
|
||||||
|
"@codemirror/language",
|
||||||
|
"@codemirror/lint",
|
||||||
|
"@codemirror/search",
|
||||||
|
"@codemirror/state",
|
||||||
|
"@codemirror/view",
|
||||||
|
"@lezer/common",
|
||||||
|
"@lezer/highlight",
|
||||||
|
"@lezer/lr",
|
||||||
|
...builtins,
|
||||||
|
];
|
||||||
|
|
||||||
|
const LIB_IMPORT = /^\$lib\//;
|
||||||
|
const APP_IMPORT = /^\$app\//;
|
||||||
|
const SONNER_IMPORT = /^svelte-sonner$/;
|
||||||
|
const OBSIDIAN_IMPORT = /^obsidian$/;
|
||||||
|
const BARE_IMPORT = /^[^./#]/;
|
||||||
|
|
||||||
|
function isExternal(specifier) {
|
||||||
|
return (
|
||||||
|
specifier.startsWith("node:") ||
|
||||||
|
EXTERNAL.some(
|
||||||
|
(name) => specifier === name || specifier.startsWith(`${name}/`)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The panel is beaver-gateway's ``ui/src/lib``, compiled into this plugin.
|
||||||
|
// Two rules keep it one bundle: ``$lib/*`` points at that directory, and
|
||||||
|
// every bare import - svelte, bits-ui, the icons - resolves from this
|
||||||
|
// plugin's node_modules, never from the gateway's, so there is exactly one
|
||||||
|
// Svelte runtime. ``svelte-sonner`` becomes Obsidian notices; ``$app/*``
|
||||||
|
// is SvelteKit-only and refused.
|
||||||
|
export function sharedLib({ obsidian = "external" } = {}) {
|
||||||
|
return {
|
||||||
|
name: "beaver-shared-lib",
|
||||||
|
setup(build) {
|
||||||
|
build.onResolve({ filter: LIB_IMPORT }, (args) =>
|
||||||
|
build.resolve(`./${args.path.slice("$lib/".length)}`, {
|
||||||
|
kind: args.kind,
|
||||||
|
pluginData: { pinned: true },
|
||||||
|
resolveDir: LIB,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
build.onResolve({ filter: APP_IMPORT }, (args) => ({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
text: `${args.path} is SvelteKit-only; the panel must not import it`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
build.onResolve({ filter: SONNER_IMPORT }, () => ({
|
||||||
|
path: path.join(root, "src/shims/sonner.ts"),
|
||||||
|
}));
|
||||||
|
build.onResolve({ filter: OBSIDIAN_IMPORT }, () =>
|
||||||
|
obsidian === "external"
|
||||||
|
? { external: true, path: "obsidian" }
|
||||||
|
: { path: obsidian }
|
||||||
|
);
|
||||||
|
build.onResolve({ filter: BARE_IMPORT }, (args) => {
|
||||||
|
if (args.pluginData?.pinned) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isExternal(args.path)) {
|
||||||
|
return { external: true, path: args.path };
|
||||||
|
}
|
||||||
|
return build.resolve(args.path, {
|
||||||
|
kind: args.kind,
|
||||||
|
pluginData: { pinned: true },
|
||||||
|
resolveDir: root,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const svelte = () =>
|
||||||
|
sveltePlugin({
|
||||||
|
compilerOptions: { css: "injected", runes: true },
|
||||||
|
filterWarnings: (warning) => !warning.filename?.includes("node_modules"),
|
||||||
|
});
|
||||||
|
|
||||||
const prod = process.argv[2] === "production";
|
const prod = process.argv[2] === "production";
|
||||||
|
|
||||||
const ctx = await esbuild.context({
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||||
entryPoints: ["src/main.ts"],
|
const ctx = await esbuild.context({
|
||||||
bundle: true,
|
bundle: true,
|
||||||
external: [
|
conditions: ["svelte", "browser"],
|
||||||
"obsidian",
|
entryPoints: ["src/main.ts"],
|
||||||
"electron",
|
format: "cjs",
|
||||||
"@codemirror/autocomplete",
|
logLevel: "info",
|
||||||
"@codemirror/collab",
|
mainFields: ["svelte", "browser", "module", "main"],
|
||||||
"@codemirror/commands",
|
minify: prod,
|
||||||
"@codemirror/language",
|
outfile: "main.js",
|
||||||
"@codemirror/lint",
|
plugins: [sharedLib(), svelte()],
|
||||||
"@codemirror/search",
|
sourcemap: prod ? false : "inline",
|
||||||
"@codemirror/state",
|
target: "es2022",
|
||||||
"@codemirror/view",
|
treeShaking: true,
|
||||||
"@lezer/common",
|
});
|
||||||
"@lezer/highlight",
|
if (prod) {
|
||||||
"@lezer/lr",
|
await ctx.rebuild();
|
||||||
...builtins,
|
await ctx.dispose();
|
||||||
],
|
} else {
|
||||||
format: "cjs",
|
await ctx.watch();
|
||||||
target: "es2022",
|
}
|
||||||
logLevel: "info",
|
|
||||||
sourcemap: prod ? false : "inline",
|
|
||||||
treeShaking: true,
|
|
||||||
outfile: "main.js",
|
|
||||||
minify: prod,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (prod) {
|
|
||||||
await ctx.rebuild();
|
|
||||||
await ctx.dispose();
|
|
||||||
} else {
|
|
||||||
await ctx.watch();
|
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"id": "beaver",
|
"id": "beaver",
|
||||||
"name": "Beaver",
|
"name": "Beaver",
|
||||||
"version": "0.1.0",
|
"version": "0.3.0",
|
||||||
"minAppVersion": "1.5.0",
|
"minAppVersion": "1.5.0",
|
||||||
"description": "Send the current note to a Beaver agent via the markdown frontend.",
|
"description": "Beaver's panel: conversations, live tool calls and branches next to your notes; sends notes to the markdown frontend.",
|
||||||
"author": "h",
|
"author": "h",
|
||||||
"isDesktopOnly": false
|
"isDesktopOnly": false
|
||||||
}
|
}
|
||||||
Generated
-602
@@ -1,602 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "beaver-plugin-obsidian",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "beaver-plugin-obsidian",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^20.11.0",
|
|
||||||
"builtin-modules": "^4.0.0",
|
|
||||||
"esbuild": "^0.21.5",
|
|
||||||
"obsidian": "^1.5.7",
|
|
||||||
"typescript": "^5.4.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@codemirror/state": {
|
|
||||||
"version": "6.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
|
||||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@marijn/find-cluster-break": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@codemirror/view": {
|
|
||||||
"version": "6.38.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
|
||||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@codemirror/state": "^6.5.0",
|
|
||||||
"crelt": "^1.0.6",
|
|
||||||
"style-mod": "^4.1.0",
|
|
||||||
"w3c-keyname": "^2.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"aix"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
|
||||||
"cpu": [
|
|
||||||
"mips64el"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"sunos"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-x64": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@marijn/find-cluster-break": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/codemirror": {
|
|
||||||
"version": "5.60.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
|
|
||||||
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/tern": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
|
||||||
"version": "1.0.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
|
||||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "20.19.41",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
|
||||||
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": "~6.21.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/tern": {
|
|
||||||
"version": "0.23.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
|
||||||
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/estree": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/builtin-modules": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/crelt": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/esbuild": {
|
|
||||||
"version": "0.21.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
|
||||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"esbuild": "bin/esbuild"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@esbuild/aix-ppc64": "0.21.5",
|
|
||||||
"@esbuild/android-arm": "0.21.5",
|
|
||||||
"@esbuild/android-arm64": "0.21.5",
|
|
||||||
"@esbuild/android-x64": "0.21.5",
|
|
||||||
"@esbuild/darwin-arm64": "0.21.5",
|
|
||||||
"@esbuild/darwin-x64": "0.21.5",
|
|
||||||
"@esbuild/freebsd-arm64": "0.21.5",
|
|
||||||
"@esbuild/freebsd-x64": "0.21.5",
|
|
||||||
"@esbuild/linux-arm": "0.21.5",
|
|
||||||
"@esbuild/linux-arm64": "0.21.5",
|
|
||||||
"@esbuild/linux-ia32": "0.21.5",
|
|
||||||
"@esbuild/linux-loong64": "0.21.5",
|
|
||||||
"@esbuild/linux-mips64el": "0.21.5",
|
|
||||||
"@esbuild/linux-ppc64": "0.21.5",
|
|
||||||
"@esbuild/linux-riscv64": "0.21.5",
|
|
||||||
"@esbuild/linux-s390x": "0.21.5",
|
|
||||||
"@esbuild/linux-x64": "0.21.5",
|
|
||||||
"@esbuild/netbsd-x64": "0.21.5",
|
|
||||||
"@esbuild/openbsd-x64": "0.21.5",
|
|
||||||
"@esbuild/sunos-x64": "0.21.5",
|
|
||||||
"@esbuild/win32-arm64": "0.21.5",
|
|
||||||
"@esbuild/win32-ia32": "0.21.5",
|
|
||||||
"@esbuild/win32-x64": "0.21.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/moment": {
|
|
||||||
"version": "2.29.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
|
||||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/obsidian": {
|
|
||||||
"version": "1.12.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
|
|
||||||
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/codemirror": "5.60.8",
|
|
||||||
"moment": "2.29.4"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@codemirror/state": "6.5.0",
|
|
||||||
"@codemirror/view": "6.38.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/style-mod": {
|
|
||||||
"version": "4.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
|
||||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.9.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undici-types": {
|
|
||||||
"version": "6.21.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/w3c-keyname": {
|
|
||||||
"version": "2.2.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
|
||||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+36
-10
@@ -1,18 +1,44 @@
|
|||||||
{
|
{
|
||||||
"name": "beaver-plugin-obsidian",
|
"name": "beaver-plugin-obsidian",
|
||||||
"version": "0.1.0",
|
"version": "0.3.0",
|
||||||
"description": "Obsidian plugin for Beaver's markdown frontend.",
|
|
||||||
"main": "main.js",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"description": "Beaver's panel and markdown frontend for Obsidian.",
|
||||||
|
"type": "module",
|
||||||
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node esbuild.config.mjs",
|
"build": "bun run check && node scripts/css.mjs --minify && node esbuild.config.mjs production",
|
||||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
|
"check": "svelte-check --tsconfig ./tsconfig.json --threshold error",
|
||||||
|
"css": "node scripts/css.mjs",
|
||||||
|
"css:watch": "node scripts/css.mjs --watch",
|
||||||
|
"dev": "bun run css && node esbuild.config.mjs",
|
||||||
|
"fix": "ultracite fix",
|
||||||
|
"lint": "ultracite check",
|
||||||
|
"preview": "node preview/serve.mjs",
|
||||||
|
"test": "vitest run && node tests/smoke.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@biomejs/biome": "^2.5.11",
|
||||||
"builtin-modules": "^4.0.0",
|
"@electron/asar": "^4.3.0",
|
||||||
"esbuild": "^0.21.5",
|
"@lucide/svelte": "^1.37.0",
|
||||||
"obsidian": "^1.5.7",
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
"typescript": "^5.4.5"
|
"@types/node": "^26.4.0",
|
||||||
|
"bits-ui": "^2.19.0",
|
||||||
|
"builtin-modules": "^5.3.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"d3-force": "^3.0.0",
|
||||||
|
"dompurify": "^3.4.14",
|
||||||
|
"esbuild": "^0.28.2",
|
||||||
|
"esbuild-svelte": "^0.9.5",
|
||||||
|
"jsdom": "^30.0.1",
|
||||||
|
"marked": "^18.0.11",
|
||||||
|
"obsidian": "^1.13.1",
|
||||||
|
"svelte": "^5.57.0",
|
||||||
|
"svelte-check": "^4.7.6",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tailwind-variants": "^3.3.1",
|
||||||
|
"tailwindcss": "^4.3.3",
|
||||||
|
"typescript": "^6",
|
||||||
|
"ultracite": "^7.10.7",
|
||||||
|
"vitest": "^4.1.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { Component } from "obsidian";
|
||||||
|
import { mount } from "svelte";
|
||||||
|
import { PanelState } from "../src/state.svelte";
|
||||||
|
import App from "../src/ui/app.svelte";
|
||||||
|
import { BRANCH_BLASTER, DEEP_PANEL, FakeClient, MASTER } from "./fake-client";
|
||||||
|
|
||||||
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
// bits-ui opens menus on pointerdown and tabs on pointer or click: send both.
|
||||||
|
function press(element: Element | null | undefined): void {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
element.dispatchEvent(
|
||||||
|
new PointerEvent("pointerdown", {
|
||||||
|
bubbles: true,
|
||||||
|
button: 0,
|
||||||
|
cancelable: true,
|
||||||
|
pointerType: "mouse",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
(element as HTMLElement).focus();
|
||||||
|
(element as HTMLElement).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scenes for screenshots: open the page with a hash to land in a state.
|
||||||
|
// Width comes from the window (``--window-size``), the state from here.
|
||||||
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one flat scene list
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const client = new FakeClient();
|
||||||
|
const state = new PanelState();
|
||||||
|
const scene = location.hash.slice(1) || "thread";
|
||||||
|
state.selected = MASTER;
|
||||||
|
if (scene === "empty") {
|
||||||
|
state.selected = null;
|
||||||
|
}
|
||||||
|
if (scene === "question" || scene === "toggle") {
|
||||||
|
state.selected = BRANCH_BLASTER;
|
||||||
|
}
|
||||||
|
if (scene === "deep") {
|
||||||
|
state.selected = DEEP_PANEL;
|
||||||
|
}
|
||||||
|
if (scene === "offline") {
|
||||||
|
state.selected = MASTER;
|
||||||
|
}
|
||||||
|
document.body.classList.toggle("theme-light", scene.endsWith("-light"));
|
||||||
|
document.body.classList.toggle("theme-dark", !scene.endsWith("-light"));
|
||||||
|
|
||||||
|
const app = {
|
||||||
|
workspace: {
|
||||||
|
openLinkText: (target: string) => console.log("open", target),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mount(App, {
|
||||||
|
props: {
|
||||||
|
app: app as never,
|
||||||
|
chatsFolder: () => "💬 чаты",
|
||||||
|
client: scene === "offline" ? null : client,
|
||||||
|
component: new Component() as never,
|
||||||
|
onOpenSettings: () => console.log("settings"),
|
||||||
|
state,
|
||||||
|
},
|
||||||
|
target: document.getElementById("app") as HTMLElement,
|
||||||
|
});
|
||||||
|
await wait(80);
|
||||||
|
|
||||||
|
if (scene.startsWith("switcher")) {
|
||||||
|
document
|
||||||
|
.querySelector<HTMLButtonElement>('[aria-label="Switch conversation"]')
|
||||||
|
?.click();
|
||||||
|
}
|
||||||
|
if (scene.startsWith("menu")) {
|
||||||
|
const row =
|
||||||
|
document.querySelector<HTMLElement>(`[data-row="${BRANCH_BLASTER}"]`) ??
|
||||||
|
(await (async () => {
|
||||||
|
document
|
||||||
|
.querySelector<HTMLButtonElement>(
|
||||||
|
'[aria-label="Switch conversation"]'
|
||||||
|
)
|
||||||
|
?.click();
|
||||||
|
await wait(60);
|
||||||
|
return document.querySelector<HTMLElement>(
|
||||||
|
`[data-row="${BRANCH_BLASTER}"]`
|
||||||
|
);
|
||||||
|
})());
|
||||||
|
row?.dispatchEvent(
|
||||||
|
new MouseEvent("contextmenu", {
|
||||||
|
bubbles: true,
|
||||||
|
clientX: row.getBoundingClientRect().left + 120,
|
||||||
|
clientY: row.getBoundingClientRect().top + 16,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (scene.startsWith("toggle")) {
|
||||||
|
press(document.querySelector('[data-slot="switch"]'));
|
||||||
|
}
|
||||||
|
if (scene.startsWith("actions")) {
|
||||||
|
press(document.querySelector('[aria-label="Conversation actions"]'));
|
||||||
|
}
|
||||||
|
if (scene.startsWith("activity")) {
|
||||||
|
for (const trigger of document.querySelectorAll<HTMLButtonElement>(
|
||||||
|
".rule-word"
|
||||||
|
)) {
|
||||||
|
if (trigger.textContent?.trim() === "Activity") {
|
||||||
|
press(trigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scene.startsWith("context")) {
|
||||||
|
for (const trigger of document.querySelectorAll<HTMLButtonElement>(
|
||||||
|
".rule-word"
|
||||||
|
)) {
|
||||||
|
if (trigger.textContent?.trim() === "Context") {
|
||||||
|
press(trigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scene.startsWith("branch")) {
|
||||||
|
document
|
||||||
|
.querySelector<HTMLButtonElement>(
|
||||||
|
'[aria-label="Branch off this conversation"]'
|
||||||
|
)
|
||||||
|
?.click();
|
||||||
|
}
|
||||||
|
await wait(60);
|
||||||
|
if (scene.startsWith("stream")) {
|
||||||
|
// A tool lands while you watch: the tree grows without a reload.
|
||||||
|
client.emit({
|
||||||
|
conversation_id: MASTER,
|
||||||
|
input: { command: "curl -sI https://b.hhhhh.dev/healthz" },
|
||||||
|
name: "Bash",
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
tool_use_id: "tu_live",
|
||||||
|
turn_id: "t-run",
|
||||||
|
type: "tool",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
@@ -0,0 +1,611 @@
|
|||||||
|
import { ApiClient, ApiError } from "$lib/api/client";
|
||||||
|
import type {
|
||||||
|
BusEvent,
|
||||||
|
ConversationInfo,
|
||||||
|
ConversationSummary,
|
||||||
|
HistoryMessage,
|
||||||
|
} from "$lib/api/types";
|
||||||
|
|
||||||
|
const MINUTE = 60_000;
|
||||||
|
const HOUR = 60 * MINUTE;
|
||||||
|
// Anchored inside the day rather than at the wall clock: the fixtures are
|
||||||
|
// spread over hours, and a run just after midnight would push half of them
|
||||||
|
// into yesterday and empty the "Today" group the rail is checked against.
|
||||||
|
const wall = new Date();
|
||||||
|
const now =
|
||||||
|
wall.getHours() < 12 ? new Date(wall).setHours(12, 0, 0, 0) : wall.getTime();
|
||||||
|
const ago = (ms: number) => new Date(now - ms).toISOString();
|
||||||
|
|
||||||
|
export const MASTER = "c1master0000";
|
||||||
|
export const BRANCH_UFW = "c2ufw0000000";
|
||||||
|
export const BRANCH_BLASTER = "c3blaster000";
|
||||||
|
export const DEEP_PANEL = "c4panel00000";
|
||||||
|
export const JOB_TRIAGE = "c5triage0000";
|
||||||
|
export const FORK_MERGE = "c6fork000000";
|
||||||
|
|
||||||
|
function summary(
|
||||||
|
over: Partial<ConversationSummary> & Pick<ConversationSummary, "id" | "kind">
|
||||||
|
): ConversationSummary {
|
||||||
|
return {
|
||||||
|
agent: "beaver-dispatcher",
|
||||||
|
context_tokens: 84_000,
|
||||||
|
created_at: ago(9 * HOUR),
|
||||||
|
flags: {},
|
||||||
|
last_activity_at: ago(4 * MINUTE),
|
||||||
|
last_item: null,
|
||||||
|
last_user_activity_at: ago(12 * MINUTE),
|
||||||
|
origin: "telegram",
|
||||||
|
parent_row: null,
|
||||||
|
pending_question: false,
|
||||||
|
running_turn: null,
|
||||||
|
session_id: "9c0c3fb2-2ac7-4d6b-8d4e-1a3f0c1a2b3c",
|
||||||
|
status: "open",
|
||||||
|
title: null,
|
||||||
|
...over,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROWS: ConversationSummary[] = [
|
||||||
|
summary({
|
||||||
|
id: MASTER,
|
||||||
|
kind: "master",
|
||||||
|
last_item: {
|
||||||
|
created_at: ago(4 * MINUTE),
|
||||||
|
id: 41,
|
||||||
|
origin: "telegram",
|
||||||
|
priority: "user",
|
||||||
|
status: "running",
|
||||||
|
text: "посмотри, что с ufw на dell - порт 62990 снаружи не отвечает",
|
||||||
|
},
|
||||||
|
running_turn: "t-run",
|
||||||
|
title: "Master · 29 Aug",
|
||||||
|
}),
|
||||||
|
summary({
|
||||||
|
id: BRANCH_UFW,
|
||||||
|
kind: "branch",
|
||||||
|
last_activity_at: ago(35 * MINUTE),
|
||||||
|
last_item: {
|
||||||
|
created_at: ago(35 * MINUTE),
|
||||||
|
id: 40,
|
||||||
|
origin: "telegram",
|
||||||
|
priority: "user",
|
||||||
|
status: "done",
|
||||||
|
text: "правило для 62990 добавлено, проверь снаружи",
|
||||||
|
},
|
||||||
|
parent: MASTER,
|
||||||
|
parent_row: 1,
|
||||||
|
title: "ufw на dell",
|
||||||
|
}),
|
||||||
|
summary({
|
||||||
|
id: BRANCH_BLASTER,
|
||||||
|
kind: "branch",
|
||||||
|
last_activity_at: ago(3 * HOUR),
|
||||||
|
parent: MASTER,
|
||||||
|
parent_row: 1,
|
||||||
|
pending_question: true,
|
||||||
|
title: "бластер - что заказать",
|
||||||
|
}),
|
||||||
|
summary({
|
||||||
|
agent: "beaver-opus-high",
|
||||||
|
id: DEEP_PANEL,
|
||||||
|
kind: "deep",
|
||||||
|
last_activity_at: ago(50 * MINUTE),
|
||||||
|
origin: "markdown",
|
||||||
|
title: "2026-08-29 - панель в Obsidian",
|
||||||
|
}),
|
||||||
|
summary({
|
||||||
|
agent: "beaver-triage",
|
||||||
|
id: JOB_TRIAGE,
|
||||||
|
kind: "job",
|
||||||
|
last_activity_at: ago(2 * HOUR),
|
||||||
|
origin: "cron",
|
||||||
|
status: "closed",
|
||||||
|
title: "триаж 14:00",
|
||||||
|
}),
|
||||||
|
summary({
|
||||||
|
id: FORK_MERGE,
|
||||||
|
kind: "fork",
|
||||||
|
last_activity_at: ago(6 * HOUR),
|
||||||
|
origin: "api",
|
||||||
|
status: "merged",
|
||||||
|
title: "слив: утренняя ветка",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const INFO: Record<string, Partial<ConversationInfo>> = {
|
||||||
|
[MASTER]: {
|
||||||
|
bindings: [
|
||||||
|
{ external_id: "-1002233:general", frontend: "telegram", visible: true },
|
||||||
|
],
|
||||||
|
busy: true,
|
||||||
|
live: true,
|
||||||
|
queue: [
|
||||||
|
{
|
||||||
|
created_at: ago(2 * MINUTE),
|
||||||
|
id: 42,
|
||||||
|
origin: "watch",
|
||||||
|
priority: "normal",
|
||||||
|
status: "queued",
|
||||||
|
text: "vault: 📅 дни/2026-08-29.md +3 строки",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
turn: {
|
||||||
|
id: "t-run",
|
||||||
|
origin: "user",
|
||||||
|
started_at: ago(48_000),
|
||||||
|
text: "посмотри, что с ufw на dell - порт 62990 снаружи не отвечает",
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
content: null,
|
||||||
|
ended_at: null,
|
||||||
|
input: {
|
||||||
|
description: "проверить ufw на dell",
|
||||||
|
prompt:
|
||||||
|
"ssh dell 'sudo ufw status numbered' и сравнить с site.caddy",
|
||||||
|
},
|
||||||
|
is_error: null,
|
||||||
|
name: "Task",
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
started_at: ago(40_000),
|
||||||
|
tool_use_id: "tu_task",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"Status: active\n[ 1] 22/tcp ALLOW IN Anywhere\n[ 2] 443/tcp ALLOW IN Anywhere",
|
||||||
|
ended_at: ago(30_000),
|
||||||
|
input: { command: "ssh dell 'sudo ufw status numbered'" },
|
||||||
|
is_error: false,
|
||||||
|
name: "Bash",
|
||||||
|
parent_tool_use_id: "tu_task",
|
||||||
|
started_at: ago(38_000),
|
||||||
|
tool_use_id: "tu_bash",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: null,
|
||||||
|
ended_at: null,
|
||||||
|
input: { file_path: "/vault/мета/бобер/скиллы/ops/komodo/SKILL.md" },
|
||||||
|
is_error: null,
|
||||||
|
name: "Read",
|
||||||
|
parent_tool_use_id: "tu_task",
|
||||||
|
started_at: ago(8000),
|
||||||
|
tool_use_id: "tu_read",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[BRANCH_UFW]: {
|
||||||
|
bindings: [
|
||||||
|
{ external_id: "-1002233:517", frontend: "telegram", visible: true },
|
||||||
|
],
|
||||||
|
parent: MASTER,
|
||||||
|
},
|
||||||
|
[BRANCH_BLASTER]: {
|
||||||
|
bindings: [
|
||||||
|
{ external_id: "-1002233:498", frontend: "telegram", visible: false },
|
||||||
|
],
|
||||||
|
parent: MASTER,
|
||||||
|
question: {
|
||||||
|
id: "q1",
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
options: [
|
||||||
|
{ description: "Ozon, 3 дня", label: "Xiaomi Mi Blaster" },
|
||||||
|
{ description: "AliExpress, 2 недели", label: "Broadlink RM4" },
|
||||||
|
],
|
||||||
|
question: "Какой ИК-бластер заказать?",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[DEEP_PANEL]: {
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
external_id: "2026-08-29 - панель в Obsidian.md",
|
||||||
|
frontend: "markdown",
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
flags: { memory: true },
|
||||||
|
},
|
||||||
|
[JOB_TRIAGE]: { flags: { critical: false } },
|
||||||
|
[FORK_MERGE]: { parent: MASTER },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ten minutes apart, ending now: enough to show a time on every message.
|
||||||
|
const STEP_MS = 600_000;
|
||||||
|
function stamped(messages: HistoryMessage[]): HistoryMessage[] {
|
||||||
|
const end = Date.now();
|
||||||
|
return messages.map((message, index) => ({
|
||||||
|
...message,
|
||||||
|
ts: new Date(end - (messages.length - index) * STEP_MS).toISOString(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const HISTORY: Record<string, HistoryMessage[]> = {
|
||||||
|
[MASTER]: [
|
||||||
|
{ content: "с утра: дневник приехал? что в хендауте", role: "user" },
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"Приехал. В [[2026-08-28|хендауте]] два хвоста: **бластер** ждёт выбора модели, и **ufw на dell** - порт 62990 снаружи молчит. Ветка по ufw открыта, см. [[ufw на dell]].",
|
||||||
|
role: "assistant",
|
||||||
|
},
|
||||||
|
{ content: "ок, гони ufw", role: "user" },
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: "Смотрю правила через `ssh dell` и сверяю с `site.caddy`.",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tu_prev",
|
||||||
|
input: { command: "cat /root/beaver-agent/caddy/site.caddy" },
|
||||||
|
name: "Bash",
|
||||||
|
type: "tool_use",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
role: "assistant",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
content: "beaver.hhhhh.dev {\n reverse_proxy localhost:62990\n}",
|
||||||
|
tool_use_id: "tu_prev",
|
||||||
|
type: "tool_result",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
role: "user",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"Caddy проксирует на `localhost:62990`, значит снаружи должен отвечать 443, а не 62990 - проверю, открыт ли он вообще.",
|
||||||
|
role: "assistant",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[BRANCH_UFW]: [
|
||||||
|
{ content: "правило для 62990 добавлено, проверь снаружи", role: "user" },
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"Снаружи `curl -sI https://b.hhhhh.dev/healthz` → 200. Порт 62990 наружу не нужен: его слушает только Caddy. Предлагаю правило убрать и слить ветку.",
|
||||||
|
role: "assistant",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[BRANCH_BLASTER]: [
|
||||||
|
{ content: "нужен ИК-бластер для кондиционера, выбери", role: "user" },
|
||||||
|
{ content: "Два варианта, спрошу кнопками.", role: "assistant" },
|
||||||
|
],
|
||||||
|
[DEEP_PANEL]: [
|
||||||
|
{
|
||||||
|
content: "как панель в Obsidian должна вести себя в узкой колонке?",
|
||||||
|
role: "user",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"Тред всегда на виду; переключатель разговоров - в шапке. Список как отдельный экран - только на телефоне.\n\n- узкая колонка: шапка + тред + композер\n- вкладка: рейл слева\n\nСм. [[архитектура#2. Окна]].",
|
||||||
|
role: "assistant",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Route {
|
||||||
|
handle: (match: RegExpMatchArray, body: unknown) => unknown;
|
||||||
|
method: string;
|
||||||
|
pattern: RegExp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The gateway as the panel sees it, in memory: enough of ``/api`` for the
|
||||||
|
// preview scenes and the smoke test, with a scripted event stream.
|
||||||
|
export class FakeClient extends ApiClient {
|
||||||
|
rows = ROWS.map((row) => ({ ...row }));
|
||||||
|
readonly sent: { id: string; text: string; origin: string }[] = [];
|
||||||
|
private readonly listeners = new Set<(event: BusEvent) => void>();
|
||||||
|
private seq = 1;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super("http://fake", "token");
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly routes: Route[] = [
|
||||||
|
{
|
||||||
|
handle: () => ({ agents: [], frontends: [], mcps: [] }),
|
||||||
|
method: "GET",
|
||||||
|
pattern: /^\/api\/agents$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: () => ({ conversations: [], files: [], query: "" }),
|
||||||
|
method: "GET",
|
||||||
|
pattern: /^\/api\/search/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: () => ({ edges: [], nodes: [], root: "" }),
|
||||||
|
method: "GET",
|
||||||
|
pattern: /^\/api\/vault\/graph/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id]) => ({
|
||||||
|
agent: {
|
||||||
|
effort: "high",
|
||||||
|
model: "claude-opus-5",
|
||||||
|
name: "beaver-dispatcher",
|
||||||
|
},
|
||||||
|
context_tokens: 84_000,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
last_at: ago(MINUTE),
|
||||||
|
other: 0,
|
||||||
|
path: "📅 дни/2026-08-29.md",
|
||||||
|
reads: 1,
|
||||||
|
writes: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
last_at: ago(5 * MINUTE),
|
||||||
|
other: 0,
|
||||||
|
path: "👤 люди/Марта Ветрова.md",
|
||||||
|
reads: 2,
|
||||||
|
writes: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
history_tokens_est: 60_000,
|
||||||
|
id,
|
||||||
|
kind: "master",
|
||||||
|
prompt: {
|
||||||
|
bytes: 24_000,
|
||||||
|
granules: [
|
||||||
|
{
|
||||||
|
bytes: 9000,
|
||||||
|
path: "мета/бобер/промпты/гранулы/голос/role.md",
|
||||||
|
tag: "role",
|
||||||
|
tokens_est: 2800,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bytes: 15_000,
|
||||||
|
path: "мета/бобер/промпты/гранулы/диспетчер.md",
|
||||||
|
tag: "dispatcher",
|
||||||
|
tokens_est: 4700,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tokens_est: 7500,
|
||||||
|
},
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
path: "мета/бобер/скиллы/общие",
|
||||||
|
set: "общие",
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
description: "серверы и стеки",
|
||||||
|
name: "ops",
|
||||||
|
path: "мета/бобер/скиллы/общие/ops",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
skills_tokens_est: 20,
|
||||||
|
tool_counts: { Bash: 1, Read: 3 },
|
||||||
|
tools: {
|
||||||
|
allowed: null,
|
||||||
|
disallowed: [],
|
||||||
|
gateway: ["say", "spawn"],
|
||||||
|
mcps: ["komodo"],
|
||||||
|
},
|
||||||
|
turns: 4,
|
||||||
|
}),
|
||||||
|
method: "GET",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/context/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: () => ({ conversations: this.rows }),
|
||||||
|
method: "GET",
|
||||||
|
pattern: /^\/api\/conversations$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id]) => this.info(id),
|
||||||
|
method: "GET",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id]) => ({ id, messages: stamped(HISTORY[id] ?? []) }),
|
||||||
|
method: "GET",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/history$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id]) => ({
|
||||||
|
entries: [],
|
||||||
|
id,
|
||||||
|
offset: 0,
|
||||||
|
subpath: "",
|
||||||
|
subpaths: [],
|
||||||
|
total: 0,
|
||||||
|
}),
|
||||||
|
method: "GET",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/entries$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id], body) => {
|
||||||
|
const { text, origin } = body as { text: string; origin: string };
|
||||||
|
this.sent.push({ id, origin, text });
|
||||||
|
return { id, item: this.sent.length, status: "queued" };
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/messages$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id], body) => {
|
||||||
|
const { text, origin } = body as { text: string; origin: string };
|
||||||
|
this.sent.push({ id, origin: `inject:${origin}`, text });
|
||||||
|
return { id, item: this.sent.length, priority: "normal" };
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/inject$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id], body) => {
|
||||||
|
const child = summary({
|
||||||
|
id: `child${this.rows.length}`,
|
||||||
|
kind: "branch",
|
||||||
|
parent_row: 1,
|
||||||
|
title: (body as { title?: string }).title ?? "новая ветка",
|
||||||
|
});
|
||||||
|
this.rows.unshift(child);
|
||||||
|
return this.info(child.id, id);
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/branch$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id], body) => {
|
||||||
|
const {
|
||||||
|
frontend,
|
||||||
|
external_id: external,
|
||||||
|
visible,
|
||||||
|
} = body as {
|
||||||
|
frontend: string;
|
||||||
|
external_id?: string;
|
||||||
|
visible: boolean;
|
||||||
|
};
|
||||||
|
if (!INFO[id]) {
|
||||||
|
INFO[id] = {};
|
||||||
|
}
|
||||||
|
const info = INFO[id];
|
||||||
|
const bindings = (info.bindings ?? []).filter(
|
||||||
|
(b) => b.frontend !== frontend
|
||||||
|
);
|
||||||
|
bindings.push({
|
||||||
|
external_id: external ?? `${frontend}:new`,
|
||||||
|
frontend,
|
||||||
|
visible,
|
||||||
|
});
|
||||||
|
info.bindings = bindings;
|
||||||
|
return this.info(id);
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/bind$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id], body) => {
|
||||||
|
const row = this.row(id);
|
||||||
|
row.flags = { ...row.flags, ...(body as Record<string, unknown>) };
|
||||||
|
return row;
|
||||||
|
},
|
||||||
|
method: "PATCH",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/flags$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id], body) => {
|
||||||
|
const row = this.row(id);
|
||||||
|
const patch = body as {
|
||||||
|
status?: ConversationSummary["status"];
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
if (patch.status) {
|
||||||
|
row.status = patch.status;
|
||||||
|
}
|
||||||
|
if (typeof patch.title === "string") {
|
||||||
|
row.title = patch.title;
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
},
|
||||||
|
method: "PATCH",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id]) => {
|
||||||
|
this.row(id).status = "merged";
|
||||||
|
return { fork: "f", id, status: "merged", text: "слито" };
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/merge$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id]) => {
|
||||||
|
this.row(id).status = "closed";
|
||||||
|
return {
|
||||||
|
digest: "мета/бобер/выжимки/2026-08-29 - панель.md",
|
||||||
|
id,
|
||||||
|
status: "closed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/close$/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id], body) => ({
|
||||||
|
id,
|
||||||
|
question_id: (body as { question_id: string }).question_id,
|
||||||
|
}),
|
||||||
|
method: "POST",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/answer$/,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
private row(id: string): ConversationSummary {
|
||||||
|
const row = this.rows.find((r) => r.id === id);
|
||||||
|
if (!row) {
|
||||||
|
throw new ApiError(404, `unknown conversation ${id}`, null);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private info(id: string, parent?: string): ConversationInfo {
|
||||||
|
const row = this.row(id);
|
||||||
|
const extra = INFO[id] ?? {};
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
bindings: [],
|
||||||
|
busy: false,
|
||||||
|
live: Boolean(row.session_id),
|
||||||
|
parent: parent ?? null,
|
||||||
|
question: null,
|
||||||
|
queue: [],
|
||||||
|
turn: null,
|
||||||
|
...extra,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private route(method: string, path: string, body?: unknown): unknown {
|
||||||
|
for (const route of this.routes) {
|
||||||
|
const match = route.method === method ? path.match(route.pattern) : null;
|
||||||
|
if (match) {
|
||||||
|
return route.handle(match, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new ApiError(404, `no route for ${method} ${path}`, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
override get<T>(path: string): Promise<T> {
|
||||||
|
return Promise.resolve(this.route("GET", path) as T);
|
||||||
|
}
|
||||||
|
|
||||||
|
override post<T>(path: string, body?: unknown): Promise<T> {
|
||||||
|
return Promise.resolve(this.route("POST", path, body ?? {}) as T);
|
||||||
|
}
|
||||||
|
|
||||||
|
override patch<T>(path: string, body?: unknown): Promise<T> {
|
||||||
|
return Promise.resolve(this.route("PATCH", path, body ?? {}) as T);
|
||||||
|
}
|
||||||
|
|
||||||
|
override stream(
|
||||||
|
_path: string,
|
||||||
|
onEvent: (event: BusEvent) => void,
|
||||||
|
signal: AbortSignal
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
onEvent({ seq: 0, ts: new Date().toISOString(), type: "hello" });
|
||||||
|
this.listeners.add(onEvent);
|
||||||
|
signal.addEventListener("abort", () => {
|
||||||
|
this.listeners.delete(onEvent);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: Omit<BusEvent, "seq" | "ts"> & { ts?: string }): void {
|
||||||
|
this.seq += 1;
|
||||||
|
const full = {
|
||||||
|
seq: this.seq,
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
...event,
|
||||||
|
} as BusEvent;
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
listener(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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}/`);
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// node scripts/css.mjs [--minify] [--watch]
|
||||||
|
//
|
||||||
|
// Tailwind writes to a scratch file; the scoped, optionally minified result
|
||||||
|
// is what lands in styles.css. See scope-css.mjs for why the extra step.
|
||||||
|
import { spawn, spawnSync } from "node:child_process";
|
||||||
|
import { readFileSync, watch, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { scopeCss } from "./scope-css.mjs";
|
||||||
|
|
||||||
|
const root = new URL("..", import.meta.url).pathname;
|
||||||
|
const args = new Set(process.argv.slice(2));
|
||||||
|
const minify = args.has("--minify");
|
||||||
|
const watching = args.has("--watch");
|
||||||
|
|
||||||
|
const input = join(root, "src/tailwind.css");
|
||||||
|
const scratch = join(root, ".tailwind.css");
|
||||||
|
const output = join(root, "styles.css");
|
||||||
|
const cli = join(root, "node_modules/.bin/tailwindcss");
|
||||||
|
|
||||||
|
function finish() {
|
||||||
|
const built = readFileSync(scratch, "utf8");
|
||||||
|
writeFileSync(output, scopeCss(built, { minify }));
|
||||||
|
console.log(`css: wrote styles.css (${minify ? "minified, " : ""}scoped)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watching) {
|
||||||
|
writeFileSync(scratch, "");
|
||||||
|
const child = spawn(cli, ["-i", input, "-o", scratch, "--watch"], {
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
let timer = null;
|
||||||
|
watch(scratch, () => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
timer = null;
|
||||||
|
try {
|
||||||
|
finish();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("css:", error);
|
||||||
|
}
|
||||||
|
}, 60);
|
||||||
|
});
|
||||||
|
child.on("exit", (code) => process.exit(code ?? 0));
|
||||||
|
} else {
|
||||||
|
const run = spawnSync(cli, ["-i", input, "-o", scratch], {
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
if (run.status !== 0) {
|
||||||
|
process.exit(run.status ?? 1);
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { transform } from "lightningcss";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obsidian loads every plugin's stylesheet into one document, and Tailwind
|
||||||
|
* puts its utilities into a cascade layer called `utilities` in each of them.
|
||||||
|
* Layers with the same name merge, so another Tailwind plugin's
|
||||||
|
* `.hidden{display:none!important}` and this plugin's
|
||||||
|
* `@container … .\@min-\[30rem\]\:inline-block{…!important}` end up in the
|
||||||
|
* same layer with the same specificity, and whichever plugin loaded last
|
||||||
|
* wins. Prefixing every selector with `:is(.beaver-root, .beaver-root *)` adds a
|
||||||
|
* class of specificity that no unscoped utility can match, whatever the load
|
||||||
|
* order, while still reaching the view root itself and the portal layer,
|
||||||
|
* both of which carry `.beaver-root`.
|
||||||
|
*
|
||||||
|
* Tailwind's own theme variables stay on `:root, :host`: moved onto the view
|
||||||
|
* root they would sit above Obsidian's `body` values, and `--color-accent`
|
||||||
|
* is a name both sides use — the accent would turn into the hover grey.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ROOT = "beaver-root";
|
||||||
|
|
||||||
|
const SCOPE = {
|
||||||
|
kind: "is",
|
||||||
|
selectors: [
|
||||||
|
[{ name: ROOT, type: "class" }],
|
||||||
|
[
|
||||||
|
{ name: ROOT, type: "class" },
|
||||||
|
{ type: "combinator", value: "descendant" },
|
||||||
|
{ type: "universal" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
type: "pseudo-class",
|
||||||
|
};
|
||||||
|
|
||||||
|
function isPseudo(selector, kind) {
|
||||||
|
return (
|
||||||
|
selector.length === 1 &&
|
||||||
|
selector[0].type === "pseudo-class" &&
|
||||||
|
selector[0].kind === kind
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mentions(selector, name) {
|
||||||
|
for (const part of selector) {
|
||||||
|
if (part.type === "class" && part.name === name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (part.selectors?.some((inner) => mentions(inner, name))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scopeSelector(selector) {
|
||||||
|
if (isPseudo(selector, "root") || isPseudo(selector, "host")) {
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
if (mentions(selector, ROOT)) {
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
// The scope goes on the subject — the last compound — not the first:
|
||||||
|
// `.theme-dark .hue-chip` must keep matching with `.theme-dark` on body,
|
||||||
|
// outside the root.
|
||||||
|
let at = 0;
|
||||||
|
for (let i = 0; i < selector.length; i += 1) {
|
||||||
|
if (selector[i].type === "combinator") {
|
||||||
|
at = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const head = selector.slice(0, at);
|
||||||
|
const subject = selector.slice(at);
|
||||||
|
// `*` and type selectors must open a compound; :is() goes in their place
|
||||||
|
// or right after them.
|
||||||
|
if (subject[0]?.type === "universal") {
|
||||||
|
return [...head, SCOPE, ...subject.slice(1)];
|
||||||
|
}
|
||||||
|
if (subject[0]?.type === "type") {
|
||||||
|
return [...head, subject[0], SCOPE, ...subject.slice(1)];
|
||||||
|
}
|
||||||
|
return [...head, SCOPE, ...subject];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rewrites a built stylesheet so its rules only reach the plugin's own DOM. */
|
||||||
|
export function scopeCss(code, { minify = false } = {}) {
|
||||||
|
// The selector visitor, not the rule one: handing a whole rule back makes
|
||||||
|
// lightningcss re-read its declarations, and some of Tailwind's it cannot.
|
||||||
|
const result = transform({
|
||||||
|
code: Buffer.from(code),
|
||||||
|
filename: "styles.css",
|
||||||
|
minify,
|
||||||
|
visitor: { Selector: scopeSelector },
|
||||||
|
});
|
||||||
|
return result.code.toString();
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { App, FuzzySuggestModal } from "obsidian";
|
import { type App, FuzzySuggestModal } from "obsidian";
|
||||||
|
|
||||||
export function pickAgent(app: App, agents: string[]): Promise<string | null> {
|
export function pickAgent(app: App, agents: string[]): Promise<string | null> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
const settle = (value: string | null) => {
|
const settle = (value: string | null) => {
|
||||||
if (resolved) return;
|
if (resolved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
resolved = true;
|
resolved = true;
|
||||||
resolve(value);
|
resolve(value);
|
||||||
};
|
};
|
||||||
+141
-140
@@ -1,18 +1,20 @@
|
|||||||
import { requestUrl, RequestUrlParam } from "obsidian";
|
import type { RequestUrlParam } from "obsidian";
|
||||||
|
import { requestUrl } from "obsidian";
|
||||||
import type { BeaverSettings } from "./settings";
|
import type { BeaverSettings } from "./settings";
|
||||||
|
import { readSse } from "./sse";
|
||||||
|
|
||||||
export interface ChatRequest {
|
export interface ChatRequest {
|
||||||
filename: string;
|
|
||||||
content: string;
|
|
||||||
agent?: string;
|
agent?: string;
|
||||||
|
content: string;
|
||||||
|
filename: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatResponse {
|
export interface ChatResponse {
|
||||||
status: "ok" | "nothing_to_do" | "in_progress";
|
|
||||||
reason?: string;
|
|
||||||
agent?: string;
|
agent?: string;
|
||||||
turns_appended?: number;
|
|
||||||
new_content?: string;
|
new_content?: string;
|
||||||
|
reason?: string;
|
||||||
|
status: "ok" | "nothing_to_do" | "in_progress";
|
||||||
|
turns_appended?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BeaverApiError extends Error {
|
export class BeaverApiError extends Error {
|
||||||
@@ -25,53 +27,106 @@ export class BeaverApiError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function baseUrl(settings: BeaverSettings): string {
|
// Two hosts, one token. ``mdBase`` is the markdown frontend (``/chat``,
|
||||||
const url = settings.baseUrl.trim().replace(/\/+$/, "");
|
// ``/agents``); ``apiBase`` is the origin under which the conversations
|
||||||
if (!url) throw new Error("Beaver: base URL is not configured");
|
// API lives (``<apiBase>/api/conversations/…``). Behind the gateway's
|
||||||
|
// Caddy the markdown root is ``https://host/md`` and the API root is the
|
||||||
|
// bare ``https://host``, so when the API origin isn't set explicitly we
|
||||||
|
// derive it: strip a trailing ``/md``, or bump the default local port.
|
||||||
|
const TRAILING_SLASHES = /\/+$/;
|
||||||
|
const LOCAL_MD_PORT = /:62993$/;
|
||||||
|
|
||||||
|
export function mdBase(settings: BeaverSettings): string {
|
||||||
|
const url = settings.baseUrl.trim().replace(TRAILING_SLASHES, "");
|
||||||
|
if (!url) {
|
||||||
|
throw new Error("Beaver: base URL is not configured");
|
||||||
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function apiBase(settings: BeaverSettings): string {
|
||||||
|
const explicit = settings.apiBaseUrl.trim().replace(TRAILING_SLASHES, "");
|
||||||
|
if (explicit) {
|
||||||
|
return explicit;
|
||||||
|
}
|
||||||
|
const md = mdBase(settings);
|
||||||
|
if (md.endsWith("/md")) {
|
||||||
|
return md.slice(0, -"/md".length);
|
||||||
|
}
|
||||||
|
if (LOCAL_MD_PORT.test(md)) {
|
||||||
|
return md.replace(LOCAL_MD_PORT, ":62994");
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Beaver: API origin is not configured and can't be derived from ${md}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function authHeader(settings: BeaverSettings): Record<string, string> {
|
function authHeader(settings: BeaverSettings): Record<string, string> {
|
||||||
const token = settings.token.trim();
|
const token = settings.token.trim();
|
||||||
if (!token) throw new Error("Beaver: bearer token is not configured");
|
if (!token) {
|
||||||
|
throw new Error("Beaver: bearer token is not configured");
|
||||||
|
}
|
||||||
return { Authorization: `Bearer ${token}` };
|
return { Authorization: `Bearer ${token}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FastAPI's default error body is ``{detail: …}``; the API frontend
|
||||||
|
// rewrites it to ``{error: …}``. Accept both, fall back to the raw text.
|
||||||
|
function errorMessage(status: number, body: unknown): string {
|
||||||
|
if (body && typeof body === "object") {
|
||||||
|
const obj = body as Record<string, unknown>;
|
||||||
|
for (const key of ["detail", "error"]) {
|
||||||
|
if (typeof obj[key] === "string") {
|
||||||
|
return obj[key] as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof body === "string" && body) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
return `HTTP ${status}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function call(
|
async function call(
|
||||||
settings: BeaverSettings,
|
settings: BeaverSettings,
|
||||||
init: Omit<RequestUrlParam, "url"> & { path: string },
|
init: Omit<RequestUrlParam, "url"> & { url: string }
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
const { path, ...rest } = init;
|
|
||||||
const url = `${baseUrl(settings)}${path}`;
|
|
||||||
const headers = {
|
const headers = {
|
||||||
...authHeader(settings),
|
...authHeader(settings),
|
||||||
...(rest.headers ?? {}),
|
...(init.headers ?? {}),
|
||||||
};
|
};
|
||||||
// throw=false → we handle non-2xx ourselves so we can extract FastAPI's
|
// throw=false → we handle non-2xx ourselves so we can extract the
|
||||||
// {detail: "..."} body and surface a useful message.
|
// error body and surface a useful message.
|
||||||
const res = await requestUrl({ url, throw: false, ...rest, headers });
|
const res = await requestUrl({ throw: false, ...init, headers });
|
||||||
if (res.status < 200 || res.status >= 300) {
|
if (res.status < 200 || res.status >= 300) {
|
||||||
let detail: unknown;
|
let body: unknown;
|
||||||
try {
|
try {
|
||||||
detail = res.json;
|
body = res.json;
|
||||||
} catch {
|
} catch {
|
||||||
detail = res.text;
|
body = res.text;
|
||||||
}
|
}
|
||||||
const detailMsg =
|
throw new BeaverApiError(res.status, errorMessage(res.status, body), body);
|
||||||
detail && typeof detail === "object" && "detail" in detail
|
|
||||||
? String((detail as { detail: unknown }).detail)
|
|
||||||
: typeof detail === "string"
|
|
||||||
? detail
|
|
||||||
: `HTTP ${res.status}`;
|
|
||||||
throw new BeaverApiError(res.status, detailMsg, detail);
|
|
||||||
}
|
}
|
||||||
return res.json;
|
return res.json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function errorFromResponse(res: Response): Promise<BeaverApiError> {
|
||||||
|
// Drain the body so we can surface a useful detail. 409 in particular
|
||||||
|
// returns JSON; the rest may be JSON or plain text.
|
||||||
|
const text = await res.text().catch(() => "");
|
||||||
|
let body: unknown = text;
|
||||||
|
try {
|
||||||
|
body = JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
// not JSON; leave body as the raw text
|
||||||
|
}
|
||||||
|
return new BeaverApiError(res.status, errorMessage(res.status, body), body);
|
||||||
|
}
|
||||||
|
|
||||||
export async function listAgents(settings: BeaverSettings): Promise<string[]> {
|
export async function listAgents(settings: BeaverSettings): Promise<string[]> {
|
||||||
const body = (await call(settings, { path: "/agents", method: "GET" })) as {
|
const body = (await call(settings, {
|
||||||
agents?: Array<{ name?: unknown }>;
|
method: "GET",
|
||||||
};
|
url: `${mdBase(settings)}/agents`,
|
||||||
|
})) as { agents?: Array<{ name?: unknown }> };
|
||||||
const list = body.agents ?? [];
|
const list = body.agents ?? [];
|
||||||
return list
|
return list
|
||||||
.map((a) => (typeof a?.name === "string" ? a.name : null))
|
.map((a) => (typeof a?.name === "string" ? a.name : null))
|
||||||
@@ -80,13 +135,13 @@ export async function listAgents(settings: BeaverSettings): Promise<string[]> {
|
|||||||
|
|
||||||
export async function sendChat(
|
export async function sendChat(
|
||||||
settings: BeaverSettings,
|
settings: BeaverSettings,
|
||||||
req: ChatRequest,
|
req: ChatRequest
|
||||||
): Promise<ChatResponse> {
|
): Promise<ChatResponse> {
|
||||||
return (await call(settings, {
|
return (await call(settings, {
|
||||||
path: "/chat",
|
|
||||||
method: "POST",
|
|
||||||
contentType: "application/json",
|
|
||||||
body: JSON.stringify(req),
|
body: JSON.stringify(req),
|
||||||
|
contentType: "application/json",
|
||||||
|
method: "POST",
|
||||||
|
url: `${mdBase(settings)}/chat`,
|
||||||
})) as ChatResponse;
|
})) as ChatResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,128 +151,74 @@ export interface StreamChatCallbacks {
|
|||||||
// responsible for splicing it into the editor — we don't ship the
|
// responsible for splicing it into the editor — we don't ship the
|
||||||
// diff because the gateway already renders the canonical view and we
|
// diff because the gateway already renders the canonical view and we
|
||||||
// don't want two slightly-different renderers to drift.
|
// don't want two slightly-different renderers to drift.
|
||||||
onDelta(newContent: string): void;
|
onDelta: (newContent: string) => void;
|
||||||
// Fires exactly once at end-of-turn (success or nothing_to_do).
|
// Fires exactly once at end-of-turn (success or nothing_to_do).
|
||||||
onDone(response: ChatResponse): void;
|
onDone: (response: ChatResponse) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-Sent Events arrive as ``event: <name>\ndata: <json>\n\n``
|
// CORS is allowed by the gateway's ``CORSMiddleware``; auth is the same
|
||||||
// frames. We can't use ``requestUrl`` (it buffers the whole body), so
|
// bearer token as the other endpoints.
|
||||||
// SSE is the one place in the plugin that goes through native
|
|
||||||
// ``fetch``. CORS is allowed by the gateway's ``CORSMiddleware``; auth
|
|
||||||
// is the same bearer token as the other endpoints.
|
|
||||||
export async function sendChatStream(
|
export async function sendChatStream(
|
||||||
settings: BeaverSettings,
|
settings: BeaverSettings,
|
||||||
req: ChatRequest,
|
req: ChatRequest,
|
||||||
cb: StreamChatCallbacks,
|
cb: StreamChatCallbacks,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const url = `${baseUrl(settings)}/chat/stream`;
|
const res = await fetch(`${mdBase(settings)}/chat/stream`, {
|
||||||
const headers: Record<string, string> = {
|
|
||||||
...authHeader(settings),
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "text/event-stream",
|
|
||||||
};
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(req),
|
body: JSON.stringify(req),
|
||||||
|
headers: {
|
||||||
|
...authHeader(settings),
|
||||||
|
Accept: "text/event-stream",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
if (!res.ok || !res.body) {
|
if (!(res.ok && res.body)) {
|
||||||
// Drain the body so we can surface a useful detail. 409 in
|
throw await errorFromResponse(res);
|
||||||
// particular returns JSON; the rest may be JSON or plain text.
|
}
|
||||||
const text = await res.text().catch(() => "");
|
|
||||||
let detail: unknown = text;
|
await readSse(res.body, (frame) => {
|
||||||
|
let data: unknown;
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(text) as { detail?: unknown };
|
data = JSON.parse(frame.data);
|
||||||
detail =
|
|
||||||
parsed && typeof parsed === "object" && "detail" in parsed
|
|
||||||
? parsed.detail
|
|
||||||
: parsed;
|
|
||||||
} catch {
|
} catch {
|
||||||
// not JSON; leave detail as the raw text
|
return "continue";
|
||||||
}
|
}
|
||||||
const msg =
|
const obj = data as Record<string, unknown>;
|
||||||
typeof detail === "string" && detail
|
if (frame.event === "delta") {
|
||||||
? detail
|
const content = obj.new_content;
|
||||||
: `HTTP ${res.status}`;
|
if (typeof content === "string") {
|
||||||
throw new BeaverApiError(res.status, msg, detail);
|
cb.onDelta(content);
|
||||||
}
|
|
||||||
|
|
||||||
const reader = res.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
// SSE frames are separated by a blank line (``\n\n``). We buffer
|
|
||||||
// partial frames across reads, then flush full ones in order.
|
|
||||||
let buf = "";
|
|
||||||
let done = false;
|
|
||||||
while (!done) {
|
|
||||||
const chunk = await reader.read();
|
|
||||||
done = chunk.done;
|
|
||||||
if (chunk.value) buf += decoder.decode(chunk.value, { stream: !done });
|
|
||||||
let sep = buf.indexOf("\n\n");
|
|
||||||
while (sep >= 0) {
|
|
||||||
const frame = buf.slice(0, sep);
|
|
||||||
buf = buf.slice(sep + 2);
|
|
||||||
const handled = handleSseFrame(frame, cb);
|
|
||||||
if (handled === "stop") {
|
|
||||||
// ``done``/``error`` is terminal — stop reading even if the
|
|
||||||
// server sends extra padding before closing.
|
|
||||||
try {
|
|
||||||
await reader.cancel();
|
|
||||||
} catch {
|
|
||||||
// best-effort cleanup
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
sep = buf.indexOf("\n\n");
|
return "continue";
|
||||||
}
|
}
|
||||||
}
|
if (frame.event === "done") {
|
||||||
|
cb.onDone(obj as unknown as ChatResponse);
|
||||||
|
// ``done``/``error`` is terminal — stop reading even if the
|
||||||
|
// server sends extra padding before closing.
|
||||||
|
return "stop";
|
||||||
|
}
|
||||||
|
if (frame.event === "error") {
|
||||||
|
const code = typeof obj.status_code === "number" ? obj.status_code : 500;
|
||||||
|
const detail =
|
||||||
|
typeof obj.detail === "string" ? obj.detail : `HTTP ${code}`;
|
||||||
|
throw new BeaverApiError(code, detail, obj);
|
||||||
|
}
|
||||||
|
return "continue";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSseFrame(
|
// ---- conversations API (``/api``, bearer scope ``api``) ----
|
||||||
frame: string,
|
|
||||||
cb: StreamChatCallbacks,
|
export async function listApiAgents(
|
||||||
): "continue" | "stop" {
|
settings: BeaverSettings
|
||||||
// SSE lines: ``event: <name>`` / ``data: <json>``. ``data`` may span
|
): Promise<string[]> {
|
||||||
// multiple lines (concatenated with ``\n``) per the spec; we honour
|
const body = (await call(settings, {
|
||||||
// that even though the gateway emits single-line ``data:`` today.
|
method: "GET",
|
||||||
let event = "message";
|
url: `${apiBase(settings)}/api/agents`,
|
||||||
const dataLines: string[] = [];
|
})) as { agents?: Array<{ name?: unknown }> };
|
||||||
for (const rawLine of frame.split("\n")) {
|
return (body.agents ?? [])
|
||||||
const line = rawLine.replace(/\r$/, "");
|
.map((a) => (typeof a?.name === "string" ? a.name : null))
|
||||||
if (!line || line.startsWith(":")) continue;
|
.filter((n): n is string => !!n);
|
||||||
const colon = line.indexOf(":");
|
|
||||||
if (colon < 0) continue;
|
|
||||||
const field = line.slice(0, colon);
|
|
||||||
let value = line.slice(colon + 1);
|
|
||||||
if (value.startsWith(" ")) value = value.slice(1);
|
|
||||||
if (field === "event") event = value;
|
|
||||||
else if (field === "data") dataLines.push(value);
|
|
||||||
}
|
|
||||||
if (dataLines.length === 0) return "continue";
|
|
||||||
let data: unknown;
|
|
||||||
try {
|
|
||||||
data = JSON.parse(dataLines.join("\n"));
|
|
||||||
} catch {
|
|
||||||
return "continue";
|
|
||||||
}
|
|
||||||
const obj = data as Record<string, unknown>;
|
|
||||||
if (event === "delta") {
|
|
||||||
const content = obj.new_content;
|
|
||||||
if (typeof content === "string") cb.onDelta(content);
|
|
||||||
return "continue";
|
|
||||||
}
|
|
||||||
if (event === "done") {
|
|
||||||
cb.onDone(obj as unknown as ChatResponse);
|
|
||||||
return "stop";
|
|
||||||
}
|
|
||||||
if (event === "error") {
|
|
||||||
const code =
|
|
||||||
typeof obj.status_code === "number" ? obj.status_code : 500;
|
|
||||||
const detail =
|
|
||||||
typeof obj.detail === "string" ? obj.detail : `HTTP ${code}`;
|
|
||||||
throw new BeaverApiError(code, detail, obj);
|
|
||||||
}
|
|
||||||
return "continue";
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* How much of a leaf's bottom is covered by something the operator cannot
|
||||||
|
* move: Obsidian's desktop status bar, a phone's navbar (which paints over a
|
||||||
|
* leaf rather than shortening it), and the software keyboard, which shrinks
|
||||||
|
* the visual viewport while the layout keeps its full height.
|
||||||
|
*
|
||||||
|
* Pure on purpose - the view feeds it live rectangles, the tests feed it the
|
||||||
|
* numbers a phone actually reports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Rect {
|
||||||
|
bottom: number;
|
||||||
|
height: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
top: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Visible {
|
||||||
|
/** `visualViewport.height`: what is left once the keyboard is up. */
|
||||||
|
height: number;
|
||||||
|
/** `visualViewport.offsetTop`: where the visible area starts. */
|
||||||
|
offsetTop: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insetFor(
|
||||||
|
leaf: Rect,
|
||||||
|
bars: Rect[],
|
||||||
|
visible: Visible | null
|
||||||
|
): number {
|
||||||
|
if (leaf.height <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let overlap = 0;
|
||||||
|
for (const bar of bars) {
|
||||||
|
// A bar that does not sit over this leaf horizontally covers nothing of
|
||||||
|
// it - the desktop status bar next to a left-hand sidedock, say.
|
||||||
|
if (bar.height > 0 && bar.left < leaf.right && bar.right > leaf.left) {
|
||||||
|
overlap = Math.max(overlap, leaf.bottom - bar.top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (visible) {
|
||||||
|
const floor = visible.offsetTop + visible.height;
|
||||||
|
overlap = Math.max(overlap, leaf.bottom - floor);
|
||||||
|
}
|
||||||
|
return Math.max(0, Math.round(overlap));
|
||||||
|
}
|
||||||
+245
-126
@@ -1,69 +1,66 @@
|
|||||||
|
import type { WorkspaceLeaf } from "obsidian";
|
||||||
import {
|
import {
|
||||||
Editor,
|
type Editor,
|
||||||
MarkdownView,
|
type MarkdownView,
|
||||||
Notice,
|
Notice,
|
||||||
Plugin,
|
Plugin,
|
||||||
TFile,
|
type TFile,
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
import {
|
import { ApiClient } from "$lib/api/client";
|
||||||
BeaverApiError,
|
import { pickAgent } from "./agent-picker";
|
||||||
ChatResponse,
|
import type { ChatResponse } from "./api";
|
||||||
listAgents,
|
import { apiBase, BeaverApiError, listAgents, sendChatStream } from "./api";
|
||||||
sendChatStream,
|
import type { BeaverSettings } from "./settings";
|
||||||
} from "./api";
|
import { BeaverSettingsTab, DEFAULT_SETTINGS, errorText } from "./settings";
|
||||||
import { pickAgent } from "./agentPicker";
|
import { PanelView, VIEW_TYPE_PANEL } from "./view";
|
||||||
import {
|
|
||||||
BeaverSettings,
|
|
||||||
BeaverSettingsTab,
|
|
||||||
DEFAULT_SETTINGS,
|
|
||||||
} from "./settings";
|
|
||||||
|
|
||||||
const AGENT_CACHE_TTL_MS = 5 * 60 * 1000;
|
const AGENT_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
// Obsidian's ``Editor`` is a thin shim over a CodeMirror 6
|
// Obsidian's ``Editor`` is a thin shim over a CodeMirror 6 ``EditorView``;
|
||||||
// ``EditorView``; reaching for it directly is the only way to grab
|
// reaching for it directly is the only way to grab the real scrollable DOM
|
||||||
// the real scrollable DOM node (``.cm-scroller``). Property name is
|
// node (``.cm-scroller``). Property name is unofficial but stable across
|
||||||
// unofficial but stable across recent Obsidian releases.
|
// recent Obsidian releases.
|
||||||
interface CMHandle {
|
interface CMHandle {
|
||||||
scrollDOM: HTMLElement;
|
scrollDOM: HTMLElement;
|
||||||
}
|
}
|
||||||
function cmOf(editor: Editor): CMHandle | null {
|
function cmOf(editor: Editor): CMHandle | null {
|
||||||
const cm = (editor as unknown as { cm?: CMHandle }).cm;
|
const { cm } = editor as unknown as { cm?: CMHandle };
|
||||||
return cm && cm.scrollDOM instanceof HTMLElement ? cm : null;
|
return cm && cm.scrollDOM instanceof HTMLElement ? cm : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boundary of the YAML frontmatter block. ``---\n…---\n`` at the very
|
// Boundary of the YAML frontmatter block: ``---\n…---\n`` at the very start
|
||||||
// start of the file; anything else is a no-frontmatter file and we
|
// of the file; anything else is a no-frontmatter file and we return 0.
|
||||||
// return 0.
|
|
||||||
function frontmatterEnd(s: string): number {
|
function frontmatterEnd(s: string): number {
|
||||||
if (!s.startsWith("---\n")) return 0;
|
if (!s.startsWith("---\n")) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
const idx = s.indexOf("\n---\n", 4);
|
const idx = s.indexOf("\n---\n", 4);
|
||||||
return idx >= 0 ? idx + 5 : 0;
|
return idx >= 0 ? idx + 5 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace only the regions that actually changed instead of
|
// Replace only the regions that actually changed instead of
|
||||||
// ``editor.setValue`` (which resets cursor + scroll and is expensive
|
// ``editor.setValue`` (which resets cursor + scroll and is expensive on
|
||||||
// on every snapshot for long files).
|
// every snapshot for long files).
|
||||||
//
|
//
|
||||||
// Fast path: when the body after the frontmatter is identical except
|
// Fast path: when the body after the frontmatter is identical except for a
|
||||||
// for a tail append (the common case for the final write — gateway
|
// tail append (the common case for the final write - the gateway refreshes
|
||||||
// refreshes frontmatter at the top and appends USER_SCAFFOLD at the
|
// frontmatter at the top and appends USER_SCAFFOLD at the bottom, body in
|
||||||
// bottom, body in between is unchanged), we apply TWO small edits
|
// between unchanged), we apply TWO small edits instead of one huge one. A
|
||||||
// instead of one huge one. The frontmatter rewrite stays a small,
|
// single big ``replaceRange`` spanning the frontmatter triggers Obsidian's
|
||||||
// localised change; the tail append touches only the very last bytes.
|
// Properties-widget rebuild + an asynchronous scroll-to-start-of-content
|
||||||
// A single big ``replaceRange`` spanning the frontmatter triggers
|
// that is hard to override.
|
||||||
// Obsidian's Properties-widget rebuild + an asynchronous scroll-to-
|
|
||||||
// start-of-content that's hard to override.
|
|
||||||
//
|
//
|
||||||
// Slow path: prefix/suffix-trimmed single splice for any other shape.
|
// Slow path: prefix/suffix-trimmed single splice for any other shape.
|
||||||
//
|
//
|
||||||
// After every edit we restore scroll on the scrollDOM directly,
|
// After every edit we restore scroll on the scrollDOM directly, repeatedly
|
||||||
// repeatedly across several frames, because the widget rebuild keeps
|
// across several frames, because the widget rebuild keeps resetting scroll
|
||||||
// resetting scroll asynchronously and a single restore loses the
|
// asynchronously and a single restore loses the race.
|
||||||
// race.
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: three splice shapes belong together
|
||||||
function spliceIntoEditor(editor: Editor, newContent: string): void {
|
function spliceIntoEditor(editor: Editor, newContent: string): void {
|
||||||
const current = editor.getValue();
|
const current = editor.getValue();
|
||||||
if (current === newContent) return;
|
if (current === newContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const cm = cmOf(editor);
|
const cm = cmOf(editor);
|
||||||
const savedScrollTop = cm?.scrollDOM.scrollTop ?? null;
|
const savedScrollTop = cm?.scrollDOM.scrollTop ?? null;
|
||||||
|
|
||||||
@@ -74,11 +71,13 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
|
|||||||
const fmsDiffer =
|
const fmsDiffer =
|
||||||
current.slice(0, oldFmEnd) !== newContent.slice(0, newFmEnd);
|
current.slice(0, oldFmEnd) !== newContent.slice(0, newFmEnd);
|
||||||
|
|
||||||
if (newBody.startsWith(oldBody) && (fmsDiffer || newBody.length > oldBody.length)) {
|
if (
|
||||||
// Tail append first, then (maybe) frontmatter rewrite. Order
|
newBody.startsWith(oldBody) &&
|
||||||
// matters: appending at the current end keeps the frontmatter
|
(fmsDiffer || newBody.length > oldBody.length)
|
||||||
// splice positions valid. Reversing the order would invalidate
|
) {
|
||||||
// the append offset after the frontmatter grew.
|
// Tail append first, then (maybe) frontmatter rewrite. Order matters:
|
||||||
|
// appending at the current end keeps the frontmatter splice positions
|
||||||
|
// valid; the reverse would invalidate the append offset.
|
||||||
if (newBody.length > oldBody.length) {
|
if (newBody.length > oldBody.length) {
|
||||||
const appendStart = editor.offsetToPos(current.length);
|
const appendStart = editor.offsetToPos(current.length);
|
||||||
editor.replaceRange(newBody.slice(oldBody.length), appendStart);
|
editor.replaceRange(newBody.slice(oldBody.length), appendStart);
|
||||||
@@ -88,9 +87,12 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
|
|||||||
const fmEnd = editor.offsetToPos(oldFmEnd);
|
const fmEnd = editor.offsetToPos(oldFmEnd);
|
||||||
editor.replaceRange(newContent.slice(0, newFmEnd), fmStart, fmEnd);
|
editor.replaceRange(newContent.slice(0, newFmEnd), fmStart, fmEnd);
|
||||||
}
|
}
|
||||||
} else if (oldBody.startsWith(newBody) && (fmsDiffer || oldBody.length > newBody.length)) {
|
} else if (
|
||||||
// Symmetric case: tail truncation. Unlikely on the streaming
|
oldBody.startsWith(newBody) &&
|
||||||
// path but cheap to handle.
|
(fmsDiffer || oldBody.length > newBody.length)
|
||||||
|
) {
|
||||||
|
// Symmetric case: tail truncation. Unlikely on the streaming path but
|
||||||
|
// cheap to handle.
|
||||||
if (oldBody.length > newBody.length) {
|
if (oldBody.length > newBody.length) {
|
||||||
const truncStart = editor.offsetToPos(oldFmEnd + newBody.length);
|
const truncStart = editor.offsetToPos(oldFmEnd + newBody.length);
|
||||||
const truncEnd = editor.offsetToPos(current.length);
|
const truncEnd = editor.offsetToPos(current.length);
|
||||||
@@ -109,7 +111,7 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
|
|||||||
prefix < cap &&
|
prefix < cap &&
|
||||||
current.charCodeAt(prefix) === newContent.charCodeAt(prefix)
|
current.charCodeAt(prefix) === newContent.charCodeAt(prefix)
|
||||||
) {
|
) {
|
||||||
prefix++;
|
prefix += 1;
|
||||||
}
|
}
|
||||||
let suffix = 0;
|
let suffix = 0;
|
||||||
const maxSuffix = cap - prefix;
|
const maxSuffix = cap - prefix;
|
||||||
@@ -118,22 +120,21 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
|
|||||||
current.charCodeAt(current.length - 1 - suffix) ===
|
current.charCodeAt(current.length - 1 - suffix) ===
|
||||||
newContent.charCodeAt(newContent.length - 1 - suffix)
|
newContent.charCodeAt(newContent.length - 1 - suffix)
|
||||||
) {
|
) {
|
||||||
suffix++;
|
suffix += 1;
|
||||||
}
|
}
|
||||||
const start = editor.offsetToPos(prefix);
|
const start = editor.offsetToPos(prefix);
|
||||||
const end = editor.offsetToPos(current.length - suffix);
|
const end = editor.offsetToPos(current.length - suffix);
|
||||||
editor.replaceRange(
|
editor.replaceRange(
|
||||||
newContent.slice(prefix, newContent.length - suffix),
|
newContent.slice(prefix, newContent.length - suffix),
|
||||||
start,
|
start,
|
||||||
end,
|
end
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cm != null && savedScrollTop != null) {
|
if (cm !== null && savedScrollTop !== null) {
|
||||||
// Race against Obsidian's deferred scroll-reset. Sync restore +
|
// Race against Obsidian's deferred scroll-reset: sync restore + two
|
||||||
// two animation frames + two timeouts; whichever fires after
|
// animation frames + two timeouts; whichever fires after Obsidian's own
|
||||||
// Obsidian's own scroll write wins. Cheap noop on the
|
// scroll write wins. Cheap noop on the already-correct frames.
|
||||||
// already-correct frames.
|
|
||||||
const restore = () => {
|
const restore = () => {
|
||||||
if (cm.scrollDOM.scrollTop !== savedScrollTop) {
|
if (cm.scrollDOM.scrollTop !== savedScrollTop) {
|
||||||
cm.scrollDOM.scrollTop = savedScrollTop;
|
cm.scrollDOM.scrollTop = savedScrollTop;
|
||||||
@@ -151,6 +152,7 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
|
|||||||
|
|
||||||
export default class BeaverPlugin extends Plugin {
|
export default class BeaverPlugin extends Plugin {
|
||||||
settings: BeaverSettings = { ...DEFAULT_SETTINGS };
|
settings: BeaverSettings = { ...DEFAULT_SETTINGS };
|
||||||
|
readonly views = new Set<PanelView>();
|
||||||
|
|
||||||
private cachedAgents: string[] | null = null;
|
private cachedAgents: string[] | null = null;
|
||||||
private cachedAt = 0;
|
private cachedAt = 0;
|
||||||
@@ -159,40 +161,112 @@ export default class BeaverPlugin extends Plugin {
|
|||||||
async onload(): Promise<void> {
|
async onload(): Promise<void> {
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
this.addSettingTab(new BeaverSettingsTab(this.app, this));
|
this.addSettingTab(new BeaverSettingsTab(this.app, this));
|
||||||
|
this.registerView(VIEW_TYPE_PANEL, (leaf) => new PanelView(leaf, this));
|
||||||
|
this.addRibbonIcon("bot", "Beaver", () => {
|
||||||
|
this.openPanel("tab").catch(() => undefined);
|
||||||
|
});
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "send-selected",
|
callback: () => {
|
||||||
name: "Send using selected agent",
|
this.openPanel("side").catch(() => undefined);
|
||||||
checkCallback: (checking) => {
|
|
||||||
const ctx = this.getFrontmatterAgentContext();
|
|
||||||
if (!ctx) return false;
|
|
||||||
if (!checking) void this.dispatch(ctx.file, ctx.agent);
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
id: "open-panel",
|
||||||
|
name: "Open panel",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
callback: () => {
|
||||||
|
this.openPanel("tab").catch(() => undefined);
|
||||||
|
},
|
||||||
|
id: "open-panel-tab",
|
||||||
|
name: "Open panel in a tab",
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "send-different",
|
|
||||||
name: "Send using different agent",
|
|
||||||
checkCallback: (checking) => {
|
checkCallback: (checking) => {
|
||||||
const ctx = this.getFrontmatterAgentContext();
|
const file = this.app.workspace.getActiveFile();
|
||||||
if (!ctx) return false;
|
const id = this.conversationIdOf(file);
|
||||||
if (!checking) void this.runDifferent(ctx.file);
|
if (!id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!checking) {
|
||||||
|
this.openPanel("side", id).catch(() => undefined);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
id: "open-in-panel",
|
||||||
|
name: "Open chat in panel",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
checkCallback: (checking) => {
|
||||||
|
const ctx = this.getFrontmatterAgentContext();
|
||||||
|
if (!ctx) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!checking) {
|
||||||
|
this.dispatch(ctx.file, ctx.agent).catch(() => undefined);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
id: "send-selected",
|
||||||
|
name: "Send using selected agent",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
checkCallback: (checking) => {
|
||||||
|
const ctx = this.getFrontmatterAgentContext();
|
||||||
|
if (!ctx) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!checking) {
|
||||||
|
this.runDifferent(ctx.file).catch(() => undefined);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
id: "send-different",
|
||||||
|
name: "Send using different agent",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings(): Promise<void> {
|
async loadSettings(): Promise<void> {
|
||||||
this.settings = Object.assign(
|
const stored = ((await this.loadData()) ??
|
||||||
{},
|
{}) as Partial<BeaverSettings> & {
|
||||||
DEFAULT_SETTINGS,
|
vaultSubpath?: string;
|
||||||
await this.loadData(),
|
};
|
||||||
);
|
// The setting used to be called "vault subpath"; carry the value over.
|
||||||
|
const { vaultSubpath, ...rest } = stored;
|
||||||
|
this.settings = { ...DEFAULT_SETTINGS, ...rest };
|
||||||
|
if (vaultSubpath && !rest.chatsFolder) {
|
||||||
|
this.settings.chatsFolder = vaultSubpath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings(): Promise<void> {
|
async saveSettings(): Promise<void> {
|
||||||
await this.saveData(this.settings);
|
await this.saveData(this.settings);
|
||||||
|
await Promise.all([...this.views].map((view) => view.remount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The conversations API client for the panel, or null until the settings
|
||||||
|
// name a reachable origin and a token.
|
||||||
|
apiClient(): ApiClient | null {
|
||||||
|
const token = this.settings.token.trim();
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new ApiClient(apiBase(this.settings), token);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openSettings(): void {
|
||||||
|
const { setting } = this.app as unknown as {
|
||||||
|
setting: { open: () => void; openTabById: (id: string) => void };
|
||||||
|
};
|
||||||
|
setting.open();
|
||||||
|
setting.openTabById(this.manifest.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheAgents(agents: string[]): void {
|
cacheAgents(agents: string[]): void {
|
||||||
@@ -201,15 +275,57 @@ export default class BeaverPlugin extends Plugin {
|
|||||||
this.lastListFailed = false;
|
this.lastListFailed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFrontmatterAgentContext():
|
// ``conversation_id`` from the note's frontmatter, or null. The panel and
|
||||||
| { file: TFile; agent: string }
|
// the command both key off this; the gateway writes it after the first
|
||||||
| null {
|
// turn (§3.10: frontmatter is only ``agent`` + ``conversation_id``).
|
||||||
|
conversationIdOf(file: TFile | null): string | null {
|
||||||
|
if (file?.extension !== "md") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cache = this.app.metadataCache.getFileCache(file);
|
||||||
|
const raw = cache?.frontmatter?.conversation_id;
|
||||||
|
return typeof raw === "string" && raw.trim() ? raw.trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reveal a panel (creating one on first use) and, given an id, show it.
|
||||||
|
// A tab and a sidedock column can coexist; each placement reuses its own.
|
||||||
|
async openPanel(placement: "side" | "tab", id?: string): Promise<void> {
|
||||||
|
const { workspace } = this.app;
|
||||||
|
const inTabs = (candidate: WorkspaceLeaf) =>
|
||||||
|
candidate.getRoot() === workspace.rootSplit;
|
||||||
|
let leaf: WorkspaceLeaf | null =
|
||||||
|
workspace
|
||||||
|
.getLeavesOfType(VIEW_TYPE_PANEL)
|
||||||
|
.find((candidate) => inTabs(candidate) === (placement === "tab")) ??
|
||||||
|
null;
|
||||||
|
if (!leaf) {
|
||||||
|
leaf =
|
||||||
|
placement === "tab"
|
||||||
|
? workspace.getLeaf("tab")
|
||||||
|
: workspace.getRightLeaf(false);
|
||||||
|
if (!leaf) {
|
||||||
|
new Notice("Beaver: couldn't open the panel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await leaf.setViewState({ active: true, type: VIEW_TYPE_PANEL });
|
||||||
|
}
|
||||||
|
await workspace.revealLeaf(leaf);
|
||||||
|
if (id && leaf.view instanceof PanelView) {
|
||||||
|
leaf.view.show(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFrontmatterAgentContext(): { file: TFile; agent: string } | null {
|
||||||
const file = this.app.workspace.getActiveFile();
|
const file = this.app.workspace.getActiveFile();
|
||||||
if (!file || file.extension !== "md") return null;
|
if (file?.extension !== "md") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const cache = this.app.metadataCache.getFileCache(file);
|
const cache = this.app.metadataCache.getFileCache(file);
|
||||||
const raw = cache?.frontmatter?.agent;
|
const raw = cache?.frontmatter?.agent;
|
||||||
if (typeof raw !== "string" || !raw.trim()) return null;
|
if (typeof raw !== "string" || !raw.trim()) {
|
||||||
return { file, agent: raw.trim() };
|
return null;
|
||||||
|
}
|
||||||
|
return { agent: raw.trim(), file };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runDifferent(file: TFile): Promise<void> {
|
private async runDifferent(file: TFile): Promise<void> {
|
||||||
@@ -224,7 +340,9 @@ export default class BeaverPlugin extends Plugin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const picked = await pickAgent(this.app, agents);
|
const picked = await pickAgent(this.app, agents);
|
||||||
if (!picked) return;
|
if (!picked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.dispatch(file, picked);
|
await this.dispatch(file, picked);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.notifyError("send-different", err);
|
this.notifyError("send-different", err);
|
||||||
@@ -236,7 +354,9 @@ export default class BeaverPlugin extends Plugin {
|
|||||||
this.cachedAgents &&
|
this.cachedAgents &&
|
||||||
!this.lastListFailed &&
|
!this.lastListFailed &&
|
||||||
Date.now() - this.cachedAt < AGENT_CACHE_TTL_MS;
|
Date.now() - this.cachedAt < AGENT_CACHE_TTL_MS;
|
||||||
if (fresh) return this.cachedAgents;
|
if (fresh) {
|
||||||
|
return this.cachedAgents;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const agents = await listAgents(this.settings);
|
const agents = await listAgents(this.settings);
|
||||||
this.cacheAgents(agents);
|
this.cacheAgents(agents);
|
||||||
@@ -250,13 +370,15 @@ export default class BeaverPlugin extends Plugin {
|
|||||||
|
|
||||||
private async dispatch(file: TFile, agent: string): Promise<void> {
|
private async dispatch(file: TFile, agent: string): Promise<void> {
|
||||||
const filename = this.gatewayFilename(file);
|
const filename = this.gatewayFilename(file);
|
||||||
if (!filename) return;
|
if (!filename) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Snapshot the buffer if the file is open, otherwise read disk. We
|
// Snapshot the buffer if the file is open, otherwise read disk. We don't
|
||||||
// don't keep the editor: an ``Editor`` belongs to a leaf, not a
|
// keep the editor: an ``Editor`` belongs to a leaf, not a file, so if the
|
||||||
// file, so if the user opens another note in the same tab mid-
|
// user opens another note in the same tab mid-stream the old handle now
|
||||||
// stream the old handle now points at the wrong document. Every
|
// points at the wrong document. Every delta and the final write
|
||||||
// delta and the final write re-resolve the leaf by ``file.path``.
|
// re-resolve the leaf by ``file.path``.
|
||||||
const initialEditor = this.findEditorFor(file);
|
const initialEditor = this.findEditorFor(file);
|
||||||
const content = initialEditor
|
const content = initialEditor
|
||||||
? initialEditor.getValue()
|
? initialEditor.getValue()
|
||||||
@@ -267,23 +389,24 @@ export default class BeaverPlugin extends Plugin {
|
|||||||
try {
|
try {
|
||||||
await sendChatStream(
|
await sendChatStream(
|
||||||
this.settings,
|
this.settings,
|
||||||
{ filename, content, agent },
|
{ agent, content, filename },
|
||||||
{
|
{
|
||||||
onDelta: (newContent) => {
|
onDelta: (newContent) => {
|
||||||
// Live splice into the editor only — we deliberately don't
|
// Live splice into the editor only - the partial never touches
|
||||||
// write the partial to disk. The gateway also skips its own
|
// disk. The gateway also skips its own intermediate writes on
|
||||||
// intermediate file writes on this endpoint, so the local
|
// this endpoint, so the file (and Obsidian Sync's copy) sees the
|
||||||
// file (and Obsidian Sync's copy) only sees the final state
|
// final state once. Re-resolved per delta: if ``file`` is no
|
||||||
// once. Re-resolved per delta: if ``file`` is no longer
|
// longer open anywhere, the delta is dropped and the final
|
||||||
// open anywhere, the delta is dropped and the final
|
|
||||||
// ``vault.modify`` catches the buffer up.
|
// ``vault.modify`` catches the buffer up.
|
||||||
const editor = this.findEditorFor(file);
|
const editor = this.findEditorFor(file);
|
||||||
if (editor) spliceIntoEditor(editor, newContent);
|
if (editor) {
|
||||||
|
spliceIntoEditor(editor, newContent);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDone: (resp) => {
|
onDone: (response) => {
|
||||||
finalResponse = resp;
|
finalResponse = response;
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof BeaverApiError && err.status === 409) {
|
if (err instanceof BeaverApiError && err.status === 409) {
|
||||||
@@ -297,11 +420,9 @@ export default class BeaverPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!finalResponse) {
|
if (!finalResponse) {
|
||||||
// Stream ended without a ``done`` event — shouldn't happen but
|
|
||||||
// we surface it instead of pretending nothing was wrong.
|
|
||||||
this.notifyError(
|
this.notifyError(
|
||||||
`sending to ${agent}`,
|
`sending to ${agent}`,
|
||||||
new Error("stream ended without a done event"),
|
new Error("stream ended without a done event")
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -319,29 +440,33 @@ export default class BeaverPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private gatewayFilename(file: TFile): string | null {
|
private gatewayFilename(file: TFile): string | null {
|
||||||
const subpath = this.settings.vaultSubpath;
|
const folder = this.settings.chatsFolder;
|
||||||
if (!subpath) return file.path;
|
if (!folder) {
|
||||||
const prefix = subpath + "/";
|
return file.path;
|
||||||
if (file.path === subpath || file.path.startsWith(prefix)) {
|
}
|
||||||
|
const prefix = `${folder}/`;
|
||||||
|
if (file.path === folder || file.path.startsWith(prefix)) {
|
||||||
return file.path.slice(prefix.length);
|
return file.path.slice(prefix.length);
|
||||||
}
|
}
|
||||||
new Notice(
|
new Notice(
|
||||||
`Beaver: file ${file.path} is outside the configured vault subpath (${subpath})`,
|
`Beaver: file ${file.path} is outside the chats folder (${folder})`,
|
||||||
8000,
|
8000
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private findEditorFor(file: TFile): Editor | null {
|
private findEditorFor(file: TFile): Editor | null {
|
||||||
// Walk open markdown views — we want the editor instance that owns
|
// Walk open markdown views: we want the editor instance that owns
|
||||||
// ``file`` *right now* so we can splice (keeps the buffer's edit
|
// ``file`` *right now* so we can splice (keeps the buffer's edit history
|
||||||
// history intact) instead of falling back to vault.modify. Never
|
// intact) instead of falling back to vault.modify. Never cached by
|
||||||
// cached by callers: ``view.file`` changes when the user navigates
|
// callers: ``view.file`` changes when the user navigates within the same
|
||||||
// within the same leaf, and is ``null`` between switches.
|
// leaf, and is ``null`` between switches.
|
||||||
const leaves = this.app.workspace.getLeavesOfType("markdown");
|
const leaves = this.app.workspace.getLeavesOfType("markdown");
|
||||||
for (const leaf of leaves) {
|
for (const leaf of leaves) {
|
||||||
const view = leaf.view as MarkdownView;
|
const view = leaf.view as MarkdownView;
|
||||||
if (view?.file?.path === file.path) return view.editor;
|
if (view?.file?.path === file.path) {
|
||||||
|
return view.editor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -349,26 +474,20 @@ export default class BeaverPlugin extends Plugin {
|
|||||||
private async writeBack(file: TFile, newContent: string): Promise<void> {
|
private async writeBack(file: TFile, newContent: string): Promise<void> {
|
||||||
const editor = this.findEditorFor(file);
|
const editor = this.findEditorFor(file);
|
||||||
if (editor) {
|
if (editor) {
|
||||||
// Reuse the splice path so the final write (frontmatter refresh
|
// Reuse the splice path so the final write (frontmatter refresh at the
|
||||||
// at the top + USER_SCAFFOLD appended at the bottom) preserves
|
// top + USER_SCAFFOLD appended at the bottom) preserves scroll just
|
||||||
// scroll just like the streaming deltas did.
|
// like the streaming deltas did.
|
||||||
spliceIntoEditor(editor, newContent);
|
spliceIntoEditor(editor, newContent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// File not open (anymore): write to disk. Obsidian refreshes any
|
// File not open (anymore): write to disk. Obsidian refreshes any buffer
|
||||||
// buffer that opens it later, and a leaf that shows it in another
|
// that opens it later, and a leaf that shows it in another pane gets the
|
||||||
// pane gets the update through the vault event.
|
// update through the vault event.
|
||||||
await this.app.vault.modify(file, newContent);
|
await this.app.vault.modify(file, newContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private notifyError(action: string, err: unknown): void {
|
private notifyError(action: string, err: unknown): void {
|
||||||
const msg =
|
new Notice(`Beaver (${action}): ${errorText(err)}`, 8000);
|
||||||
err instanceof BeaverApiError
|
|
||||||
? `${err.status}: ${err.message}`
|
|
||||||
: err instanceof Error
|
|
||||||
? err.message
|
|
||||||
: String(err);
|
|
||||||
new Notice(`Beaver (${action}): ${msg}`, 8000);
|
|
||||||
console.error("Beaver:", action, err);
|
console.error("Beaver:", action, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+75
-23
@@ -1,23 +1,36 @@
|
|||||||
import { App, Notice, PluginSettingTab, Setting } from "obsidian";
|
import { type App, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||||
import { listAgents, BeaverApiError } from "./api";
|
import { apiBase, BeaverApiError, listAgents, listApiAgents } from "./api";
|
||||||
import type BeaverPlugin from "./main";
|
import type BeaverPlugin from "./main";
|
||||||
|
|
||||||
export interface BeaverSettings {
|
export interface BeaverSettings {
|
||||||
|
apiBaseUrl: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
// Folder in this vault where the deep chats live: the gateway's markdown
|
||||||
|
// frontend reads and writes there, so paths it hands out are relative to it.
|
||||||
|
chatsFolder: string;
|
||||||
token: string;
|
token: string;
|
||||||
vaultSubpath: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: BeaverSettings = {
|
export const DEFAULT_SETTINGS: BeaverSettings = {
|
||||||
|
apiBaseUrl: "",
|
||||||
baseUrl: "http://localhost:62993",
|
baseUrl: "http://localhost:62993",
|
||||||
|
chatsFolder: "",
|
||||||
token: "",
|
token: "",
|
||||||
vaultSubpath: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function normalizeSubpath(raw: string): string {
|
export function normalizeFolder(raw: string): string {
|
||||||
return raw.trim().replace(/^\/+|\/+$/g, "");
|
return raw.trim().replace(/^\/+|\/+$/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TRAILING_SLASHES = /\/+$/;
|
||||||
|
|
||||||
|
export function errorText(err: unknown): string {
|
||||||
|
if (err instanceof BeaverApiError) {
|
||||||
|
return `${err.status}: ${err.message}`;
|
||||||
|
}
|
||||||
|
return err instanceof Error ? err.message : String(err);
|
||||||
|
}
|
||||||
|
|
||||||
export class BeaverSettingsTab extends PluginSettingTab {
|
export class BeaverSettingsTab extends PluginSettingTab {
|
||||||
plugin: BeaverPlugin;
|
plugin: BeaverPlugin;
|
||||||
|
|
||||||
@@ -38,14 +51,38 @@ export class BeaverSettingsTab extends PluginSettingTab {
|
|||||||
.setPlaceholder("http://localhost:62993")
|
.setPlaceholder("http://localhost:62993")
|
||||||
.setValue(this.plugin.settings.baseUrl)
|
.setValue(this.plugin.settings.baseUrl)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.baseUrl = value.trim().replace(/\/+$/, "");
|
this.plugin.settings.baseUrl = value
|
||||||
|
.trim()
|
||||||
|
.replace(TRAILING_SLASHES, "");
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
}),
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName("API origin")
|
||||||
|
.setDesc(
|
||||||
|
"Origin of the conversations API; `/api/…` is appended to it. " +
|
||||||
|
"Leave empty to derive from Base URL (`…/md` → `…`, port 62993 → 62994). " +
|
||||||
|
"Example: https://beaver.example.com or http://localhost:62994."
|
||||||
|
)
|
||||||
|
.addText((text) =>
|
||||||
|
text
|
||||||
|
.setPlaceholder("(derived)")
|
||||||
|
.setValue(this.plugin.settings.apiBaseUrl)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.apiBaseUrl = value
|
||||||
|
.trim()
|
||||||
|
.replace(TRAILING_SLASHES, "");
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("Bearer token")
|
.setName("Bearer token")
|
||||||
.setDesc("Token with the `messages` scope.")
|
.setDesc(
|
||||||
|
"Token with the `messages` scope for sending and the `api` scope " +
|
||||||
|
"for the panel (or a `*` bootstrap token)."
|
||||||
|
)
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.inputEl.type = "password";
|
text.inputEl.type = "password";
|
||||||
text
|
text
|
||||||
@@ -58,24 +95,27 @@ export class BeaverSettingsTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("Vault subpath")
|
.setName("Chats folder")
|
||||||
.setDesc(
|
.setDesc(
|
||||||
"Folder in this Obsidian vault that maps to the gateway's vault root. " +
|
"Folder in this vault where the deep chats live - the one the gateway's " +
|
||||||
"Leave empty if the two vault roots match. Example: `💬 чаты`.",
|
"markdown frontend writes to. Leave empty if the chats sit at the vault root. " +
|
||||||
|
"Example: `💬 чаты`."
|
||||||
)
|
)
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
text
|
text
|
||||||
.setPlaceholder("")
|
.setPlaceholder("")
|
||||||
.setValue(this.plugin.settings.vaultSubpath)
|
.setValue(this.plugin.settings.chatsFolder)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.vaultSubpath = normalizeSubpath(value);
|
this.plugin.settings.chatsFolder = normalizeFolder(value);
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("Test connection")
|
.setName("Test connection")
|
||||||
.setDesc("Calls GET /agents and reports the count.")
|
.setDesc(
|
||||||
|
"Calls GET /agents on the markdown frontend and reports the count."
|
||||||
|
)
|
||||||
.addButton((btn) =>
|
.addButton((btn) =>
|
||||||
btn.setButtonText("Test").onClick(async () => {
|
btn.setButtonText("Test").onClick(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -83,15 +123,27 @@ export class BeaverSettingsTab extends PluginSettingTab {
|
|||||||
this.plugin.cacheAgents(agents);
|
this.plugin.cacheAgents(agents);
|
||||||
new Notice(`Beaver: found ${agents.length} agents`);
|
new Notice(`Beaver: found ${agents.length} agents`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg =
|
new Notice(`Beaver: ${errorText(err)}`, 8000);
|
||||||
err instanceof BeaverApiError
|
|
||||||
? `${err.status}: ${err.message}`
|
|
||||||
: err instanceof Error
|
|
||||||
? err.message
|
|
||||||
: String(err);
|
|
||||||
new Notice(`Beaver: ${msg}`, 8000);
|
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName("Test API")
|
||||||
|
.setDesc(
|
||||||
|
"Calls GET /api/agents on the API origin (needs the `api` scope)."
|
||||||
|
)
|
||||||
|
.addButton((btn) =>
|
||||||
|
btn.setButtonText("Test").onClick(async () => {
|
||||||
|
try {
|
||||||
|
const agents = await listApiAgents(this.plugin.settings);
|
||||||
|
new Notice(
|
||||||
|
`Beaver: API at ${apiBase(this.plugin.settings)} - ${agents.length} agents`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
new Notice(`Beaver: ${errorText(err)}`, 8000);
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Notice } from "obsidian";
|
||||||
|
|
||||||
|
const SHORT_MS = 4000;
|
||||||
|
const LONG_MS = 8000;
|
||||||
|
|
||||||
|
function show(message: string, duration: number): Notice {
|
||||||
|
return new Notice(message, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ``svelte-sonner`` as the shared panel code imports it, backed by Obsidian
|
||||||
|
// notices; the esbuild resolver points the bare import here.
|
||||||
|
export const toast = {
|
||||||
|
error: (message: string) => show(`Beaver: ${message}`, LONG_MS),
|
||||||
|
info: (message: string) => show(message, SHORT_MS),
|
||||||
|
message: (message: string) => show(message, SHORT_MS),
|
||||||
|
success: (message: string) => show(message, SHORT_MS),
|
||||||
|
warning: (message: string) => show(message, LONG_MS),
|
||||||
|
};
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
// Server-Sent Events over a ``fetch`` body. ``EventSource`` can't send a
|
||||||
|
// bearer header and ``requestUrl`` buffers the whole response, so every
|
||||||
|
// SSE consumer in the plugin (chat streaming, the activity panel) goes
|
||||||
|
// through this reader. Frames are ``event: <name>\ndata: <json>\n\n``;
|
||||||
|
// ``data`` may span several lines per the spec, comment lines (``:
|
||||||
|
// keepalive``) are dropped.
|
||||||
|
|
||||||
|
const CARRIAGE_RETURN = /\r$/;
|
||||||
|
|
||||||
|
export interface SseFrame {
|
||||||
|
data: string;
|
||||||
|
event: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSseFrame(frame: string): SseFrame | null {
|
||||||
|
let event = "message";
|
||||||
|
const dataLines: string[] = [];
|
||||||
|
for (const rawLine of frame.split("\n")) {
|
||||||
|
const line = rawLine.replace(CARRIAGE_RETURN, "");
|
||||||
|
if (!line || line.startsWith(":")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const colon = line.indexOf(":");
|
||||||
|
if (colon < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const field = line.slice(0, colon);
|
||||||
|
let value = line.slice(colon + 1);
|
||||||
|
if (value.startsWith(" ")) {
|
||||||
|
value = value.slice(1);
|
||||||
|
}
|
||||||
|
if (field === "event") {
|
||||||
|
event = value;
|
||||||
|
} else if (field === "data") {
|
||||||
|
dataLines.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dataLines.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { data: dataLines.join("\n"), event };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads ``body`` to the end (or until ``onFrame`` returns ``"stop"``),
|
||||||
|
// delivering complete frames in order. Partial frames are buffered
|
||||||
|
// across reads. Aborting the underlying fetch rejects ``reader.read()``
|
||||||
|
// with an ``AbortError`` that propagates to the caller.
|
||||||
|
export async function readSse(
|
||||||
|
body: ReadableStream<Uint8Array>,
|
||||||
|
onFrame: (frame: SseFrame) => "continue" | "stop"
|
||||||
|
): Promise<void> {
|
||||||
|
const reader = body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buf = "";
|
||||||
|
let done = false;
|
||||||
|
try {
|
||||||
|
while (!done) {
|
||||||
|
// biome-ignore lint/performance/noAwaitInLoops: a stream is read chunk by chunk
|
||||||
|
const { done: finished, value } = await reader.read();
|
||||||
|
done = finished;
|
||||||
|
if (value) {
|
||||||
|
buf += decoder.decode(value, { stream: !done });
|
||||||
|
}
|
||||||
|
let sep = buf.indexOf("\n\n");
|
||||||
|
while (sep >= 0) {
|
||||||
|
const frame = parseSseFrame(buf.slice(0, sep));
|
||||||
|
buf = buf.slice(sep + 2);
|
||||||
|
if (frame && onFrame(frame) === "stop") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sep = buf.indexOf("\n\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await reader.cancel();
|
||||||
|
} catch {
|
||||||
|
// best-effort cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// What a panel leaf remembers: the conversation on screen, whether it follows
|
||||||
|
// the active note, and which note that is. Reactive so the view and the
|
||||||
|
// Svelte tree share one truth.
|
||||||
|
export class PanelState {
|
||||||
|
selected = $state<string | null>(null);
|
||||||
|
follow = $state(true);
|
||||||
|
sourcePath = $state("");
|
||||||
|
// The selected conversation is the open note's own: show activity, not a copy.
|
||||||
|
companion = $state(false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,488 @@
|
|||||||
|
@layer theme, base, components, utilities;
|
||||||
|
@import "tailwindcss/theme.css" layer(theme);
|
||||||
|
@import "tailwindcss/utilities.css" layer(utilities) important;
|
||||||
|
@import "../../beaver-gateway/ui/src/lib/panel/panel.css";
|
||||||
|
|
||||||
|
@source "./";
|
||||||
|
@source "../../beaver-gateway/ui/src/lib";
|
||||||
|
|
||||||
|
@custom-variant dark (&:where(.theme-dark, .theme-dark *));
|
||||||
|
|
||||||
|
/* The panel's tokens are Obsidian's own variables, exactly as beaver-calendar
|
||||||
|
maps them onto .bcal-root: the theme, the accent, the font and the radii
|
||||||
|
come from the app and the user's theme. The msos palette never enters. */
|
||||||
|
.beaver-root {
|
||||||
|
--background: var(--background-primary);
|
||||||
|
--foreground: var(--text-normal);
|
||||||
|
--card: var(--background-primary-alt);
|
||||||
|
--card-foreground: var(--text-normal);
|
||||||
|
--popover: var(--background-secondary);
|
||||||
|
--popover-foreground: var(--text-normal);
|
||||||
|
--primary: var(--color-accent);
|
||||||
|
--primary-foreground: var(--text-on-accent);
|
||||||
|
--secondary: var(--background-modifier-hover);
|
||||||
|
--secondary-foreground: var(--text-normal);
|
||||||
|
--muted: var(--background-modifier-hover);
|
||||||
|
--muted-foreground: var(--text-muted);
|
||||||
|
--accent: var(--background-modifier-hover);
|
||||||
|
--accent-foreground: var(--text-normal);
|
||||||
|
--destructive: var(--text-error);
|
||||||
|
--destructive-foreground: var(--text-on-accent);
|
||||||
|
--border: var(--background-modifier-border);
|
||||||
|
--input: var(--background-modifier-border);
|
||||||
|
--ring: var(--color-accent);
|
||||||
|
--icon: var(--text-muted);
|
||||||
|
--signal: var(--color-accent);
|
||||||
|
--link: var(--link-color, var(--text-accent));
|
||||||
|
--note: oklch(0.52 0.13 78);
|
||||||
|
--warn: oklch(0.55 0.13 75);
|
||||||
|
|
||||||
|
--status-new: var(--color-accent);
|
||||||
|
--status-done: oklch(0.55 0.14 155);
|
||||||
|
--status-skip: var(--text-faint);
|
||||||
|
--status-reply: oklch(0.55 0.14 235);
|
||||||
|
--status-snooze: oklch(0.66 0.13 75);
|
||||||
|
--status-meeting: oklch(0.5 0.2 300);
|
||||||
|
--status-work: oklch(0.55 0.13 195);
|
||||||
|
|
||||||
|
--sidebar: var(--background-secondary);
|
||||||
|
--sidebar-accent: var(--background-modifier-hover);
|
||||||
|
--rack: var(--background-secondary);
|
||||||
|
--strip: var(--background-primary);
|
||||||
|
--attention: oklch(0.72 0.16 72);
|
||||||
|
--attention-foreground: oklch(0.5 0.14 70);
|
||||||
|
--shadow-lift:
|
||||||
|
0 6px 16px -10px rgb(0 0 0 / 0.35), 0 1px 2px rgb(0 0 0 / 0.08);
|
||||||
|
--shadow-float:
|
||||||
|
0 20px 50px -20px rgb(0 0 0 / 0.5), 0 2px 6px rgb(0 0 0 / 0.1);
|
||||||
|
--radius: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .beaver-root {
|
||||||
|
--attention: oklch(0.8 0.15 75);
|
||||||
|
--attention-foreground: oklch(0.85 0.14 78);
|
||||||
|
--strip: var(--background-primary-alt);
|
||||||
|
--note: oklch(0.84 0.14 88);
|
||||||
|
--warn: oklch(0.8 0.13 75);
|
||||||
|
--status-done: oklch(0.74 0.14 155);
|
||||||
|
--status-reply: oklch(0.72 0.13 235);
|
||||||
|
--status-snooze: oklch(0.8 0.13 75);
|
||||||
|
--status-meeting: oklch(0.72 0.16 300);
|
||||||
|
--status-work: oklch(0.75 0.12 195);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--font-sans: var(--font-interface);
|
||||||
|
|
||||||
|
--text-xs: 0.75rem;
|
||||||
|
--text-sm: 0.8125rem;
|
||||||
|
--text-base: 0.875rem;
|
||||||
|
--text-lg: 1rem;
|
||||||
|
--text-xl: 1.25rem;
|
||||||
|
--text-2xl: 1.5rem;
|
||||||
|
|
||||||
|
--color-icon: var(--icon);
|
||||||
|
--color-signal: var(--signal);
|
||||||
|
--color-note: var(--note);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-rack: var(--rack);
|
||||||
|
--color-strip: var(--strip);
|
||||||
|
--color-attention: var(--attention);
|
||||||
|
--color-attention-foreground: var(--attention-foreground);
|
||||||
|
--shadow-lift: var(--shadow-lift);
|
||||||
|
--shadow-float: var(--shadow-float);
|
||||||
|
|
||||||
|
--color-status-new: var(--status-new);
|
||||||
|
--color-status-done: var(--status-done);
|
||||||
|
--color-status-skip: var(--status-skip);
|
||||||
|
--color-status-reply: var(--status-reply);
|
||||||
|
--color-status-snooze: var(--status-snooze);
|
||||||
|
--color-status-meeting: var(--status-meeting);
|
||||||
|
--color-status-work: var(--status-work);
|
||||||
|
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-background: var(--background);
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
padding-bottom: var(--beaver-inset-bottom, 0px);
|
||||||
|
font-family: var(--font-interface);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Portal layer for menus and dialogs: a zero-sized anchor at body level, above
|
||||||
|
the sidedocks, carrying the theme aliases so popovers stay on theme. */
|
||||||
|
.beaver-portal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: var(--layer-menu, 65);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root *,
|
||||||
|
.beaver-root.beaver-root *::before,
|
||||||
|
.beaver-root.beaver-root *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 0 solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root
|
||||||
|
:is(h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd) {
|
||||||
|
margin: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root :is(ul, ol) {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root :is(button, input, select, textarea) {
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
border: 0 solid var(--border);
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root :is(button, input, select, textarea):hover,
|
||||||
|
.beaver-root.beaver-root :is(button, input, select, textarea):focus,
|
||||||
|
.beaver-root.beaver-root :is(button, input, select, textarea):active {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root button {
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: auto;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A grid button centres by `place-items`, which the rule above would override
|
||||||
|
for its inline axis: an icon-only button ends up pinned to its left edge. */
|
||||||
|
.beaver-root.beaver-root button:is(.grid, .inline-grid) {
|
||||||
|
justify-content: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root button:hover {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root input::placeholder,
|
||||||
|
.beaver-root.beaver-root textarea::placeholder {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root :is(input, textarea) {
|
||||||
|
caret-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root :focus-visible {
|
||||||
|
outline: 2px solid var(--ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root svg {
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root a,
|
||||||
|
.beaver-root.beaver-root a:hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root :is(code, pre, kbd) {
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat text rendered by Obsidian itself, kept compact for a sidedock. */
|
||||||
|
.beaver-root.beaver-root .beaver-md {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md :is(p, ul, ol, pre, blockquote, table) {
|
||||||
|
margin-block: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md :is(h1, h2, h3, h4, h5, h6) {
|
||||||
|
margin-block: 0.8em 0.4em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md :is(ul, ol) {
|
||||||
|
padding-left: 1.25em;
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md ol {
|
||||||
|
list-style: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md li + li {
|
||||||
|
margin-top: 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md a {
|
||||||
|
color: var(--link);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md a.is-unresolved {
|
||||||
|
color: var(--link-unresolved-color, var(--muted-foreground));
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md code {
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 500;
|
||||||
|
background: color-mix(in oklch, var(--muted) 70%, transparent);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md pre {
|
||||||
|
padding: 0.6em 0.8em;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--muted);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md pre code {
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md blockquote {
|
||||||
|
padding-left: 0.75em;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md > :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beaver-root.beaver-root .beaver-md > :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The luma primitives are pill-shaped for the browser; inside Obsidian every
|
||||||
|
control takes the app's own corner language, like beaver-calendar's. */
|
||||||
|
@layer utilities {
|
||||||
|
/* Utilities are !important here, so the hidden attribute needs its own weight. */
|
||||||
|
.beaver-root [hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.beaver-root [data-slot="button"],
|
||||||
|
.beaver-root [data-slot="select-trigger"] {
|
||||||
|
border-radius: var(--button-radius, var(--radius-s)) !important;
|
||||||
|
}
|
||||||
|
.beaver-root [data-slot="input"],
|
||||||
|
.beaver-root [data-slot="tabs-trigger"],
|
||||||
|
.beaver-root [data-slot$="-item"] {
|
||||||
|
border-radius: var(--input-radius, var(--radius-s)) !important;
|
||||||
|
}
|
||||||
|
.beaver-root [data-slot="tabs-list"],
|
||||||
|
.beaver-root [data-slot="skeleton"] {
|
||||||
|
border-radius: var(--radius-m, 8px) !important;
|
||||||
|
}
|
||||||
|
/* The luma tabs mark the active trigger with the page colour, which on a
|
||||||
|
dark Obsidian theme is the same as the surface behind the list. */
|
||||||
|
.beaver-root [data-slot="tabs-list"] {
|
||||||
|
background: color-mix(
|
||||||
|
in oklab,
|
||||||
|
var(--text-normal) 6%,
|
||||||
|
transparent
|
||||||
|
) !important;
|
||||||
|
}
|
||||||
|
.beaver-root [data-slot="tabs-trigger"][data-state="active"] {
|
||||||
|
color: var(--text-normal) !important;
|
||||||
|
background: color-mix(
|
||||||
|
in oklab,
|
||||||
|
var(--text-normal) 14%,
|
||||||
|
transparent
|
||||||
|
) !important;
|
||||||
|
}
|
||||||
|
.beaver-root [data-slot="dialog-content"],
|
||||||
|
.beaver-root [data-slot="popover-content"],
|
||||||
|
.beaver-root [data-slot="dropdown-menu-content"],
|
||||||
|
.beaver-root [data-slot="context-menu-content"],
|
||||||
|
.beaver-root [data-slot="select-content"] {
|
||||||
|
border: 1px solid var(--border) !important;
|
||||||
|
border-radius: var(--radius-m, 8px) !important;
|
||||||
|
box-shadow: var(--shadow-s) !important;
|
||||||
|
}
|
||||||
|
.beaver-root [data-slot="dropdown-menu-content"],
|
||||||
|
.beaver-root [data-slot="context-menu-content"],
|
||||||
|
.beaver-root [data-slot="select-content"] {
|
||||||
|
background: var(--popover) !important;
|
||||||
|
}
|
||||||
|
.beaver-root [data-slot="dropdown-menu-content"]::before,
|
||||||
|
.beaver-root [data-slot="context-menu-content"]::before,
|
||||||
|
.beaver-root [data-slot="select-content"]::before {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
/* Obsidian's own toggle: border-hover track, accent when on, a white thumb.
|
||||||
|
Tripled class: luma's group/data variants carry three classes' worth of
|
||||||
|
specificity, and the utilities are all !important. */
|
||||||
|
.beaver-root.beaver-root.beaver-root [data-slot="switch"] {
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
width: 40px !important;
|
||||||
|
height: 20px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
background: var(--background-modifier-border-hover) !important;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: var(--toggle-radius, 18px) !important;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root.beaver-root
|
||||||
|
[data-slot="switch"][data-state="checked"] {
|
||||||
|
background: var(--interactive-accent) !important;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root.beaver-root [data-slot="switch-thumb"] {
|
||||||
|
display: block !important;
|
||||||
|
width: 16px !important;
|
||||||
|
height: 16px !important;
|
||||||
|
margin-left: 2px !important;
|
||||||
|
background: var(--toggle-thumb-color, white) !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
box-shadow: var(--shadow-s) !important;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root.beaver-root [data-slot="switch-thumb"] {
|
||||||
|
translate: 0 !important;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root.beaver-root
|
||||||
|
[data-slot="switch-thumb"][data-state="checked"] {
|
||||||
|
translate: 20px 0 !important;
|
||||||
|
}
|
||||||
|
.beaver-root [data-slot="button"][data-variant="default"],
|
||||||
|
.beaver-root [data-slot="button"]:not([class*="bg-"]) {
|
||||||
|
font-weight: var(--font-medium, 500) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strips and the island are buttons here; the reset above flattens buttons,
|
||||||
|
so the vocabulary is restated at the reset's own specificity. */
|
||||||
|
.beaver-root.beaver-root .strip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.5rem minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: stretch;
|
||||||
|
justify-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.375rem 0.875rem 0.375rem 0.625rem;
|
||||||
|
text-align: left;
|
||||||
|
background: var(--strip);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip:hover,
|
||||||
|
.beaver-root.beaver-root .strip:focus-visible {
|
||||||
|
background: var(--strip);
|
||||||
|
box-shadow: var(--shadow-lift);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip:first-child {
|
||||||
|
border-top-left-radius: var(--radius-md);
|
||||||
|
border-top-right-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
border-bottom-right-radius: var(--radius-md);
|
||||||
|
border-bottom-left-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip[aria-current="true"] {
|
||||||
|
background: color-mix(in oklab, var(--strip) 85%, var(--primary) 15%);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip[data-state="waiting"] {
|
||||||
|
box-shadow: var(--shadow-lift);
|
||||||
|
transform: translateX(0.5rem);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip.strip-child {
|
||||||
|
padding-left: 1.75rem;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root button.island {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.625rem;
|
||||||
|
align-items: center;
|
||||||
|
height: 2.125rem;
|
||||||
|
padding: 0 0.875rem 0 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--strip);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root button.island:hover {
|
||||||
|
background: var(--strip);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root button.island:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .bay-rack {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: calc(var(--radius-md) + 1px);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip-child::before {
|
||||||
|
left: 1.125rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SettingsIcon from "@lucide/svelte/icons/settings";
|
||||||
|
import { BitsConfig } from "bits-ui";
|
||||||
|
import type { Component, App as ObsidianApp } from "obsidian";
|
||||||
|
import type { ApiClient } from "$lib/api/client";
|
||||||
|
import EmptyState from "$lib/components/empty-state.svelte";
|
||||||
|
import { Button } from "$lib/components/ui/button";
|
||||||
|
import { panelCache } from "$lib/panel/cache";
|
||||||
|
import { providePanelHost } from "$lib/panel/host";
|
||||||
|
import { ConversationIndex } from "$lib/panel/index.svelte";
|
||||||
|
import PanelShell from "$lib/panel/panel-shell.svelte";
|
||||||
|
import type { PanelState } from "../state.svelte";
|
||||||
|
import { obsidianHost } from "./host";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
app: ObsidianApp;
|
||||||
|
chatsFolder: () => string;
|
||||||
|
client: ApiClient | null;
|
||||||
|
component: Component;
|
||||||
|
onOpenSettings: () => void;
|
||||||
|
state: PanelState;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { app, client, component, onOpenSettings, state, chatsFolder }: Props =
|
||||||
|
$props();
|
||||||
|
|
||||||
|
// Menus hang off body, not off the view: Obsidian's sidedocks paint above
|
||||||
|
// the leaf, so anything portalled inside it is covered once it crosses a
|
||||||
|
// pane edge. The layer carries .beaver-root so the theme reaches popovers.
|
||||||
|
const layer = document.createElement("div");
|
||||||
|
layer.className = "beaver-root beaver-portal";
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
document.body.append(layer);
|
||||||
|
return () => layer.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Props are read once: the view remounts the tree when settings change.
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
const cache = panelCache(`beaver.panel:${client?.base ?? "none"}`);
|
||||||
|
const host = providePanelHost({
|
||||||
|
...obsidianHost({
|
||||||
|
app,
|
||||||
|
chatsFolder,
|
||||||
|
component,
|
||||||
|
sourcePath: () => state.sourcePath,
|
||||||
|
}),
|
||||||
|
cache,
|
||||||
|
});
|
||||||
|
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
const index = client ? new ConversationIndex(() => client, cache) : null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<BitsConfig defaultPortalTo={layer}>
|
||||||
|
<div class="beaver-root">
|
||||||
|
{#if client && index}
|
||||||
|
<PanelShell
|
||||||
|
{client}
|
||||||
|
companion={state.companion}
|
||||||
|
fallbackToMaster
|
||||||
|
{index}
|
||||||
|
onOpenFile={(path) => host.openNote?.(`мета/бобер/${path}`)}
|
||||||
|
showFollow
|
||||||
|
bind:follow={state.follow}
|
||||||
|
bind:selected={state.selected}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="p-4">
|
||||||
|
<EmptyState
|
||||||
|
hint="Set the gateway URL and a token with the api scope in the plugin settings."
|
||||||
|
title="Beaver is not connected"
|
||||||
|
>
|
||||||
|
<Button onclick={onOpenSettings} size="sm" variant="outline">
|
||||||
|
<SettingsIcon class="size-4" />
|
||||||
|
Open settings
|
||||||
|
</Button>
|
||||||
|
</EmptyState>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</BitsConfig>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type { App, Component } from "obsidian";
|
||||||
|
import { MarkdownRenderer, Platform } from "obsidian";
|
||||||
|
import type { PanelHost } from "$lib/panel/host";
|
||||||
|
|
||||||
|
export interface HostOptions {
|
||||||
|
app: App;
|
||||||
|
// Folder in this vault that maps to the gateway's vault root.
|
||||||
|
// Where the deep chats live in this vault; gateway paths are relative to it.
|
||||||
|
chatsFolder: () => string;
|
||||||
|
component: Component;
|
||||||
|
// The note the panel currently follows; link resolution starts from it.
|
||||||
|
sourcePath: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Obsidian side of the panel: markdown through the app's own renderer,
|
||||||
|
// [[links]] opened in place, a conversation's note opened by its binding.
|
||||||
|
export function obsidianHost(options: HostOptions): PanelHost {
|
||||||
|
const { app, component } = options;
|
||||||
|
return {
|
||||||
|
markdown(node, text) {
|
||||||
|
let alive = true;
|
||||||
|
const staging = document.createElement("div");
|
||||||
|
MarkdownRenderer.render(
|
||||||
|
app,
|
||||||
|
text,
|
||||||
|
staging,
|
||||||
|
options.sourcePath(),
|
||||||
|
component
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (alive) {
|
||||||
|
node.replaceChildren(...staging.childNodes);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
name: "obsidian",
|
||||||
|
openLink(target, kind) {
|
||||||
|
if (kind === "internal") {
|
||||||
|
app.workspace
|
||||||
|
.openLinkText(target, options.sourcePath(), false)
|
||||||
|
.catch(() => undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.open(target);
|
||||||
|
},
|
||||||
|
openNote(path) {
|
||||||
|
const folder = options.chatsFolder();
|
||||||
|
const full = folder ? `${folder}/${path}` : path;
|
||||||
|
app.workspace.openLinkText(full, "", false).catch(() => undefined);
|
||||||
|
},
|
||||||
|
touch: Platform.isMobile,
|
||||||
|
};
|
||||||
|
}
|
||||||
+250
@@ -0,0 +1,250 @@
|
|||||||
|
import type { ViewStateResult, WorkspaceLeaf } from "obsidian";
|
||||||
|
import { ItemView, TFile } from "obsidian";
|
||||||
|
import { mount, unmount } from "svelte";
|
||||||
|
import type { Rect } from "./inset";
|
||||||
|
import { insetFor } from "./inset";
|
||||||
|
import type BeaverPlugin from "./main";
|
||||||
|
import { PanelState } from "./state.svelte";
|
||||||
|
import App from "./ui/app.svelte";
|
||||||
|
|
||||||
|
export const VIEW_TYPE_PANEL = "beaver-panel";
|
||||||
|
|
||||||
|
// Bars that paint over a leaf instead of shortening it. On a phone the navbar
|
||||||
|
// sits under every view, including the one filling the screen.
|
||||||
|
const BOTTOM_BARS = [".status-bar", ".mobile-navbar", ".mobile-toolbar"];
|
||||||
|
|
||||||
|
// The keyboard and the navbar both animate, and Obsidian hides the navbar
|
||||||
|
// *after* the keyboard is up: the inset is re-measured every frame until the
|
||||||
|
// reading has held still long enough to outlast both moves.
|
||||||
|
const STEADY_FRAMES = 12;
|
||||||
|
|
||||||
|
/** Obsidian's Capacitor keyboard events on mobile; absent on the desktop. */
|
||||||
|
const KEYBOARD_EVENTS = [
|
||||||
|
"keyboardWillShow",
|
||||||
|
"keyboardDidShow",
|
||||||
|
"keyboardWillHide",
|
||||||
|
"keyboardDidHide",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
interface SavedState {
|
||||||
|
follow?: unknown;
|
||||||
|
selected?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One panel leaf: a sidedock column, a tab, or the phone screen. Follows
|
||||||
|
// the active note's ``conversation_id`` unless pinned, and remembers both
|
||||||
|
// across restarts through the workspace layout.
|
||||||
|
export class PanelView extends ItemView {
|
||||||
|
readonly state = new PanelState();
|
||||||
|
private readonly plugin: BeaverPlugin;
|
||||||
|
private root: Record<string, unknown> | null = null;
|
||||||
|
private settling: number | null = null;
|
||||||
|
|
||||||
|
constructor(leaf: WorkspaceLeaf, plugin: BeaverPlugin) {
|
||||||
|
super(leaf);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.navigation = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewType(): string {
|
||||||
|
return VIEW_TYPE_PANEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayText(): string {
|
||||||
|
return "Beaver";
|
||||||
|
}
|
||||||
|
|
||||||
|
getIcon(): string {
|
||||||
|
return "bot";
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(): Record<string, unknown> {
|
||||||
|
return { follow: this.state.follow, selected: this.state.selected };
|
||||||
|
}
|
||||||
|
|
||||||
|
async setState(state: unknown, result: ViewStateResult): Promise<void> {
|
||||||
|
const saved = (state ?? {}) as SavedState;
|
||||||
|
if (typeof saved.follow === "boolean") {
|
||||||
|
this.state.follow = saved.follow;
|
||||||
|
}
|
||||||
|
if (typeof saved.selected === "string" || saved.selected === null) {
|
||||||
|
this.state.selected = saved.selected;
|
||||||
|
}
|
||||||
|
await super.setState(state, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen(): Promise<void> {
|
||||||
|
this.contentEl.empty();
|
||||||
|
this.contentEl.style.padding = "0";
|
||||||
|
this.contentEl.style.overflow = "hidden";
|
||||||
|
this.registerEvent(
|
||||||
|
this.app.workspace.on("file-open", (file) => {
|
||||||
|
// ``null`` means the active leaf is not a file (this panel got
|
||||||
|
// focus, say): keep following the last note rather than drop it.
|
||||||
|
if (file) {
|
||||||
|
this.follow(file);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.registerEvent(
|
||||||
|
this.app.metadataCache.on("changed", (file) => {
|
||||||
|
// The note we follow just got its conversation_id - typically
|
||||||
|
// right after the first send. Re-resolve.
|
||||||
|
if (file.path === this.state.sourcePath) {
|
||||||
|
this.follow(file);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.registerEvent(this.app.workspace.on("resize", () => this.settle()));
|
||||||
|
this.registerEvent(
|
||||||
|
this.app.workspace.on("layout-change", () => this.settle())
|
||||||
|
);
|
||||||
|
// The keyboard resizes the visual viewport only: no workspace event
|
||||||
|
// fires, so the composer would stay behind it without these two.
|
||||||
|
const vv = window.visualViewport;
|
||||||
|
if (vv) {
|
||||||
|
const onViewport = () => this.settle();
|
||||||
|
vv.addEventListener("resize", onViewport);
|
||||||
|
vv.addEventListener("scroll", onViewport);
|
||||||
|
this.register(() => {
|
||||||
|
vv.removeEventListener("resize", onViewport);
|
||||||
|
vv.removeEventListener("scroll", onViewport);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Focus opens the keyboard, blur closes it; both animate, and on the way
|
||||||
|
// out the navbar comes back, so the leaf has to be re-measured after.
|
||||||
|
this.registerDomEvent(this.contentEl, "focusin", () => this.settle());
|
||||||
|
this.registerDomEvent(this.contentEl, "focusout", () => this.settle());
|
||||||
|
// Obsidian's own keyboard events (Capacitor, mobile only) fire before the
|
||||||
|
// animation and again as the navbar returns, which visualViewport alone
|
||||||
|
// does not always report.
|
||||||
|
for (const name of KEYBOARD_EVENTS) {
|
||||||
|
const onKeyboard = () => this.settle();
|
||||||
|
window.addEventListener(name, onKeyboard);
|
||||||
|
this.register(() => window.removeEventListener(name, onKeyboard));
|
||||||
|
}
|
||||||
|
this.follow(this.app.workspace.getActiveFile());
|
||||||
|
this.plugin.views.add(this);
|
||||||
|
this.mountApp();
|
||||||
|
this.settle();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-measure until the viewport stops moving. The keyboard animates both
|
||||||
|
* ways and Obsidian hides its navbar while it is up, so a fixed delay lands
|
||||||
|
* mid-animation; a frame loop that stops once two readings agree does not
|
||||||
|
* care how long the animation takes, or whether any event fires at all -
|
||||||
|
* which is the case in a sidedock drawer.
|
||||||
|
*/
|
||||||
|
private settle(): void {
|
||||||
|
let last = Number.NaN;
|
||||||
|
let steady = 0;
|
||||||
|
// A fresh trigger restarts the watch rather than being dropped: the navbar
|
||||||
|
// coming back is a second move, after the first one has settled.
|
||||||
|
if (this.settling !== null) {
|
||||||
|
cancelAnimationFrame(this.settling);
|
||||||
|
}
|
||||||
|
const tick = () => {
|
||||||
|
const floor = this.inset();
|
||||||
|
steady = floor === last ? steady + 1 : 0;
|
||||||
|
last = floor;
|
||||||
|
if (steady >= STEADY_FRAMES) {
|
||||||
|
this.settling = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.settling = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
this.settling = requestAnimationFrame(tick);
|
||||||
|
this.register(() => {
|
||||||
|
if (this.settling !== null) {
|
||||||
|
cancelAnimationFrame(this.settling);
|
||||||
|
this.settling = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whatever floats over the bottom of this leaf, measured rather than
|
||||||
|
// guessed. Returns the inset it wrote, so the settle loop can tell when the
|
||||||
|
// reading stops changing; the arithmetic itself lives in ``inset.ts``.
|
||||||
|
private inset(): number {
|
||||||
|
const box = this.contentEl.getBoundingClientRect();
|
||||||
|
if (box.height <= 0) {
|
||||||
|
return Number.NaN;
|
||||||
|
}
|
||||||
|
const bars: Rect[] = [];
|
||||||
|
for (const selector of BOTTOM_BARS) {
|
||||||
|
const node = document.querySelector<HTMLElement>(selector);
|
||||||
|
if (node && node.offsetParent !== null) {
|
||||||
|
bars.push(node.getBoundingClientRect());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const vv = window.visualViewport;
|
||||||
|
const inset = insetFor(
|
||||||
|
box,
|
||||||
|
bars,
|
||||||
|
vv ? { height: vv.height, offsetTop: vv.offsetTop } : null
|
||||||
|
);
|
||||||
|
this.contentEl.style.setProperty("--beaver-inset-bottom", `${inset}px`);
|
||||||
|
return inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClose(): Promise<void> {
|
||||||
|
this.plugin.views.delete(this);
|
||||||
|
await this.unmountApp();
|
||||||
|
this.contentEl.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The active note changed: remember it, and switch to its conversation
|
||||||
|
// when following and it has one.
|
||||||
|
follow(file: TFile | null): void {
|
||||||
|
if (!(file instanceof TFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state.sourcePath = file.path;
|
||||||
|
if (!this.state.follow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = this.plugin.conversationIdOf(file);
|
||||||
|
if (id) {
|
||||||
|
this.state.selected = id;
|
||||||
|
this.state.companion = !this.inTabs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show(id: string): void {
|
||||||
|
this.state.selected = id;
|
||||||
|
this.state.companion = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private inTabs(): boolean {
|
||||||
|
return this.leaf.getRoot() === this.app.workspace.rootSplit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings changed: the client is built from them, so rebuild the tree.
|
||||||
|
async remount(): Promise<void> {
|
||||||
|
await this.unmountApp();
|
||||||
|
this.mountApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
private mountApp(): void {
|
||||||
|
this.root = mount(App, {
|
||||||
|
props: {
|
||||||
|
app: this.app,
|
||||||
|
chatsFolder: () => this.plugin.settings.chatsFolder,
|
||||||
|
client: this.plugin.apiClient(),
|
||||||
|
component: this,
|
||||||
|
onOpenSettings: () => this.plugin.openSettings(),
|
||||||
|
state: this.state,
|
||||||
|
},
|
||||||
|
target: this.contentEl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async unmountApp(): Promise<void> {
|
||||||
|
if (this.root) {
|
||||||
|
await unmount(this.root);
|
||||||
|
this.root = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Evaluate an expression inside a running Obsidian started with
|
||||||
|
// --remote-debugging-port. Usage: bun t/cdp-eval.ts <port> "<js expression>"
|
||||||
|
const [port, expression] = process.argv.slice(2);
|
||||||
|
const targets = (await (
|
||||||
|
await fetch(`http://localhost:${port}/json`)
|
||||||
|
).json()) as {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
webSocketDebuggerUrl: string;
|
||||||
|
}[];
|
||||||
|
const page = targets.find((t) => t.type === "page");
|
||||||
|
if (!page) {
|
||||||
|
throw new Error("no page target");
|
||||||
|
}
|
||||||
|
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||||
|
await new Promise((resolve) => ws.addEventListener("open", resolve));
|
||||||
|
const reply = new Promise<string>((resolve) => {
|
||||||
|
ws.addEventListener("message", (event) => {
|
||||||
|
const data = JSON.parse(String(event.data));
|
||||||
|
if (data.id === 1) {
|
||||||
|
resolve(
|
||||||
|
JSON.stringify(data.result?.result?.value ?? data.result ?? data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
id: 1,
|
||||||
|
method: "Runtime.evaluate",
|
||||||
|
params: { awaitPromise: true, expression, returnByValue: true },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
console.log(await reply);
|
||||||
|
ws.close();
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { Rect } from "../src/inset";
|
||||||
|
import { insetFor } from "../src/inset";
|
||||||
|
|
||||||
|
const rect = (over: Partial<Rect> = {}): Rect => ({
|
||||||
|
bottom: 780,
|
||||||
|
height: 780,
|
||||||
|
left: 0,
|
||||||
|
right: 390,
|
||||||
|
top: 0,
|
||||||
|
...over,
|
||||||
|
});
|
||||||
|
|
||||||
|
// A phone: 780 tall, a 56px navbar over the bottom, 340px of keyboard.
|
||||||
|
const NAVBAR = rect({ bottom: 780, height: 56, top: 724 });
|
||||||
|
|
||||||
|
describe("insetFor", () => {
|
||||||
|
it("clears nothing when the leaf stands alone", () => {
|
||||||
|
expect(insetFor(rect(), [], null)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the navbar that paints over the leaf", () => {
|
||||||
|
expect(insetFor(rect(), [NAVBAR], null)).toBe(56);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the keyboard, which shortens no layout", () => {
|
||||||
|
expect(insetFor(rect(), [], { height: 440, offsetTop: 0 })).toBe(340);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("takes the deeper of the two rather than their sum", () => {
|
||||||
|
// While the keyboard is up the navbar is behind it: adding them would
|
||||||
|
// push the composer twice as far as it has to go.
|
||||||
|
expect(insetFor(rect(), [NAVBAR], { height: 440, offsetTop: 0 })).toBe(340);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("comes back to the navbar once the keyboard goes", () => {
|
||||||
|
expect(insetFor(rect(), [NAVBAR], { height: 780, offsetTop: 0 })).toBe(56);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores a bar that sits beside the leaf, not over it", () => {
|
||||||
|
// The desktop status bar floats bottom-right; a left-hand sidedock never
|
||||||
|
// reaches it.
|
||||||
|
const sidedock = rect({ right: 300 });
|
||||||
|
const statusBar = rect({
|
||||||
|
bottom: 780,
|
||||||
|
height: 20,
|
||||||
|
left: 700,
|
||||||
|
right: 980,
|
||||||
|
top: 760,
|
||||||
|
});
|
||||||
|
expect(insetFor(sidedock, [statusBar], null)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("measures from where the visible area starts, not from zero", () => {
|
||||||
|
// iOS scrolls the page up under the keyboard instead of resizing it.
|
||||||
|
expect(insetFor(rect(), [], { height: 440, offsetTop: 100 })).toBe(240);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("says nothing about a leaf with no height yet", () => {
|
||||||
|
expect(insetFor(rect({ bottom: 0, height: 0 }), [NAVBAR], null)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never returns a negative inset", () => {
|
||||||
|
const short = rect({ bottom: 300, height: 300 });
|
||||||
|
expect(insetFor(short, [], { height: 780, offsetTop: 0 })).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { scopeCss } from "../scripts/scope-css.mjs";
|
||||||
|
|
||||||
|
const one = (css: string) => scopeCss(css).replace(/\s+/gu, " ").trim();
|
||||||
|
|
||||||
|
describe("scoping the built stylesheet", () => {
|
||||||
|
it("prefixes a utility with the view root, keeping the root itself in reach", () => {
|
||||||
|
expect(one(".flex{display:flex!important}")).toBe(
|
||||||
|
":is(.beaver-root, .beaver-root *).flex { display: flex !important; }"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("outranks the same utility from another plugin's Tailwind", () => {
|
||||||
|
// Class specificity: theirs is (0,1,0); ours must be (0,2,0).
|
||||||
|
const out = one(".hidden{display:none!important}");
|
||||||
|
expect(out.startsWith(":is(.beaver-root, .beaver-root *).hidden")).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps pseudo-elements at the end where they are valid", () => {
|
||||||
|
expect(one(".x::placeholder{color:red}")).toBe(
|
||||||
|
":is(.beaver-root, .beaver-root *).x::placeholder { color: red; }"
|
||||||
|
);
|
||||||
|
expect(one(".y::first-letter{t:1}")).toContain(".y:first-letter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scopes rules nested in container queries and layers", () => {
|
||||||
|
const out = one(
|
||||||
|
"@layer utilities{@container (min-width:30rem){.a{display:inline-block!important}}}"
|
||||||
|
);
|
||||||
|
// lightningcss rewrites the query into range syntax; Obsidian's Chromium
|
||||||
|
// has understood that inside @container since container queries landed.
|
||||||
|
expect(out).toContain("@container (width >= 30rem)");
|
||||||
|
expect(out).toContain(":is(.beaver-root, .beaver-root *).a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops a leading universal selector instead of producing `:is()*`", () => {
|
||||||
|
const out = one("*,:before,::backdrop{--tw-a:initial}");
|
||||||
|
expect(out).toContain(":is(.beaver-root, .beaver-root *),");
|
||||||
|
expect(out).toContain(":is(.beaver-root, .beaver-root *):before");
|
||||||
|
expect(out).not.toContain("*)*");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves tailwind's theme variables on :root, under obsidian's body values", () => {
|
||||||
|
expect(one(":root,:host{--spacing:.25rem}")).toBe(
|
||||||
|
":root, :host { --spacing: .25rem; }"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scopes the subject, so an ancestor outside the root still matches", () => {
|
||||||
|
expect(one(".theme-dark .hue-chip{c:1}")).toBe(
|
||||||
|
".theme-dark :is(.beaver-root, .beaver-root *).hue-chip { c: 1; }"
|
||||||
|
);
|
||||||
|
expect(one(".a > button{c:2}")).toBe(
|
||||||
|
".a > button:is(.beaver-root, .beaver-root *) { c: 2; }"
|
||||||
|
);
|
||||||
|
expect(one(".a *{c:3}")).toBe(
|
||||||
|
".a :is(.beaver-root, .beaver-root *) { c: 3; }"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves rules that already name the root alone", () => {
|
||||||
|
const reset = ".beaver-root.beaver-root button{cursor:pointer}";
|
||||||
|
expect(one(reset)).toBe(
|
||||||
|
".beaver-root.beaver-root button { cursor: pointer; }"
|
||||||
|
);
|
||||||
|
expect(one(".theme-dark .beaver-root{--note:red}")).toBe(
|
||||||
|
".theme-dark .beaver-root { --note: red; }"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scopes variant selectors without breaking their inner :where()", () => {
|
||||||
|
const out = one(".dark\\:bg-x:where(.theme-dark,.theme-dark *){b:1}");
|
||||||
|
expect(out).toBe(
|
||||||
|
":is(.beaver-root, .beaver-root *).dark\\:bg-x:where(.theme-dark, .theme-dark *) { b: 1; }"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
+320
@@ -0,0 +1,320 @@
|
|||||||
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
import esbuild from "esbuild";
|
||||||
|
import { JSDOM } from "jsdom";
|
||||||
|
import { root, sharedLib, svelte } from "../esbuild.config.mjs";
|
||||||
|
|
||||||
|
const out = join(mkdtempSync(join(tmpdir(), "beaver-")), "bundle.mjs");
|
||||||
|
writeFileSync(
|
||||||
|
join(root, "tests/.entry.ts"),
|
||||||
|
`export { default as App } from "../src/ui/app.svelte";
|
||||||
|
export { PanelState } from "../src/state.svelte";
|
||||||
|
export { FakeClient, MASTER, BRANCH_UFW, BRANCH_BLASTER, DEEP_PANEL } from "../preview/fake-client";
|
||||||
|
export { mount, unmount, flushSync } from "svelte";
|
||||||
|
export { Component } from "obsidian";
|
||||||
|
export { conversationActions } from "$lib/panel/actions.svelte";
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
bundle: true,
|
||||||
|
conditions: ["svelte", "browser"],
|
||||||
|
entryPoints: [join(root, "tests/.entry.ts")],
|
||||||
|
format: "esm",
|
||||||
|
logLevel: "error",
|
||||||
|
mainFields: ["svelte", "browser", "module", "main"],
|
||||||
|
outfile: out,
|
||||||
|
plugins: [
|
||||||
|
sharedLib({ obsidian: join(root, "tests/obsidian-stub.ts") }),
|
||||||
|
svelte(),
|
||||||
|
],
|
||||||
|
target: "es2022",
|
||||||
|
});
|
||||||
|
|
||||||
|
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
|
||||||
|
pretendToBeVisual: true,
|
||||||
|
url: "http://localhost/",
|
||||||
|
});
|
||||||
|
for (const key of Object.getOwnPropertyNames(dom.window)) {
|
||||||
|
if (key.startsWith("_") || globalThis[key] !== undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
globalThis[key] = dom.window[key];
|
||||||
|
} catch {
|
||||||
|
/* getter-only window properties are fine to skip */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalThis.window = dom.window;
|
||||||
|
globalThis.document = dom.window.document;
|
||||||
|
globalThis.Event = dom.window.Event;
|
||||||
|
globalThis.CustomEvent = dom.window.CustomEvent;
|
||||||
|
dom.window.Element.prototype.getBoundingClientRect = () => ({
|
||||||
|
bottom: 0,
|
||||||
|
height: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
toJSON: () => ({}),
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
dom.window.Element.prototype.setPointerCapture = () => {};
|
||||||
|
dom.window.Element.prototype.releasePointerCapture = () => {};
|
||||||
|
dom.window.Element.prototype.hasPointerCapture = () => false;
|
||||||
|
dom.window.Element.prototype.scrollIntoView = () => {};
|
||||||
|
dom.window.Element.prototype.animate = () => {
|
||||||
|
const animation = {
|
||||||
|
addEventListener() {},
|
||||||
|
cancel() {},
|
||||||
|
commitStyles() {},
|
||||||
|
currentTime: 0,
|
||||||
|
effect: { getComputedTiming: () => ({ delay: 0, duration: 0 }) },
|
||||||
|
finish() {},
|
||||||
|
finished: Promise.resolve(),
|
||||||
|
onfinish: null,
|
||||||
|
pause() {},
|
||||||
|
play() {},
|
||||||
|
playState: "finished",
|
||||||
|
removeEventListener() {},
|
||||||
|
reverse() {},
|
||||||
|
startTime: 0,
|
||||||
|
};
|
||||||
|
queueMicrotask(() => animation.onfinish?.());
|
||||||
|
return animation;
|
||||||
|
};
|
||||||
|
globalThis.ResizeObserver = class {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
};
|
||||||
|
globalThis.matchMedia = () => ({
|
||||||
|
addEventListener() {},
|
||||||
|
matches: false,
|
||||||
|
removeEventListener() {},
|
||||||
|
});
|
||||||
|
dom.window.matchMedia = globalThis.matchMedia;
|
||||||
|
|
||||||
|
const {
|
||||||
|
App,
|
||||||
|
PanelState,
|
||||||
|
FakeClient,
|
||||||
|
MASTER,
|
||||||
|
BRANCH_UFW,
|
||||||
|
BRANCH_BLASTER,
|
||||||
|
DEEP_PANEL,
|
||||||
|
mount,
|
||||||
|
unmount,
|
||||||
|
flushSync,
|
||||||
|
Component,
|
||||||
|
conversationActions,
|
||||||
|
} = await import(pathToFileURL(out).href);
|
||||||
|
|
||||||
|
const failures = [];
|
||||||
|
function check(name, condition, detail = "") {
|
||||||
|
if (condition) {
|
||||||
|
console.log(` ok ${name}`);
|
||||||
|
} else {
|
||||||
|
failures.push(name);
|
||||||
|
console.log(` FAIL ${name}${detail ? ` -- ${detail}` : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const settle = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
const client = new FakeClient();
|
||||||
|
const state = new PanelState();
|
||||||
|
state.selected = MASTER;
|
||||||
|
const opened = [];
|
||||||
|
const app = {
|
||||||
|
workspace: {
|
||||||
|
openLinkText: (link, from) => opened.push({ from, target: link }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const target = dom.window.document.body;
|
||||||
|
const instance = mount(App, {
|
||||||
|
props: {
|
||||||
|
app,
|
||||||
|
chatsFolder: () => "💬 чаты",
|
||||||
|
client,
|
||||||
|
component: new Component(),
|
||||||
|
onOpenSettings: () => {},
|
||||||
|
state,
|
||||||
|
},
|
||||||
|
target,
|
||||||
|
});
|
||||||
|
flushSync();
|
||||||
|
await settle();
|
||||||
|
flushSync();
|
||||||
|
|
||||||
|
const text = () => target.textContent ?? "";
|
||||||
|
check(
|
||||||
|
"root carries the theme class",
|
||||||
|
target.querySelector(".beaver-root") !== null
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"portal layer is on body",
|
||||||
|
target.querySelector(".beaver-portal") !== null
|
||||||
|
);
|
||||||
|
check("thread header shows the master", text().includes("Master · 29 Aug"));
|
||||||
|
check(
|
||||||
|
"the running turn's tools are on screen",
|
||||||
|
text().includes("Agent") && text().includes("Bash")
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"history rendered through the host renderer",
|
||||||
|
target.querySelector(".beaver-md") !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
const wikilink = target.querySelector("a.internal-link");
|
||||||
|
check("wikilink rendered as an internal link", wikilink !== null);
|
||||||
|
if (wikilink) {
|
||||||
|
wikilink.dispatchEvent(
|
||||||
|
new dom.window.MouseEvent("click", { bubbles: true, cancelable: true })
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"clicking it opens the note in Obsidian",
|
||||||
|
opened.some((o) => o.target === "2026-08-28"),
|
||||||
|
JSON.stringify(opened)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.selected = BRANCH_BLASTER;
|
||||||
|
flushSync();
|
||||||
|
await settle();
|
||||||
|
flushSync();
|
||||||
|
check("switching follows the state", text().includes("бластер"));
|
||||||
|
check(
|
||||||
|
"a pending question shows its buttons",
|
||||||
|
text().includes("Xiaomi Mi Blaster")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Nothing picked - the panel just opened on a phone - lands on the master
|
||||||
|
// rather than on a prompt to pick something.
|
||||||
|
state.selected = null;
|
||||||
|
flushSync();
|
||||||
|
await settle();
|
||||||
|
flushSync();
|
||||||
|
check(
|
||||||
|
"with nothing picked it falls back to the master",
|
||||||
|
state.selected === MASTER,
|
||||||
|
String(state.selected)
|
||||||
|
);
|
||||||
|
state.selected = BRANCH_BLASTER;
|
||||||
|
flushSync();
|
||||||
|
await settle();
|
||||||
|
flushSync();
|
||||||
|
check("an explicit pick still wins", state.selected === BRANCH_BLASTER);
|
||||||
|
|
||||||
|
const composer = target.querySelector("textarea");
|
||||||
|
check("composer is there", composer !== null);
|
||||||
|
if (composer) {
|
||||||
|
composer.value = "закажи Broadlink";
|
||||||
|
composer.dispatchEvent(new dom.window.Event("input", { bubbles: true }));
|
||||||
|
flushSync();
|
||||||
|
composer
|
||||||
|
.closest("form")
|
||||||
|
?.dispatchEvent(
|
||||||
|
new dom.window.Event("submit", { bubbles: true, cancelable: true })
|
||||||
|
);
|
||||||
|
await settle();
|
||||||
|
check(
|
||||||
|
"sending goes out as source=panel",
|
||||||
|
client.sent.some(
|
||||||
|
(m) => m.origin === "panel" && m.text === "закажи Broadlink"
|
||||||
|
),
|
||||||
|
JSON.stringify(client.sent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const switcher = target.querySelector('[aria-label="Switch conversation"]');
|
||||||
|
check("narrow layout shows the switcher", switcher !== null);
|
||||||
|
switcher?.dispatchEvent(
|
||||||
|
new dom.window.PointerEvent("pointerdown", {
|
||||||
|
bubbles: true,
|
||||||
|
button: 0,
|
||||||
|
pointerType: "mouse",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
switcher?.click();
|
||||||
|
flushSync();
|
||||||
|
await settle();
|
||||||
|
const rows = [...dom.window.document.querySelectorAll("[data-row]")];
|
||||||
|
check(
|
||||||
|
"switcher lists open conversations grouped by day",
|
||||||
|
rows.length >= 4 && text().includes("Today"),
|
||||||
|
`${rows.length}`
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"closed ones stay out until asked",
|
||||||
|
!rows.some((row) => row.dataset.row === "c5triage0000")
|
||||||
|
);
|
||||||
|
|
||||||
|
dom.window.document.dispatchEvent(
|
||||||
|
new dom.window.KeyboardEvent("keydown", {
|
||||||
|
bubbles: true,
|
||||||
|
code: "Escape",
|
||||||
|
key: "Escape",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
flushSync();
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
state.selected = DEEP_PANEL;
|
||||||
|
flushSync();
|
||||||
|
await settle();
|
||||||
|
flushSync();
|
||||||
|
check(
|
||||||
|
"header carries the actions menu",
|
||||||
|
target.querySelector('[aria-label="Conversation actions"]') !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
const scope = (current) => ({
|
||||||
|
client,
|
||||||
|
current,
|
||||||
|
host: { name: "obsidian", openNote: () => {}, touch: false },
|
||||||
|
onBranch: () => {},
|
||||||
|
onChanged: () => {},
|
||||||
|
onOpen: () => {},
|
||||||
|
onRename: () => {},
|
||||||
|
});
|
||||||
|
const labels = async (id, current = true) =>
|
||||||
|
conversationActions(await client.conversation(id), scope(current)).map(
|
||||||
|
(a) => a.label
|
||||||
|
);
|
||||||
|
const deep = await labels(DEEP_PANEL);
|
||||||
|
check(
|
||||||
|
"deep chat offers its note, memory and digest",
|
||||||
|
deep.includes("Open note") &&
|
||||||
|
deep.includes("Remember on close") &&
|
||||||
|
deep.includes("Close with digest"),
|
||||||
|
deep.join(", ")
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
"a deep chat has no Telegram toggle",
|
||||||
|
!deep.some((l) => l.includes("Telegram"))
|
||||||
|
);
|
||||||
|
const hidden = await labels(BRANCH_BLASTER, false);
|
||||||
|
check(
|
||||||
|
"a hidden branch can return to Telegram and be opened",
|
||||||
|
hidden.includes("Return to Telegram") && hidden.includes("Open"),
|
||||||
|
hidden.join(", ")
|
||||||
|
);
|
||||||
|
const shown = await labels(BRANCH_UFW);
|
||||||
|
check(
|
||||||
|
"a visible branch can hide from Telegram and merge",
|
||||||
|
shown.includes("Hide from Telegram") &&
|
||||||
|
shown.includes("Merge into parent") &&
|
||||||
|
!shown.includes("Open"),
|
||||||
|
shown.join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
|
await unmount(instance);
|
||||||
|
console.log(
|
||||||
|
failures.length === 0
|
||||||
|
? "\nsmoke: all good"
|
||||||
|
: `\nsmoke: ${failures.length} FAILED`
|
||||||
|
);
|
||||||
|
process.exit(failures.length === 0 ? 0 : 1);
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const source = readFileSync(
|
||||||
|
join(import.meta.dirname, "../src/tailwind.css"),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
function block(selector: string): string {
|
||||||
|
const at = source.indexOf(`\n${selector} {`);
|
||||||
|
if (at === -1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const open = source.indexOf("{", at);
|
||||||
|
const close = source.indexOf("\n}", open);
|
||||||
|
return source.slice(open, close);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("theme aliases", () => {
|
||||||
|
const OBSIDIAN_VARS = [
|
||||||
|
"--background-primary",
|
||||||
|
"--background-secondary",
|
||||||
|
"--background-modifier-border",
|
||||||
|
"--text-normal",
|
||||||
|
"--text-muted",
|
||||||
|
"--color-accent",
|
||||||
|
"--text-on-accent",
|
||||||
|
"--text-error",
|
||||||
|
];
|
||||||
|
|
||||||
|
it("are declared on .beaver-root, not :root", () => {
|
||||||
|
const root = block(".beaver-root");
|
||||||
|
expect(root).not.toBe("");
|
||||||
|
for (const name of OBSIDIAN_VARS) {
|
||||||
|
expect(root, `${name} must be aliased on .beaver-root`).toContain(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never reference Obsidian variables from :root", () => {
|
||||||
|
const rootBlocks = [...source.matchAll(/(^|\n):root\s*\{([^}]*)\}/g)];
|
||||||
|
for (const [, , body] of rootBlocks) {
|
||||||
|
for (const name of OBSIDIAN_VARS) {
|
||||||
|
expect(body, `:root must not reference ${name}`).not.toContain(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scopes the dark override under the view root", () => {
|
||||||
|
expect(source).toContain(".theme-dark .beaver-root");
|
||||||
|
expect(source).not.toMatch(/\n\.theme-dark\s*\{/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the msos palette out: the live accent is Obsidian's", () => {
|
||||||
|
const root = block(".beaver-root");
|
||||||
|
expect(root).toContain("--signal: var(--color-accent)");
|
||||||
|
expect(root).toContain("--primary: var(--color-accent)");
|
||||||
|
expect(root).not.toMatch(/oklch\([^)]*\s34[0-2]\)/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("reset specificity", () => {
|
||||||
|
it("uses a doubled class to outrank app and theme styles", () => {
|
||||||
|
expect(source).toContain(".beaver-root.beaver-root");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gives every control Obsidian's corner language", () => {
|
||||||
|
expect(source).toContain('[data-slot="button"]');
|
||||||
|
expect(source).toContain("var(--button-radius");
|
||||||
|
expect(source).toContain('[data-slot="input"]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cascade against Obsidian", () => {
|
||||||
|
it("marks utilities important so they outrank app.css", () => {
|
||||||
|
expect(source).toMatch(
|
||||||
|
/@import "tailwindcss\/utilities\.css"[^;]*\bimportant\b/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scans the shared panel sources", () => {
|
||||||
|
expect(source).toContain('@source "../../beaver-gateway/ui/src/lib"');
|
||||||
|
expect(source).toContain("panel/panel.css");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("imports only the theme and utility layers", () => {
|
||||||
|
expect(source).toContain('@import "tailwindcss/theme.css"');
|
||||||
|
expect(source).toContain('@import "tailwindcss/utilities.css"');
|
||||||
|
expect(source).not.toMatch(/@import "tailwindcss"\s*[;l]/);
|
||||||
|
expect(source).not.toMatch(/@import\s+"tailwindcss\/preflight/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Obsidian's own `button` rules are what the reset answers, but a grid
|
||||||
|
// button must keep centring itself or an icon-only one drifts left.
|
||||||
|
it("leaves grid buttons to their own placement", () => {
|
||||||
|
expect(source).toMatch(
|
||||||
|
/button:is\(\.grid, \.inline-grid\) \{\s*justify-content: initial/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
+16
-3
@@ -10,9 +10,22 @@
|
|||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"strictNullChecks": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": ["DOM", "ES2022"]
|
"skipLibCheck": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"types": ["node", "svelte"],
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"paths": {
|
||||||
|
"$lib/*": ["../beaver-gateway/ui/src/lib/*"],
|
||||||
|
"svelte-sonner": ["src/shims/sonner.ts"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.svelte",
|
||||||
|
"preview/**/*.ts",
|
||||||
|
"tests/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": ["preview/shot.ts"]
|
||||||
}
|
}
|
||||||
+3
-1
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"0.1.0": "1.5.0"
|
"0.1.0": "1.5.0",
|
||||||
|
"0.2.0": "1.5.0",
|
||||||
|
"0.3.0": "1.5.0"
|
||||||
}
|
}
|
||||||
@@ -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"],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in new issue
Block a user