feat(*): svelte 5 panel view sharing the gateway ui, obsidian theme, preview and tests
This commit is contained in:
@@ -3,7 +3,10 @@ node_modules/
|
||||
|
||||
# build output (regenerated by `make install` / `make zip`)
|
||||
main.js
|
||||
styles.css
|
||||
*.zip
|
||||
preview/bundle.js
|
||||
tests/.entry.ts
|
||||
|
||||
# plugin runtime — Obsidian writes settings here when the symlinked dir
|
||||
# is used as the live plugin; never want it in the repo
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
PLUGIN_ID := beaver
|
||||
ASSETS := manifest.json main.js styles.css versions.json
|
||||
# package-lock.json → npm; otherwise whichever of pnpm/npm is around.
|
||||
PM := $(if $(wildcard package-lock.json),$(shell command -v npm 2>/dev/null),$(shell command -v pnpm 2>/dev/null || command -v npm 2>/dev/null))
|
||||
|
||||
.PHONY: install zip _build
|
||||
.PHONY: sync check fix test build install zip preview
|
||||
|
||||
_build:
|
||||
@test -n "$(PM)" || (echo "no pnpm or npm in PATH" >&2; exit 1)
|
||||
$(PM) install --silent
|
||||
$(PM) run build
|
||||
sync:
|
||||
bun install
|
||||
|
||||
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 -d "$(VAULT)/.obsidian" || (echo "no .obsidian dir at $(VAULT)" >&2; exit 1)
|
||||
@target="$(VAULT)/.obsidian/plugins/$(PLUGIN_ID)"; \
|
||||
@@ -18,9 +27,12 @@ install: _build
|
||||
cp $(ASSETS) "$$target/"; \
|
||||
echo "installed → $$target"
|
||||
|
||||
zip: _build
|
||||
zip: build
|
||||
@version=$$(node -p "require('./manifest.json').version"); \
|
||||
out="beaver-plugin-$$version.zip"; \
|
||||
rm -f "$$out"; \
|
||||
zip -q -j "$$out" $(ASSETS); \
|
||||
echo "wrote $$out"
|
||||
|
||||
preview:
|
||||
bun run preview
|
||||
|
||||
@@ -1,73 +1,57 @@
|
||||
# Beaver — Obsidian plugin
|
||||
# Beaver - Obsidian plugin
|
||||
|
||||
[](https://sladge.net)
|
||||
|
||||
Two things:
|
||||
|
||||
- **Send the current note** to Beaver's `markdown frontend` (`POST /chat/stream` on `beaver-gateway`) and stream the agent's reply back into the buffer. Skips the Obsidian Sync round-trip by POSTing the buffer contents directly.
|
||||
- **Activity panel** (v0): a side view that follows the active note's `conversation_id` and shows, live over SSE, what the agent is doing — tool calls, subagents as a tree, turn status, usage of each turn.
|
||||
- **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
|
||||
|
||||
- **Beaver: Send using selected agent** — uses `frontmatter.agent`. Only when the active note has an `agent:` field.
|
||||
- **Beaver: Send using different agent** — fetches the agent list from the gateway and opens a fuzzy picker. Same condition.
|
||||
- **Beaver: Open chat in panel** — reveals the activity panel in the right sidebar (creates it on first use) and points it at the active note.
|
||||
- **Beaver: Open panel** - the right sidedock (also the ribbon icon).
|
||||
- **Beaver: Open panel in a tab** - the wide layout with the conversation rail on the left.
|
||||
- **Beaver: Open chat in panel** - the active note has `conversation_id` in its frontmatter; the panel shows that conversation.
|
||||
- **Beaver: Send using selected agent** / **Send using different agent** - the note has `agent:` in its frontmatter.
|
||||
|
||||
## Activity panel
|
||||
## The panel
|
||||
|
||||
Subscribes to `GET /api/conversations/{id}/events` (SSE, bearer) where `{id}` is the `conversation_id` from the active note's frontmatter. Switching notes switches the subscription; a note without `conversation_id` shows a hint instead. On connect it fetches `GET /api/conversations/{id}` for the snapshot (title, agent, kind, status, whether a turn is running), then renders bus events:
|
||||
The UI is `beaver-gateway/ui/src/lib/panel`, compiled into this plugin by `esbuild.config.mjs` (`$lib/*` points at the gateway's `ui/src/lib`; every bare import resolves from this plugin's `node_modules`, so there is one Svelte runtime; `svelte-sonner` becomes Obsidian notices). The same components render the browser admin and `/admin/panel`.
|
||||
|
||||
- `turn.start` / `turn.end` — a turn block with status (running / done / interrupted / error), origin (`user` / `inject`), start time, duration and usage (`in · out · cache read/write · $ · time`).
|
||||
- `tool` / `tool.result` — a tool row (name, input summary, result status); clicking it expands the raw input and the result preview. Rows with `parent_tool_use_id` nest under the `Task`/`Agent` tool that spawned the subagent.
|
||||
- `stream` `content_block_start` of type `tool_use` — pre-creates the row so a long-running tool shows up before its input is complete.
|
||||
- `say` — text the dispatcher sent out of an inject turn.
|
||||
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.
|
||||
|
||||
The stream reconnects with exponential backoff (1 s → 30 s, jittered) and resets on `hello`. `401`/`403`/`404` are not retried: the panel shows the error and waits for you to fix the token or the id. When it reconnects and the snapshot says nothing is running, turns still shown as running are marked interrupted — their `turn.end` was lost with the connection.
|
||||
Markdown in the thread goes through Obsidian's `MarkdownRenderer`: `[[links]]` open the note, external links open the browser. A deep chat's row offers **Open note** (its markdown binding, under the vault subpath).
|
||||
|
||||
Plain DOM and CSS on Obsidian's theme variables; no framework. It is the v0 of the panel from `архитектура.md` §2 — the full Svelte UI (M5) replaces it.
|
||||
Layout: below 48rem of panel width the conversation switcher sits in the header (kind, title, live dot, follow toggle); wider than that the grouped rail (master, branches, deep chats, jobs) sits on the left. Right-click a row for its actions; on touch the `…` button is always visible. `↑`/`↓` walk the rows, `/` jumps to the search, `⌘↩` sends.
|
||||
|
||||
## Settings
|
||||
|
||||
- **Base URL** — markdown-frontend root, e.g. `http://localhost:62993` or `https://host/md`.
|
||||
- **API origin** — where `/api/…` lives, e.g. `http://localhost:62994` or `https://host`. Empty = derived from Base URL (`…/md` → `…`, port `62993` → `62994`).
|
||||
- **Bearer token** — needs the `messages` scope for sending and the `api` scope for the panel; a `*` bootstrap token covers both.
|
||||
- **Vault subpath** — folder in this vault that maps to the gateway's vault root.
|
||||
- **Test connection** / **Test API** — `GET /agents` on the markdown frontend, `GET /api/agents` on the API origin.
|
||||
- **Base URL** - markdown frontend root, e.g. `https://host/md`.
|
||||
- **API origin** - where `/api/…` lives; empty derives it from the base URL (`…/md` → `…`).
|
||||
- **Bearer token** - `messages` scope for sending, `api` scope for the panel (or a `*` bootstrap token).
|
||||
- **Vault subpath** - the folder in this vault that maps to the gateway's vault root.
|
||||
|
||||
## Note format
|
||||
Changing settings remounts open panels.
|
||||
|
||||
A minimal note that the plugin can send:
|
||||
## Build
|
||||
|
||||
```markdown
|
||||
---
|
||||
agent: beaver-opus-medium
|
||||
---
|
||||
|
||||
hi there
|
||||
```
|
||||
bun install
|
||||
bun run dev # css + esbuild watch
|
||||
bun run build # svelte-check + css (minified) + bundle
|
||||
bun run test # vitest (theme rules) + tests/smoke.mjs (jsdom, fake gateway)
|
||||
bun run preview # the panel in a browser, no Obsidian: http://localhost:4174/#<scene>
|
||||
make check # ultracite + svelte-check
|
||||
make install VAULT=~/Obsidian/my-vault
|
||||
make zip
|
||||
```
|
||||
|
||||
The gateway parses `### User:` / `### Assistant:` markers; a note with no markers is treated as a single user turn. After the first turn the gateway adds `conversation_id` to the frontmatter — that is what the activity panel keys off.
|
||||
Preview scenes (`#thread` default, `#switcher`, `#closed`, `#menu`, `#actions`, `#activity`, `#branch`, `#question`, `#deep`, `#empty`, `#offline`, `#stream`; suffix `-light` for the light theme). Width is the window: run headless Chrome with `--window-size=340,720` for the sidedock, `1100,720` for a tab, `390,800` for a phone. Set `VAULT=` or `THEME=` to preview against a community theme; `app.css` is read from the Obsidian install.
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
make install VAULT=~/Obsidian/my-vault # build + copy manifest.json, main.js, styles.css, versions.json into <vault>/.obsidian/plugins/beaver
|
||||
make zip # build → beaver-plugin-<version>.zip you can carry to any vault
|
||||
```
|
||||
`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.
|
||||
|
||||
Then enable **Beaver** in Settings → Community plugins (or reload it if it was already enabled).
|
||||
## Version
|
||||
|
||||
### Mobile
|
||||
|
||||
The plugin is desktop-and-mobile (`isDesktopOnly: false`). Two things to get right on a phone:
|
||||
|
||||
- The base URL / API origin in plugin settings have to be reachable from the phone — `http://localhost:…` won't work; use the gateway's LAN IP, a tunnel, or a public hostname.
|
||||
- Get the plugin files onto the phone vault via Obsidian Sync (toggle "Installed community plugins" in your sync settings) or any file-sync tool you already use (Working Copy, Syncthing, etc.). `make install` only knows how to write to a local path.
|
||||
|
||||
### Hand-rolled
|
||||
|
||||
Four files are all Obsidian needs — `manifest.json`, `main.js`, `styles.css`, `versions.json`. Drop them in `<vault>/.obsidian/plugins/beaver/` however you like (unzip, `scp`, `rsync`, your own git, …).
|
||||
|
||||
## Bumping the version
|
||||
|
||||
Edit the version field in `manifest.json`, `package.json`, and `versions.json` by hand. Three files, one change each.
|
||||
Edit `manifest.json`, `package.json` and `versions.json` by hand.
|
||||
|
||||
+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/**"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"performance": {
|
||||
"useTopLevelRegex": "off"
|
||||
},
|
||||
"style": {
|
||||
"noMagicNumbers": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noEmptyBlockStatements": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,712 @@
|
||||
{
|
||||
"lockfileVersion": 2,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "beaver-plugin-obsidian",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.5.11",
|
||||
"@electron/asar": "^4.3.0",
|
||||
"@lucide/svelte": "^1.37.0",
|
||||
"@tailwindcss/cli": "^4.3.3",
|
||||
"@types/node": "^26.4.0",
|
||||
"bits-ui": "^2.19.0",
|
||||
"builtin-modules": "^5.3.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dompurify": "^3.4.14",
|
||||
"esbuild": "^0.28.2",
|
||||
"esbuild-svelte": "^0.9.5",
|
||||
"jsdom": "^30.0.1",
|
||||
"marked": "^18.0.11",
|
||||
"obsidian": "^1.13.1",
|
||||
"svelte": "^5.57.0",
|
||||
"svelte-check": "^4.7.6",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwind-variants": "^3.3.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^6",
|
||||
"ultracite": "^7.10.7",
|
||||
"vitest": "^4.1.11",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@6.0.7", "", { "dependencies": { "@csstools/css-calc": "^3.3.0", "@csstools/css-color-parser": "^4.1.10", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.5.2" } }, "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw=="],
|
||||
|
||||
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@8.3.2", "", { "dependencies": { "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2" } }, "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.5.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.11", "@biomejs/cli-darwin-x64": "2.5.11", "@biomejs/cli-linux-arm64": "2.5.11", "@biomejs/cli-linux-arm64-musl": "2.5.11", "@biomejs/cli-linux-x64": "2.5.11", "@biomejs/cli-linux-x64-musl": "2.5.11", "@biomejs/cli-win32-arm64": "2.5.11", "@biomejs/cli-win32-x64": "2.5.11" }, "bin": { "biome": "bin/biome" } }, "sha512-Tj0dnkLPdW0ASjHfj2D/ZkkvPU2wrFmnE1jWTD2xzV1ycapV1DutbYXk4NDnR3rYTi1ZCbNFD4G2gRMEY65WaA=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6SGZxoKbXvUjMn1t6A98HqWISPnGNbYs0R/Rt2JarmXBSev+lva4QxUMWEBX9lX1Wo1XTJ78uk5xVDtG58SRZg=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-nYkXY7tLBEgnGbYapDKAyKzgt44ZEyG+AKalvTXtCWKYgepI9dw327q+cVgedxm+Udi1ZzHKUyZrIusHi/KQbw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-3PVLSTD9RR73rvVPt5G3T1gc+ycggWEGfTD7RvzzbtcDPD27NxgxBbAFfpm7DXJKW6VLHWE1lLMGvFt2Qxjcow=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-qhyZUMyCbWYFV2bAwRNVvfMVZ+hv7WYl6mossGrxC+uiQQXhvsuWWU8zz6jYX0mChZd9MgQZbm4vozTmG/5iGw=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-JOytptlsgM33B2MMFUg8iBrb4IKpbD5JnJrSeYiaFEeAj4vuXx0iQSQZ4qK7sqyMtfjZxxPdNdMZZVL4y/mFyA=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-oRRlrchG5EfrEL/EmtT1qUjSNHk3/5LGeZhQqADBBAJF1b1ET6964xEKe7aGlGARzDfza8H/seEsFJl7S6Ql9w=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-e49E6K9hzH/ohJNx8Y26mY8HaV4I4ZViIeoqhKsmoXLKHhQnMeBAVqCgsGf2Wa3lXlS7RkporDXMHHWkzvZzFw=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.11", "", { "os": "win32", "cpu": "x64" }, "sha512-QSQr/KjOgXA7OzXJUWS+oguKyAZ3Q0l/lnlDGbu397eKo83atuWUjBPJrsqbKNF6CARGw8XXJLGzpHC8Ryhd4Q=="],
|
||||
|
||||
"@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
|
||||
|
||||
"@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="],
|
||||
|
||||
"@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="],
|
||||
|
||||
"@codemirror/state": ["@codemirror/state@6.5.0", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw=="],
|
||||
|
||||
"@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="],
|
||||
|
||||
"@csstools/color-helpers": ["@csstools/color-helpers@6.1.1", "", {}, "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w=="],
|
||||
|
||||
"@csstools/css-calc": ["@csstools/css-calc@3.3.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ=="],
|
||||
|
||||
"@csstools/css-color-parser": ["@csstools/css-color-parser@4.2.1", "", { "dependencies": { "@csstools/color-helpers": "^6.1.1", "@csstools/css-calc": "^3.3.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-YpAJZhaHplYQkG8ib+/Fx5Y0eF2lVWi3tIvMJA6i39TLyUNp2439cifzW8VMjhlqrBjHzK5hVGugRRm2zTKI/A=="],
|
||||
|
||||
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
|
||||
|
||||
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.9", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A=="],
|
||||
|
||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
|
||||
|
||||
"@electron/asar": ["@electron/asar@4.3.0", "", { "dependencies": { "glob": "^13.0.2", "minimatch": "^10.0.1" }, "bin": { "asar": "bin/asar.mjs" } }, "sha512-k/FFC/NQoTykGBi/Ga4L4KorsAKDdkK0LfNVG90eUG6vPVpJaL3iR9V1ZLbzBAF3QpojK5DcaGOdQOheKZv4JQ=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||
|
||||
"@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
|
||||
|
||||
"@internationalized/date": ["@internationalized/date@3.12.3", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@lucide/svelte": ["@lucide/svelte@1.37.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-AxaiSxeuErlt4fNmmN28oaL5R7yVoQLsfSeiAONxCEK+l0/SrLAjRktQ0DkTQpomv1oRzOA3hohWY0ZHTZCMmw=="],
|
||||
|
||||
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.4", "", {}, "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.147.0", "", {}, "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
|
||||
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
|
||||
|
||||
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="],
|
||||
|
||||
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="],
|
||||
|
||||
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="],
|
||||
|
||||
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="],
|
||||
|
||||
"@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="],
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||
|
||||
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.6", "", { "os": "android", "cpu": "arm" }, "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.6", "", { "os": "android", "cpu": "arm64" }, "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.6", "", { "os": "linux", "cpu": "arm" }, "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.6", "", { "os": "none", "cpu": "arm64" }, "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="],
|
||||
|
||||
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
|
||||
|
||||
"@tailwindcss/cli": ["@tailwindcss/cli@4.3.3", "", { "dependencies": { "@parcel/watcher": "2.5.1", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "enhanced-resolve": "^5.24.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.3.3" }, "bin": { "tailwindcss": "./dist/index.mjs" } }, "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
"@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
|
||||
|
||||
"@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="],
|
||||
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="],
|
||||
|
||||
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
||||
|
||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||
|
||||
"bits-ui": ["bits-ui@2.19.0", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-dHDm5Jw2NZJgLRvHAwPeuR4BnZij+AqTIq33OqVfo+qiMXontoC/yIvGT5TZaviv7OuBdJ2QUy2JPQ2yKBvF6w=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"builtin-modules": ["builtin-modules@5.3.0", "", {}, "sha512-hMQUl2bUFG339QygPM97E+mc8OY1IAchORZxm4a/frcYwKzozMzRVDBwHW0NjOqGElLm2O37AVQE8ikxlZHrMQ=="],
|
||||
|
||||
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||
|
||||
"cli-truncate": ["cli-truncate@6.1.1", "", { "dependencies": { "slice-ansi": "^9.0.0", "string-width": "^8.2.0" } }, "sha512-06p9vyLahLa4zkGcgsGxU6iEkSOiuI4fhCH6Emhe2lPAcoUv73n72DnODsnHA+5wwXGnV0n9M9/qOQJSjYhFhw=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
|
||||
|
||||
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"crelt": ["crelt@1.0.7", "", {}, "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA=="],
|
||||
|
||||
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
|
||||
|
||||
"data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
|
||||
|
||||
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
||||
|
||||
"devalue": ["devalue@5.9.2", "", {}, "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w=="],
|
||||
|
||||
"dompurify": ["dompurify@3.4.14", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg=="],
|
||||
|
||||
"empathic": ["empathic@2.0.1", "", {}, "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||
|
||||
"entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
|
||||
|
||||
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
||||
|
||||
"esbuild-svelte": ["esbuild-svelte@0.9.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.19" }, "peerDependencies": { "esbuild": ">=0.17.0", "svelte": ">=4.2.1 <6" } }, "sha512-16FUSj64aiS28CCxYeK3hVxCyU4PwGBSkeArawAtnWSV7l0Gc+frIOP88MNj8Q7tDAGyEJAHT6OpOvM24BG46w=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"esrap": ["esrap@2.3.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"execa": ["execa@10.0.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "figures": "^6.1.0", "get-stream": "^9.0.1", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.3.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "which-command": "^0.1.0", "yoctocolors": "^2.1.2" } }, "sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw=="],
|
||||
|
||||
"expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
|
||||
|
||||
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
|
||||
|
||||
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
|
||||
|
||||
"fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"find-workspaces": ["find-workspaces@0.3.1", "", { "dependencies": { "fast-glob": "^3.3.2", "pkg-types": "^1.0.3", "yaml": "^2.3.4" } }, "sha512-UDkGILGJSA1LN5Aa7McxCid4sqW3/e+UYsVwyxki3dDT0F8+ym0rAfnCkEfkL0rO7M+8/mvkim4t/s3IPHmg+w=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||
|
||||
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
||||
|
||||
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
||||
|
||||
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||
|
||||
"jsdom": ["jsdom@30.0.1", "", { "dependencies": { "@asamuzakjp/css-color": "^6.0.5", "@asamuzakjp/dom-selector": "^8.3.0", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.7", "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.2", "undici": "^8.9.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^17.1.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.2.3" }, "optionalPeers": ["canvas"] }, "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA=="],
|
||||
|
||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||
|
||||
"log-update": ["log-update@8.0.0", "", { "dependencies": { "ansi-escapes": "^7.3.0", "cli-cursor": "^5.0.0", "slice-ansi": "^9.0.0", "string-width": "^8.2.0", "strip-ansi": "^7.2.0", "wrap-ansi": "^10.0.0" } }, "sha512-lddSgOt3bPASrylL54ZSpy8nBHns+vBVSoILlVOx+dei300pnLRN958rj/EdlVLKuWlSESU3qdnDZdAI7FXYGg=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
|
||||
|
||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"magicast": ["magicast@0.5.4", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w=="],
|
||||
|
||||
"marked": ["marked@18.0.11", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||
|
||||
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
|
||||
|
||||
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||
|
||||
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
|
||||
|
||||
"moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
|
||||
|
||||
"nypm": ["nypm@0.6.9", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.2.4" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w=="],
|
||||
|
||||
"obsidian": ["obsidian@1.13.1", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-qtTEA2pmhJzhuhJqzbBFRYhpIOqvW+krDYjtFynv66KbxBbumHBlsJfWw3I4jtnK/6fZwbQhCrmmDdRwXmX56w=="],
|
||||
|
||||
"obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
|
||||
|
||||
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
||||
|
||||
"parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
|
||||
|
||||
"path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
|
||||
|
||||
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||
|
||||
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
|
||||
|
||||
"pretty-ms": ["pretty-ms@9.3.1", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rolldown": ["rolldown@1.2.6", "", { "dependencies": { "@oxc-project/types": "=0.147.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.6", "@rolldown/binding-android-arm64": "1.2.6", "@rolldown/binding-darwin-arm64": "1.2.6", "@rolldown/binding-darwin-x64": "1.2.6", "@rolldown/binding-freebsd-x64": "1.2.6", "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", "@rolldown/binding-linux-arm64-gnu": "1.2.6", "@rolldown/binding-linux-arm64-musl": "1.2.6", "@rolldown/binding-linux-ppc64-gnu": "1.2.6", "@rolldown/binding-linux-s390x-gnu": "1.2.6", "@rolldown/binding-linux-x64-gnu": "1.2.6", "@rolldown/binding-linux-x64-musl": "1.2.6", "@rolldown/binding-openharmony-arm64": "1.2.6", "@rolldown/binding-win32-arm64-msvc": "1.2.6", "@rolldown/binding-win32-x64-msvc": "1.2.6" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
|
||||
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"slice-ansi": ["slice-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
"std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
|
||||
|
||||
"string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
||||
|
||||
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"svelte": ["svelte@5.57.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg=="],
|
||||
|
||||
"svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
|
||||
|
||||
"svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="],
|
||||
|
||||
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
|
||||
|
||||
"tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
|
||||
|
||||
"tailwind-variants": ["tailwind-variants@3.3.1", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge", "tailwindcss"] }, "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="],
|
||||
|
||||
"tldts": ["tldts@7.4.11", "", { "dependencies": { "tldts-core": "^7.4.11" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw=="],
|
||||
|
||||
"tldts-core": ["tldts-core@7.4.11", "", {}, "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="],
|
||||
|
||||
"tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
|
||||
|
||||
"ultracite": ["ultracite@7.10.7", "", { "dependencies": { "@clack/prompts": "^1.5.1", "cli-truncate": "^6.1.1", "commander": "^15.0.0", "deepmerge": "^4.3.1", "empathic": "^2.0.1", "execa": "^10.0.1", "fast-glob": "^3.3.3", "find-workspaces": "^0.3.1", "jsonc-parser": "^3.3.1", "log-update": "^8.0.0", "magicast": "^0.5.4", "nypm": "^0.6.9", "resolve.exports": "^2.0.3", "string-width": "^8.2.2", "yaml": "^2.9.0", "zod": "^4.4.3" }, "peerDependencies": { "oxfmt": ">=0.1.0", "oxlint": "^1.79.0" }, "optionalPeers": ["oxfmt", "oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-rc+TG/zLCV+Uk1PL4XPmsq7CW8GtlaV4g7Q53Bt5o7UNGSKKNRoPY4syz1BDWwSoNBcvWKpSnfSVflsecok4EA=="],
|
||||
|
||||
"undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
|
||||
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
"vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="],
|
||||
|
||||
"vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="],
|
||||
|
||||
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
||||
|
||||
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
|
||||
|
||||
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@17.1.0", "", { "dependencies": { "@exodus/bytes": "^1.15.1", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw=="],
|
||||
|
||||
"which-command": ["which-command@0.1.0", "", { "bin": { "which-command": "cli.js" } }, "sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA=="],
|
||||
|
||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@10.0.1", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0" } }, "sha512-M0N4xzyzosiIok3svYlEo1sdLZts/8FPgYH/GPC3wvlmPoRvnoManGMrE54waYj3tISA8w6lsdesfVv67qSr8Q=="],
|
||||
|
||||
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
|
||||
|
||||
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
|
||||
|
||||
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||
|
||||
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||
|
||||
"zod": ["zod@4.5.2", "", {}, "sha512-XkYXCol10+ba/6F/cueWV+TezUeOqXW0hdeJt5CdXjTYeAgAQg5N03RQdJ80mhfFE72+pblvYMW4wy2Qp4Qbrg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"data-urls/whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
|
||||
|
||||
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||
|
||||
"vite/lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
|
||||
|
||||
"vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
|
||||
}
|
||||
}
|
||||
+98
-17
@@ -1,13 +1,14 @@
|
||||
import esbuild from "esbuild";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import builtins from "builtin-modules";
|
||||
import esbuild from "esbuild";
|
||||
import sveltePlugin from "esbuild-svelte";
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
export const root = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const LIB = path.resolve(root, "../beaver-gateway/ui/src/lib");
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
const EXTERNAL = [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
@@ -22,19 +23,99 @@ const ctx = await esbuild.context({
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins,
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2022",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
];
|
||||
|
||||
if (prod) {
|
||||
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";
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const ctx = await esbuild.context({
|
||||
bundle: true,
|
||||
conditions: ["svelte", "browser"],
|
||||
entryPoints: ["src/main.ts"],
|
||||
format: "cjs",
|
||||
logLevel: "info",
|
||||
mainFields: ["svelte", "browser", "module", "main"],
|
||||
minify: prod,
|
||||
outfile: "main.js",
|
||||
plugins: [sharedLib(), svelte()],
|
||||
sourcemap: prod ? false : "inline",
|
||||
target: "es2022",
|
||||
treeShaking: true,
|
||||
});
|
||||
if (prod) {
|
||||
await ctx.rebuild();
|
||||
await ctx.dispose();
|
||||
} else {
|
||||
} else {
|
||||
await ctx.watch();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"id": "beaver",
|
||||
"name": "Beaver",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.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",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
||||
Generated
-602
@@ -1,602 +0,0 @@
|
||||
{
|
||||
"name": "beaver-plugin-obsidian",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "beaver-plugin-obsidian",
|
||||
"version": "0.2.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"builtin-modules": "^4.0.0",
|
||||
"esbuild": "^0.21.5",
|
||||
"obsidian": "^1.5.7",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.38.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
|
||||
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
||||
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
|
||||
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
|
||||
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/codemirror": "5.60.8",
|
||||
"moment": "2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/state": "6.5.0",
|
||||
"@codemirror/view": "6.38.6"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-10
@@ -1,18 +1,43 @@
|
||||
{
|
||||
"name": "beaver-plugin-obsidian",
|
||||
"version": "0.2.0",
|
||||
"description": "Obsidian plugin for Beaver's markdown frontend.",
|
||||
"main": "main.js",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"description": "Beaver's panel and markdown frontend for Obsidian.",
|
||||
"type": "module",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
|
||||
"build": "bun run check && bun run css -- --minify && node esbuild.config.mjs production",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json --threshold error",
|
||||
"css": "tailwindcss -i src/tailwind.css -o styles.css",
|
||||
"css:watch": "tailwindcss -i src/tailwind.css -o styles.css --watch",
|
||||
"dev": "bun run css && node esbuild.config.mjs",
|
||||
"fix": "ultracite fix",
|
||||
"lint": "ultracite check",
|
||||
"preview": "node preview/serve.mjs",
|
||||
"test": "vitest run && node tests/smoke.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"builtin-modules": "^4.0.0",
|
||||
"esbuild": "^0.21.5",
|
||||
"obsidian": "^1.5.7",
|
||||
"typescript": "^5.4.5"
|
||||
"@biomejs/biome": "^2.5.11",
|
||||
"@electron/asar": "^4.3.0",
|
||||
"@lucide/svelte": "^1.37.0",
|
||||
"@tailwindcss/cli": "^4.3.3",
|
||||
"@types/node": "^26.4.0",
|
||||
"bits-ui": "^2.19.0",
|
||||
"builtin-modules": "^5.3.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dompurify": "^3.4.14",
|
||||
"esbuild": "^0.28.2",
|
||||
"esbuild-svelte": "^0.9.5",
|
||||
"jsdom": "^30.0.1",
|
||||
"marked": "^18.0.11",
|
||||
"obsidian": "^1.13.1",
|
||||
"svelte": "^5.57.0",
|
||||
"svelte-check": "^4.7.6",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwind-variants": "^3.3.1",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^6",
|
||||
"ultracite": "^7.10.7",
|
||||
"vitest": "^4.1.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Component } from "obsidian";
|
||||
import { mount } from "svelte";
|
||||
import { PanelState } from "../src/state.svelte";
|
||||
import App from "../src/ui/app.svelte";
|
||||
import { BRANCH_BLASTER, DEEP_PANEL, FakeClient, MASTER } from "./fake-client";
|
||||
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// bits-ui opens menus on pointerdown and tabs on pointer or click: send both.
|
||||
function press(element: Element | null | undefined): void {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
element.dispatchEvent(
|
||||
new PointerEvent("pointerdown", {
|
||||
bubbles: true,
|
||||
button: 0,
|
||||
cancelable: true,
|
||||
pointerType: "mouse",
|
||||
})
|
||||
);
|
||||
(element as HTMLElement).focus();
|
||||
(element as HTMLElement).click();
|
||||
}
|
||||
|
||||
// Scenes for screenshots: open the page with a hash to land in a state.
|
||||
// Width comes from the window (``--window-size``), the state from here.
|
||||
async function main(): Promise<void> {
|
||||
const client = new FakeClient();
|
||||
const state = new PanelState();
|
||||
const scene = location.hash.slice(1) || "thread";
|
||||
state.selected = MASTER;
|
||||
if (scene === "empty") {
|
||||
state.selected = null;
|
||||
}
|
||||
if (scene === "question") {
|
||||
state.selected = BRANCH_BLASTER;
|
||||
}
|
||||
if (scene === "deep") {
|
||||
state.selected = DEEP_PANEL;
|
||||
}
|
||||
if (scene === "offline") {
|
||||
state.selected = MASTER;
|
||||
}
|
||||
document.body.classList.toggle("theme-light", scene.endsWith("-light"));
|
||||
document.body.classList.toggle("theme-dark", !scene.endsWith("-light"));
|
||||
|
||||
const app = {
|
||||
workspace: {
|
||||
openLinkText: (target: string) => console.log("open", target),
|
||||
},
|
||||
};
|
||||
mount(App, {
|
||||
props: {
|
||||
app: app as never,
|
||||
client: scene === "offline" ? null : client,
|
||||
component: new Component() as never,
|
||||
onOpenSettings: () => console.log("settings"),
|
||||
state,
|
||||
vaultSubpath: () => "💬 чаты",
|
||||
},
|
||||
target: document.getElementById("app") as HTMLElement,
|
||||
});
|
||||
await wait(80);
|
||||
|
||||
if (scene.startsWith("closed")) {
|
||||
press(document.querySelector('[aria-label="Switch conversation"]'));
|
||||
await wait(60);
|
||||
press(document.querySelector('[title="Show closed conversations"]'));
|
||||
}
|
||||
if (scene.startsWith("switcher")) {
|
||||
document
|
||||
.querySelector<HTMLButtonElement>('[aria-label="Switch conversation"]')
|
||||
?.click();
|
||||
}
|
||||
if (scene.startsWith("menu")) {
|
||||
const row =
|
||||
document.querySelector<HTMLElement>(`[data-row="${BRANCH_BLASTER}"]`) ??
|
||||
(await (async () => {
|
||||
document
|
||||
.querySelector<HTMLButtonElement>(
|
||||
'[aria-label="Switch conversation"]'
|
||||
)
|
||||
?.click();
|
||||
await wait(60);
|
||||
return document.querySelector<HTMLElement>(
|
||||
`[data-row="${BRANCH_BLASTER}"]`
|
||||
);
|
||||
})());
|
||||
row?.dispatchEvent(
|
||||
new MouseEvent("contextmenu", {
|
||||
bubbles: true,
|
||||
clientX: row.getBoundingClientRect().left + 120,
|
||||
clientY: row.getBoundingClientRect().top + 16,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (scene.startsWith("actions")) {
|
||||
press(document.querySelector('[aria-label="Conversation actions"]'));
|
||||
}
|
||||
if (scene.startsWith("activity")) {
|
||||
for (const trigger of document.querySelectorAll<HTMLButtonElement>(
|
||||
'[data-slot="tabs-trigger"]'
|
||||
)) {
|
||||
if (trigger.textContent?.trim() === "Activity") {
|
||||
press(trigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scene.startsWith("branch")) {
|
||||
document
|
||||
.querySelector<HTMLButtonElement>(
|
||||
'[aria-label="Branch off this conversation"]'
|
||||
)
|
||||
?.click();
|
||||
}
|
||||
await wait(60);
|
||||
if (scene.startsWith("stream")) {
|
||||
// A tool lands while you watch: the tree grows without a reload.
|
||||
client.emit({
|
||||
conversation_id: MASTER,
|
||||
input: { command: "curl -sI https://b.hhhhh.dev/healthz" },
|
||||
name: "Bash",
|
||||
parent_tool_use_id: null,
|
||||
tool_use_id: "tu_live",
|
||||
turn_id: "t-run",
|
||||
type: "tool",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,512 @@
|
||||
import { ApiClient, ApiError } from "$lib/api/client";
|
||||
import type {
|
||||
BusEvent,
|
||||
ConversationInfo,
|
||||
ConversationSummary,
|
||||
HistoryMessage,
|
||||
} from "$lib/api/types";
|
||||
|
||||
const MINUTE = 60_000;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const now = Date.now();
|
||||
const ago = (ms: number) => new Date(now - ms).toISOString();
|
||||
|
||||
export const MASTER = "c1master0000";
|
||||
export const BRANCH_UFW = "c2ufw0000000";
|
||||
export const BRANCH_BLASTER = "c3blaster000";
|
||||
export const DEEP_PANEL = "c4panel00000";
|
||||
export const JOB_TRIAGE = "c5triage0000";
|
||||
export const FORK_MERGE = "c6fork000000";
|
||||
|
||||
function summary(
|
||||
over: Partial<ConversationSummary> & Pick<ConversationSummary, "id" | "kind">
|
||||
): ConversationSummary {
|
||||
return {
|
||||
agent: "beaver-dispatcher",
|
||||
created_at: ago(9 * HOUR),
|
||||
flags: {},
|
||||
last_activity_at: ago(4 * MINUTE),
|
||||
last_item: null,
|
||||
last_user_activity_at: ago(12 * MINUTE),
|
||||
origin: "telegram",
|
||||
parent_row: null,
|
||||
pending_question: false,
|
||||
running_turn: null,
|
||||
session_id: "9c0c3fb2-2ac7-4d6b-8d4e-1a3f0c1a2b3c",
|
||||
status: "open",
|
||||
title: null,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
const ROWS: ConversationSummary[] = [
|
||||
summary({
|
||||
id: MASTER,
|
||||
kind: "master",
|
||||
last_item: {
|
||||
created_at: ago(4 * MINUTE),
|
||||
id: 41,
|
||||
origin: "telegram",
|
||||
priority: "user",
|
||||
status: "running",
|
||||
text: "посмотри, что с ufw на dell - порт 62990 снаружи не отвечает",
|
||||
},
|
||||
running_turn: "t-run",
|
||||
title: "Master · 29 Aug",
|
||||
}),
|
||||
summary({
|
||||
id: BRANCH_UFW,
|
||||
kind: "branch",
|
||||
last_activity_at: ago(35 * MINUTE),
|
||||
last_item: {
|
||||
created_at: ago(35 * MINUTE),
|
||||
id: 40,
|
||||
origin: "telegram",
|
||||
priority: "user",
|
||||
status: "done",
|
||||
text: "правило для 62990 добавлено, проверь снаружи",
|
||||
},
|
||||
parent_row: 1,
|
||||
title: "ufw на dell",
|
||||
}),
|
||||
summary({
|
||||
id: BRANCH_BLASTER,
|
||||
kind: "branch",
|
||||
last_activity_at: ago(3 * HOUR),
|
||||
parent_row: 1,
|
||||
pending_question: true,
|
||||
title: "бластер - что заказать",
|
||||
}),
|
||||
summary({
|
||||
agent: "beaver-opus-high",
|
||||
id: DEEP_PANEL,
|
||||
kind: "deep",
|
||||
last_activity_at: ago(50 * MINUTE),
|
||||
origin: "markdown",
|
||||
title: "2026-08-29 - панель в Obsidian",
|
||||
}),
|
||||
summary({
|
||||
agent: "beaver-triage",
|
||||
id: JOB_TRIAGE,
|
||||
kind: "job",
|
||||
last_activity_at: ago(2 * HOUR),
|
||||
origin: "cron",
|
||||
status: "closed",
|
||||
title: "триаж 14:00",
|
||||
}),
|
||||
summary({
|
||||
id: FORK_MERGE,
|
||||
kind: "fork",
|
||||
last_activity_at: ago(6 * HOUR),
|
||||
origin: "api",
|
||||
status: "merged",
|
||||
title: "слив: утренняя ветка",
|
||||
}),
|
||||
];
|
||||
|
||||
const INFO: Record<string, Partial<ConversationInfo>> = {
|
||||
[MASTER]: {
|
||||
bindings: [
|
||||
{ external_id: "-1002233:general", frontend: "telegram", visible: true },
|
||||
],
|
||||
busy: true,
|
||||
live: true,
|
||||
queue: [
|
||||
{
|
||||
created_at: ago(2 * MINUTE),
|
||||
id: 42,
|
||||
origin: "watch",
|
||||
priority: "normal",
|
||||
status: "queued",
|
||||
text: "vault: 📅 дни/2026-08-29.md +3 строки",
|
||||
},
|
||||
],
|
||||
turn: {
|
||||
id: "t-run",
|
||||
origin: "user",
|
||||
started_at: ago(48_000),
|
||||
text: "посмотри, что с ufw на dell - порт 62990 снаружи не отвечает",
|
||||
tools: [
|
||||
{
|
||||
content: null,
|
||||
ended_at: null,
|
||||
input: {
|
||||
description: "проверить ufw на dell",
|
||||
prompt:
|
||||
"ssh dell 'sudo ufw status numbered' и сравнить с site.caddy",
|
||||
},
|
||||
is_error: null,
|
||||
name: "Task",
|
||||
parent_tool_use_id: null,
|
||||
started_at: ago(40_000),
|
||||
tool_use_id: "tu_task",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"Status: active\n[ 1] 22/tcp ALLOW IN Anywhere\n[ 2] 443/tcp ALLOW IN Anywhere",
|
||||
ended_at: ago(30_000),
|
||||
input: { command: "ssh dell 'sudo ufw status numbered'" },
|
||||
is_error: false,
|
||||
name: "Bash",
|
||||
parent_tool_use_id: "tu_task",
|
||||
started_at: ago(38_000),
|
||||
tool_use_id: "tu_bash",
|
||||
},
|
||||
{
|
||||
content: null,
|
||||
ended_at: null,
|
||||
input: { file_path: "/vault/мета/бобер/скиллы/ops/komodo/SKILL.md" },
|
||||
is_error: null,
|
||||
name: "Read",
|
||||
parent_tool_use_id: "tu_task",
|
||||
started_at: ago(8000),
|
||||
tool_use_id: "tu_read",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
[BRANCH_UFW]: {
|
||||
bindings: [
|
||||
{ external_id: "-1002233:517", frontend: "telegram", visible: true },
|
||||
],
|
||||
parent: MASTER,
|
||||
},
|
||||
[BRANCH_BLASTER]: {
|
||||
bindings: [
|
||||
{ external_id: "-1002233:498", frontend: "telegram", visible: false },
|
||||
],
|
||||
parent: MASTER,
|
||||
question: {
|
||||
id: "q1",
|
||||
questions: [
|
||||
{
|
||||
options: [
|
||||
{ description: "Ozon, 3 дня", label: "Xiaomi Mi Blaster" },
|
||||
{ description: "AliExpress, 2 недели", label: "Broadlink RM4" },
|
||||
],
|
||||
question: "Какой ИК-бластер заказать?",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
[DEEP_PANEL]: {
|
||||
bindings: [
|
||||
{
|
||||
external_id: "2026-08-29 - панель в Obsidian.md",
|
||||
frontend: "markdown",
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
flags: { memory: true },
|
||||
},
|
||||
[JOB_TRIAGE]: { flags: { critical: false } },
|
||||
[FORK_MERGE]: { parent: MASTER },
|
||||
};
|
||||
|
||||
const HISTORY: Record<string, HistoryMessage[]> = {
|
||||
[MASTER]: [
|
||||
{ content: "с утра: дневник приехал? что в хендауте", role: "user" },
|
||||
{
|
||||
content:
|
||||
"Приехал. В [[2026-08-28|хендауте]] два хвоста: **бластер** ждёт выбора модели, и **ufw на dell** - порт 62990 снаружи молчит. Ветка по ufw открыта, см. [[ufw на dell]].",
|
||||
role: "assistant",
|
||||
},
|
||||
{ content: "ок, гони ufw", role: "user" },
|
||||
{
|
||||
content: [
|
||||
{
|
||||
text: "Смотрю правила через `ssh dell` и сверяю с `site.caddy`.",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
id: "tu_prev",
|
||||
input: { command: "cat /root/beaver-agent/caddy/site.caddy" },
|
||||
name: "Bash",
|
||||
type: "tool_use",
|
||||
},
|
||||
],
|
||||
role: "assistant",
|
||||
},
|
||||
{
|
||||
content: [
|
||||
{
|
||||
content: "beaver.hhhhh.dev {\n reverse_proxy localhost:62990\n}",
|
||||
tool_use_id: "tu_prev",
|
||||
type: "tool_result",
|
||||
},
|
||||
],
|
||||
role: "user",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"Caddy проксирует на `localhost:62990`, значит снаружи должен отвечать 443, а не 62990 - проверю, открыт ли он вообще.",
|
||||
role: "assistant",
|
||||
},
|
||||
],
|
||||
[BRANCH_UFW]: [
|
||||
{ content: "правило для 62990 добавлено, проверь снаружи", role: "user" },
|
||||
{
|
||||
content:
|
||||
"Снаружи `curl -sI https://b.hhhhh.dev/healthz` → 200. Порт 62990 наружу не нужен: его слушает только Caddy. Предлагаю правило убрать и слить ветку.",
|
||||
role: "assistant",
|
||||
},
|
||||
],
|
||||
[BRANCH_BLASTER]: [
|
||||
{ content: "нужен ИК-бластер для кондиционера, выбери", role: "user" },
|
||||
{ content: "Два варианта, спрошу кнопками.", role: "assistant" },
|
||||
],
|
||||
[DEEP_PANEL]: [
|
||||
{
|
||||
content: "как панель в Obsidian должна вести себя в узкой колонке?",
|
||||
role: "user",
|
||||
},
|
||||
{
|
||||
content:
|
||||
"Тред всегда на виду; переключатель разговоров - в шапке. Список как отдельный экран - только на телефоне.\n\n- узкая колонка: шапка + тред + композер\n- вкладка: рейл слева\n\nСм. [[архитектура#2. Окна]].",
|
||||
role: "assistant",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface Route {
|
||||
handle: (match: RegExpMatchArray, body: unknown) => unknown;
|
||||
method: string;
|
||||
pattern: RegExp;
|
||||
}
|
||||
|
||||
// The gateway as the panel sees it, in memory: enough of ``/api`` for the
|
||||
// preview scenes and the smoke test, with a scripted event stream.
|
||||
export class FakeClient extends ApiClient {
|
||||
rows = ROWS.map((row) => ({ ...row }));
|
||||
readonly sent: { id: string; text: string; origin: string }[] = [];
|
||||
private readonly listeners = new Set<(event: BusEvent) => void>();
|
||||
private seq = 1;
|
||||
|
||||
constructor() {
|
||||
super("http://fake", "token");
|
||||
}
|
||||
|
||||
private readonly routes: Route[] = [
|
||||
{
|
||||
handle: () => ({ agents: [], frontends: [], mcps: [] }),
|
||||
method: "GET",
|
||||
pattern: /^\/api\/agents$/,
|
||||
},
|
||||
{
|
||||
handle: () => ({ conversations: this.rows }),
|
||||
method: "GET",
|
||||
pattern: /^\/api\/conversations$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id]) => this.info(id),
|
||||
method: "GET",
|
||||
pattern: /^\/api\/conversations\/([^/]+)$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id]) => ({ id, messages: HISTORY[id] ?? [] }),
|
||||
method: "GET",
|
||||
pattern: /^\/api\/conversations\/([^/]+)\/history$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id]) => ({
|
||||
entries: [],
|
||||
id,
|
||||
offset: 0,
|
||||
subpath: "",
|
||||
subpaths: [],
|
||||
total: 0,
|
||||
}),
|
||||
method: "GET",
|
||||
pattern: /^\/api\/conversations\/([^/]+)\/entries$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id], body) => {
|
||||
const { text, origin } = body as { text: string; origin: string };
|
||||
this.sent.push({ id, origin, text });
|
||||
return { id, item: this.sent.length, status: "queued" };
|
||||
},
|
||||
method: "POST",
|
||||
pattern: /^\/api\/conversations\/([^/]+)\/messages$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id], body) => {
|
||||
const { text, origin } = body as { text: string; origin: string };
|
||||
this.sent.push({ id, origin: `inject:${origin}`, text });
|
||||
return { id, item: this.sent.length, priority: "normal" };
|
||||
},
|
||||
method: "POST",
|
||||
pattern: /^\/api\/conversations\/([^/]+)\/inject$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id], body) => {
|
||||
const child = summary({
|
||||
id: `child${this.rows.length}`,
|
||||
kind: "branch",
|
||||
parent_row: 1,
|
||||
title: (body as { title?: string }).title ?? "новая ветка",
|
||||
});
|
||||
this.rows.unshift(child);
|
||||
return this.info(child.id, id);
|
||||
},
|
||||
method: "POST",
|
||||
pattern: /^\/api\/conversations\/([^/]+)\/branch$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id], body) => {
|
||||
const {
|
||||
frontend,
|
||||
external_id: external,
|
||||
visible,
|
||||
} = body as {
|
||||
frontend: string;
|
||||
external_id?: string;
|
||||
visible: boolean;
|
||||
};
|
||||
if (!INFO[id]) {
|
||||
INFO[id] = {};
|
||||
}
|
||||
const info = INFO[id];
|
||||
const bindings = (info.bindings ?? []).filter(
|
||||
(b) => b.frontend !== frontend
|
||||
);
|
||||
bindings.push({
|
||||
external_id: external ?? `${frontend}:new`,
|
||||
frontend,
|
||||
visible,
|
||||
});
|
||||
info.bindings = bindings;
|
||||
return this.info(id);
|
||||
},
|
||||
method: "POST",
|
||||
pattern: /^\/api\/conversations\/([^/]+)\/bind$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id], body) => {
|
||||
const row = this.row(id);
|
||||
row.flags = { ...row.flags, ...(body as Record<string, unknown>) };
|
||||
return row;
|
||||
},
|
||||
method: "PATCH",
|
||||
pattern: /^\/api\/conversations\/([^/]+)\/flags$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id], body) => {
|
||||
const row = this.row(id);
|
||||
const patch = body as {
|
||||
status?: ConversationSummary["status"];
|
||||
title?: string;
|
||||
};
|
||||
if (patch.status) {
|
||||
row.status = patch.status;
|
||||
}
|
||||
if (typeof patch.title === "string") {
|
||||
row.title = patch.title;
|
||||
}
|
||||
return row;
|
||||
},
|
||||
method: "PATCH",
|
||||
pattern: /^\/api\/conversations\/([^/]+)$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id]) => {
|
||||
this.row(id).status = "merged";
|
||||
return { fork: "f", id, status: "merged", text: "слито" };
|
||||
},
|
||||
method: "POST",
|
||||
pattern: /^\/api\/conversations\/([^/]+)\/merge$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id]) => {
|
||||
this.row(id).status = "closed";
|
||||
return {
|
||||
digest: "мета/бобер/выжимки/2026-08-29 - панель.md",
|
||||
id,
|
||||
status: "closed",
|
||||
};
|
||||
},
|
||||
method: "POST",
|
||||
pattern: /^\/api\/conversations\/([^/]+)\/close$/,
|
||||
},
|
||||
{
|
||||
handle: ([, id], body) => ({
|
||||
id,
|
||||
question_id: (body as { question_id: string }).question_id,
|
||||
}),
|
||||
method: "POST",
|
||||
pattern: /^\/api\/conversations\/([^/]+)\/answer$/,
|
||||
},
|
||||
];
|
||||
|
||||
private row(id: string): ConversationSummary {
|
||||
const row = this.rows.find((r) => r.id === id);
|
||||
if (!row) {
|
||||
throw new ApiError(404, `unknown conversation ${id}`, null);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private info(id: string, parent?: string): ConversationInfo {
|
||||
const row = this.row(id);
|
||||
const extra = INFO[id] ?? {};
|
||||
return {
|
||||
...row,
|
||||
bindings: [],
|
||||
busy: false,
|
||||
live: Boolean(row.session_id),
|
||||
parent: parent ?? null,
|
||||
question: null,
|
||||
queue: [],
|
||||
turn: null,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
private route(method: string, path: string, body?: unknown): unknown {
|
||||
for (const route of this.routes) {
|
||||
const match = route.method === method ? path.match(route.pattern) : null;
|
||||
if (match) {
|
||||
return route.handle(match, body);
|
||||
}
|
||||
}
|
||||
throw new ApiError(404, `no route for ${method} ${path}`, null);
|
||||
}
|
||||
|
||||
override get<T>(path: string): Promise<T> {
|
||||
return Promise.resolve(this.route("GET", path) as T);
|
||||
}
|
||||
|
||||
override post<T>(path: string, body?: unknown): Promise<T> {
|
||||
return Promise.resolve(this.route("POST", path, body ?? {}) as T);
|
||||
}
|
||||
|
||||
override patch<T>(path: string, body?: unknown): Promise<T> {
|
||||
return Promise.resolve(this.route("PATCH", path, body ?? {}) as T);
|
||||
}
|
||||
|
||||
override stream(
|
||||
_path: string,
|
||||
onEvent: (event: BusEvent) => void,
|
||||
signal: AbortSignal
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
onEvent({ seq: 0, ts: new Date().toISOString(), type: "hello" });
|
||||
this.listeners.add(onEvent);
|
||||
signal.addEventListener("abort", () => {
|
||||
this.listeners.delete(onEvent);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
emit(event: Omit<BusEvent, "seq" | "ts"> & { ts?: string }): void {
|
||||
this.seq += 1;
|
||||
const full = {
|
||||
seq: this.seq,
|
||||
ts: new Date().toISOString(),
|
||||
...event,
|
||||
} as BusEvent;
|
||||
for (const listener of this.listeners) {
|
||||
listener(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
-657
@@ -1,657 +0,0 @@
|
||||
import { ItemView, TFile, WorkspaceLeaf, setIcon } from "obsidian";
|
||||
import {
|
||||
BeaverApiError,
|
||||
BusEvent,
|
||||
ConversationInfo,
|
||||
getConversation,
|
||||
streamConversationEvents,
|
||||
} from "./api";
|
||||
import type BeaverPlugin from "./main";
|
||||
|
||||
export const ACTIVITY_VIEW_TYPE = "beaver-activity";
|
||||
|
||||
// ---- model ----
|
||||
|
||||
type ToolStatus = "running" | "done" | "error" | "aborted";
|
||||
|
||||
export interface ToolNode {
|
||||
id: string;
|
||||
name: string;
|
||||
input: unknown;
|
||||
parent: string | null;
|
||||
status: ToolStatus;
|
||||
startedAt: string;
|
||||
endedAt: string | null;
|
||||
result: string;
|
||||
children: ToolNode[];
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
type TurnStatus = "running" | "done" | "interrupted" | "error";
|
||||
|
||||
export interface TurnUsage {
|
||||
input?: number;
|
||||
output?: number;
|
||||
cache_read?: number;
|
||||
cache_creation?: number;
|
||||
cost_usd?: number | null;
|
||||
duration_ms?: number | null;
|
||||
}
|
||||
|
||||
export interface Turn {
|
||||
id: string;
|
||||
origin: string;
|
||||
status: TurnStatus;
|
||||
startedAt: string;
|
||||
endedAt: string | null;
|
||||
usage: TurnUsage | null;
|
||||
resultSubtype: string | null;
|
||||
roots: ToolNode[];
|
||||
nodes: Map<string, ToolNode>;
|
||||
says: string[];
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
state: "off" | "connecting" | "open" | "retrying" | "failed";
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
const MAX_TURNS = 20;
|
||||
|
||||
// Pure state: bus events in, tree of turns → tool calls → subagent tool
|
||||
// calls out. ``apply`` returns whether anything the panel shows changed,
|
||||
// so the chatty ``stream`` deltas don't trigger re-renders.
|
||||
export class ActivityModel {
|
||||
conversation: ConversationInfo | null = null;
|
||||
turns: Turn[] = []; // newest first
|
||||
connection: Connection = { state: "off" };
|
||||
|
||||
setConversation(info: ConversationInfo): void {
|
||||
this.conversation = info;
|
||||
const now = new Date().toISOString();
|
||||
// The snapshot is the truth about what runs *now*. A turn we still
|
||||
// show as running but the server doesn't (we were disconnected when
|
||||
// it ended, or the gateway restarted) is over; its ``turn.end`` is
|
||||
// not coming.
|
||||
for (const turn of this.turns) {
|
||||
if (turn.status !== "running" || turn.id === info.running_turn) continue;
|
||||
turn.status = "interrupted";
|
||||
turn.endedAt = now;
|
||||
for (const node of turn.nodes.values()) {
|
||||
if (node.status === "running") {
|
||||
node.status = "aborted";
|
||||
node.endedAt = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Connected mid-turn: the events for this turn started before we
|
||||
// subscribed. Open the turn so its tools have somewhere to land.
|
||||
if (info.running_turn) this.ensureTurn(info.running_turn, now, null);
|
||||
}
|
||||
|
||||
apply(ev: BusEvent): boolean {
|
||||
const ts = typeof ev.ts === "string" ? ev.ts : new Date().toISOString();
|
||||
const turnId = typeof ev.turn_id === "string" ? ev.turn_id : null;
|
||||
const parent =
|
||||
typeof ev.parent_tool_use_id === "string" ? ev.parent_tool_use_id : null;
|
||||
switch (ev.type) {
|
||||
case "turn.start": {
|
||||
if (!turnId) return false;
|
||||
const turn = this.ensureTurn(turnId, ts, str(ev.origin));
|
||||
turn.status = "running";
|
||||
turn.startedAt = ts;
|
||||
return true;
|
||||
}
|
||||
case "stream": {
|
||||
// Only the opening of a tool_use block is interesting here: it
|
||||
// gives us id + name before the full ``tool`` event arrives with
|
||||
// the input, so long-running tools show up as soon as they start.
|
||||
if (!turnId) return false;
|
||||
const raw = ev.event;
|
||||
if (!raw || typeof raw !== "object") return false;
|
||||
const sdk = raw as Record<string, unknown>;
|
||||
if (sdk.type !== "content_block_start") return false;
|
||||
const block = sdk.content_block as Record<string, unknown> | undefined;
|
||||
if (!block || block.type !== "tool_use") return false;
|
||||
if (typeof block.id !== "string") return false;
|
||||
const turn = this.ensureTurn(turnId, ts, null);
|
||||
const existed = turn.nodes.has(block.id);
|
||||
this.ensureNode(turn, block.id, str(block.name) ?? "?", parent, ts);
|
||||
return !existed;
|
||||
}
|
||||
case "tool": {
|
||||
if (!turnId || typeof ev.tool_use_id !== "string") return false;
|
||||
const turn = this.ensureTurn(turnId, ts, null);
|
||||
const node = this.ensureNode(
|
||||
turn,
|
||||
ev.tool_use_id,
|
||||
str(ev.name) ?? "?",
|
||||
parent,
|
||||
ts,
|
||||
);
|
||||
node.input = ev.input ?? null;
|
||||
return true;
|
||||
}
|
||||
case "tool.result": {
|
||||
if (!turnId || typeof ev.tool_use_id !== "string") return false;
|
||||
const turn = this.ensureTurn(turnId, ts, null);
|
||||
const node = this.ensureNode(turn, ev.tool_use_id, "?", parent, ts);
|
||||
node.status = ev.is_error ? "error" : "done";
|
||||
node.endedAt = ts;
|
||||
node.result = str(ev.content) ?? "";
|
||||
return true;
|
||||
}
|
||||
case "result": {
|
||||
if (!turnId) return false;
|
||||
const turn = this.ensureTurn(turnId, ts, null);
|
||||
turn.resultSubtype = str(ev.subtype);
|
||||
if (ev.is_error) turn.status = "error";
|
||||
return true;
|
||||
}
|
||||
case "turn.end": {
|
||||
if (!turnId) return false;
|
||||
const turn = this.ensureTurn(turnId, ts, str(ev.origin));
|
||||
const stop = str(ev.stop);
|
||||
turn.status =
|
||||
stop === "end_turn"
|
||||
? turn.status === "error"
|
||||
? "error"
|
||||
: "done"
|
||||
: stop === "interrupted"
|
||||
? "interrupted"
|
||||
: "error";
|
||||
turn.endedAt = ts;
|
||||
turn.usage =
|
||||
ev.usage && typeof ev.usage === "object"
|
||||
? (ev.usage as TurnUsage)
|
||||
: null;
|
||||
for (const node of turn.nodes.values()) {
|
||||
if (node.status === "running") {
|
||||
node.status = "aborted";
|
||||
node.endedAt = ts;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case "say": {
|
||||
if (typeof ev.text !== "string") return false;
|
||||
const turn = turnId
|
||||
? this.ensureTurn(turnId, ts, null)
|
||||
: this.turns[0];
|
||||
if (!turn) return false;
|
||||
turn.says.push(ev.text);
|
||||
return true;
|
||||
}
|
||||
case "conversation.updated":
|
||||
case "conversation.created": {
|
||||
if (!this.conversation || ev.id !== this.conversation.id) return false;
|
||||
this.conversation = {
|
||||
...this.conversation,
|
||||
...(ev as unknown as Partial<ConversationInfo>),
|
||||
};
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ensureTurn(id: string, ts: string, origin: string | null): Turn {
|
||||
let turn = this.turns.find((t) => t.id === id);
|
||||
if (!turn) {
|
||||
turn = {
|
||||
id,
|
||||
origin: origin ?? "?",
|
||||
status: "running",
|
||||
startedAt: ts,
|
||||
endedAt: null,
|
||||
usage: null,
|
||||
resultSubtype: null,
|
||||
roots: [],
|
||||
nodes: new Map(),
|
||||
says: [],
|
||||
};
|
||||
this.turns.unshift(turn);
|
||||
if (this.turns.length > MAX_TURNS) this.turns.length = MAX_TURNS;
|
||||
} else if (origin && turn.origin === "?") {
|
||||
turn.origin = origin;
|
||||
}
|
||||
return turn;
|
||||
}
|
||||
|
||||
private ensureNode(
|
||||
turn: Turn,
|
||||
id: string,
|
||||
name: string,
|
||||
parent: string | null,
|
||||
ts: string,
|
||||
): ToolNode {
|
||||
let node = turn.nodes.get(id);
|
||||
if (node) {
|
||||
if (node.name === "?" && name !== "?") node.name = name;
|
||||
return node;
|
||||
}
|
||||
node = {
|
||||
id,
|
||||
name,
|
||||
input: null,
|
||||
parent,
|
||||
status: "running",
|
||||
startedAt: ts,
|
||||
endedAt: null,
|
||||
result: "",
|
||||
children: [],
|
||||
expanded: false,
|
||||
};
|
||||
turn.nodes.set(id, node);
|
||||
if (parent) {
|
||||
// A subagent's tool call: hang it under the Task/Agent tool that
|
||||
// spawned it. Events can arrive before that tool's own ``tool``
|
||||
// event (the SDK streams the child first), so create a placeholder.
|
||||
this.ensureNode(turn, parent, "?", null, ts).children.push(node);
|
||||
} else {
|
||||
turn.roots.push(node);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
function str(v: unknown): string | null {
|
||||
return typeof v === "string" ? v : null;
|
||||
}
|
||||
|
||||
// ---- presentation helpers ----
|
||||
|
||||
function pick(input: unknown, ...keys: string[]): string | null {
|
||||
if (!input || typeof input !== "object") return null;
|
||||
const obj = input as Record<string, unknown>;
|
||||
for (const key of keys) {
|
||||
const v = obj[key];
|
||||
if (typeof v === "string" && v.trim()) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function clip(s: string, max: number): string {
|
||||
const one = s.replace(/\s+/g, " ").trim();
|
||||
return one.length > max ? one.slice(0, max - 1) + "…" : one;
|
||||
}
|
||||
|
||||
export function summarizeInput(name: string, input: unknown): string {
|
||||
if (input == null) return "";
|
||||
let s: string | null = null;
|
||||
switch (name) {
|
||||
case "Bash":
|
||||
s = pick(input, "command", "description");
|
||||
break;
|
||||
case "Read":
|
||||
case "Edit":
|
||||
case "Write":
|
||||
case "MultiEdit":
|
||||
case "NotebookEdit":
|
||||
s = pick(input, "file_path", "notebook_path");
|
||||
break;
|
||||
case "Grep":
|
||||
case "Glob":
|
||||
s = pick(input, "pattern");
|
||||
break;
|
||||
case "Task":
|
||||
case "Agent":
|
||||
s = pick(input, "description", "prompt");
|
||||
break;
|
||||
case "WebFetch":
|
||||
s = pick(input, "url");
|
||||
break;
|
||||
case "WebSearch":
|
||||
s = pick(input, "query");
|
||||
break;
|
||||
case "Skill":
|
||||
s = pick(input, "skill");
|
||||
break;
|
||||
default:
|
||||
s = null;
|
||||
}
|
||||
if (s === null) {
|
||||
if (typeof input === "object") {
|
||||
const first = Object.values(input as Record<string, unknown>).find(
|
||||
(v) => typeof v === "string" && v.trim(),
|
||||
);
|
||||
s = typeof first === "string" ? first : JSON.stringify(input);
|
||||
} else {
|
||||
s = String(input);
|
||||
}
|
||||
}
|
||||
return clip(s, 140);
|
||||
}
|
||||
|
||||
function fmtTime(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
return Number.isNaN(d.getTime()) ? iso : d.toLocaleTimeString();
|
||||
}
|
||||
|
||||
function fmtDuration(ms: number): string {
|
||||
if (ms < 1000) return `${Math.round(ms)}ms`;
|
||||
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
const m = Math.floor(ms / 60_000);
|
||||
const s = Math.round((ms % 60_000) / 1000);
|
||||
return `${m}m${s.toString().padStart(2, "0")}s`;
|
||||
}
|
||||
|
||||
function fmtTokens(n: number | undefined): string {
|
||||
if (n == null) return "-";
|
||||
return n >= 1000 ? `${(n / 1000).toFixed(n >= 10_000 ? 0 : 1)}k` : String(n);
|
||||
}
|
||||
|
||||
function fmtUsage(u: TurnUsage): string {
|
||||
const parts = [
|
||||
`in ${fmtTokens(u.input)}`,
|
||||
`out ${fmtTokens(u.output)}`,
|
||||
`cache ${fmtTokens(u.cache_read)}/${fmtTokens(u.cache_creation)}`,
|
||||
];
|
||||
if (typeof u.cost_usd === "number") parts.push(`$${u.cost_usd.toFixed(4)}`);
|
||||
if (typeof u.duration_ms === "number") parts.push(fmtDuration(u.duration_ms));
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
function elapsed(from: string, to: string | null): number {
|
||||
const a = new Date(from).getTime();
|
||||
const b = to ? new Date(to).getTime() : Date.now();
|
||||
return Number.isNaN(a) || Number.isNaN(b) ? 0 : Math.max(0, b - a);
|
||||
}
|
||||
|
||||
function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (signal.aborted) return resolve();
|
||||
const timer = window.setTimeout(done, ms);
|
||||
function done() {
|
||||
window.clearTimeout(timer);
|
||||
signal.removeEventListener("abort", done);
|
||||
resolve();
|
||||
}
|
||||
signal.addEventListener("abort", done, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
const BACKOFF_MIN_MS = 1000;
|
||||
const BACKOFF_MAX_MS = 30_000;
|
||||
|
||||
// ---- view ----
|
||||
|
||||
export class ActivityView extends ItemView {
|
||||
private readonly plugin: BeaverPlugin;
|
||||
private model = new ActivityModel();
|
||||
private abort: AbortController | null = null;
|
||||
private conversationId: string | null = null;
|
||||
private file: TFile | null = null;
|
||||
private root: HTMLElement | null = null;
|
||||
private renderQueued = false;
|
||||
private ticker: number | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: BeaverPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return ACTIVITY_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return "Beaver: activity";
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return "activity";
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
this.contentEl.empty();
|
||||
this.root = this.contentEl.createDiv({ cls: "beaver-activity" });
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("file-open", (file) => {
|
||||
// ``null`` means the active leaf isn't a file (e.g. this panel
|
||||
// got focus) — keep following the last note rather than drop it.
|
||||
if (file) this.follow(file);
|
||||
}),
|
||||
);
|
||||
this.registerEvent(
|
||||
this.app.metadataCache.on("changed", (file) => {
|
||||
// The note we follow just got (or lost) its conversation_id —
|
||||
// typically right after the first send. Re-resolve.
|
||||
if (this.file && file.path === this.file.path) this.follow(file);
|
||||
}),
|
||||
);
|
||||
// Once a second while a turn runs, so "running for 12s" ticks.
|
||||
this.ticker = window.setInterval(() => {
|
||||
if (this.model.turns.some((t) => t.status === "running")) this.render();
|
||||
}, 1000);
|
||||
this.registerInterval(this.ticker);
|
||||
this.follow(this.app.workspace.getActiveFile());
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
this.unsubscribe();
|
||||
this.root = null;
|
||||
}
|
||||
|
||||
// Follow ``file``: subscribe to the conversation its frontmatter names,
|
||||
// or show why we can't. Cheap when nothing changed.
|
||||
follow(file: TFile | null): void {
|
||||
this.file = file;
|
||||
const id = this.plugin.conversationIdOf(file);
|
||||
if (id === this.conversationId) {
|
||||
this.scheduleRender();
|
||||
return;
|
||||
}
|
||||
this.unsubscribe();
|
||||
this.conversationId = id;
|
||||
this.model = new ActivityModel();
|
||||
if (id) this.subscribe(id);
|
||||
this.scheduleRender();
|
||||
}
|
||||
|
||||
private subscribe(id: string): void {
|
||||
const controller = new AbortController();
|
||||
this.abort = controller;
|
||||
void this.loop(id, controller.signal).catch((err) => {
|
||||
console.error("Beaver activity: subscription loop died", err);
|
||||
});
|
||||
}
|
||||
|
||||
private unsubscribe(): void {
|
||||
this.abort?.abort();
|
||||
this.abort = null;
|
||||
this.model.connection = { state: "off" };
|
||||
}
|
||||
|
||||
private async loop(id: string, signal: AbortSignal): Promise<void> {
|
||||
let delay = BACKOFF_MIN_MS;
|
||||
while (!signal.aborted) {
|
||||
this.model.connection = {
|
||||
state: delay === BACKOFF_MIN_MS ? "connecting" : "retrying",
|
||||
detail: this.model.connection.detail,
|
||||
};
|
||||
this.scheduleRender();
|
||||
try {
|
||||
// Snapshot first: status/title and, if a turn is already
|
||||
// running, its id, so events that follow land in the right place.
|
||||
const info = await getConversation(this.plugin.settings, id);
|
||||
if (signal.aborted) return;
|
||||
this.model.setConversation(info);
|
||||
this.scheduleRender();
|
||||
await streamConversationEvents(
|
||||
this.plugin.settings,
|
||||
id,
|
||||
(ev) => {
|
||||
if (ev.type === "hello") {
|
||||
delay = BACKOFF_MIN_MS;
|
||||
this.model.connection = { state: "open" };
|
||||
this.scheduleRender();
|
||||
return;
|
||||
}
|
||||
if (this.model.apply(ev)) this.scheduleRender();
|
||||
},
|
||||
signal,
|
||||
);
|
||||
if (signal.aborted) return;
|
||||
this.model.connection = { state: "retrying", detail: "stream closed" };
|
||||
} catch (err) {
|
||||
if (signal.aborted) return;
|
||||
if (
|
||||
err instanceof BeaverApiError &&
|
||||
(err.status === 401 || err.status === 403 || err.status === 404)
|
||||
) {
|
||||
// Not transient: wrong token/scope or unknown conversation.
|
||||
// Retrying would only spam the gateway; the user has to act.
|
||||
this.model.connection = {
|
||||
state: "failed",
|
||||
detail: `${err.status}: ${err.message}`,
|
||||
};
|
||||
this.scheduleRender();
|
||||
return;
|
||||
}
|
||||
this.model.connection = {
|
||||
state: "retrying",
|
||||
detail: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
this.scheduleRender();
|
||||
await sleep(delay + Math.random() * 250, signal);
|
||||
delay = Math.min(delay * 2, BACKOFF_MAX_MS);
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleRender(): void {
|
||||
if (this.renderQueued) return;
|
||||
this.renderQueued = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.renderQueued = false;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
const root = this.root;
|
||||
if (!root) return;
|
||||
root.empty();
|
||||
this.renderHeader(root);
|
||||
const body = root.createDiv({ cls: "beaver-activity-body" });
|
||||
if (!this.file) {
|
||||
body.createDiv({
|
||||
cls: "beaver-activity-empty",
|
||||
text: "Open a note with conversation_id in its frontmatter.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.conversationId) {
|
||||
body.createDiv({
|
||||
cls: "beaver-activity-empty",
|
||||
text: `«${this.file.basename}» has no conversation_id in its frontmatter.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.model.turns.length === 0) {
|
||||
body.createDiv({
|
||||
cls: "beaver-activity-empty",
|
||||
text:
|
||||
this.model.connection.state === "open"
|
||||
? "No activity yet - waiting for the next turn."
|
||||
: "Connecting…",
|
||||
});
|
||||
return;
|
||||
}
|
||||
for (const turn of this.model.turns) this.renderTurn(body, turn);
|
||||
}
|
||||
|
||||
private renderHeader(root: HTMLElement): void {
|
||||
const head = root.createDiv({ cls: "beaver-activity-head" });
|
||||
const conv = this.model.conversation;
|
||||
const title = head.createDiv({ cls: "beaver-activity-title" });
|
||||
title.createSpan({
|
||||
text:
|
||||
conv?.title ||
|
||||
this.file?.basename ||
|
||||
this.conversationId ||
|
||||
"Beaver: activity",
|
||||
});
|
||||
const conn = this.model.connection;
|
||||
const dot = title.createSpan({
|
||||
cls: `beaver-activity-conn is-${conn.state}`,
|
||||
attr: { "aria-label": conn.detail ?? conn.state, title: conn.detail ?? conn.state },
|
||||
});
|
||||
dot.setText(conn.state);
|
||||
|
||||
const meta = head.createDiv({ cls: "beaver-activity-meta" });
|
||||
if (conv) {
|
||||
meta.createSpan({ text: conv.agent });
|
||||
meta.createSpan({ text: conv.kind });
|
||||
meta.createSpan({ text: conv.status });
|
||||
if (conv.running_turn) meta.createSpan({ cls: "is-running", text: "turn running" });
|
||||
if (conv.pending_question) meta.createSpan({ cls: "is-pending", text: "question pending" });
|
||||
} else if (this.conversationId) {
|
||||
meta.createSpan({ text: this.conversationId });
|
||||
}
|
||||
if (conn.state === "failed" || conn.state === "retrying") {
|
||||
head.createDiv({
|
||||
cls: `beaver-activity-note is-${conn.state}`,
|
||||
text: conn.detail ?? conn.state,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private renderTurn(parent: HTMLElement, turn: Turn): void {
|
||||
const el = parent.createDiv({ cls: `beaver-activity-turn is-${turn.status}` });
|
||||
const line = el.createDiv({ cls: "beaver-activity-turn-line" });
|
||||
line.createSpan({ cls: "beaver-activity-status", text: turn.status });
|
||||
line.createSpan({ cls: "beaver-activity-origin", text: turn.origin });
|
||||
line.createSpan({ cls: "beaver-activity-time", text: fmtTime(turn.startedAt) });
|
||||
const dur = turn.usage?.duration_ms ?? elapsed(turn.startedAt, turn.endedAt);
|
||||
line.createSpan({ cls: "beaver-activity-duration", text: fmtDuration(dur) });
|
||||
if (turn.usage) {
|
||||
el.createDiv({ cls: "beaver-activity-usage", text: fmtUsage(turn.usage) });
|
||||
}
|
||||
for (const text of turn.says) {
|
||||
const say = el.createDiv({ cls: "beaver-activity-say" });
|
||||
const icon = say.createSpan({ cls: "beaver-activity-icon" });
|
||||
setIcon(icon, "message-square");
|
||||
say.createSpan({ text: clip(text, 300) });
|
||||
}
|
||||
if (turn.roots.length > 0) {
|
||||
const tree = el.createDiv({ cls: "beaver-activity-tree" });
|
||||
for (const node of turn.roots) this.renderNode(tree, node);
|
||||
}
|
||||
}
|
||||
|
||||
private renderNode(parent: HTMLElement, node: ToolNode): void {
|
||||
const el = parent.createDiv({ cls: `beaver-activity-tool is-${node.status}` });
|
||||
const row = el.createDiv({ cls: "beaver-activity-tool-row" });
|
||||
row.createSpan({ cls: "beaver-activity-dot" });
|
||||
row.createSpan({ cls: "beaver-activity-tool-name", text: node.name });
|
||||
const summary = summarizeInput(node.name, node.input);
|
||||
if (summary) row.createSpan({ cls: "beaver-activity-tool-summary", text: summary });
|
||||
const dur = elapsed(node.startedAt, node.endedAt);
|
||||
if (node.status !== "running" || dur >= 2000) {
|
||||
row.createSpan({ cls: "beaver-activity-duration", text: fmtDuration(dur) });
|
||||
}
|
||||
row.addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
node.expanded = !node.expanded;
|
||||
this.render();
|
||||
});
|
||||
if (node.expanded) {
|
||||
const details = el.createDiv({ cls: "beaver-activity-tool-details" });
|
||||
if (node.input != null) {
|
||||
details.createEl("pre", {
|
||||
text: JSON.stringify(node.input, null, 2),
|
||||
});
|
||||
}
|
||||
if (node.result) {
|
||||
details.createEl("pre", { cls: "is-result", text: node.result });
|
||||
}
|
||||
}
|
||||
if (node.children.length > 0) {
|
||||
const kids = el.createDiv({ cls: "beaver-activity-children" });
|
||||
for (const child of node.children) this.renderNode(kids, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
const settle = (value: string | null) => {
|
||||
if (resolved) return;
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
resolved = true;
|
||||
resolve(value);
|
||||
};
|
||||
+55
-97
@@ -1,19 +1,20 @@
|
||||
import { requestUrl, RequestUrlParam } from "obsidian";
|
||||
import type { RequestUrlParam } from "obsidian";
|
||||
import { requestUrl } from "obsidian";
|
||||
import type { BeaverSettings } from "./settings";
|
||||
import { readSse } from "./sse";
|
||||
|
||||
export interface ChatRequest {
|
||||
filename: string;
|
||||
content: string;
|
||||
agent?: string;
|
||||
content: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface ChatResponse {
|
||||
status: "ok" | "nothing_to_do" | "in_progress";
|
||||
reason?: string;
|
||||
agent?: string;
|
||||
turns_appended?: number;
|
||||
new_content?: string;
|
||||
reason?: string;
|
||||
status: "ok" | "nothing_to_do" | "in_progress";
|
||||
turns_appended?: number;
|
||||
}
|
||||
|
||||
export class BeaverApiError extends Error {
|
||||
@@ -32,26 +33,39 @@ export class BeaverApiError extends Error {
|
||||
// Caddy the markdown root is ``https://host/md`` and the API root is the
|
||||
// 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(/\/+$/, "");
|
||||
if (!url) throw new Error("Beaver: base URL is not configured");
|
||||
const url = settings.baseUrl.trim().replace(TRAILING_SLASHES, "");
|
||||
if (!url) {
|
||||
throw new Error("Beaver: base URL is not configured");
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export function apiBase(settings: BeaverSettings): string {
|
||||
const explicit = settings.apiBaseUrl.trim().replace(/\/+$/, "");
|
||||
if (explicit) return explicit;
|
||||
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 (/:62993$/.test(md)) return md.replace(/:62993$/, ":62994");
|
||||
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}`,
|
||||
`Beaver: API origin is not configured and can't be derived from ${md}`
|
||||
);
|
||||
}
|
||||
|
||||
function authHeader(settings: BeaverSettings): Record<string, string> {
|
||||
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}` };
|
||||
}
|
||||
|
||||
@@ -61,16 +75,20 @@ 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 obj[key] === "string") {
|
||||
return obj[key] as string;
|
||||
}
|
||||
}
|
||||
if (typeof body === "string" && body) return body;
|
||||
}
|
||||
if (typeof body === "string" && body) {
|
||||
return body;
|
||||
}
|
||||
return `HTTP ${status}`;
|
||||
}
|
||||
|
||||
async function call(
|
||||
settings: BeaverSettings,
|
||||
init: Omit<RequestUrlParam, "url"> & { url: string },
|
||||
init: Omit<RequestUrlParam, "url"> & { url: string }
|
||||
): Promise<unknown> {
|
||||
const headers = {
|
||||
...authHeader(settings),
|
||||
@@ -106,8 +124,8 @@ async function errorFromResponse(res: Response): Promise<BeaverApiError> {
|
||||
|
||||
export async function listAgents(settings: BeaverSettings): Promise<string[]> {
|
||||
const body = (await call(settings, {
|
||||
url: `${mdBase(settings)}/agents`,
|
||||
method: "GET",
|
||||
url: `${mdBase(settings)}/agents`,
|
||||
})) as { agents?: Array<{ name?: unknown }> };
|
||||
const list = body.agents ?? [];
|
||||
return list
|
||||
@@ -117,13 +135,13 @@ export async function listAgents(settings: BeaverSettings): Promise<string[]> {
|
||||
|
||||
export async function sendChat(
|
||||
settings: BeaverSettings,
|
||||
req: ChatRequest,
|
||||
req: ChatRequest
|
||||
): Promise<ChatResponse> {
|
||||
return (await call(settings, {
|
||||
url: `${mdBase(settings)}/chat`,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(req),
|
||||
contentType: "application/json",
|
||||
method: "POST",
|
||||
url: `${mdBase(settings)}/chat`,
|
||||
})) as ChatResponse;
|
||||
}
|
||||
|
||||
@@ -133,9 +151,9 @@ export interface StreamChatCallbacks {
|
||||
// responsible for splicing it into the editor — we don't ship the
|
||||
// diff because the gateway already renders the canonical view and we
|
||||
// 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).
|
||||
onDone(response: ChatResponse): void;
|
||||
onDone: (response: ChatResponse) => void;
|
||||
}
|
||||
|
||||
// CORS is allowed by the gateway's ``CORSMiddleware``; auth is the same
|
||||
@@ -144,19 +162,21 @@ export async function sendChatStream(
|
||||
settings: BeaverSettings,
|
||||
req: ChatRequest,
|
||||
cb: StreamChatCallbacks,
|
||||
signal?: AbortSignal,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> {
|
||||
const res = await fetch(`${mdBase(settings)}/chat/stream`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(req),
|
||||
headers: {
|
||||
...authHeader(settings),
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(req),
|
||||
method: "POST",
|
||||
signal,
|
||||
});
|
||||
if (!res.ok || !res.body) throw await errorFromResponse(res);
|
||||
if (!(res.ok && res.body)) {
|
||||
throw await errorFromResponse(res);
|
||||
}
|
||||
|
||||
await readSse(res.body, (frame) => {
|
||||
let data: unknown;
|
||||
@@ -168,7 +188,9 @@ export async function sendChatStream(
|
||||
const obj = data as Record<string, unknown>;
|
||||
if (frame.event === "delta") {
|
||||
const content = obj.new_content;
|
||||
if (typeof content === "string") cb.onDelta(content);
|
||||
if (typeof content === "string") {
|
||||
cb.onDelta(content);
|
||||
}
|
||||
return "continue";
|
||||
}
|
||||
if (frame.event === "done") {
|
||||
@@ -178,8 +200,7 @@ export async function sendChatStream(
|
||||
return "stop";
|
||||
}
|
||||
if (frame.event === "error") {
|
||||
const code =
|
||||
typeof obj.status_code === "number" ? obj.status_code : 500;
|
||||
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);
|
||||
@@ -190,77 +211,14 @@ export async function sendChatStream(
|
||||
|
||||
// ---- conversations API (``/api``, bearer scope ``api``) ----
|
||||
|
||||
export interface ConversationInfo {
|
||||
id: string;
|
||||
kind: string;
|
||||
agent: string;
|
||||
title: string | null;
|
||||
status: string;
|
||||
running_turn: string | null;
|
||||
pending_question: unknown;
|
||||
parent?: string | null;
|
||||
live?: boolean;
|
||||
busy?: boolean;
|
||||
}
|
||||
|
||||
// One record of the gateway bus as it arrives over SSE: ``type`` is the
|
||||
// SSE event name (``turn.start``, ``stream``, ``tool``, ``tool.result``,
|
||||
// ``result``, ``turn.end``, ``say``, ``conversation.*`` …), the rest is
|
||||
// the event's own payload.
|
||||
export type BusEvent = {
|
||||
type: string;
|
||||
seq?: number;
|
||||
ts?: string;
|
||||
conversation_id?: string | null;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
export async function listApiAgents(
|
||||
settings: BeaverSettings,
|
||||
settings: BeaverSettings
|
||||
): Promise<string[]> {
|
||||
const body = (await call(settings, {
|
||||
url: `${apiBase(settings)}/api/agents`,
|
||||
method: "GET",
|
||||
url: `${apiBase(settings)}/api/agents`,
|
||||
})) as { agents?: Array<{ name?: unknown }> };
|
||||
return (body.agents ?? [])
|
||||
.map((a) => (typeof a?.name === "string" ? a.name : null))
|
||||
.filter((n): n is string => !!n);
|
||||
}
|
||||
|
||||
export async function getConversation(
|
||||
settings: BeaverSettings,
|
||||
id: string,
|
||||
): Promise<ConversationInfo> {
|
||||
return (await call(settings, {
|
||||
url: `${apiBase(settings)}/api/conversations/${encodeURIComponent(id)}`,
|
||||
method: "GET",
|
||||
})) as ConversationInfo;
|
||||
}
|
||||
|
||||
// Resolves when the server closes the stream; rejects on HTTP errors,
|
||||
// network failures and abort. Reconnection is the caller's business.
|
||||
export async function streamConversationEvents(
|
||||
settings: BeaverSettings,
|
||||
id: string,
|
||||
onEvent: (event: BusEvent) => void,
|
||||
signal: AbortSignal,
|
||||
): Promise<void> {
|
||||
const url = `${apiBase(settings)}/api/conversations/${encodeURIComponent(id)}/events`;
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { ...authHeader(settings), Accept: "text/event-stream" },
|
||||
signal,
|
||||
});
|
||||
if (!res.ok || !res.body) throw await errorFromResponse(res);
|
||||
await readSse(res.body, (frame) => {
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(frame.data);
|
||||
} catch {
|
||||
return "continue";
|
||||
}
|
||||
if (data && typeof data === "object") {
|
||||
onEvent({ ...(data as Record<string, unknown>), type: frame.event });
|
||||
}
|
||||
return "continue";
|
||||
});
|
||||
}
|
||||
|
||||
+202
-144
@@ -1,71 +1,66 @@
|
||||
import type { WorkspaceLeaf } from "obsidian";
|
||||
import {
|
||||
Editor,
|
||||
MarkdownView,
|
||||
type Editor,
|
||||
type MarkdownView,
|
||||
Notice,
|
||||
Plugin,
|
||||
TFile,
|
||||
WorkspaceLeaf,
|
||||
type TFile,
|
||||
} from "obsidian";
|
||||
import {
|
||||
BeaverApiError,
|
||||
ChatResponse,
|
||||
listAgents,
|
||||
sendChatStream,
|
||||
} from "./api";
|
||||
import { ACTIVITY_VIEW_TYPE, ActivityView } from "./activity";
|
||||
import { pickAgent } from "./agentPicker";
|
||||
import {
|
||||
BeaverSettings,
|
||||
BeaverSettingsTab,
|
||||
DEFAULT_SETTINGS,
|
||||
} from "./settings";
|
||||
import { ApiClient } from "$lib/api/client";
|
||||
import { pickAgent } from "./agent-picker";
|
||||
import type { ChatResponse } from "./api";
|
||||
import { apiBase, BeaverApiError, listAgents, sendChatStream } from "./api";
|
||||
import type { BeaverSettings } from "./settings";
|
||||
import { BeaverSettingsTab, DEFAULT_SETTINGS, errorText } from "./settings";
|
||||
import { PanelView, VIEW_TYPE_PANEL } from "./view";
|
||||
|
||||
const AGENT_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
// Obsidian's ``Editor`` is a thin shim over a CodeMirror 6
|
||||
// ``EditorView``; reaching for it directly is the only way to grab
|
||||
// the real scrollable DOM node (``.cm-scroller``). Property name is
|
||||
// unofficial but stable across recent Obsidian releases.
|
||||
// Obsidian's ``Editor`` is a thin shim over a CodeMirror 6 ``EditorView``;
|
||||
// reaching for it directly is the only way to grab the real scrollable DOM
|
||||
// node (``.cm-scroller``). Property name is unofficial but stable across
|
||||
// recent Obsidian releases.
|
||||
interface CMHandle {
|
||||
scrollDOM: HTMLElement;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Boundary of the YAML frontmatter block. ``---\n…---\n`` at the very
|
||||
// start of the file; anything else is a no-frontmatter file and we
|
||||
// return 0.
|
||||
// Boundary of the YAML frontmatter block: ``---\n…---\n`` at the very start
|
||||
// of the file; anything else is a no-frontmatter file and we return 0.
|
||||
function frontmatterEnd(s: string): number {
|
||||
if (!s.startsWith("---\n")) return 0;
|
||||
if (!s.startsWith("---\n")) {
|
||||
return 0;
|
||||
}
|
||||
const idx = s.indexOf("\n---\n", 4);
|
||||
return idx >= 0 ? idx + 5 : 0;
|
||||
}
|
||||
|
||||
// Replace only the regions that actually changed instead of
|
||||
// ``editor.setValue`` (which resets cursor + scroll and is expensive
|
||||
// on every snapshot for long files).
|
||||
// ``editor.setValue`` (which resets cursor + scroll and is expensive on
|
||||
// every snapshot for long files).
|
||||
//
|
||||
// Fast path: when the body after the frontmatter is identical except
|
||||
// for a tail append (the common case for the final write — gateway
|
||||
// refreshes frontmatter at the top and appends USER_SCAFFOLD at the
|
||||
// bottom, body in between is unchanged), we apply TWO small edits
|
||||
// instead of one huge one. The frontmatter rewrite stays a small,
|
||||
// localised change; the tail append touches only the very last bytes.
|
||||
// A single big ``replaceRange`` spanning the frontmatter triggers
|
||||
// Obsidian's Properties-widget rebuild + an asynchronous scroll-to-
|
||||
// start-of-content that's hard to override.
|
||||
// Fast path: when the body after the frontmatter is identical except for a
|
||||
// tail append (the common case for the final write - the gateway refreshes
|
||||
// frontmatter at the top and appends USER_SCAFFOLD at the bottom, body in
|
||||
// between unchanged), we apply TWO small edits instead of one huge one. A
|
||||
// single big ``replaceRange`` spanning the frontmatter triggers Obsidian's
|
||||
// Properties-widget rebuild + an asynchronous scroll-to-start-of-content
|
||||
// that is hard to override.
|
||||
//
|
||||
// Slow path: prefix/suffix-trimmed single splice for any other shape.
|
||||
//
|
||||
// After every edit we restore scroll on the scrollDOM directly,
|
||||
// repeatedly across several frames, because the widget rebuild keeps
|
||||
// resetting scroll asynchronously and a single restore loses the
|
||||
// race.
|
||||
// After every edit we restore scroll on the scrollDOM directly, repeatedly
|
||||
// across several frames, because the widget rebuild keeps resetting scroll
|
||||
// asynchronously and a single restore loses the race.
|
||||
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: three splice shapes belong together
|
||||
function spliceIntoEditor(editor: Editor, newContent: string): void {
|
||||
const current = editor.getValue();
|
||||
if (current === newContent) return;
|
||||
if (current === newContent) {
|
||||
return;
|
||||
}
|
||||
const cm = cmOf(editor);
|
||||
const savedScrollTop = cm?.scrollDOM.scrollTop ?? null;
|
||||
|
||||
@@ -76,11 +71,13 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
|
||||
const fmsDiffer =
|
||||
current.slice(0, oldFmEnd) !== newContent.slice(0, newFmEnd);
|
||||
|
||||
if (newBody.startsWith(oldBody) && (fmsDiffer || newBody.length > oldBody.length)) {
|
||||
// Tail append first, then (maybe) frontmatter rewrite. Order
|
||||
// matters: appending at the current end keeps the frontmatter
|
||||
// splice positions valid. Reversing the order would invalidate
|
||||
// the append offset after the frontmatter grew.
|
||||
if (
|
||||
newBody.startsWith(oldBody) &&
|
||||
(fmsDiffer || newBody.length > oldBody.length)
|
||||
) {
|
||||
// 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) {
|
||||
const appendStart = editor.offsetToPos(current.length);
|
||||
editor.replaceRange(newBody.slice(oldBody.length), appendStart);
|
||||
@@ -90,9 +87,12 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
|
||||
const fmEnd = editor.offsetToPos(oldFmEnd);
|
||||
editor.replaceRange(newContent.slice(0, newFmEnd), fmStart, fmEnd);
|
||||
}
|
||||
} else if (oldBody.startsWith(newBody) && (fmsDiffer || oldBody.length > newBody.length)) {
|
||||
// Symmetric case: tail truncation. Unlikely on the streaming
|
||||
// path but cheap to handle.
|
||||
} else if (
|
||||
oldBody.startsWith(newBody) &&
|
||||
(fmsDiffer || oldBody.length > newBody.length)
|
||||
) {
|
||||
// Symmetric case: tail truncation. Unlikely on the streaming path but
|
||||
// cheap to handle.
|
||||
if (oldBody.length > newBody.length) {
|
||||
const truncStart = editor.offsetToPos(oldFmEnd + newBody.length);
|
||||
const truncEnd = editor.offsetToPos(current.length);
|
||||
@@ -111,7 +111,7 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
|
||||
prefix < cap &&
|
||||
current.charCodeAt(prefix) === newContent.charCodeAt(prefix)
|
||||
) {
|
||||
prefix++;
|
||||
prefix += 1;
|
||||
}
|
||||
let suffix = 0;
|
||||
const maxSuffix = cap - prefix;
|
||||
@@ -120,22 +120,21 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
|
||||
current.charCodeAt(current.length - 1 - suffix) ===
|
||||
newContent.charCodeAt(newContent.length - 1 - suffix)
|
||||
) {
|
||||
suffix++;
|
||||
suffix += 1;
|
||||
}
|
||||
const start = editor.offsetToPos(prefix);
|
||||
const end = editor.offsetToPos(current.length - suffix);
|
||||
editor.replaceRange(
|
||||
newContent.slice(prefix, newContent.length - suffix),
|
||||
start,
|
||||
end,
|
||||
end
|
||||
);
|
||||
}
|
||||
|
||||
if (cm != null && savedScrollTop != null) {
|
||||
// Race against Obsidian's deferred scroll-reset. Sync restore +
|
||||
// two animation frames + two timeouts; whichever fires after
|
||||
// Obsidian's own scroll write wins. Cheap noop on the
|
||||
// already-correct frames.
|
||||
if (cm !== null && savedScrollTop !== null) {
|
||||
// Race against Obsidian's deferred scroll-reset: sync restore + two
|
||||
// animation frames + two timeouts; whichever fires after Obsidian's own
|
||||
// scroll write wins. Cheap noop on the already-correct frames.
|
||||
const restore = () => {
|
||||
if (cm.scrollDOM.scrollTop !== savedScrollTop) {
|
||||
cm.scrollDOM.scrollTop = savedScrollTop;
|
||||
@@ -153,6 +152,7 @@ function spliceIntoEditor(editor: Editor, newContent: string): void {
|
||||
|
||||
export default class BeaverPlugin extends Plugin {
|
||||
settings: BeaverSettings = { ...DEFAULT_SETTINGS };
|
||||
readonly views = new Set<PanelView>();
|
||||
|
||||
private cachedAgents: string[] | null = null;
|
||||
private cachedAt = 0;
|
||||
@@ -161,55 +161,103 @@ export default class BeaverPlugin extends Plugin {
|
||||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new BeaverSettingsTab(this.app, this));
|
||||
this.registerView(
|
||||
ACTIVITY_VIEW_TYPE,
|
||||
(leaf) => new ActivityView(leaf, this),
|
||||
);
|
||||
this.registerView(VIEW_TYPE_PANEL, (leaf) => new PanelView(leaf, this));
|
||||
this.addRibbonIcon("bot", "Beaver", () => {
|
||||
this.openPanel("side").catch(() => undefined);
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
callback: () => {
|
||||
this.openPanel("side").catch(() => undefined);
|
||||
},
|
||||
id: "open-panel",
|
||||
name: "Open panel",
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
callback: () => {
|
||||
this.openPanel("tab").catch(() => undefined);
|
||||
},
|
||||
id: "open-panel-tab",
|
||||
name: "Open panel in a tab",
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "open-in-panel",
|
||||
name: "Open chat in panel",
|
||||
checkCallback: (checking) => {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file || file.extension !== "md") return false;
|
||||
if (!checking) void this.openActivityPanel(file);
|
||||
const id = this.conversationIdOf(file);
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
if (!checking) {
|
||||
this.openPanel("side", id).catch(() => undefined);
|
||||
}
|
||||
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",
|
||||
checkCallback: (checking) => {
|
||||
const ctx = this.getFrontmatterAgentContext();
|
||||
if (!ctx) return false;
|
||||
if (!checking) void this.dispatch(ctx.file, ctx.agent);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "send-different",
|
||||
name: "Send using different agent",
|
||||
checkCallback: (checking) => {
|
||||
const ctx = this.getFrontmatterAgentContext();
|
||||
if (!ctx) return false;
|
||||
if (!checking) void this.runDifferent(ctx.file);
|
||||
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> {
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
await this.loadData(),
|
||||
);
|
||||
this.settings = { ...DEFAULT_SETTINGS, ...(await this.loadData()) };
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
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 {
|
||||
@@ -218,44 +266,51 @@ export default class BeaverPlugin extends Plugin {
|
||||
this.lastListFailed = false;
|
||||
}
|
||||
|
||||
// ``conversation_id`` from the note's frontmatter, or null. The panel
|
||||
// and the command both key off this; the gateway writes it after the
|
||||
// first turn (§3.10: frontmatter is only ``agent`` + ``conversation_id``).
|
||||
// ``conversation_id`` from the note's frontmatter, or null. The panel and
|
||||
// the command both key off this; the gateway writes it after the first
|
||||
// turn (§3.10: frontmatter is only ``agent`` + ``conversation_id``).
|
||||
conversationIdOf(file: TFile | null): string | null {
|
||||
if (!file || file.extension !== "md") return null;
|
||||
if (file?.extension !== "md") {
|
||||
return null;
|
||||
}
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
const raw = cache?.frontmatter?.conversation_id;
|
||||
return typeof raw === "string" && raw.trim() ? raw.trim() : null;
|
||||
}
|
||||
|
||||
// Reveal the activity panel (creating it in the right sidebar on first
|
||||
// use) and point it at ``file``.
|
||||
async openActivityPanel(file: TFile | null): Promise<void> {
|
||||
// Reveal a panel (creating one on first use) and, given an id, show it.
|
||||
async openPanel(placement: "side" | "tab", id?: string): Promise<void> {
|
||||
const { workspace } = this.app;
|
||||
let leaf: WorkspaceLeaf | null =
|
||||
workspace.getLeavesOfType(ACTIVITY_VIEW_TYPE)[0] ?? null;
|
||||
workspace.getLeavesOfType(VIEW_TYPE_PANEL)[0] ?? null;
|
||||
if (!leaf) {
|
||||
leaf = workspace.getRightLeaf(false);
|
||||
leaf =
|
||||
placement === "tab"
|
||||
? workspace.getLeaf("tab")
|
||||
: workspace.getRightLeaf(false);
|
||||
if (!leaf) {
|
||||
new Notice("Beaver: couldn't open the side panel");
|
||||
new Notice("Beaver: couldn't open the panel");
|
||||
return;
|
||||
}
|
||||
await leaf.setViewState({ type: ACTIVITY_VIEW_TYPE, active: true });
|
||||
await leaf.setViewState({ active: true, type: VIEW_TYPE_PANEL });
|
||||
}
|
||||
await workspace.revealLeaf(leaf);
|
||||
const view = leaf.view;
|
||||
if (view instanceof ActivityView) view.follow(file);
|
||||
if (id && leaf.view instanceof PanelView) {
|
||||
leaf.view.show(id);
|
||||
}
|
||||
}
|
||||
|
||||
private getFrontmatterAgentContext():
|
||||
| { file: TFile; agent: string }
|
||||
| null {
|
||||
private getFrontmatterAgentContext(): { file: TFile; agent: string } | null {
|
||||
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 raw = cache?.frontmatter?.agent;
|
||||
if (typeof raw !== "string" || !raw.trim()) return null;
|
||||
return { file, agent: raw.trim() };
|
||||
if (typeof raw !== "string" || !raw.trim()) {
|
||||
return null;
|
||||
}
|
||||
return { agent: raw.trim(), file };
|
||||
}
|
||||
|
||||
private async runDifferent(file: TFile): Promise<void> {
|
||||
@@ -270,7 +325,9 @@ export default class BeaverPlugin extends Plugin {
|
||||
return;
|
||||
}
|
||||
const picked = await pickAgent(this.app, agents);
|
||||
if (!picked) return;
|
||||
if (!picked) {
|
||||
return;
|
||||
}
|
||||
await this.dispatch(file, picked);
|
||||
} catch (err) {
|
||||
this.notifyError("send-different", err);
|
||||
@@ -282,7 +339,9 @@ export default class BeaverPlugin extends Plugin {
|
||||
this.cachedAgents &&
|
||||
!this.lastListFailed &&
|
||||
Date.now() - this.cachedAt < AGENT_CACHE_TTL_MS;
|
||||
if (fresh) return this.cachedAgents;
|
||||
if (fresh) {
|
||||
return this.cachedAgents;
|
||||
}
|
||||
try {
|
||||
const agents = await listAgents(this.settings);
|
||||
this.cacheAgents(agents);
|
||||
@@ -296,13 +355,15 @@ export default class BeaverPlugin extends Plugin {
|
||||
|
||||
private async dispatch(file: TFile, agent: string): Promise<void> {
|
||||
const filename = this.gatewayFilename(file);
|
||||
if (!filename) return;
|
||||
if (!filename) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Snapshot the buffer if the file is open, otherwise read disk. We
|
||||
// don't keep the editor: an ``Editor`` belongs to a leaf, not a
|
||||
// file, so if the user opens another note in the same tab mid-
|
||||
// stream the old handle now points at the wrong document. Every
|
||||
// delta and the final write re-resolve the leaf by ``file.path``.
|
||||
// Snapshot the buffer if the file is open, otherwise read disk. We don't
|
||||
// keep the editor: an ``Editor`` belongs to a leaf, not a file, so if the
|
||||
// user opens another note in the same tab mid-stream the old handle now
|
||||
// points at the wrong document. Every delta and the final write
|
||||
// re-resolve the leaf by ``file.path``.
|
||||
const initialEditor = this.findEditorFor(file);
|
||||
const content = initialEditor
|
||||
? initialEditor.getValue()
|
||||
@@ -313,23 +374,24 @@ export default class BeaverPlugin extends Plugin {
|
||||
try {
|
||||
await sendChatStream(
|
||||
this.settings,
|
||||
{ filename, content, agent },
|
||||
{ agent, content, filename },
|
||||
{
|
||||
onDelta: (newContent) => {
|
||||
// Live splice into the editor only — we deliberately don't
|
||||
// write the partial to disk. The gateway also skips its own
|
||||
// intermediate file writes on this endpoint, so the local
|
||||
// file (and Obsidian Sync's copy) only sees the final state
|
||||
// once. Re-resolved per delta: if ``file`` is no longer
|
||||
// open anywhere, the delta is dropped and the final
|
||||
// Live splice into the editor only - the partial never touches
|
||||
// disk. The gateway also skips its own intermediate writes on
|
||||
// this endpoint, so the file (and Obsidian Sync's copy) sees the
|
||||
// final state once. Re-resolved per delta: if ``file`` is no
|
||||
// longer open anywhere, the delta is dropped and the final
|
||||
// ``vault.modify`` catches the buffer up.
|
||||
const editor = this.findEditorFor(file);
|
||||
if (editor) spliceIntoEditor(editor, newContent);
|
||||
},
|
||||
onDone: (resp) => {
|
||||
finalResponse = resp;
|
||||
if (editor) {
|
||||
spliceIntoEditor(editor, newContent);
|
||||
}
|
||||
},
|
||||
onDone: (response) => {
|
||||
finalResponse = response;
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof BeaverApiError && err.status === 409) {
|
||||
@@ -343,11 +405,9 @@ export default class BeaverPlugin extends Plugin {
|
||||
}
|
||||
|
||||
if (!finalResponse) {
|
||||
// Stream ended without a ``done`` event — shouldn't happen but
|
||||
// we surface it instead of pretending nothing was wrong.
|
||||
this.notifyError(
|
||||
`sending to ${agent}`,
|
||||
new Error("stream ended without a done event"),
|
||||
new Error("stream ended without a done event")
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -366,28 +426,32 @@ export default class BeaverPlugin extends Plugin {
|
||||
|
||||
private gatewayFilename(file: TFile): string | null {
|
||||
const subpath = this.settings.vaultSubpath;
|
||||
if (!subpath) return file.path;
|
||||
const prefix = subpath + "/";
|
||||
if (!subpath) {
|
||||
return file.path;
|
||||
}
|
||||
const prefix = `${subpath}/`;
|
||||
if (file.path === subpath || file.path.startsWith(prefix)) {
|
||||
return file.path.slice(prefix.length);
|
||||
}
|
||||
new Notice(
|
||||
`Beaver: file ${file.path} is outside the configured vault subpath (${subpath})`,
|
||||
8000,
|
||||
8000
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
private findEditorFor(file: TFile): Editor | null {
|
||||
// Walk open markdown views — we want the editor instance that owns
|
||||
// ``file`` *right now* so we can splice (keeps the buffer's edit
|
||||
// history intact) instead of falling back to vault.modify. Never
|
||||
// cached by callers: ``view.file`` changes when the user navigates
|
||||
// within the same leaf, and is ``null`` between switches.
|
||||
// Walk open markdown views: we want the editor instance that owns
|
||||
// ``file`` *right now* so we can splice (keeps the buffer's edit history
|
||||
// intact) instead of falling back to vault.modify. Never cached by
|
||||
// callers: ``view.file`` changes when the user navigates within the same
|
||||
// leaf, and is ``null`` between switches.
|
||||
const leaves = this.app.workspace.getLeavesOfType("markdown");
|
||||
for (const leaf of leaves) {
|
||||
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;
|
||||
}
|
||||
@@ -395,26 +459,20 @@ export default class BeaverPlugin extends Plugin {
|
||||
private async writeBack(file: TFile, newContent: string): Promise<void> {
|
||||
const editor = this.findEditorFor(file);
|
||||
if (editor) {
|
||||
// Reuse the splice path so the final write (frontmatter refresh
|
||||
// at the top + USER_SCAFFOLD appended at the bottom) preserves
|
||||
// scroll just like the streaming deltas did.
|
||||
// Reuse the splice path so the final write (frontmatter refresh at the
|
||||
// top + USER_SCAFFOLD appended at the bottom) preserves scroll just
|
||||
// like the streaming deltas did.
|
||||
spliceIntoEditor(editor, newContent);
|
||||
return;
|
||||
}
|
||||
// File not open (anymore): write to disk. Obsidian refreshes any
|
||||
// buffer that opens it later, and a leaf that shows it in another
|
||||
// pane gets the update through the vault event.
|
||||
// File not open (anymore): write to disk. Obsidian refreshes any buffer
|
||||
// that opens it later, and a leaf that shows it in another pane gets the
|
||||
// update through the vault event.
|
||||
await this.app.vault.modify(file, newContent);
|
||||
}
|
||||
|
||||
private notifyError(action: string, err: unknown): void {
|
||||
const msg =
|
||||
err instanceof BeaverApiError
|
||||
? `${err.status}: ${err.message}`
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: String(err);
|
||||
new Notice(`Beaver (${action}): ${msg}`, 8000);
|
||||
new Notice(`Beaver (${action}): ${errorText(err)}`, 8000);
|
||||
console.error("Beaver:", action, err);
|
||||
}
|
||||
}
|
||||
|
||||
+32
-23
@@ -1,17 +1,17 @@
|
||||
import { App, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
import { BeaverApiError, apiBase, listAgents, listApiAgents } from "./api";
|
||||
import { type App, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
import { apiBase, BeaverApiError, listAgents, listApiAgents } from "./api";
|
||||
import type BeaverPlugin from "./main";
|
||||
|
||||
export interface BeaverSettings {
|
||||
baseUrl: string;
|
||||
apiBaseUrl: string;
|
||||
baseUrl: string;
|
||||
token: string;
|
||||
vaultSubpath: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: BeaverSettings = {
|
||||
baseUrl: "http://localhost:62993",
|
||||
apiBaseUrl: "",
|
||||
baseUrl: "http://localhost:62993",
|
||||
token: "",
|
||||
vaultSubpath: "",
|
||||
};
|
||||
@@ -20,12 +20,13 @@ export function normalizeSubpath(raw: string): string {
|
||||
return raw.trim().replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
function errorText(err: unknown): string {
|
||||
return err instanceof BeaverApiError
|
||||
? `${err.status}: ${err.message}`
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: String(err);
|
||||
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 {
|
||||
@@ -48,9 +49,11 @@ export class BeaverSettingsTab extends PluginSettingTab {
|
||||
.setPlaceholder("http://localhost:62993")
|
||||
.setValue(this.plugin.settings.baseUrl)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.baseUrl = value.trim().replace(/\/+$/, "");
|
||||
this.plugin.settings.baseUrl = value
|
||||
.trim()
|
||||
.replace(TRAILING_SLASHES, "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
@@ -58,23 +61,25 @@ export class BeaverSettingsTab extends PluginSettingTab {
|
||||
.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.",
|
||||
"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(/\/+$/, "");
|
||||
this.plugin.settings.apiBaseUrl = value
|
||||
.trim()
|
||||
.replace(TRAILING_SLASHES, "");
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Bearer token")
|
||||
.setDesc(
|
||||
"Token with the `messages` scope for sending and the `api` scope " +
|
||||
"for the activity panel (or a `*` bootstrap token).",
|
||||
"for the panel (or a `*` bootstrap token)."
|
||||
)
|
||||
.addText((text) => {
|
||||
text.inputEl.type = "password";
|
||||
@@ -91,7 +96,7 @@ export class BeaverSettingsTab extends PluginSettingTab {
|
||||
.setName("Vault subpath")
|
||||
.setDesc(
|
||||
"Folder in this Obsidian vault that maps to the gateway's vault root. " +
|
||||
"Leave empty if the two vault roots match. Example: `💬 чаты`.",
|
||||
"Leave empty if the two vault roots match. Example: `💬 чаты`."
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
@@ -100,12 +105,14 @@ export class BeaverSettingsTab extends PluginSettingTab {
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.vaultSubpath = normalizeSubpath(value);
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Test connection")
|
||||
.setDesc("Calls GET /agents on the markdown frontend and reports the count.")
|
||||
.setDesc(
|
||||
"Calls GET /agents on the markdown frontend and reports the count."
|
||||
)
|
||||
.addButton((btn) =>
|
||||
btn.setButtonText("Test").onClick(async () => {
|
||||
try {
|
||||
@@ -115,23 +122,25 @@ export class BeaverSettingsTab extends PluginSettingTab {
|
||||
} catch (err) {
|
||||
new Notice(`Beaver: ${errorText(err)}`, 8000);
|
||||
}
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Test API")
|
||||
.setDesc("Calls GET /api/agents on the API origin (needs the `api` scope).")
|
||||
.setDesc(
|
||||
"Calls GET /api/agents on the API origin (needs the `api` scope)."
|
||||
)
|
||||
.addButton((btn) =>
|
||||
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`,
|
||||
`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),
|
||||
};
|
||||
+32
-14
@@ -5,27 +5,40 @@
|
||||
// ``data`` may span several lines per the spec, comment lines (``:
|
||||
// keepalive``) are dropped.
|
||||
|
||||
const CARRIAGE_RETURN = /\r$/;
|
||||
|
||||
export interface SseFrame {
|
||||
event: string;
|
||||
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(/\r$/, "");
|
||||
if (!line || line.startsWith(":")) continue;
|
||||
const line = rawLine.replace(CARRIAGE_RETURN, "");
|
||||
if (!line || line.startsWith(":")) {
|
||||
continue;
|
||||
}
|
||||
const colon = line.indexOf(":");
|
||||
if (colon < 0) continue;
|
||||
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 (value.startsWith(" ")) {
|
||||
value = value.slice(1);
|
||||
}
|
||||
if (dataLines.length === 0) return null;
|
||||
return { event, data: dataLines.join("\n") };
|
||||
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"``),
|
||||
@@ -34,7 +47,7 @@ export function parseSseFrame(frame: string): SseFrame | null {
|
||||
// with an ``AbortError`` that propagates to the caller.
|
||||
export async function readSse(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
onFrame: (frame: SseFrame) => "continue" | "stop",
|
||||
onFrame: (frame: SseFrame) => "continue" | "stop"
|
||||
): Promise<void> {
|
||||
const reader = body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
@@ -42,14 +55,19 @@ export async function readSse(
|
||||
let done = false;
|
||||
try {
|
||||
while (!done) {
|
||||
const chunk = await reader.read();
|
||||
done = chunk.done;
|
||||
if (chunk.value) buf += decoder.decode(chunk.value, { stream: !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;
|
||||
if (frame && onFrame(frame) === "stop") {
|
||||
return;
|
||||
}
|
||||
sep = buf.indexOf("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// What a panel leaf remembers: the conversation on screen, whether it follows
|
||||
// the active note, and which note that is. Reactive so the view and the
|
||||
// Svelte tree share one truth.
|
||||
export class PanelState {
|
||||
selected = $state<string | null>(null);
|
||||
follow = $state(true);
|
||||
sourcePath = $state("");
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
@layer theme, base, components, utilities;
|
||||
@import "tailwindcss/theme.css" layer(theme);
|
||||
@import "tailwindcss/utilities.css" layer(utilities) important;
|
||||
@import "../../beaver-gateway/ui/src/lib/panel/panel.css";
|
||||
|
||||
@source "./";
|
||||
@source "../../beaver-gateway/ui/src/lib";
|
||||
|
||||
@custom-variant dark (&:where(.theme-dark, .theme-dark *));
|
||||
|
||||
/* The panel's tokens are Obsidian's own variables, exactly as beaver-calendar
|
||||
maps them onto .bcal-root: the theme, the accent, the font and the radii
|
||||
come from the app and the user's theme. The msos palette never enters. */
|
||||
.beaver-root {
|
||||
--background: var(--background-primary);
|
||||
--foreground: var(--text-normal);
|
||||
--card: var(--background-primary-alt);
|
||||
--card-foreground: var(--text-normal);
|
||||
--popover: var(--background-secondary);
|
||||
--popover-foreground: var(--text-normal);
|
||||
--primary: var(--color-accent);
|
||||
--primary-foreground: var(--text-on-accent);
|
||||
--secondary: var(--background-modifier-hover);
|
||||
--secondary-foreground: var(--text-normal);
|
||||
--muted: var(--background-modifier-hover);
|
||||
--muted-foreground: var(--text-muted);
|
||||
--accent: var(--background-modifier-hover);
|
||||
--accent-foreground: var(--text-normal);
|
||||
--destructive: var(--text-error);
|
||||
--destructive-foreground: var(--text-on-accent);
|
||||
--border: var(--background-modifier-border);
|
||||
--input: var(--background-modifier-border);
|
||||
--ring: var(--color-accent);
|
||||
--icon: var(--text-muted);
|
||||
--signal: var(--color-accent);
|
||||
--link: var(--link-color, var(--text-accent));
|
||||
--note: oklch(0.52 0.13 78);
|
||||
--warn: oklch(0.55 0.13 75);
|
||||
|
||||
--status-new: var(--color-accent);
|
||||
--status-done: oklch(0.55 0.14 155);
|
||||
--status-skip: var(--text-faint);
|
||||
--status-reply: oklch(0.55 0.14 235);
|
||||
--status-snooze: oklch(0.66 0.13 75);
|
||||
--status-meeting: oklch(0.5 0.2 300);
|
||||
--status-work: oklch(0.55 0.13 195);
|
||||
|
||||
--sidebar: var(--background-secondary);
|
||||
--sidebar-accent: var(--background-modifier-hover);
|
||||
--radius: 0.45rem;
|
||||
}
|
||||
|
||||
.theme-dark .beaver-root {
|
||||
--note: oklch(0.84 0.14 88);
|
||||
--warn: oklch(0.8 0.13 75);
|
||||
--status-done: oklch(0.74 0.14 155);
|
||||
--status-reply: oklch(0.72 0.13 235);
|
||||
--status-snooze: oklch(0.8 0.13 75);
|
||||
--status-meeting: oklch(0.72 0.16 300);
|
||||
--status-work: oklch(0.75 0.12 195);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-interface);
|
||||
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.8125rem;
|
||||
--text-base: 0.875rem;
|
||||
--text-lg: 1rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
|
||||
--color-icon: var(--icon);
|
||||
--color-signal: var(--signal);
|
||||
--color-note: var(--note);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
|
||||
--color-status-new: var(--status-new);
|
||||
--color-status-done: var(--status-done);
|
||||
--color-status-skip: var(--status-skip);
|
||||
--color-status-reply: var(--status-reply);
|
||||
--color-status-snooze: var(--status-snooze);
|
||||
--color-status-meeting: var(--status-meeting);
|
||||
--color-status-work: var(--status-work);
|
||||
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
}
|
||||
|
||||
.beaver-root {
|
||||
height: 100%;
|
||||
font-family: var(--font-interface);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
/* Portal layer for menus and dialogs: a zero-sized anchor at body level, above
|
||||
the sidedocks, carrying the theme aliases so popovers stay on theme. */
|
||||
.beaver-portal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--layer-menu, 65);
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root *,
|
||||
.beaver-root.beaver-root *::before,
|
||||
.beaver-root.beaver-root *::after {
|
||||
box-sizing: border-box;
|
||||
border: 0 solid var(--border);
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root
|
||||
:is(h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd) {
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root :is(ul, ol) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root :is(button, input, select, textarea) {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
letter-spacing: inherit;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
border: 0 solid var(--border);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root :is(button, input, select, textarea):hover,
|
||||
.beaver-root.beaver-root :is(button, input, select, textarea):focus,
|
||||
.beaver-root.beaver-root :is(button, input, select, textarea):active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root button {
|
||||
text-align: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root button:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root input::placeholder,
|
||||
.beaver-root.beaver-root textarea::placeholder {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root :is(input, textarea) {
|
||||
caret-color: var(--primary);
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root :focus-visible {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root svg {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root a,
|
||||
.beaver-root.beaver-root a:hover {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root :is(code, pre, kbd) {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Chat text rendered by Obsidian itself, kept compact for a sidedock. */
|
||||
.beaver-root.beaver-root .beaver-md {
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md :is(p, ul, ol, pre, blockquote, table) {
|
||||
margin-block: 0.4em;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md :is(h1, h2, h3, h4, h5, h6) {
|
||||
margin-block: 0.8em 0.4em;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md :is(ul, ol) {
|
||||
padding-left: 1.25em;
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md li + li {
|
||||
margin-top: 0.15em;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md a {
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md a.is-unresolved {
|
||||
color: var(--link-unresolved-color, var(--muted-foreground));
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md code {
|
||||
padding: 0.1em 0.3em;
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
background: color-mix(in oklch, var(--muted) 70%, transparent);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md pre {
|
||||
padding: 0.6em 0.8em;
|
||||
overflow: auto;
|
||||
background: var(--muted);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md pre code {
|
||||
padding: 0;
|
||||
font-weight: 400;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md blockquote {
|
||||
padding-left: 0.75em;
|
||||
color: var(--muted-foreground);
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.beaver-root.beaver-root .beaver-md > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* The luma primitives are pill-shaped for the browser; inside Obsidian every
|
||||
control takes the app's own corner language, like beaver-calendar's. */
|
||||
@layer utilities {
|
||||
/* Utilities are !important here, so the hidden attribute needs its own weight. */
|
||||
.beaver-root [hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
.beaver-root [data-slot="button"],
|
||||
.beaver-root [data-slot="select-trigger"] {
|
||||
border-radius: var(--button-radius, var(--radius-s)) !important;
|
||||
}
|
||||
.beaver-root [data-slot="input"],
|
||||
.beaver-root [data-slot="tabs-trigger"],
|
||||
.beaver-root [data-slot$="-item"] {
|
||||
border-radius: var(--input-radius, var(--radius-s)) !important;
|
||||
}
|
||||
.beaver-root [data-slot="tabs-list"],
|
||||
.beaver-root [data-slot="skeleton"] {
|
||||
border-radius: var(--radius-m, 8px) !important;
|
||||
}
|
||||
/* The luma tabs mark the active trigger with the page colour, which on a
|
||||
dark Obsidian theme is the same as the surface behind the list. */
|
||||
.beaver-root [data-slot="tabs-list"] {
|
||||
background: color-mix(
|
||||
in oklab,
|
||||
var(--text-normal) 6%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
.beaver-root [data-slot="tabs-trigger"][data-state="active"] {
|
||||
color: var(--text-normal) !important;
|
||||
background: color-mix(
|
||||
in oklab,
|
||||
var(--text-normal) 14%,
|
||||
transparent
|
||||
) !important;
|
||||
}
|
||||
.beaver-root [data-slot="dialog-content"],
|
||||
.beaver-root [data-slot="popover-content"],
|
||||
.beaver-root [data-slot="dropdown-menu-content"],
|
||||
.beaver-root [data-slot="context-menu-content"],
|
||||
.beaver-root [data-slot="select-content"] {
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius-m, 8px) !important;
|
||||
box-shadow: var(--shadow-s) !important;
|
||||
}
|
||||
.beaver-root [data-slot="dropdown-menu-content"],
|
||||
.beaver-root [data-slot="context-menu-content"],
|
||||
.beaver-root [data-slot="select-content"] {
|
||||
background: var(--popover) !important;
|
||||
}
|
||||
.beaver-root [data-slot="dropdown-menu-content"]::before,
|
||||
.beaver-root [data-slot="context-menu-content"]::before,
|
||||
.beaver-root [data-slot="select-content"]::before {
|
||||
display: none !important;
|
||||
}
|
||||
.beaver-root [data-slot="button"][data-variant="default"],
|
||||
.beaver-root [data-slot="button"]:not([class*="bg-"]) {
|
||||
font-weight: var(--font-medium, 500) !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import SettingsIcon from "@lucide/svelte/icons/settings";
|
||||
import { BitsConfig } from "bits-ui";
|
||||
import type { Component, App as ObsidianApp } from "obsidian";
|
||||
import type { ApiClient } from "$lib/api/client";
|
||||
import EmptyState from "$lib/components/empty-state.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { providePanelHost } from "$lib/panel/host";
|
||||
import { ConversationIndex } from "$lib/panel/index.svelte";
|
||||
import PanelShell from "$lib/panel/panel-shell.svelte";
|
||||
import type { PanelState } from "../state.svelte";
|
||||
import { obsidianHost } from "./host";
|
||||
|
||||
interface Props {
|
||||
app: ObsidianApp;
|
||||
client: ApiClient | null;
|
||||
component: Component;
|
||||
onOpenSettings: () => void;
|
||||
state: PanelState;
|
||||
vaultSubpath: () => string;
|
||||
}
|
||||
|
||||
let { app, client, component, onOpenSettings, state, vaultSubpath }: Props =
|
||||
$props();
|
||||
|
||||
// Menus hang off body, not off the view: Obsidian's sidedocks paint above
|
||||
// the leaf, so anything portalled inside it is covered once it crosses a
|
||||
// pane edge. The layer carries .beaver-root so the theme reaches popovers.
|
||||
const layer = document.createElement("div");
|
||||
layer.className = "beaver-root beaver-portal";
|
||||
|
||||
$effect(() => {
|
||||
document.body.append(layer);
|
||||
return () => layer.remove();
|
||||
});
|
||||
|
||||
// Props are read once: the view remounts the tree when settings change.
|
||||
// svelte-ignore state_referenced_locally
|
||||
providePanelHost(
|
||||
obsidianHost({
|
||||
app,
|
||||
component,
|
||||
sourcePath: () => state.sourcePath,
|
||||
vaultSubpath,
|
||||
})
|
||||
);
|
||||
|
||||
// svelte-ignore state_referenced_locally
|
||||
const index = client ? new ConversationIndex(() => client) : null;
|
||||
</script>
|
||||
|
||||
<BitsConfig defaultPortalTo={layer}>
|
||||
<div class="beaver-root">
|
||||
{#if client && index}
|
||||
<PanelShell
|
||||
{client}
|
||||
{index}
|
||||
showFollow
|
||||
bind:follow={state.follow}
|
||||
bind:selected={state.selected}
|
||||
/>
|
||||
{:else}
|
||||
<div class="p-4">
|
||||
<EmptyState
|
||||
hint="Set the gateway URL and a token with the api scope in the plugin settings."
|
||||
title="Beaver is not connected"
|
||||
>
|
||||
<Button onclick={onOpenSettings} size="sm" variant="outline">
|
||||
<SettingsIcon class="size-4" />
|
||||
Open settings
|
||||
</Button>
|
||||
</EmptyState>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</BitsConfig>
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { App, Component } from "obsidian";
|
||||
import { MarkdownRenderer, Platform } from "obsidian";
|
||||
import type { PanelHost } from "$lib/panel/host";
|
||||
|
||||
export interface HostOptions {
|
||||
app: App;
|
||||
component: Component;
|
||||
// The note the panel currently follows; link resolution starts from it.
|
||||
sourcePath: () => string;
|
||||
// Folder in this vault that maps to the gateway's vault root.
|
||||
vaultSubpath: () => string;
|
||||
}
|
||||
|
||||
// The Obsidian side of the panel: markdown through the app's own renderer,
|
||||
// [[links]] opened in place, a conversation's note opened by its binding.
|
||||
export function obsidianHost(options: HostOptions): PanelHost {
|
||||
const { app, component } = options;
|
||||
return {
|
||||
markdown(node, text) {
|
||||
let alive = true;
|
||||
const staging = document.createElement("div");
|
||||
MarkdownRenderer.render(
|
||||
app,
|
||||
text,
|
||||
staging,
|
||||
options.sourcePath(),
|
||||
component
|
||||
)
|
||||
.then(() => {
|
||||
if (alive) {
|
||||
node.replaceChildren(...staging.childNodes);
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
},
|
||||
name: "obsidian",
|
||||
openLink(target, kind) {
|
||||
if (kind === "internal") {
|
||||
app.workspace
|
||||
.openLinkText(target, options.sourcePath(), false)
|
||||
.catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
window.open(target);
|
||||
},
|
||||
openNote(path) {
|
||||
const subpath = options.vaultSubpath();
|
||||
const full = subpath ? `${subpath}/${path}` : path;
|
||||
app.workspace.openLinkText(full, "", false).catch(() => undefined);
|
||||
},
|
||||
touch: Platform.isMobile,
|
||||
};
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import type { ViewStateResult, WorkspaceLeaf } from "obsidian";
|
||||
import { ItemView, TFile } from "obsidian";
|
||||
import { mount, unmount } from "svelte";
|
||||
import type BeaverPlugin from "./main";
|
||||
import { PanelState } from "./state.svelte";
|
||||
import App from "./ui/app.svelte";
|
||||
|
||||
export const VIEW_TYPE_PANEL = "beaver-panel";
|
||||
|
||||
interface SavedState {
|
||||
follow?: unknown;
|
||||
selected?: unknown;
|
||||
}
|
||||
|
||||
// One panel leaf: a sidedock column, a tab, or the phone screen. Follows
|
||||
// the active note's ``conversation_id`` unless pinned, and remembers both
|
||||
// across restarts through the workspace layout.
|
||||
export class PanelView extends ItemView {
|
||||
readonly state = new PanelState();
|
||||
private readonly plugin: BeaverPlugin;
|
||||
private root: Record<string, unknown> | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: BeaverPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
this.navigation = false;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE_PANEL;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return "Beaver";
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return "bot";
|
||||
}
|
||||
|
||||
getState(): Record<string, unknown> {
|
||||
return { follow: this.state.follow, selected: this.state.selected };
|
||||
}
|
||||
|
||||
async setState(state: unknown, result: ViewStateResult): Promise<void> {
|
||||
const saved = (state ?? {}) as SavedState;
|
||||
if (typeof saved.follow === "boolean") {
|
||||
this.state.follow = saved.follow;
|
||||
}
|
||||
if (typeof saved.selected === "string" || saved.selected === null) {
|
||||
this.state.selected = saved.selected;
|
||||
}
|
||||
await super.setState(state, result);
|
||||
}
|
||||
|
||||
onOpen(): Promise<void> {
|
||||
this.contentEl.empty();
|
||||
this.contentEl.style.padding = "0";
|
||||
this.contentEl.style.overflow = "hidden";
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("file-open", (file) => {
|
||||
// ``null`` means the active leaf is not a file (this panel got
|
||||
// focus, say): keep following the last note rather than drop it.
|
||||
if (file) {
|
||||
this.follow(file);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.registerEvent(
|
||||
this.app.metadataCache.on("changed", (file) => {
|
||||
// The note we follow just got its conversation_id - typically
|
||||
// right after the first send. Re-resolve.
|
||||
if (file.path === this.state.sourcePath) {
|
||||
this.follow(file);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.follow(this.app.workspace.getActiveFile());
|
||||
this.plugin.views.add(this);
|
||||
this.mountApp();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
this.plugin.views.delete(this);
|
||||
await this.unmountApp();
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
// The active note changed: remember it, and switch to its conversation
|
||||
// when following and it has one.
|
||||
follow(file: TFile | null): void {
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
this.state.sourcePath = file.path;
|
||||
if (!this.state.follow) {
|
||||
return;
|
||||
}
|
||||
const id = this.plugin.conversationIdOf(file);
|
||||
if (id) {
|
||||
this.state.selected = id;
|
||||
}
|
||||
}
|
||||
|
||||
show(id: string): void {
|
||||
this.state.selected = id;
|
||||
}
|
||||
|
||||
// Settings changed: the client is built from them, so rebuild the tree.
|
||||
async remount(): Promise<void> {
|
||||
await this.unmountApp();
|
||||
this.mountApp();
|
||||
}
|
||||
|
||||
private mountApp(): void {
|
||||
this.root = mount(App, {
|
||||
props: {
|
||||
app: this.app,
|
||||
client: this.plugin.apiClient(),
|
||||
component: this,
|
||||
onOpenSettings: () => this.plugin.openSettings(),
|
||||
state: this.state,
|
||||
vaultSubpath: () => this.plugin.settings.vaultSubpath,
|
||||
},
|
||||
target: this.contentEl,
|
||||
});
|
||||
}
|
||||
|
||||
private async unmountApp(): Promise<void> {
|
||||
if (this.root) {
|
||||
await unmount(this.root);
|
||||
this.root = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
/* Beaver: activity panel (plain DOM, v0). Theme through Obsidian variables. */
|
||||
.beaver-activity {
|
||||
font-size: var(--font-ui-small);
|
||||
line-height: 1.4;
|
||||
user-select: text;
|
||||
}
|
||||
.beaver-activity-head {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding-bottom: var(--size-4-2);
|
||||
margin-bottom: var(--size-4-2);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-primary);
|
||||
}
|
||||
.beaver-activity-title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: var(--size-4-2);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.beaver-activity-conn {
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-normal);
|
||||
color: var(--text-faint);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.beaver-activity-conn::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
background: var(--text-faint);
|
||||
}
|
||||
.beaver-activity-conn.is-open::before { background: var(--color-green); }
|
||||
.beaver-activity-conn.is-connecting::before,
|
||||
.beaver-activity-conn.is-retrying::before { background: var(--color-yellow); }
|
||||
.beaver-activity-conn.is-failed::before { background: var(--color-red); }
|
||||
|
||||
.beaver-activity-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-4-1) var(--size-4-2);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
.beaver-activity-meta .is-running { color: var(--color-yellow); }
|
||||
.beaver-activity-meta .is-pending { color: var(--color-orange); }
|
||||
.beaver-activity-note {
|
||||
margin-top: var(--size-4-1);
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.beaver-activity-note.is-failed { color: var(--color-red); }
|
||||
|
||||
.beaver-activity-empty {
|
||||
color: var(--text-muted);
|
||||
padding: var(--size-4-2) 0;
|
||||
}
|
||||
|
||||
.beaver-activity-turn {
|
||||
padding: var(--size-4-2) 0;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
.beaver-activity-turn:last-child { border-bottom: none; }
|
||||
.beaver-activity-turn-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--size-4-2);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
.beaver-activity-status {
|
||||
font-weight: var(--font-semibold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.beaver-activity-turn.is-running .beaver-activity-status { color: var(--color-yellow); }
|
||||
.beaver-activity-turn.is-done .beaver-activity-status { color: var(--color-green); }
|
||||
.beaver-activity-turn.is-interrupted .beaver-activity-status { color: var(--color-orange); }
|
||||
.beaver-activity-turn.is-error .beaver-activity-status { color: var(--color-red); }
|
||||
.beaver-activity-duration { margin-left: auto; color: var(--text-faint); white-space: nowrap; }
|
||||
.beaver-activity-usage {
|
||||
color: var(--text-faint);
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.beaver-activity-say {
|
||||
display: flex;
|
||||
gap: var(--size-4-1);
|
||||
margin-top: var(--size-4-1);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.beaver-activity-icon svg { width: 14px; height: 14px; vertical-align: -2px; }
|
||||
|
||||
.beaver-activity-tree { margin-top: var(--size-4-1); }
|
||||
.beaver-activity-tool-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--size-4-1);
|
||||
padding: 1px 0;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
}
|
||||
.beaver-activity-tool-row:hover { background: var(--background-modifier-hover); }
|
||||
.beaver-activity-dot {
|
||||
flex: none;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-faint);
|
||||
align-self: center;
|
||||
}
|
||||
.beaver-activity-tool.is-running > .beaver-activity-tool-row .beaver-activity-dot {
|
||||
background: var(--color-yellow);
|
||||
animation: beaver-activity-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
.beaver-activity-tool.is-done > .beaver-activity-tool-row .beaver-activity-dot { background: var(--color-green); }
|
||||
.beaver-activity-tool.is-error > .beaver-activity-tool-row .beaver-activity-dot { background: var(--color-red); }
|
||||
.beaver-activity-tool.is-aborted > .beaver-activity-tool-row .beaver-activity-dot { background: var(--color-orange); }
|
||||
@keyframes beaver-activity-pulse {
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
.beaver-activity-tool-name {
|
||||
flex: none;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.beaver-activity-tool-summary {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.beaver-activity-tool-details pre {
|
||||
margin: var(--size-4-1) 0 var(--size-4-1) 12px;
|
||||
padding: var(--size-4-1) var(--size-4-2);
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
font-size: var(--font-ui-smaller);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
.beaver-activity-tool-details pre.is-result { border-left: 2px solid var(--color-green); }
|
||||
.beaver-activity-tool.is-error > .beaver-activity-tool-details pre.is-result { border-left-color: var(--color-red); }
|
||||
.beaver-activity-children {
|
||||
margin-left: 12px;
|
||||
padding-left: var(--size-4-2);
|
||||
border-left: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import esbuild from "esbuild";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { root, sharedLib, svelte } from "../esbuild.config.mjs";
|
||||
|
||||
const out = join(mkdtempSync(join(tmpdir(), "beaver-")), "bundle.mjs");
|
||||
writeFileSync(
|
||||
join(root, "tests/.entry.ts"),
|
||||
`export { default as App } from "../src/ui/app.svelte";
|
||||
export { PanelState } from "../src/state.svelte";
|
||||
export { FakeClient, MASTER, BRANCH_UFW, BRANCH_BLASTER, DEEP_PANEL } from "../preview/fake-client";
|
||||
export { mount, unmount, flushSync } from "svelte";
|
||||
export { Component } from "obsidian";
|
||||
export { conversationActions } from "$lib/panel/actions.svelte";
|
||||
`
|
||||
);
|
||||
|
||||
await esbuild.build({
|
||||
bundle: true,
|
||||
conditions: ["svelte", "browser"],
|
||||
entryPoints: [join(root, "tests/.entry.ts")],
|
||||
format: "esm",
|
||||
logLevel: "error",
|
||||
mainFields: ["svelte", "browser", "module", "main"],
|
||||
outfile: out,
|
||||
plugins: [
|
||||
sharedLib({ obsidian: join(root, "tests/obsidian-stub.ts") }),
|
||||
svelte(),
|
||||
],
|
||||
target: "es2022",
|
||||
});
|
||||
|
||||
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
|
||||
pretendToBeVisual: true,
|
||||
url: "http://localhost/",
|
||||
});
|
||||
for (const key of Object.getOwnPropertyNames(dom.window)) {
|
||||
if (key.startsWith("_") || globalThis[key] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
globalThis[key] = dom.window[key];
|
||||
} catch {
|
||||
/* getter-only window properties are fine to skip */
|
||||
}
|
||||
}
|
||||
globalThis.window = dom.window;
|
||||
globalThis.document = dom.window.document;
|
||||
globalThis.Event = dom.window.Event;
|
||||
globalThis.CustomEvent = dom.window.CustomEvent;
|
||||
dom.window.Element.prototype.getBoundingClientRect = () => ({
|
||||
bottom: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
toJSON: () => ({}),
|
||||
top: 0,
|
||||
width: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
dom.window.Element.prototype.setPointerCapture = () => {};
|
||||
dom.window.Element.prototype.releasePointerCapture = () => {};
|
||||
dom.window.Element.prototype.hasPointerCapture = () => false;
|
||||
dom.window.Element.prototype.scrollIntoView = () => {};
|
||||
dom.window.Element.prototype.animate = () => {
|
||||
const animation = {
|
||||
addEventListener() {},
|
||||
cancel() {},
|
||||
commitStyles() {},
|
||||
currentTime: 0,
|
||||
effect: { getComputedTiming: () => ({ delay: 0, duration: 0 }) },
|
||||
finish() {},
|
||||
finished: Promise.resolve(),
|
||||
onfinish: null,
|
||||
pause() {},
|
||||
play() {},
|
||||
playState: "finished",
|
||||
removeEventListener() {},
|
||||
reverse() {},
|
||||
startTime: 0,
|
||||
};
|
||||
queueMicrotask(() => animation.onfinish?.());
|
||||
return animation;
|
||||
};
|
||||
globalThis.ResizeObserver = class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
globalThis.matchMedia = () => ({
|
||||
addEventListener() {},
|
||||
matches: false,
|
||||
removeEventListener() {},
|
||||
});
|
||||
dom.window.matchMedia = globalThis.matchMedia;
|
||||
|
||||
const {
|
||||
App,
|
||||
PanelState,
|
||||
FakeClient,
|
||||
MASTER,
|
||||
BRANCH_UFW,
|
||||
BRANCH_BLASTER,
|
||||
DEEP_PANEL,
|
||||
mount,
|
||||
unmount,
|
||||
flushSync,
|
||||
Component,
|
||||
conversationActions,
|
||||
} = await import(pathToFileURL(out).href);
|
||||
|
||||
const failures = [];
|
||||
function check(name, condition, detail = "") {
|
||||
if (condition) {
|
||||
console.log(` ok ${name}`);
|
||||
} else {
|
||||
failures.push(name);
|
||||
console.log(` FAIL ${name}${detail ? ` -- ${detail}` : ""}`);
|
||||
}
|
||||
}
|
||||
const settle = (ms = 60) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const client = new FakeClient();
|
||||
const state = new PanelState();
|
||||
state.selected = MASTER;
|
||||
const opened = [];
|
||||
const app = {
|
||||
workspace: {
|
||||
openLinkText: (link, from) => opened.push({ from, target: link }),
|
||||
},
|
||||
};
|
||||
const target = dom.window.document.body;
|
||||
const instance = mount(App, {
|
||||
props: {
|
||||
app,
|
||||
client,
|
||||
component: new Component(),
|
||||
onOpenSettings: () => {},
|
||||
state,
|
||||
vaultSubpath: () => "💬 чаты",
|
||||
},
|
||||
target,
|
||||
});
|
||||
flushSync();
|
||||
await settle();
|
||||
flushSync();
|
||||
|
||||
const text = () => target.textContent ?? "";
|
||||
check(
|
||||
"root carries the theme class",
|
||||
target.querySelector(".beaver-root") !== null
|
||||
);
|
||||
check(
|
||||
"portal layer is on body",
|
||||
target.querySelector(".beaver-portal") !== null
|
||||
);
|
||||
check("thread header shows the master", text().includes("Master · 29 Aug"));
|
||||
check(
|
||||
"the running turn's tools are on screen",
|
||||
text().includes("Task") && text().includes("Bash")
|
||||
);
|
||||
check(
|
||||
"history rendered through the host renderer",
|
||||
target.querySelector(".beaver-md") !== null
|
||||
);
|
||||
|
||||
const wikilink = target.querySelector("a.internal-link");
|
||||
check("wikilink rendered as an internal link", wikilink !== null);
|
||||
if (wikilink) {
|
||||
wikilink.dispatchEvent(
|
||||
new dom.window.MouseEvent("click", { bubbles: true, cancelable: true })
|
||||
);
|
||||
check(
|
||||
"clicking it opens the note in Obsidian",
|
||||
opened.some((o) => o.target === "2026-08-28"),
|
||||
JSON.stringify(opened)
|
||||
);
|
||||
}
|
||||
|
||||
state.selected = BRANCH_BLASTER;
|
||||
flushSync();
|
||||
await settle();
|
||||
flushSync();
|
||||
check("switching follows the state", text().includes("бластер"));
|
||||
check(
|
||||
"a pending question shows its buttons",
|
||||
text().includes("Xiaomi Mi Blaster")
|
||||
);
|
||||
|
||||
const composer = target.querySelector("textarea");
|
||||
check("composer is there", composer !== null);
|
||||
if (composer) {
|
||||
composer.value = "закажи Broadlink";
|
||||
composer.dispatchEvent(new dom.window.Event("input", { bubbles: true }));
|
||||
flushSync();
|
||||
composer
|
||||
.closest("form")
|
||||
?.dispatchEvent(
|
||||
new dom.window.Event("submit", { bubbles: true, cancelable: true })
|
||||
);
|
||||
await settle();
|
||||
check(
|
||||
"sending goes out as source=panel",
|
||||
client.sent.some(
|
||||
(m) => m.origin === "panel" && m.text === "закажи Broadlink"
|
||||
),
|
||||
JSON.stringify(client.sent)
|
||||
);
|
||||
}
|
||||
|
||||
const switcher = target.querySelector('[aria-label="Switch conversation"]');
|
||||
check("narrow layout shows the switcher", switcher !== null);
|
||||
switcher?.dispatchEvent(
|
||||
new dom.window.PointerEvent("pointerdown", {
|
||||
bubbles: true,
|
||||
button: 0,
|
||||
pointerType: "mouse",
|
||||
})
|
||||
);
|
||||
switcher?.click();
|
||||
flushSync();
|
||||
await settle();
|
||||
const rows = [...dom.window.document.querySelectorAll("[data-row]")];
|
||||
check(
|
||||
"switcher lists open conversations grouped",
|
||||
rows.length >= 4 &&
|
||||
text().includes("Branches") &&
|
||||
text().includes("Deep chats"),
|
||||
`${rows.length}`
|
||||
);
|
||||
check(
|
||||
"closed ones stay out until asked",
|
||||
!rows.some((row) => row.dataset.row === "c5triage0000")
|
||||
);
|
||||
|
||||
dom.window.document.dispatchEvent(
|
||||
new dom.window.KeyboardEvent("keydown", {
|
||||
bubbles: true,
|
||||
code: "Escape",
|
||||
key: "Escape",
|
||||
})
|
||||
);
|
||||
flushSync();
|
||||
await settle();
|
||||
|
||||
state.selected = DEEP_PANEL;
|
||||
flushSync();
|
||||
await settle();
|
||||
flushSync();
|
||||
check(
|
||||
"header carries the actions menu",
|
||||
target.querySelector('[aria-label="Conversation actions"]') !== null
|
||||
);
|
||||
|
||||
const scope = (current) => ({
|
||||
client,
|
||||
current,
|
||||
host: { name: "obsidian", openNote: () => {}, touch: false },
|
||||
onBranch: () => {},
|
||||
onChanged: () => {},
|
||||
onOpen: () => {},
|
||||
onRename: () => {},
|
||||
});
|
||||
const labels = async (id, current = true) =>
|
||||
conversationActions(await client.conversation(id), scope(current)).map(
|
||||
(a) => a.label
|
||||
);
|
||||
const deep = await labels(DEEP_PANEL);
|
||||
check(
|
||||
"deep chat offers its note, memory and digest",
|
||||
deep.includes("Open note") &&
|
||||
deep.includes("Remember on close") &&
|
||||
deep.includes("Close with digest"),
|
||||
deep.join(", ")
|
||||
);
|
||||
check(
|
||||
"a deep chat has no Telegram toggle",
|
||||
!deep.some((l) => l.includes("Telegram"))
|
||||
);
|
||||
const hidden = await labels(BRANCH_BLASTER, false);
|
||||
check(
|
||||
"a hidden branch can return to Telegram and be opened",
|
||||
hidden.includes("Return to Telegram") && hidden.includes("Open"),
|
||||
hidden.join(", ")
|
||||
);
|
||||
const shown = await labels(BRANCH_UFW);
|
||||
check(
|
||||
"a visible branch can hide from Telegram and merge",
|
||||
shown.includes("Hide from Telegram") &&
|
||||
shown.includes("Merge into parent") &&
|
||||
!shown.includes("Open"),
|
||||
shown.join(", ")
|
||||
);
|
||||
|
||||
await unmount(instance);
|
||||
console.log(
|
||||
failures.length === 0
|
||||
? "\nsmoke: all good"
|
||||
: `\nsmoke: ${failures.length} FAILED`
|
||||
);
|
||||
process.exit(failures.length === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,92 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(
|
||||
join(import.meta.dirname, "../src/tailwind.css"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
function block(selector: string): string {
|
||||
const at = source.indexOf(`\n${selector} {`);
|
||||
if (at === -1) {
|
||||
return "";
|
||||
}
|
||||
const open = source.indexOf("{", at);
|
||||
const close = source.indexOf("\n}", open);
|
||||
return source.slice(open, close);
|
||||
}
|
||||
|
||||
describe("theme aliases", () => {
|
||||
const OBSIDIAN_VARS = [
|
||||
"--background-primary",
|
||||
"--background-secondary",
|
||||
"--background-modifier-border",
|
||||
"--text-normal",
|
||||
"--text-muted",
|
||||
"--color-accent",
|
||||
"--text-on-accent",
|
||||
"--text-error",
|
||||
];
|
||||
|
||||
it("are declared on .beaver-root, not :root", () => {
|
||||
const root = block(".beaver-root");
|
||||
expect(root).not.toBe("");
|
||||
for (const name of OBSIDIAN_VARS) {
|
||||
expect(root, `${name} must be aliased on .beaver-root`).toContain(name);
|
||||
}
|
||||
});
|
||||
|
||||
it("never reference Obsidian variables from :root", () => {
|
||||
const rootBlocks = [...source.matchAll(/(^|\n):root\s*\{([^}]*)\}/g)];
|
||||
for (const [, , body] of rootBlocks) {
|
||||
for (const name of OBSIDIAN_VARS) {
|
||||
expect(body, `:root must not reference ${name}`).not.toContain(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("scopes the dark override under the view root", () => {
|
||||
expect(source).toContain(".theme-dark .beaver-root");
|
||||
expect(source).not.toMatch(/\n\.theme-dark\s*\{/);
|
||||
});
|
||||
|
||||
it("keeps the msos palette out: the live accent is Obsidian's", () => {
|
||||
const root = block(".beaver-root");
|
||||
expect(root).toContain("--signal: var(--color-accent)");
|
||||
expect(root).toContain("--primary: var(--color-accent)");
|
||||
expect(root).not.toMatch(/oklch\([^)]*\s34[0-2]\)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reset specificity", () => {
|
||||
it("uses a doubled class to outrank app and theme styles", () => {
|
||||
expect(source).toContain(".beaver-root.beaver-root");
|
||||
});
|
||||
|
||||
it("gives every control Obsidian's corner language", () => {
|
||||
expect(source).toContain('[data-slot="button"]');
|
||||
expect(source).toContain("var(--button-radius");
|
||||
expect(source).toContain('[data-slot="input"]');
|
||||
});
|
||||
});
|
||||
|
||||
describe("cascade against Obsidian", () => {
|
||||
it("marks utilities important so they outrank app.css", () => {
|
||||
expect(source).toMatch(
|
||||
/@import "tailwindcss\/utilities\.css"[^;]*\bimportant\b/
|
||||
);
|
||||
});
|
||||
|
||||
it("scans the shared panel sources", () => {
|
||||
expect(source).toContain('@source "../../beaver-gateway/ui/src/lib"');
|
||||
expect(source).toContain("panel/panel.css");
|
||||
});
|
||||
|
||||
it("imports only the theme and utility layers", () => {
|
||||
expect(source).toContain('@import "tailwindcss/theme.css"');
|
||||
expect(source).toContain('@import "tailwindcss/utilities.css"');
|
||||
expect(source).not.toMatch(/@import "tailwindcss"\s*[;l]/);
|
||||
expect(source).not.toMatch(/@import\s+"tailwindcss\/preflight/);
|
||||
});
|
||||
});
|
||||
+16
-3
@@ -10,9 +10,22 @@
|
||||
"moduleResolution": "Bundler",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["DOM", "ES2022"]
|
||||
"skipLibCheck": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["node", "svelte"],
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"paths": {
|
||||
"$lib/*": ["../beaver-gateway/ui/src/lib/*"],
|
||||
"svelte-sonner": ["src/shims/sonner.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.svelte",
|
||||
"preview/**/*.ts",
|
||||
"tests/**/*.ts"
|
||||
],
|
||||
"exclude": ["preview/shot.ts"]
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"0.1.0": "1.5.0",
|
||||
"0.2.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