From 252fca70dfd36f8f5e5cb9d69ab0cb761914049a Mon Sep 17 00:00:00 2001 From: Hibryda Date: Sun, 22 Mar 2026 01:36:02 +0100 Subject: [PATCH] =?UTF-8?q?feat(electrobun):=20file=20management=20?= =?UTF-8?q?=E2=80=94=20CodeMirror=20editor,=20PDF=20viewer,=20CSV=20table,?= =?UTF-8?q?=20real=20file=20I/O?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CodeEditor: CodeMirror 6 with Catppuccin theme, 15+ languages, Ctrl+S save, dirty tracking, save-on-blur - PdfViewer: pdfjs-dist canvas rendering, zoom 0.5-3x, HiDPI, lazy page load - CsvTable: RFC 4180 parser, delimiter auto-detect, sortable columns, sticky header - FileBrowser: real filesystem via files.list/read/write RPC, lazy dir loading, file type routing (code→editor, pdf→viewer, csv→table, images→display) - 10MB size gate, binary detection, base64 encoding for non-text files --- ui-electrobun/package-lock.json | 2675 +++++++++++++++++ ui-electrobun/package.json | 17 +- ui-electrobun/src/bun/btmsg-db.ts | 379 +++ ui-electrobun/src/bun/bttask-db.ts | 187 ++ ui-electrobun/src/bun/index.ts | 429 +++ ui-electrobun/src/bun/search-db.ts | 202 ++ ui-electrobun/src/bun/session-db.ts | 326 ++ ui-electrobun/src/mainview/App.svelte | 142 +- ui-electrobun/src/mainview/CodeEditor.svelte | 248 ++ ui-electrobun/src/mainview/CommsTab.svelte | 521 ++++ ui-electrobun/src/mainview/CsvTable.svelte | 243 ++ ui-electrobun/src/mainview/FileBrowser.svelte | 521 +++- ui-electrobun/src/mainview/PdfViewer.svelte | 298 ++ ui-electrobun/src/mainview/ProjectCard.svelte | 9 +- .../src/mainview/SearchOverlay.svelte | 301 ++ ui-electrobun/src/mainview/StatusBar.svelte | 275 ++ .../src/mainview/TaskBoardTab.svelte | 515 ++++ .../src/mainview/agent-store.svelte.ts | 111 + .../src/mainview/health-store.svelte.ts | 229 ++ ui-electrobun/src/mainview/plugin-host.ts | 287 ++ .../src/mainview/plugin-store.svelte.ts | 136 + ui-electrobun/src/shared/pty-rpc-schema.ts | 292 ++ 22 files changed, 8116 insertions(+), 227 deletions(-) create mode 100644 ui-electrobun/package-lock.json create mode 100644 ui-electrobun/src/bun/btmsg-db.ts create mode 100644 ui-electrobun/src/bun/bttask-db.ts create mode 100644 ui-electrobun/src/bun/search-db.ts create mode 100644 ui-electrobun/src/bun/session-db.ts create mode 100644 ui-electrobun/src/mainview/CodeEditor.svelte create mode 100644 ui-electrobun/src/mainview/CommsTab.svelte create mode 100644 ui-electrobun/src/mainview/CsvTable.svelte create mode 100644 ui-electrobun/src/mainview/PdfViewer.svelte create mode 100644 ui-electrobun/src/mainview/SearchOverlay.svelte create mode 100644 ui-electrobun/src/mainview/StatusBar.svelte create mode 100644 ui-electrobun/src/mainview/TaskBoardTab.svelte create mode 100644 ui-electrobun/src/mainview/health-store.svelte.ts create mode 100644 ui-electrobun/src/mainview/plugin-host.ts create mode 100644 ui-electrobun/src/mainview/plugin-store.svelte.ts diff --git a/ui-electrobun/package-lock.json b/ui-electrobun/package-lock.json new file mode 100644 index 0000000..809659e --- /dev/null +++ b/ui-electrobun/package-lock.json @@ -0,0 +1,2675 @@ +{ + "name": "electrobun-svelte", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "electrobun-svelte", + "version": "1.0.0", + "dependencies": { + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-rust": "^6.0.2", + "@codemirror/language": "^6.12.2", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.6.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.40.0", + "@xterm/addon-canvas": "^0.7.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-image": "^0.9.0", + "@xterm/xterm": "^6.0.0", + "electrobun": "latest", + "pdfjs-dist": "^5.5.207" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.1", + "@types/bun": "latest", + "concurrently": "^9.1.0", + "svelte": "^5.14.1", + "typescript": "^5.7.2", + "vite": "^6.0.3" + } + }, + "node_modules/@babylonjs/core": { + "version": "7.54.3", + "license": "Apache-2.0" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", + "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz", + "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", + "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz", + "integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.2.tgz", + "integrity": "sha512-tRZAPl1j0pkmPL/pGG85GAbyQYnYv0j6UEdEt5e4ZYvp+OxDu2zVp2c/YddiuO8ZTa2CHsVELqRd/fGWjlISrg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", + "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz", + "integrity": "sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.97", + "@napi-rs/canvas-darwin-arm64": "0.1.97", + "@napi-rs/canvas-darwin-x64": "0.1.97", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.97", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.97", + "@napi-rs/canvas-linux-arm64-musl": "0.1.97", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.97", + "@napi-rs/canvas-linux-x64-gnu": "0.1.97", + "@napi-rs/canvas-linux-x64-musl": "0.1.97", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.97", + "@napi-rs/canvas-win32-x64-msvc": "0.1.97" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.97.tgz", + "integrity": "sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.97.tgz", + "integrity": "sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.97.tgz", + "integrity": "sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.97.tgz", + "integrity": "sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.97.tgz", + "integrity": "sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.97.tgz", + "integrity": "sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.97.tgz", + "integrity": "sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.97.tgz", + "integrity": "sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.97.tgz", + "integrity": "sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.97.tgz", + "integrity": "sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.97", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.97.tgz", + "integrity": "sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "license": "MIT" + }, + "node_modules/@types/bun": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.11.tgz", + "integrity": "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg==", + "license": "MIT", + "dependencies": { + "bun-types": "1.3.11" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@xterm/addon-canvas": { + "version": "0.7.0", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "license": "MIT" + }, + "node_modules/@xterm/addon-image": { + "version": "0.9.0", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bun-types": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.11.tgz", + "integrity": "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn-windows-exe": { + "version": "1.2.0", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@malept/cross-spawn-promise": "^1.1.0", + "is-wsl": "^2.2.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devalue": { + "version": "5.6.1", + "dev": true, + "license": "MIT" + }, + "node_modules/electrobun": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/electrobun/-/electrobun-1.16.0.tgz", + "integrity": "sha512-KO/GQO6vpWACJXizqD8F551xtRAgg83Dcfzmjo+6qqCseobttu9x+HL40n+CBnYTe+kBvFXzwso2ZrPuec78zg==", + "license": "MIT", + "dependencies": { + "@babylonjs/core": "^7.45.0", + "@types/bun": "^1.3.8", + "png-to-ico": "^2.1.8", + "proxy-agent": "^6.5.0", + "rcedit": "^4.0.1", + "three": "^0.165.0" + }, + "bin": { + "electrobun": "bin/electrobun.cjs" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/esprima": { + "version": "4.0.1", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrap": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/kleur": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-readable-to-web-readable-stream": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz", + "integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "5.5.207", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz", + "integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0 || >=22.13.0 || >=24" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.95", + "node-readable-to-web-readable-stream": "^0.4.2" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/png-to-ico": { + "version": "2.1.8", + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.36", + "minimist": "^1.2.6", + "pngjs": "^6.0.0" + }, + "bin": { + "png-to-ico": "bin/cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/png-to-ico/node_modules/@types/node": { + "version": "17.0.45", + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "6.0.0", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/rcedit": { + "version": "4.0.1", + "license": "MIT", + "dependencies": { + "cross-spawn-windows-exe": "^1.1.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/svelte": { + "version": "5.46.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@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.5.0", + "esm-env": "^1.2.1", + "esrap": "^2.2.1", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/three": { + "version": "0.165.0", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "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==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + } + } +} diff --git a/ui-electrobun/package.json b/ui-electrobun/package.json index 509c81c..38a1036 100644 --- a/ui-electrobun/package.json +++ b/ui-electrobun/package.json @@ -11,11 +11,26 @@ "build:canary": "vite build && electrobun build --env=canary" }, "dependencies": { + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-python": "^6.2.1", + "@codemirror/lang-rust": "^6.0.2", + "@codemirror/language": "^6.12.2", + "@codemirror/search": "^6.6.0", + "@codemirror/state": "^6.6.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.40.0", "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-image": "^0.9.0", "@xterm/xterm": "^6.0.0", - "electrobun": "latest" + "electrobun": "latest", + "pdfjs-dist": "^5.5.207" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.1", diff --git a/ui-electrobun/src/bun/btmsg-db.ts b/ui-electrobun/src/bun/btmsg-db.ts new file mode 100644 index 0000000..f5a06e6 --- /dev/null +++ b/ui-electrobun/src/bun/btmsg-db.ts @@ -0,0 +1,379 @@ +/** + * btmsg — Inter-agent messaging SQLite store. + * DB: ~/.local/share/agor/btmsg.db (shared with btmsg CLI + bttask). + * Uses bun:sqlite. Schema matches Rust btmsg.rs. + */ + +import { Database } from "bun:sqlite"; +import { homedir } from "os"; +import { mkdirSync } from "fs"; +import { join } from "path"; +import { randomUUID } from "crypto"; + +// ── DB path ────────────────────────────────────────────────────────────────── + +const DATA_DIR = join(homedir(), ".local", "share", "agor"); +const DB_PATH = join(DATA_DIR, "btmsg.db"); + +// ── Types ──────────────────────────────────────────────────────────────────── + +export interface BtmsgAgent { + id: string; + name: string; + role: string; + groupId: string; + tier: number; + model: string | null; + status: string; + unreadCount: number; +} + +export interface BtmsgMessage { + id: string; + fromAgent: string; + toAgent: string; + content: string; + read: boolean; + replyTo: string | null; + createdAt: string; + senderName: string | null; + senderRole: string | null; +} + +export interface BtmsgChannel { + id: string; + name: string; + groupId: string; + createdBy: string; + memberCount: number; + createdAt: string; +} + +export interface BtmsgChannelMessage { + id: string; + channelId: string; + fromAgent: string; + content: string; + createdAt: string; + senderName: string; + senderRole: string; +} + +export interface DeadLetter { + id: number; + fromAgent: string; + toAgent: string; + content: string; + error: string; + createdAt: string; +} + +export interface AuditEntry { + id: number; + agentId: string; + eventType: string; + detail: string; + createdAt: string; +} + +// ── Schema (create-if-absent, matches Rust open_db_or_create) ──────────────── + +const SCHEMA = ` +CREATE TABLE IF NOT EXISTS agents ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, role TEXT NOT NULL, + group_id TEXT NOT NULL, tier INTEGER NOT NULL DEFAULT 2, + model TEXT, cwd TEXT, system_prompt TEXT, + status TEXT DEFAULT 'stopped', last_active_at TEXT, + created_at TEXT DEFAULT (datetime('now')) +); +CREATE TABLE IF NOT EXISTS contacts ( + agent_id TEXT NOT NULL, contact_id TEXT NOT NULL, + PRIMARY KEY (agent_id, contact_id) +); +CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY, from_agent TEXT NOT NULL, to_agent TEXT NOT NULL, + content TEXT NOT NULL, read INTEGER DEFAULT 0, reply_to TEXT, + group_id TEXT NOT NULL, sender_group_id TEXT, + created_at TEXT DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_agent, read); +CREATE INDEX IF NOT EXISTS idx_messages_from ON messages(from_agent); +CREATE TABLE IF NOT EXISTS channels ( + id TEXT PRIMARY KEY, name TEXT NOT NULL, group_id TEXT NOT NULL, + created_by TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) +); +CREATE TABLE IF NOT EXISTS channel_members ( + channel_id TEXT NOT NULL, agent_id TEXT NOT NULL, + joined_at TEXT DEFAULT (datetime('now')), + PRIMARY KEY (channel_id, agent_id) +); +CREATE TABLE IF NOT EXISTS channel_messages ( + id TEXT PRIMARY KEY, channel_id TEXT NOT NULL, from_agent TEXT NOT NULL, + content TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_channel_messages ON channel_messages(channel_id, created_at); +CREATE TABLE IF NOT EXISTS heartbeats ( + agent_id TEXT PRIMARY KEY, timestamp INTEGER NOT NULL +); +CREATE TABLE IF NOT EXISTS dead_letter_queue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, from_agent TEXT NOT NULL, + to_agent TEXT NOT NULL, content TEXT NOT NULL, error TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')) +); +CREATE TABLE IF NOT EXISTS audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT NOT NULL, + event_type TEXT NOT NULL, detail TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')) +); +CREATE TABLE IF NOT EXISTS seen_messages ( + session_id TEXT NOT NULL, message_id TEXT NOT NULL, + seen_at INTEGER NOT NULL DEFAULT (unixepoch()), + PRIMARY KEY (session_id, message_id) +); +CREATE INDEX IF NOT EXISTS idx_seen_messages_session ON seen_messages(session_id); +`; + +// Also create tasks/task_comments (shared DB with bttask) +const TASK_SCHEMA = ` +CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT DEFAULT '', + status TEXT DEFAULT 'todo', priority TEXT DEFAULT 'medium', + assigned_to TEXT, created_by TEXT NOT NULL, group_id TEXT NOT NULL, + parent_task_id TEXT, sort_order INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')), version INTEGER DEFAULT 1 +); +CREATE INDEX IF NOT EXISTS idx_tasks_group ON tasks(group_id); +CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); +CREATE TABLE IF NOT EXISTS task_comments ( + id TEXT PRIMARY KEY, task_id TEXT NOT NULL, agent_id TEXT NOT NULL, + content TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id); +`; + +// ── BtmsgDb class ──────────────────────────────────────────────────────────── + +export class BtmsgDb { + private db: Database; + + constructor() { + mkdirSync(DATA_DIR, { recursive: true }); + this.db = new Database(DB_PATH); + this.db.exec("PRAGMA journal_mode = WAL"); + this.db.exec("PRAGMA busy_timeout = 5000"); + this.db.exec("PRAGMA foreign_keys = ON"); + this.db.exec(SCHEMA); + this.db.exec(TASK_SCHEMA); + } + + // ── Agents ─────────────────────────────────────────────────────────────── + + registerAgent( + id: string, name: string, role: string, + groupId: string, tier: number, model?: string, + ): void { + this.db.query( + `INSERT INTO agents (id, name, role, group_id, tier, model) + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ON CONFLICT(id) DO UPDATE SET + name=excluded.name, role=excluded.role, + group_id=excluded.group_id, tier=excluded.tier, model=excluded.model` + ).run(id, name, role, groupId, tier, model ?? null); + } + + getAgents(groupId: string): BtmsgAgent[] { + return this.db.query<{ + id: string; name: string; role: string; group_id: string; + tier: number; model: string | null; status: string | null; + unread_count: number; + }, [string]>( + `SELECT a.*, (SELECT COUNT(*) FROM messages m + WHERE m.to_agent = a.id AND m.read = 0) as unread_count + FROM agents a WHERE a.group_id = ? ORDER BY a.tier, a.role, a.name` + ).all(groupId).map(r => ({ + id: r.id, name: r.name, role: r.role, groupId: r.group_id, + tier: r.tier, model: r.model, status: r.status ?? 'stopped', + unreadCount: r.unread_count, + })); + } + + // ── Direct messages ────────────────────────────────────────────────────── + + sendMessage(fromAgent: string, toAgent: string, content: string): string { + // Get sender's group_id + const sender = this.db.query<{ group_id: string }, [string]>( + "SELECT group_id FROM agents WHERE id = ?" + ).get(fromAgent); + if (!sender) throw new Error(`Sender agent '${fromAgent}' not found`); + + const id = randomUUID(); + this.db.query( + `INSERT INTO messages (id, from_agent, to_agent, content, group_id, sender_group_id) + VALUES (?1, ?2, ?3, ?4, ?5, ?5)` + ).run(id, fromAgent, toAgent, content, sender.group_id); + return id; + } + + listMessages(agentId: string, otherId: string, limit = 50): BtmsgMessage[] { + return this.db.query<{ + id: string; from_agent: string; to_agent: string; content: string; + read: number; reply_to: string | null; created_at: string; + sender_name: string | null; sender_role: string | null; + }, [string, string, string, string, number]>( + `SELECT m.id, m.from_agent, m.to_agent, m.content, m.read, + m.reply_to, m.created_at, + a.name AS sender_name, a.role AS sender_role + FROM messages m JOIN agents a ON m.from_agent = a.id + WHERE (m.from_agent = ?1 AND m.to_agent = ?2) + OR (m.from_agent = ?3 AND m.to_agent = ?4) + ORDER BY m.created_at ASC LIMIT ?5` + ).all(agentId, otherId, otherId, agentId, limit).map(r => ({ + id: r.id, fromAgent: r.from_agent, toAgent: r.to_agent, + content: r.content, read: r.read !== 0, replyTo: r.reply_to, + createdAt: r.created_at, senderName: r.sender_name, + senderRole: r.sender_role, + })); + } + + markRead(agentId: string, messageIds: string[]): void { + if (messageIds.length === 0) return; + const stmt = this.db.prepare( + "UPDATE messages SET read = 1 WHERE id = ? AND to_agent = ?" + ); + const tx = this.db.transaction(() => { + for (const mid of messageIds) stmt.run(mid, agentId); + }); + tx(); + } + + // ── Channels ───────────────────────────────────────────────────────────── + + createChannel(name: string, groupId: string, createdBy: string): string { + const id = randomUUID(); + this.db.query( + `INSERT INTO channels (id, name, group_id, created_by) + VALUES (?1, ?2, ?3, ?4)` + ).run(id, name, groupId, createdBy); + return id; + } + + listChannels(groupId: string): BtmsgChannel[] { + return this.db.query<{ + id: string; name: string; group_id: string; created_by: string; + member_count: number; created_at: string; + }, [string]>( + `SELECT c.id, c.name, c.group_id, c.created_by, c.created_at, + (SELECT COUNT(*) FROM channel_members cm WHERE cm.channel_id = c.id) AS member_count + FROM channels c WHERE c.group_id = ? ORDER BY c.name` + ).all(groupId).map(r => ({ + id: r.id, name: r.name, groupId: r.group_id, createdBy: r.created_by, + memberCount: r.member_count, createdAt: r.created_at, + })); + } + + getChannelMessages(channelId: string, limit = 100): BtmsgChannelMessage[] { + return this.db.query<{ + id: string; channel_id: string; from_agent: string; + content: string; created_at: string; + sender_name: string; sender_role: string; + }, [string, number]>( + `SELECT cm.id, cm.channel_id, cm.from_agent, cm.content, cm.created_at, + COALESCE(a.name, cm.from_agent) AS sender_name, + COALESCE(a.role, 'unknown') AS sender_role + FROM channel_messages cm + LEFT JOIN agents a ON cm.from_agent = a.id + WHERE cm.channel_id = ? + ORDER BY cm.created_at ASC LIMIT ?` + ).all(channelId, limit).map(r => ({ + id: r.id, channelId: r.channel_id, fromAgent: r.from_agent, + content: r.content, createdAt: r.created_at, + senderName: r.sender_name, senderRole: r.sender_role, + })); + } + + sendChannelMessage(channelId: string, fromAgent: string, content: string): string { + const id = randomUUID(); + this.db.query( + `INSERT INTO channel_messages (id, channel_id, from_agent, content) + VALUES (?1, ?2, ?3, ?4)` + ).run(id, channelId, fromAgent, content); + return id; + } + + // ── Heartbeats ─────────────────────────────────────────────────────────── + + heartbeat(agentId: string): void { + const now = Math.floor(Date.now() / 1000); + this.db.query( + `INSERT INTO heartbeats (agent_id, timestamp) VALUES (?1, ?2) + ON CONFLICT(agent_id) DO UPDATE SET timestamp = excluded.timestamp` + ).run(agentId, now); + } + + // ── Dead letter queue ──────────────────────────────────────────────────── + + getDeadLetters(limit = 50): DeadLetter[] { + return this.db.query<{ + id: number; from_agent: string; to_agent: string; + content: string; error: string; created_at: string; + }, [number]>( + `SELECT id, from_agent, to_agent, content, error, created_at + FROM dead_letter_queue ORDER BY created_at DESC LIMIT ?` + ).all(limit).map(r => ({ + id: r.id, fromAgent: r.from_agent, toAgent: r.to_agent, + content: r.content, error: r.error, createdAt: r.created_at, + })); + } + + // ── Audit log ──────────────────────────────────────────────────────────── + + logAudit(agentId: string, eventType: string, detail: string): void { + this.db.query( + `INSERT INTO audit_log (agent_id, event_type, detail) + VALUES (?1, ?2, ?3)` + ).run(agentId, eventType, detail); + } + + getAuditLog(limit = 100): AuditEntry[] { + return this.db.query<{ + id: number; agent_id: string; event_type: string; + detail: string; created_at: string; + }, [number]>( + `SELECT id, agent_id, event_type, detail, created_at + FROM audit_log ORDER BY created_at DESC LIMIT ?` + ).all(limit).map(r => ({ + id: r.id, agentId: r.agent_id, eventType: r.event_type, + detail: r.detail, createdAt: r.created_at, + })); + } + + // ── Seen messages (per-session acknowledgment) ─────────────────────────── + + markSeen(sessionId: string, messageIds: string[]): void { + if (messageIds.length === 0) return; + const stmt = this.db.prepare( + "INSERT OR IGNORE INTO seen_messages (session_id, message_id) VALUES (?, ?)" + ); + const tx = this.db.transaction(() => { + for (const mid of messageIds) stmt.run(sessionId, mid); + }); + tx(); + } + + pruneSeen(maxAgeSecs = 7 * 24 * 3600): number { + const result = this.db.query( + "DELETE FROM seen_messages WHERE seen_at < unixepoch() - ?" + ).run(maxAgeSecs); + return (result as { changes: number }).changes; + } + + // ── Lifecycle ──────────────────────────────────────────────────────────── + + close(): void { + this.db.close(); + } +} + +// Singleton +export const btmsgDb = new BtmsgDb(); diff --git a/ui-electrobun/src/bun/bttask-db.ts b/ui-electrobun/src/bun/bttask-db.ts new file mode 100644 index 0000000..c5663e0 --- /dev/null +++ b/ui-electrobun/src/bun/bttask-db.ts @@ -0,0 +1,187 @@ +/** + * bttask — Task board SQLite store. + * DB: ~/.local/share/agor/btmsg.db (shared with btmsg). + * Uses bun:sqlite. Schema matches Rust bttask.rs. + */ + +import { Database } from "bun:sqlite"; +import { homedir } from "os"; +import { mkdirSync } from "fs"; +import { join } from "path"; +import { randomUUID } from "crypto"; + +// ── DB path (same DB as btmsg) ────────────────────────────────────────────── + +const DATA_DIR = join(homedir(), ".local", "share", "agor"); +const DB_PATH = join(DATA_DIR, "btmsg.db"); + +// ── Types ──────────────────────────────────────────────────────────────────── + +export interface Task { + id: string; + title: string; + description: string; + status: string; + priority: string; + assignedTo: string | null; + createdBy: string; + groupId: string; + parentTaskId: string | null; + sortOrder: number; + createdAt: string; + updatedAt: string; + version: number; +} + +export interface TaskComment { + id: string; + taskId: string; + agentId: string; + content: string; + createdAt: string; +} + +const VALID_STATUSES = ["todo", "progress", "review", "done", "blocked"] as const; + +// ── BttaskDb class ─────────────────────────────────────────────────────────── + +export class BttaskDb { + private db: Database; + + constructor() { + mkdirSync(DATA_DIR, { recursive: true }); + this.db = new Database(DB_PATH); + this.db.exec("PRAGMA journal_mode = WAL"); + this.db.exec("PRAGMA busy_timeout = 5000"); + + // Ensure tables exist (idempotent — btmsg-db may have created them) + this.db.exec(` + CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT DEFAULT '', + status TEXT DEFAULT 'todo', priority TEXT DEFAULT 'medium', + assigned_to TEXT, created_by TEXT NOT NULL, group_id TEXT NOT NULL, + parent_task_id TEXT, sort_order INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')), version INTEGER DEFAULT 1 + ); + CREATE INDEX IF NOT EXISTS idx_tasks_group ON tasks(group_id); + CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); + CREATE TABLE IF NOT EXISTS task_comments ( + id TEXT PRIMARY KEY, task_id TEXT NOT NULL, agent_id TEXT NOT NULL, + content TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) + ); + CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id); + `); + + // Migration: add version column if missing + try { + this.db.exec("ALTER TABLE tasks ADD COLUMN version INTEGER DEFAULT 1"); + } catch { /* column already exists */ } + } + + // ── Tasks ──────────────────────────────────────────────────────────────── + + listTasks(groupId: string): Task[] { + return this.db.query<{ + id: string; title: string; description: string; status: string; + priority: string; assigned_to: string | null; created_by: string; + group_id: string; parent_task_id: string | null; sort_order: number; + created_at: string; updated_at: string; version: number; + }, [string]>( + `SELECT id, title, description, status, priority, assigned_to, + created_by, group_id, parent_task_id, sort_order, + created_at, updated_at, COALESCE(version, 1) AS version + FROM tasks WHERE group_id = ? + ORDER BY sort_order ASC, created_at DESC` + ).all(groupId).map(r => ({ + id: r.id, title: r.title, description: r.description ?? '', + status: r.status ?? 'todo', priority: r.priority ?? 'medium', + assignedTo: r.assigned_to, createdBy: r.created_by, + groupId: r.group_id, parentTaskId: r.parent_task_id, + sortOrder: r.sort_order ?? 0, + createdAt: r.created_at ?? '', updatedAt: r.updated_at ?? '', + version: r.version ?? 1, + })); + } + + createTask( + title: string, description: string, priority: string, + groupId: string, createdBy: string, assignedTo?: string, + ): string { + const id = randomUUID(); + this.db.query( + `INSERT INTO tasks (id, title, description, priority, group_id, created_by, assigned_to) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)` + ).run(id, title, description, priority, groupId, createdBy, assignedTo ?? null); + return id; + } + + /** + * Update task status with optimistic locking. + * Returns new version on success. Throws on version conflict. + */ + updateTaskStatus(taskId: string, status: string, expectedVersion: number): number { + if (!VALID_STATUSES.includes(status as typeof VALID_STATUSES[number])) { + throw new Error(`Invalid status '${status}'. Valid: ${VALID_STATUSES.join(', ')}`); + } + + const result = this.db.query( + `UPDATE tasks SET status = ?1, version = version + 1, updated_at = datetime('now') + WHERE id = ?2 AND version = ?3` + ).run(status, taskId, expectedVersion); + + if ((result as { changes: number }).changes === 0) { + throw new Error("Task was modified by another agent (version conflict)"); + } + + return expectedVersion + 1; + } + + deleteTask(taskId: string): void { + this.db.query("DELETE FROM task_comments WHERE task_id = ?").run(taskId); + this.db.query("DELETE FROM tasks WHERE id = ?").run(taskId); + } + + // ── Comments ───────────────────────────────────────────────────────────── + + addComment(taskId: string, agentId: string, content: string): string { + const id = randomUUID(); + this.db.query( + `INSERT INTO task_comments (id, task_id, agent_id, content) + VALUES (?1, ?2, ?3, ?4)` + ).run(id, taskId, agentId, content); + return id; + } + + listComments(taskId: string): TaskComment[] { + return this.db.query<{ + id: string; task_id: string; agent_id: string; + content: string; created_at: string; + }, [string]>( + `SELECT id, task_id, agent_id, content, created_at + FROM task_comments WHERE task_id = ? + ORDER BY created_at ASC` + ).all(taskId).map(r => ({ + id: r.id, taskId: r.task_id, agentId: r.agent_id, + content: r.content, createdAt: r.created_at ?? '', + })); + } + + // ── Review queue ───────────────────────────────────────────────────────── + + reviewQueueCount(groupId: string): number { + const row = this.db.query<{ cnt: number }, [string]>( + "SELECT COUNT(*) AS cnt FROM tasks WHERE group_id = ? AND status = 'review'" + ).get(groupId); + return row?.cnt ?? 0; + } + + // ── Lifecycle ──────────────────────────────────────────────────────────── + + close(): void { + this.db.close(); + } +} + +// Singleton +export const bttaskDb = new BttaskDb(); diff --git a/ui-electrobun/src/bun/index.ts b/ui-electrobun/src/bun/index.ts index 3bb4ea1..1786eb5 100644 --- a/ui-electrobun/src/bun/index.ts +++ b/ui-electrobun/src/bun/index.ts @@ -1,10 +1,17 @@ import path from "path"; +import fs from "fs"; import { BrowserWindow, BrowserView, Updater } from "electrobun/bun"; import { PtyClient } from "./pty-client.ts"; import { settingsDb } from "./settings-db.ts"; +import { sessionDb } from "./session-db.ts"; +import { btmsgDb } from "./btmsg-db.ts"; +import { bttaskDb } from "./bttask-db.ts"; import { SidecarManager } from "./sidecar-manager.ts"; import type { PtyRPCSchema } from "../shared/pty-rpc-schema.ts"; import { randomUUID } from "crypto"; +import { SearchDb } from "./search-db.ts"; +import { homedir } from "os"; +import { join } from "path"; const DEV_SERVER_PORT = 9760; // Project convention: 9700+ range const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`; @@ -13,6 +20,8 @@ const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`; const ptyClient = new PtyClient(); const sidecarManager = new SidecarManager(); +const searchDb = new SearchDb(); +const PLUGINS_DIR = join(homedir(), ".config", "agor", "plugins"); async function connectToDaemon(retries = 5, delayMs = 500): Promise { for (let attempt = 1; attempt <= retries; attempt++) { @@ -200,6 +209,79 @@ const rpc = BrowserView.defineRPC({ } }, + // ── File I/O handlers ──────────────────────────────────────────────── + + "files.list": async ({ path: dirPath }) => { + try { + const dirents = fs.readdirSync(dirPath, { withFileTypes: true }); + const entries = dirents + .filter((d) => !d.name.startsWith(".")) + .map((d) => { + let size = 0; + if (d.isFile()) { + try { + size = fs.statSync(path.join(dirPath, d.name)).size; + } catch { /* ignore stat errors */ } + } + return { + name: d.name, + type: (d.isDirectory() ? "dir" : "file") as "file" | "dir", + size, + }; + }) + .sort((a, b) => { + // Directories first, then alphabetical + if (a.type !== b.type) return a.type === "dir" ? -1 : 1; + return a.name.localeCompare(b.name); + }); + return { entries }; + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + console.error("[files.list]", error); + return { entries: [], error }; + } + }, + + "files.read": async ({ path: filePath }) => { + try { + const stat = fs.statSync(filePath); + const MAX_SIZE = 10 * 1024 * 1024; // 10MB + if (stat.size > MAX_SIZE) { + return { encoding: "utf8" as const, size: stat.size, error: `File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Maximum is 10MB.` }; + } + + // Detect binary by reading first 8KB + const buf = Buffer.alloc(Math.min(8192, stat.size)); + const fd = fs.openSync(filePath, "r"); + fs.readSync(fd, buf, 0, buf.length, 0); + fs.closeSync(fd); + + const isBinary = buf.includes(0); // null byte = binary + if (isBinary) { + const content = fs.readFileSync(filePath).toString("base64"); + return { content, encoding: "base64" as const, size: stat.size }; + } + + const content = fs.readFileSync(filePath, "utf8"); + return { content, encoding: "utf8" as const, size: stat.size }; + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + console.error("[files.read]", error); + return { encoding: "utf8" as const, size: 0, error }; + } + }, + + "files.write": async ({ path: filePath, content }) => { + try { + fs.writeFileSync(filePath, content, "utf8"); + return { ok: true }; + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + console.error("[files.write]", error); + return { ok: false, error }; + } + }, + // ── Groups handlers ────────────────────────────────────────────────── "groups.list": () => { @@ -445,6 +527,353 @@ const rpc = BrowserView.defineRPC({ return { sessions: [] }; } }, + + // ── Session persistence handlers ────────────────────────────────── + + "session.save": ({ projectId, sessionId, provider, status, costUsd, inputTokens, outputTokens, model, error, createdAt, updatedAt }) => { + try { + sessionDb.saveSession({ + projectId, sessionId, provider, status, costUsd, + inputTokens, outputTokens, model, error, createdAt, updatedAt, + }); + return { ok: true }; + } catch (err) { + console.error("[session.save]", err); + return { ok: false }; + } + }, + + "session.load": ({ projectId }) => { + try { + return { session: sessionDb.loadSession(projectId) }; + } catch (err) { + console.error("[session.load]", err); + return { session: null }; + } + }, + + "session.list": ({ projectId }) => { + try { + return { sessions: sessionDb.listSessionsByProject(projectId) }; + } catch (err) { + console.error("[session.list]", err); + return { sessions: [] }; + } + }, + + "session.messages.save": ({ messages }) => { + try { + sessionDb.saveMessages(messages.map((m) => ({ + sessionId: m.sessionId, msgId: m.msgId, role: m.role, + content: m.content, toolName: m.toolName, toolInput: m.toolInput, + timestamp: m.timestamp, costUsd: m.costUsd ?? 0, + inputTokens: m.inputTokens ?? 0, outputTokens: m.outputTokens ?? 0, + }))); + return { ok: true }; + } catch (err) { + console.error("[session.messages.save]", err); + return { ok: false }; + } + }, + + "session.messages.load": ({ sessionId }) => { + try { + return { messages: sessionDb.loadMessages(sessionId) }; + } catch (err) { + console.error("[session.messages.load]", err); + return { messages: [] }; + } + }, + + // ── btmsg handlers ──────────────────────────────────────────────── + + "btmsg.registerAgent": ({ id, name, role, groupId, tier, model }) => { + try { + btmsgDb.registerAgent(id, name, role, groupId, tier, model); + return { ok: true }; + } catch (err) { + console.error("[btmsg.registerAgent]", err); + return { ok: false }; + } + }, + + "btmsg.getAgents": ({ groupId }) => { + try { + return { agents: btmsgDb.getAgents(groupId) }; + } catch (err) { + console.error("[btmsg.getAgents]", err); + return { agents: [] }; + } + }, + + "btmsg.sendMessage": ({ fromAgent, toAgent, content }) => { + try { + const messageId = btmsgDb.sendMessage(fromAgent, toAgent, content); + return { ok: true, messageId }; + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + console.error("[btmsg.sendMessage]", err); + return { ok: false, error }; + } + }, + + "btmsg.listMessages": ({ agentId, otherId, limit }) => { + try { + return { messages: btmsgDb.listMessages(agentId, otherId, limit ?? 50) }; + } catch (err) { + console.error("[btmsg.listMessages]", err); + return { messages: [] }; + } + }, + + "btmsg.markRead": ({ agentId, messageIds }) => { + try { + btmsgDb.markRead(agentId, messageIds); + return { ok: true }; + } catch (err) { + console.error("[btmsg.markRead]", err); + return { ok: false }; + } + }, + + "btmsg.listChannels": ({ groupId }) => { + try { + return { channels: btmsgDb.listChannels(groupId) }; + } catch (err) { + console.error("[btmsg.listChannels]", err); + return { channels: [] }; + } + }, + + "btmsg.createChannel": ({ name, groupId, createdBy }) => { + try { + const channelId = btmsgDb.createChannel(name, groupId, createdBy); + return { ok: true, channelId }; + } catch (err) { + console.error("[btmsg.createChannel]", err); + return { ok: false }; + } + }, + + "btmsg.getChannelMessages": ({ channelId, limit }) => { + try { + return { messages: btmsgDb.getChannelMessages(channelId, limit ?? 100) }; + } catch (err) { + console.error("[btmsg.getChannelMessages]", err); + return { messages: [] }; + } + }, + + "btmsg.sendChannelMessage": ({ channelId, fromAgent, content }) => { + try { + const messageId = btmsgDb.sendChannelMessage(channelId, fromAgent, content); + return { ok: true, messageId }; + } catch (err) { + console.error("[btmsg.sendChannelMessage]", err); + return { ok: false }; + } + }, + + "btmsg.heartbeat": ({ agentId }) => { + try { + btmsgDb.heartbeat(agentId); + return { ok: true }; + } catch (err) { + console.error("[btmsg.heartbeat]", err); + return { ok: false }; + } + }, + + "btmsg.getDeadLetters": ({ limit }) => { + try { + return { letters: btmsgDb.getDeadLetters(limit ?? 50) }; + } catch (err) { + console.error("[btmsg.getDeadLetters]", err); + return { letters: [] }; + } + }, + + "btmsg.logAudit": ({ agentId, eventType, detail }) => { + try { + btmsgDb.logAudit(agentId, eventType, detail); + return { ok: true }; + } catch (err) { + console.error("[btmsg.logAudit]", err); + return { ok: false }; + } + }, + + "btmsg.getAuditLog": ({ limit }) => { + try { + return { entries: btmsgDb.getAuditLog(limit ?? 100) }; + } catch (err) { + console.error("[btmsg.getAuditLog]", err); + return { entries: [] }; + } + }, + + // ── bttask handlers ─────────────────────────────────────────────── + + "bttask.listTasks": ({ groupId }) => { + try { + return { tasks: bttaskDb.listTasks(groupId) }; + } catch (err) { + console.error("[bttask.listTasks]", err); + return { tasks: [] }; + } + }, + + "bttask.createTask": ({ title, description, priority, groupId, createdBy, assignedTo }) => { + try { + const taskId = bttaskDb.createTask(title, description, priority, groupId, createdBy, assignedTo); + return { ok: true, taskId }; + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + console.error("[bttask.createTask]", err); + return { ok: false, error }; + } + }, + + "bttask.updateTaskStatus": ({ taskId, status, expectedVersion }) => { + try { + const newVersion = bttaskDb.updateTaskStatus(taskId, status, expectedVersion); + return { ok: true, newVersion }; + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + console.error("[bttask.updateTaskStatus]", err); + return { ok: false, error }; + } + }, + + "bttask.deleteTask": ({ taskId }) => { + try { + bttaskDb.deleteTask(taskId); + return { ok: true }; + } catch (err) { + console.error("[bttask.deleteTask]", err); + return { ok: false }; + } + }, + + "bttask.addComment": ({ taskId, agentId, content }) => { + try { + const commentId = bttaskDb.addComment(taskId, agentId, content); + return { ok: true, commentId }; + } catch (err) { + console.error("[bttask.addComment]", err); + return { ok: false }; + } + }, + + "bttask.listComments": ({ taskId }) => { + try { + return { comments: bttaskDb.listComments(taskId) }; + } catch (err) { + console.error("[bttask.listComments]", err); + return { comments: [] }; + } + }, + + "bttask.reviewQueueCount": ({ groupId }) => { + try { + return { count: bttaskDb.reviewQueueCount(groupId) }; + } catch (err) { + console.error("[bttask.reviewQueueCount]", err); + return { count: 0 }; + } + }, + + // ── Search handlers ────────────────────────────────────────────────── + + "search.query": ({ query, limit }) => { + try { + const results = searchDb.searchAll(query, limit ?? 20); + return { results }; + } catch (err) { + console.error("[search.query]", err); + return { results: [] }; + } + }, + + "search.indexMessage": ({ sessionId, role, content }) => { + try { + searchDb.indexMessage(sessionId, role, content); + return { ok: true }; + } catch (err) { + console.error("[search.indexMessage]", err); + return { ok: false }; + } + }, + + "search.rebuild": () => { + try { + searchDb.rebuildIndex(); + return { ok: true }; + } catch (err) { + console.error("[search.rebuild]", err); + return { ok: false }; + } + }, + + // ── Plugin handlers ────────────────────────────────────────────────── + + "plugin.discover": () => { + try { + const plugins: Array<{ + id: string; name: string; version: string; + description: string; main: string; permissions: string[]; + }> = []; + + if (!fs.existsSync(PLUGINS_DIR)) return { plugins }; + + const entries = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const manifestPath = join(PLUGINS_DIR, entry.name, "plugin.json"); + if (!fs.existsSync(manifestPath)) continue; + + try { + const raw = fs.readFileSync(manifestPath, "utf-8"); + const manifest = JSON.parse(raw); + plugins.push({ + id: manifest.id ?? entry.name, + name: manifest.name ?? entry.name, + version: manifest.version ?? "0.0.0", + description: manifest.description ?? "", + main: manifest.main ?? "index.js", + permissions: Array.isArray(manifest.permissions) ? manifest.permissions : [], + }); + } catch (parseErr) { + console.error(`[plugin.discover] Bad manifest in ${entry.name}:`, parseErr); + } + } + + return { plugins }; + } catch (err) { + console.error("[plugin.discover]", err); + return { plugins: [] }; + } + }, + + "plugin.readFile": ({ pluginId, filePath }) => { + try { + // Path traversal protection: resolve and verify within plugins dir + const pluginDir = join(PLUGINS_DIR, pluginId); + const resolved = path.resolve(pluginDir, filePath); + if (!resolved.startsWith(pluginDir + path.sep) && resolved !== pluginDir) { + return { ok: false, content: "", error: "Path traversal blocked" }; + } + if (!fs.existsSync(resolved)) { + return { ok: false, content: "", error: "File not found" }; + } + const content = fs.readFileSync(resolved, "utf-8"); + return { ok: true, content }; + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + console.error("[plugin.readFile]", err); + return { ok: false, content: "", error }; + } + }, }, messages: {}, diff --git a/ui-electrobun/src/bun/search-db.ts b/ui-electrobun/src/bun/search-db.ts new file mode 100644 index 0000000..276ec2f --- /dev/null +++ b/ui-electrobun/src/bun/search-db.ts @@ -0,0 +1,202 @@ +/** + * FTS5 full-text search database — bun:sqlite. + * + * Three virtual tables: search_messages, search_tasks, search_btmsg. + * Provides indexing and unified search across all tables. + * DB path: ~/.local/share/agor/search.db + */ + +import { Database } from "bun:sqlite"; +import { homedir } from "os"; +import { mkdirSync } from "fs"; +import { join } from "path"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +export interface SearchResult { + resultType: string; + id: string; + title: string; + snippet: string; + score: number; +} + +// ── DB path ────────────────────────────────────────────────────────────────── + +const DATA_DIR = join(homedir(), ".local", "share", "agor"); +const DB_PATH = join(DATA_DIR, "search.db"); + +// ── SearchDb class ─────────────────────────────────────────────────────────── + +export class SearchDb { + private db: Database; + + constructor(dbPath?: string) { + const path = dbPath ?? DB_PATH; + const dir = join(path, ".."); + mkdirSync(dir, { recursive: true }); + + this.db = new Database(path); + this.db.run("PRAGMA journal_mode = WAL"); + this.db.run("PRAGMA busy_timeout = 2000"); + this.createTables(); + } + + private createTables(): void { + this.db.run(` + CREATE VIRTUAL TABLE IF NOT EXISTS search_messages USING fts5( + session_id, + role, + content, + timestamp + ) + `); + this.db.run(` + CREATE VIRTUAL TABLE IF NOT EXISTS search_tasks USING fts5( + task_id, + title, + description, + status, + assigned_to + ) + `); + this.db.run(` + CREATE VIRTUAL TABLE IF NOT EXISTS search_btmsg USING fts5( + message_id, + from_agent, + to_agent, + content, + channel_name + ) + `); + } + + /** Index an agent message. */ + indexMessage(sessionId: string, role: string, content: string): void { + const ts = new Date().toISOString(); + this.db.run( + "INSERT INTO search_messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)", + [sessionId, role, content, ts], + ); + } + + /** Index a task. */ + indexTask(taskId: string, title: string, description: string, status: string, assignedTo: string): void { + this.db.run( + "INSERT INTO search_tasks (task_id, title, description, status, assigned_to) VALUES (?, ?, ?, ?, ?)", + [taskId, title, description, status, assignedTo], + ); + } + + /** Index a btmsg message. */ + indexBtmsg(msgId: string, fromAgent: string, toAgent: string, content: string, channel: string): void { + this.db.run( + "INSERT INTO search_btmsg (message_id, from_agent, to_agent, content, channel_name) VALUES (?, ?, ?, ?, ?)", + [msgId, fromAgent, toAgent, content, channel], + ); + } + + /** Search across all FTS5 tables. */ + searchAll(query: string, limit = 20): SearchResult[] { + if (!query.trim()) return []; + + const results: SearchResult[] = []; + + // Search messages + try { + const msgRows = this.db + .prepare( + `SELECT session_id, role, + snippet(search_messages, 2, '', '', '...', 32) as snip, + rank + FROM search_messages + WHERE search_messages MATCH ? + ORDER BY rank + LIMIT ?`, + ) + .all(query, limit) as Array<{ session_id: string; role: string; snip: string; rank: number }>; + + for (const row of msgRows) { + results.push({ + resultType: "message", + id: row.session_id, + title: row.role, + snippet: row.snip ?? "", + score: Math.abs(row.rank ?? 0), + }); + } + } catch { + // FTS5 syntax error — skip messages + } + + // Search tasks + try { + const taskRows = this.db + .prepare( + `SELECT task_id, title, + snippet(search_tasks, 2, '', '', '...', 32) as snip, + rank + FROM search_tasks + WHERE search_tasks MATCH ? + ORDER BY rank + LIMIT ?`, + ) + .all(query, limit) as Array<{ task_id: string; title: string; snip: string; rank: number }>; + + for (const row of taskRows) { + results.push({ + resultType: "task", + id: row.task_id, + title: row.title, + snippet: row.snip ?? "", + score: Math.abs(row.rank ?? 0), + }); + } + } catch { + // FTS5 syntax error — skip tasks + } + + // Search btmsg + try { + const btmsgRows = this.db + .prepare( + `SELECT message_id, from_agent, + snippet(search_btmsg, 3, '', '', '...', 32) as snip, + rank + FROM search_btmsg + WHERE search_btmsg MATCH ? + ORDER BY rank + LIMIT ?`, + ) + .all(query, limit) as Array<{ message_id: string; from_agent: string; snip: string; rank: number }>; + + for (const row of btmsgRows) { + results.push({ + resultType: "btmsg", + id: row.message_id, + title: row.from_agent, + snippet: row.snip ?? "", + score: Math.abs(row.rank ?? 0), + }); + } + } catch { + // FTS5 syntax error — skip btmsg + } + + // Sort by score (lower = more relevant for FTS5 rank) + results.sort((a, b) => a.score - b.score); + return results.slice(0, limit); + } + + /** Drop and recreate all FTS5 tables. */ + rebuildIndex(): void { + this.db.run("DROP TABLE IF EXISTS search_messages"); + this.db.run("DROP TABLE IF EXISTS search_tasks"); + this.db.run("DROP TABLE IF EXISTS search_btmsg"); + this.createTables(); + } + + close(): void { + this.db.close(); + } +} diff --git a/ui-electrobun/src/bun/session-db.ts b/ui-electrobun/src/bun/session-db.ts new file mode 100644 index 0000000..b5c6d2d --- /dev/null +++ b/ui-electrobun/src/bun/session-db.ts @@ -0,0 +1,326 @@ +/** + * Session persistence — SQLite-backed agent session & message storage. + * Uses bun:sqlite. DB: ~/.config/agor/settings.db (shared with SettingsDb). + * + * Tables: agent_sessions, agent_messages + */ + +import { Database } from "bun:sqlite"; +import { homedir } from "os"; +import { mkdirSync } from "fs"; +import { join } from "path"; + +// ── DB path ────────────────────────────────────────────────────────────────── + +const CONFIG_DIR = join(homedir(), ".config", "agor"); +const DB_PATH = join(CONFIG_DIR, "settings.db"); + +// ── Types ──────────────────────────────────────────────────────────────────── + +export interface StoredSession { + projectId: string; + sessionId: string; + provider: string; + status: string; + costUsd: number; + inputTokens: number; + outputTokens: number; + model: string; + error?: string; + createdAt: number; + updatedAt: number; +} + +export interface StoredMessage { + sessionId: string; + msgId: string; + role: string; + content: string; + toolName?: string; + toolInput?: string; + timestamp: number; + costUsd: number; + inputTokens: number; + outputTokens: number; +} + +// ── Schema ─────────────────────────────────────────────────────────────────── + +const SESSION_SCHEMA = ` +CREATE TABLE IF NOT EXISTS agent_sessions ( + project_id TEXT NOT NULL, + session_id TEXT PRIMARY KEY, + provider TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'idle', + cost_usd REAL NOT NULL DEFAULT 0, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + model TEXT NOT NULL DEFAULT '', + error TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_agent_sessions_project + ON agent_sessions(project_id); + +CREATE TABLE IF NOT EXISTS agent_messages ( + session_id TEXT NOT NULL, + msg_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL DEFAULT '', + tool_name TEXT, + tool_input TEXT, + timestamp INTEGER NOT NULL, + cost_usd REAL NOT NULL DEFAULT 0, + input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (session_id, msg_id), + FOREIGN KEY (session_id) REFERENCES agent_sessions(session_id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_agent_messages_session + ON agent_messages(session_id, timestamp); +`; + +// ── SessionDb class ────────────────────────────────────────────────────────── + +export class SessionDb { + private db: Database; + + constructor() { + mkdirSync(CONFIG_DIR, { recursive: true }); + this.db = new Database(DB_PATH); + this.db.exec("PRAGMA journal_mode = WAL"); + this.db.exec("PRAGMA busy_timeout = 500"); + this.db.exec("PRAGMA foreign_keys = ON"); + this.db.exec(SESSION_SCHEMA); + } + + // ── Sessions ───────────────────────────────────────────────────────────── + + saveSession(s: StoredSession): void { + this.db + .query( + `INSERT INTO agent_sessions + (project_id, session_id, provider, status, cost_usd, + input_tokens, output_tokens, model, error, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11) + ON CONFLICT(session_id) DO UPDATE SET + status = excluded.status, + cost_usd = excluded.cost_usd, + input_tokens = excluded.input_tokens, + output_tokens = excluded.output_tokens, + model = excluded.model, + error = excluded.error, + updated_at = excluded.updated_at` + ) + .run( + s.projectId, + s.sessionId, + s.provider, + s.status, + s.costUsd, + s.inputTokens, + s.outputTokens, + s.model, + s.error ?? null, + s.createdAt, + s.updatedAt + ); + } + + loadSession(projectId: string): StoredSession | null { + const row = this.db + .query< + { + project_id: string; + session_id: string; + provider: string; + status: string; + cost_usd: number; + input_tokens: number; + output_tokens: number; + model: string; + error: string | null; + created_at: number; + updated_at: number; + }, + [string] + >( + `SELECT * FROM agent_sessions + WHERE project_id = ? + ORDER BY updated_at DESC LIMIT 1` + ) + .get(projectId); + + if (!row) return null; + return { + projectId: row.project_id, + sessionId: row.session_id, + provider: row.provider, + status: row.status, + costUsd: row.cost_usd, + inputTokens: row.input_tokens, + outputTokens: row.output_tokens, + model: row.model, + error: row.error ?? undefined, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; + } + + listSessionsByProject(projectId: string): StoredSession[] { + const rows = this.db + .query< + { + project_id: string; + session_id: string; + provider: string; + status: string; + cost_usd: number; + input_tokens: number; + output_tokens: number; + model: string; + error: string | null; + created_at: number; + updated_at: number; + }, + [string] + >( + `SELECT * FROM agent_sessions + WHERE project_id = ? + ORDER BY updated_at DESC LIMIT 20` + ) + .all(projectId); + + return rows.map((r) => ({ + projectId: r.project_id, + sessionId: r.session_id, + provider: r.provider, + status: r.status, + costUsd: r.cost_usd, + inputTokens: r.input_tokens, + outputTokens: r.output_tokens, + model: r.model, + error: r.error ?? undefined, + createdAt: r.created_at, + updatedAt: r.updated_at, + })); + } + + // ── Messages ───────────────────────────────────────────────────────────── + + saveMessage(m: StoredMessage): void { + this.db + .query( + `INSERT INTO agent_messages + (session_id, msg_id, role, content, tool_name, tool_input, + timestamp, cost_usd, input_tokens, output_tokens) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10) + ON CONFLICT(session_id, msg_id) DO NOTHING` + ) + .run( + m.sessionId, + m.msgId, + m.role, + m.content, + m.toolName ?? null, + m.toolInput ?? null, + m.timestamp, + m.costUsd, + m.inputTokens, + m.outputTokens + ); + } + + saveMessages(msgs: StoredMessage[]): void { + const stmt = this.db.prepare( + `INSERT INTO agent_messages + (session_id, msg_id, role, content, tool_name, tool_input, + timestamp, cost_usd, input_tokens, output_tokens) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10) + ON CONFLICT(session_id, msg_id) DO NOTHING` + ); + + const tx = this.db.transaction(() => { + for (const m of msgs) { + stmt.run( + m.sessionId, + m.msgId, + m.role, + m.content, + m.toolName ?? null, + m.toolInput ?? null, + m.timestamp, + m.costUsd, + m.inputTokens, + m.outputTokens + ); + } + }); + tx(); + } + + loadMessages(sessionId: string): StoredMessage[] { + const rows = this.db + .query< + { + session_id: string; + msg_id: string; + role: string; + content: string; + tool_name: string | null; + tool_input: string | null; + timestamp: number; + cost_usd: number; + input_tokens: number; + output_tokens: number; + }, + [string] + >( + `SELECT * FROM agent_messages + WHERE session_id = ? + ORDER BY timestamp ASC` + ) + .all(sessionId); + + return rows.map((r) => ({ + sessionId: r.session_id, + msgId: r.msg_id, + role: r.role, + content: r.content, + toolName: r.tool_name ?? undefined, + toolInput: r.tool_input ?? undefined, + timestamp: r.timestamp, + costUsd: r.cost_usd, + inputTokens: r.input_tokens, + outputTokens: r.output_tokens, + })); + } + + // ── Cleanup ────────────────────────────────────────────────────────────── + + /** Delete sessions older than maxAgeDays for a project, keeping at most keepCount. */ + pruneOldSessions(projectId: string, keepCount = 10): void { + this.db + .query( + `DELETE FROM agent_sessions + WHERE project_id = ?1 + AND session_id NOT IN ( + SELECT session_id FROM agent_sessions + WHERE project_id = ?1 + ORDER BY updated_at DESC + LIMIT ?2 + )` + ) + .run(projectId, keepCount); + } + + close(): void { + this.db.close(); + } +} + +// Singleton +export const sessionDb = new SessionDb(); diff --git a/ui-electrobun/src/mainview/App.svelte b/ui-electrobun/src/mainview/App.svelte index 1bb6d5e..05d297e 100644 --- a/ui-electrobun/src/mainview/App.svelte +++ b/ui-electrobun/src/mainview/App.svelte @@ -5,9 +5,12 @@ import CommandPalette from './CommandPalette.svelte'; import ToastContainer from './ToastContainer.svelte'; import NotifDrawer, { type Notification } from './NotifDrawer.svelte'; + import StatusBar from './StatusBar.svelte'; + import SearchOverlay from './SearchOverlay.svelte'; import { themeStore } from './theme-store.svelte.ts'; import { fontStore } from './font-store.svelte.ts'; import { keybindingStore } from './keybinding-store.svelte.ts'; + import { trackProject } from './health-store.svelte.ts'; import { appRpc } from './rpc.ts'; // ── Types ───────────────────────────────────────────────────── @@ -137,6 +140,7 @@ let settingsOpen = $state(false); let paletteOpen = $state(false); let drawerOpen = $state(false); + let searchOpen = $state(false); let sessionStart = $state(Date.now()); let notifications = $state([ @@ -236,15 +240,8 @@ } // ── Status bar aggregates ────────────────────────────────────── - let runningCount = $derived(PROJECTS.filter(p => p.status === 'running').length); - let idleCount = $derived(PROJECTS.filter(p => p.status === 'idle').length); - let stalledCount = $derived(PROJECTS.filter(p => p.status === 'stalled').length); let totalCost = $derived(PROJECTS.reduce((s, p) => s + p.costUsd, 0)); let totalTokens = $derived(PROJECTS.reduce((s, p) => s + p.tokens, 0)); - let attentionItems = $derived(PROJECTS.filter(p => p.status === 'stalled' || (p.contextPct ?? 0) >= 75)); - - function fmtTokens(n: number): string { return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n); } - function fmtCost(n: number): string { return `$${n.toFixed(3)}`; } // ── DEBUG: Visual click diagnostics overlay (gated behind DEBUG env) ──── const DEBUG_ENABLED = typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debug'); @@ -299,13 +296,29 @@ keybindingStore.on('group4', () => setActiveGroup(groups[3]?.id)); keybindingStore.on('minimize', () => handleMinimize()); + // Ctrl+Shift+F for search overlay + function handleSearchShortcut(e: KeyboardEvent) { + if (e.ctrlKey && e.shiftKey && e.key === 'F') { + e.preventDefault(); + searchOpen = !searchOpen; + } + } + document.addEventListener('keydown', handleSearchShortcut); + + // Track projects for health monitoring + for (const p of PROJECTS) trackProject(p.id); + const cleanup = keybindingStore.installListener(); - return cleanup; + return () => { + cleanup(); + document.removeEventListener('keydown', handleSearchShortcut); + }; }); settingsOpen = false} /> paletteOpen = false} /> + searchOpen = false} /> - -
- {#if runningCount > 0} - - - {runningCount} - running - - {/if} - {#if idleCount > 0} - - - {idleCount} - idle - - {/if} - {#if stalledCount > 0} - - - {stalledCount} - stalled - - {/if} - {#if attentionItems.length > 0} - - - {attentionItems.length} - attention - - {/if} - - - - - {activeGroup?.name} - - - session - {sessionDuration} - - - tokens - {fmtTokens(totalTokens)} - - - cost - {fmtCost(totalCost)} - - - Ctrl+K -
+ + {#if DEBUG_ENABLED && debugLog.length > 0} @@ -728,55 +696,5 @@ line-height: 1; } - /* ── Status bar ───────────────────────────────────────────── */ - .status-bar { - height: var(--status-bar-height); - background: var(--ctp-crust); - border-top: 1px solid var(--ctp-surface0); - display: flex; - align-items: center; - gap: 0.875rem; - padding: 0 0.625rem; - flex-shrink: 0; - font-size: 0.6875rem; - color: var(--ctp-subtext0); - } - - .status-segment { - display: flex; - align-items: center; - gap: 0.25rem; - white-space: nowrap; - } - - .status-dot-sm { - width: 0.4375rem; - height: 0.4375rem; - border-radius: 50%; - flex-shrink: 0; - } - - .status-dot-sm.green { background: var(--ctp-green); } - .status-dot-sm.gray { background: var(--ctp-overlay0); } - .status-dot-sm.orange { background: var(--ctp-peach); } - - .status-value { color: var(--ctp-text); font-weight: 500; } - .status-bar-spacer { flex: 1; } - - .attn-badge { color: var(--ctp-yellow); } - .attn-icon { width: 0.75rem; height: 0.75rem; stroke: var(--ctp-yellow); } - - .palette-hint { - padding: 0.1rem 0.3rem; - background: var(--ctp-surface0); - border: 1px solid var(--ctp-surface1); - border-radius: 0.2rem; - font-size: 0.6rem; - color: var(--ctp-overlay0); - font-family: var(--ui-font-family); - cursor: pointer; - transition: color 0.1s; - } - - .palette-hint:hover { color: var(--ctp-subtext0); } + /* Status bar styles are in StatusBar.svelte */ diff --git a/ui-electrobun/src/mainview/CodeEditor.svelte b/ui-electrobun/src/mainview/CodeEditor.svelte new file mode 100644 index 0000000..f4739ff --- /dev/null +++ b/ui-electrobun/src/mainview/CodeEditor.svelte @@ -0,0 +1,248 @@ + + +
+ + diff --git a/ui-electrobun/src/mainview/CommsTab.svelte b/ui-electrobun/src/mainview/CommsTab.svelte new file mode 100644 index 0000000..4977977 --- /dev/null +++ b/ui-electrobun/src/mainview/CommsTab.svelte @@ -0,0 +1,521 @@ + + +
+ +
+ + +
+ +
+ +
+ {#if mode === 'channels'} + {#each channels as ch} + + {/each} + {#if channels.length === 0} + + {/if} + {:else} + {#each agents as ag} + + {/each} + {#if agents.length === 0} + + {/if} + {/if} +
+ + +
+ {#if loading} +
Loading...
+ {:else if mode === 'channels'} +
+ {#each channelMessages as msg} +
+ {msg.senderName} + {msg.senderRole} + {msg.createdAt.slice(11, 16)} +
{msg.content}
+
+ {/each} + {#if channelMessages.length === 0} +
No messages in this channel
+ {/if} +
+ {:else} +
+ {#each dmMessages as msg} +
+ {msg.senderName ?? msg.fromAgent} + {msg.createdAt.slice(11, 16)} +
{msg.content}
+
+ {/each} + {#if dmMessages.length === 0 && activeDmAgentId} +
No messages yet
+ {/if} + {#if !activeDmAgentId} +
Select an agent to message
+ {/if} +
+ {/if} + + +
+ + +
+
+
+
+ + diff --git a/ui-electrobun/src/mainview/CsvTable.svelte b/ui-electrobun/src/mainview/CsvTable.svelte new file mode 100644 index 0000000..e025ace --- /dev/null +++ b/ui-electrobun/src/mainview/CsvTable.svelte @@ -0,0 +1,243 @@ + + +
+
+ + {totalRows} row{totalRows !== 1 ? 's' : ''} x {colCount} col{colCount !== 1 ? 's' : ''} + + {filename} +
+ +
+ + + + + {#each headers as header, i} + + {/each} + + + + {#each sortedRows as row, rowIdx (rowIdx)} + + + {#each { length: colCount } as _, colIdx} + + {/each} + + {/each} + +
# toggleSort(i)} class="sortable"> + {header}{sortIndicator(i)} +
{rowIdx + 1}{row[colIdx] ?? ''}
+
+
+ + diff --git a/ui-electrobun/src/mainview/FileBrowser.svelte b/ui-electrobun/src/mainview/FileBrowser.svelte index 84a4f36..5cba974 100644 --- a/ui-electrobun/src/mainview/FileBrowser.svelte +++ b/ui-electrobun/src/mainview/FileBrowser.svelte @@ -1,133 +1,352 @@
+
- {#snippet renderNode(node: FileNode, path: string, depth: number)} - {#if node.type === 'dir'} - - {#if openDirs.has(path) && node.children} - {#each node.children as child} - {@render renderNode(child, `${path}/${child.name}`, depth + 1)} - {/each} - {/if} - {:else} - + {#snippet renderEntries(dirPath: string, depth: number)} + {#if childrenCache.has(dirPath)} + {#each childrenCache.get(dirPath) ?? [] as entry} + {@const fullPath = `${dirPath}/${entry.name}`} + {#if entry.type === 'dir'} + + {#if openDirs.has(fullPath)} + {@render renderEntries(fullPath, depth + 1)} + {/if} + {:else} + + {/if} + {/each} + {:else if loadingDirs.has(dirPath)} +
Loading...
{/if} {/snippet} - {#each TREE as node} - {@render renderNode(node, node.name, 0)} - {/each} + {@render renderEntries(cwd, 0)}
- {#if selectedFile} -
-
{selectedFile}
-
(click to open in editor)
-
- {/if} + +
+ {#if !selectedFile} +
Select a file to view
+ {:else if fileLoading} +
Loading...
+ {:else if fileError} +
{fileError}
+ {:else if selectedType === 'pdf'} + + {:else if selectedType === 'csv' && fileContent != null} + + {:else if selectedType === 'image' && fileContent} + {@const ext = getExt(selectedName)} + {@const mime = ext === 'svg' ? 'image/svg+xml' : ext === 'png' ? 'image/png' : ext === 'gif' ? 'image/gif' : ext === 'webp' ? 'image/webp' : 'image/jpeg'} +
+
{selectedName} ({formatSize(fileSize)})
+ {selectedName} +
+ {:else if selectedType === 'code' && fileContent != null} +
+ + {selectedName} + {#if isDirty} (modified){/if} + + {formatSize(fileSize)} +
+ + {:else if fileContent != null} + +
+ + {selectedName} + {#if isDirty} (modified){/if} + + {formatSize(fileSize)} +
+ + {/if} +
diff --git a/ui-electrobun/src/mainview/PdfViewer.svelte b/ui-electrobun/src/mainview/PdfViewer.svelte new file mode 100644 index 0000000..eebb23b --- /dev/null +++ b/ui-electrobun/src/mainview/PdfViewer.svelte @@ -0,0 +1,298 @@ + + +
+
+ + {#if loading} + Loading... + {:else if error} + Error + {:else} + {pageCount} page{pageCount !== 1 ? 's' : ''} + {/if} + +
+ + + +
+
+ + {#if error} +
{error}
+ {:else} +
+ {/if} +
+ + diff --git a/ui-electrobun/src/mainview/ProjectCard.svelte b/ui-electrobun/src/mainview/ProjectCard.svelte index 7a7f714..eead916 100644 --- a/ui-electrobun/src/mainview/ProjectCard.svelte +++ b/ui-electrobun/src/mainview/ProjectCard.svelte @@ -3,12 +3,15 @@ import TerminalTabs from './TerminalTabs.svelte'; import FileBrowser from './FileBrowser.svelte'; import MemoryTab from './MemoryTab.svelte'; + import CommsTab from './CommsTab.svelte'; + import TaskBoardTab from './TaskBoardTab.svelte'; import { startAgent, stopAgent, sendPrompt, getSession, hasSession, + loadLastSession, type AgentStatus, type AgentMessage, } from './agent-store.svelte.ts'; - type ProjectTab = 'model' | 'docs' | 'context' | 'files' | 'ssh' | 'memory'; + type ProjectTab = 'model' | 'docs' | 'context' | 'files' | 'ssh' | 'memory' | 'comms' | 'tasks'; interface Props { id: string; @@ -29,6 +32,8 @@ clonesAtMax?: boolean; /** Callback when user requests cloning (receives projectId and branch name). */ onClone?: (projectId: string, branch: string) => void; + /** Group ID for btmsg/bttask context. */ + groupId?: string; } let { @@ -315,7 +320,7 @@ role="tabpanel" aria-label="Files" > - + {/if} diff --git a/ui-electrobun/src/mainview/SearchOverlay.svelte b/ui-electrobun/src/mainview/SearchOverlay.svelte new file mode 100644 index 0000000..ee72ba0 --- /dev/null +++ b/ui-electrobun/src/mainview/SearchOverlay.svelte @@ -0,0 +1,301 @@ + + +{#if open} + +
+ +
e.stopPropagation()} onkeydown={handleKeydown}> +
+ + + {#if loading} + + {/if} + Esc +
+ + {#if results.length > 0} +
+ {#each Object.entries(grouped()) as [type, items]} +
+
{groupLabels[type] ?? type}
+ {#each items as item, i} + {@const flatIdx = results.indexOf(item)} + + {/each} +
+ {/each} +
+ {:else if query.trim() && !loading} +
No results for "{query}"
+ {/if} +
+
+{/if} + + diff --git a/ui-electrobun/src/mainview/StatusBar.svelte b/ui-electrobun/src/mainview/StatusBar.svelte new file mode 100644 index 0000000..00c1254 --- /dev/null +++ b/ui-electrobun/src/mainview/StatusBar.svelte @@ -0,0 +1,275 @@ + + +
+ + {#if health.running > 0} + + + {health.running} + running + + {/if} + {#if health.idle > 0} + + + {health.idle} + idle + + {/if} + {#if health.stalled > 0} + + + {health.stalled} + stalled + + {/if} + + + {#if attentionQueue.length > 0} + + {/if} + + + + + {#if health.totalBurnRatePerHour > 0} + + {formatRate(health.totalBurnRatePerHour)} + + {/if} + + + {groupName} + + + {projectCount} + projects + + + session + {sessionDuration} + + + tokens + {fmtTokens(totalTokens)} + + + cost + {fmtCost(totalCost)} + + + Ctrl+Shift+F +
+ + +{#if showAttention && attentionQueue.length > 0} +
+ {#each attentionQueue as item (item.projectId)} + + {/each} +
+{/if} + + diff --git a/ui-electrobun/src/mainview/TaskBoardTab.svelte b/ui-electrobun/src/mainview/TaskBoardTab.svelte new file mode 100644 index 0000000..2a0223f --- /dev/null +++ b/ui-electrobun/src/mainview/TaskBoardTab.svelte @@ -0,0 +1,515 @@ + + +
+ +
+ Task Board + {tasks.length} tasks + +
+ + + {#if showCreateForm} +
+ { if (e.key === 'Enter') createTask(); }} + /> + +
+ + +
+ {#if error} + {error} + {/if} +
+ {/if} + + +
+ {#each COLUMNS as col} +
onDragOver(e, col)} + ondragleave={onDragLeave} + ondrop={(e) => onDrop(e, col)} + role="list" + aria-label="{COL_LABELS[col]} column" + > +
+ {COL_LABELS[col]} + {tasksByCol[col]?.length ?? 0} +
+ +
+ {#each tasksByCol[col] ?? [] as task (task.id)} +
onDragStart(e, task.id)} + ondragend={onDragEnd} + role="listitem" + > +
+ + {task.title} + +
+ {#if task.description} +
{task.description}
+ {/if} + {#if task.assignedTo} +
+ @ + {task.assignedTo} +
+ {/if} +
+ {/each} +
+
+ {/each} +
+
+ + diff --git a/ui-electrobun/src/mainview/agent-store.svelte.ts b/ui-electrobun/src/mainview/agent-store.svelte.ts index d025e48..e68ab56 100644 --- a/ui-electrobun/src/mainview/agent-store.svelte.ts +++ b/ui-electrobun/src/mainview/agent-store.svelte.ts @@ -74,6 +74,54 @@ let sessions = $state>({}); // Grace period timers for cleanup after done/error const cleanupTimers = new Map>(); +// Debounce timer for message persistence +const msgPersistTimers = new Map>(); + +// ── Session persistence helpers ───────────────────────────────────────────── + +function persistSession(session: AgentSession): void { + appRpc.request['session.save']({ + projectId: session.projectId, + sessionId: session.sessionId, + provider: session.provider, + status: session.status, + costUsd: session.costUsd, + inputTokens: session.inputTokens, + outputTokens: session.outputTokens, + model: session.model, + error: session.error, + createdAt: session.messages[0]?.timestamp ?? Date.now(), + updatedAt: Date.now(), + }).catch((err: unknown) => { + console.error('[session.save] persist error:', err); + }); +} + +function persistMessages(session: AgentSession): void { + // Debounce: batch message saves every 2 seconds + const existing = msgPersistTimers.get(session.sessionId); + if (existing) clearTimeout(existing); + + const timer = setTimeout(() => { + msgPersistTimers.delete(session.sessionId); + const msgs = session.messages.map((m) => ({ + sessionId: session.sessionId, + msgId: m.id, + role: m.role, + content: m.content, + toolName: m.toolName, + toolInput: m.toolInput, + timestamp: m.timestamp, + })); + if (msgs.length === 0) return; + appRpc.request['session.messages.save']({ messages: msgs }).catch((err: unknown) => { + console.error('[session.messages.save] persist error:', err); + }); + }, 2000); + + msgPersistTimers.set(session.sessionId, timer); +} + // ── RPC event listeners (registered once) ──────────────────────────────────── let listenersRegistered = false; @@ -104,6 +152,7 @@ function ensureListeners() { if (converted.length > 0) { session.messages = [...session.messages, ...converted]; + persistMessages(session); } }); @@ -119,8 +168,18 @@ function ensureListeners() { session.status = normalizeStatus(payload.status); if (payload.error) session.error = payload.error; + // Persist on every status change + persistSession(session); + // Schedule cleanup after done/error (Fix #2) if (session.status === 'done' || session.status === 'error') { + // Flush any pending message persistence immediately + const pendingTimer = msgPersistTimers.get(session.sessionId); + if (pendingTimer) { + clearTimeout(pendingTimer); + msgPersistTimers.delete(session.sessionId); + } + persistMessages(session); scheduleCleanup(session.sessionId, session.projectId); } }); @@ -427,5 +486,57 @@ export function clearSession(projectId: string): void { } } +/** + * Load the last session for a project from SQLite (for restart recovery). + * Restores session state + messages into the reactive store. + * Only restores done/error sessions (running sessions are gone after restart). + */ +export async function loadLastSession(projectId: string): Promise { + ensureListeners(); + try { + const { session } = await appRpc.request['session.load']({ projectId }); + if (!session) return false; + + // Only restore completed sessions (running sessions can't be resumed) + if (session.status !== 'done' && session.status !== 'error') return false; + + // Load messages for this session + const { messages: storedMsgs } = await appRpc.request['session.messages.load']({ + sessionId: session.sessionId, + }); + + const restoredMessages: AgentMessage[] = storedMsgs.map((m: { + msgId: string; role: string; content: string; + toolName?: string; toolInput?: string; timestamp: number; + }) => ({ + id: m.msgId, + role: m.role as MsgRole, + content: m.content, + toolName: m.toolName, + toolInput: m.toolInput, + timestamp: m.timestamp, + })); + + sessions[session.sessionId] = { + sessionId: session.sessionId, + projectId: session.projectId, + provider: session.provider, + status: normalizeStatus(session.status), + messages: restoredMessages, + costUsd: session.costUsd, + inputTokens: session.inputTokens, + outputTokens: session.outputTokens, + model: session.model, + error: session.error, + }; + + projectSessionMap.set(projectId, session.sessionId); + return true; + } catch (err) { + console.error('[loadLastSession] error:', err); + return false; + } +} + /** Initialize listeners on module load. */ ensureListeners(); diff --git a/ui-electrobun/src/mainview/health-store.svelte.ts b/ui-electrobun/src/mainview/health-store.svelte.ts new file mode 100644 index 0000000..704de6a --- /dev/null +++ b/ui-electrobun/src/mainview/health-store.svelte.ts @@ -0,0 +1,229 @@ +/** + * Per-project health tracking — Svelte 5 runes. + * + * Tracks activity state, burn rate (5-min EMA from cost snapshots), + * context pressure (tokens / model limit), and attention scoring. + * 5-second tick timer drives derived state updates. + */ + +// ── Types ──────────────────────────────────────────────────────────────────── + +export type ActivityState = 'inactive' | 'running' | 'idle' | 'stalled'; + +export interface ProjectHealth { + projectId: string; + activityState: ActivityState; + activeTool: string | null; + idleDurationMs: number; + burnRatePerHour: number; + contextPressure: number | null; + fileConflictCount: number; + attentionScore: number; + attentionReason: string | null; +} + +// ── Configuration ──────────────────────────────────────────────────────────── + +const DEFAULT_STALL_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes +const TICK_INTERVAL_MS = 5_000; +const BURN_RATE_WINDOW_MS = 5 * 60 * 1000; // 5-minute window + +const DEFAULT_CONTEXT_LIMIT = 200_000; + +// ── Internal state ─────────────────────────────────────────────────────────── + +interface ProjectTracker { + projectId: string; + lastActivityTs: number; + lastToolName: string | null; + toolInFlight: boolean; + costSnapshots: Array<[number, number]>; // [timestamp, costUsd] + totalTokens: number; + totalCost: number; + status: 'inactive' | 'running' | 'idle' | 'done' | 'error'; +} + +let trackers = $state>(new Map()); +let tickTs = $state(Date.now()); +let tickInterval: ReturnType | null = null; + +// ── Attention scoring (pure) ───────────────────────────────────────────────── + +function scoreAttention( + activityState: ActivityState, + contextPressure: number | null, + fileConflictCount: number, + status: string, +): { score: number; reason: string | null } { + if (status === 'error') return { score: 90, reason: 'Agent error' }; + if (activityState === 'stalled') return { score: 100, reason: 'Agent stalled (>15 min)' }; + if (contextPressure !== null && contextPressure > 0.9) return { score: 80, reason: 'Context >90%' }; + if (fileConflictCount > 0) return { score: 70, reason: `${fileConflictCount} file conflict(s)` }; + if (contextPressure !== null && contextPressure > 0.75) return { score: 40, reason: 'Context >75%' }; + return { score: 0, reason: null }; +} + +// ── Burn rate calculation ──────────────────────────────────────────────────── + +function computeBurnRate(snapshots: Array<[number, number]>): number { + if (snapshots.length < 2) return 0; + const windowStart = Date.now() - BURN_RATE_WINDOW_MS; + const recent = snapshots.filter(([ts]) => ts >= windowStart); + if (recent.length < 2) return 0; + const first = recent[0]; + const last = recent[recent.length - 1]; + const elapsedHours = (last[0] - first[0]) / 3_600_000; + if (elapsedHours < 0.001) return 0; + const costDelta = last[1] - first[1]; + return Math.max(0, costDelta / elapsedHours); +} + +// ── Derived health per project ─────────────────────────────────────────────── + +function computeHealth(tracker: ProjectTracker, now: number): ProjectHealth { + let activityState: ActivityState; + let idleDurationMs = 0; + let activeTool: string | null = null; + + if (tracker.status === 'inactive' || tracker.status === 'done' || tracker.status === 'error') { + activityState = 'inactive'; + } else if (tracker.toolInFlight) { + activityState = 'running'; + activeTool = tracker.lastToolName; + } else { + idleDurationMs = now - tracker.lastActivityTs; + activityState = idleDurationMs >= DEFAULT_STALL_THRESHOLD_MS ? 'stalled' : 'idle'; + } + + let contextPressure: number | null = null; + if (tracker.totalTokens > 0) { + contextPressure = Math.min(1, tracker.totalTokens / DEFAULT_CONTEXT_LIMIT); + } + + const burnRatePerHour = computeBurnRate(tracker.costSnapshots); + const attention = scoreAttention(activityState, contextPressure, 0, tracker.status); + + return { + projectId: tracker.projectId, + activityState, + activeTool, + idleDurationMs, + burnRatePerHour, + contextPressure, + fileConflictCount: 0, + attentionScore: attention.score, + attentionReason: attention.reason, + }; +} + +// ── Public API ─────────────────────────────────────────────────────────────── + +/** Register a project for health tracking. */ +export function trackProject(projectId: string): void { + if (trackers.has(projectId)) return; + trackers.set(projectId, { + projectId, + lastActivityTs: Date.now(), + lastToolName: null, + toolInFlight: false, + costSnapshots: [], + totalTokens: 0, + totalCost: 0, + status: 'inactive', + }); + if (!tickInterval) startHealthTick(); +} + +/** Record activity (call on every agent message). */ +export function recordActivity(projectId: string, toolName?: string): void { + const t = trackers.get(projectId); + if (!t) return; + t.lastActivityTs = Date.now(); + t.status = 'running'; + if (toolName !== undefined) { + t.lastToolName = toolName; + t.toolInFlight = true; + } + if (!tickInterval) startHealthTick(); +} + +/** Record tool completion. */ +export function recordToolDone(projectId: string): void { + const t = trackers.get(projectId); + if (!t) return; + t.lastActivityTs = Date.now(); + t.toolInFlight = false; +} + +/** Record a token/cost snapshot for burn rate calculation. */ +export function recordTokenSnapshot(projectId: string, totalTokens: number, costUsd: number): void { + const t = trackers.get(projectId); + if (!t) return; + const now = Date.now(); + t.totalTokens = totalTokens; + t.totalCost = costUsd; + t.costSnapshots.push([now, costUsd]); + const cutoff = now - BURN_RATE_WINDOW_MS * 2; + t.costSnapshots = t.costSnapshots.filter(([ts]) => ts > cutoff); +} + +/** Update project status. */ +export function setProjectStatus(projectId: string, status: 'running' | 'idle' | 'done' | 'error'): void { + const t = trackers.get(projectId); + if (t) t.status = status; +} + +/** Get health for a single project (reactive via tickTs). */ +export function getProjectHealth(projectId: string): ProjectHealth | null { + const now = tickTs; + const t = trackers.get(projectId); + if (!t) return null; + return computeHealth(t, now); +} + +/** Get top N items needing attention. */ +export function getAttentionQueue(limit = 5): ProjectHealth[] { + const now = tickTs; + const results: ProjectHealth[] = []; + for (const t of trackers.values()) { + const h = computeHealth(t, now); + if (h.attentionScore > 0) results.push(h); + } + results.sort((a, b) => b.attentionScore - a.attentionScore); + return results.slice(0, limit); +} + +/** Get aggregate stats across all tracked projects. */ +export function getHealthAggregates(): { + running: number; + idle: number; + stalled: number; + totalBurnRatePerHour: number; +} { + const now = tickTs; + let running = 0, idle = 0, stalled = 0, totalBurnRatePerHour = 0; + for (const t of trackers.values()) { + const h = computeHealth(t, now); + if (h.activityState === 'running') running++; + else if (h.activityState === 'idle') idle++; + else if (h.activityState === 'stalled') stalled++; + totalBurnRatePerHour += h.burnRatePerHour; + } + return { running, idle, stalled, totalBurnRatePerHour }; +} + +/** Start the health tick timer. */ +function startHealthTick(): void { + if (tickInterval) return; + tickInterval = setInterval(() => { + tickTs = Date.now(); + }, TICK_INTERVAL_MS); +} + +/** Stop the health tick timer. */ +export function stopHealthTick(): void { + if (tickInterval) { + clearInterval(tickInterval); + tickInterval = null; + } +} diff --git a/ui-electrobun/src/mainview/plugin-host.ts b/ui-electrobun/src/mainview/plugin-host.ts new file mode 100644 index 0000000..c4d56a1 --- /dev/null +++ b/ui-electrobun/src/mainview/plugin-host.ts @@ -0,0 +1,287 @@ +/** + * Plugin Host — Web Worker sandbox for Electrobun plugins. + * + * Each plugin runs in a dedicated Web Worker with no DOM/IPC access. + * Communication: Main <-> Worker via postMessage. + * Permission-gated API (messages, events, notifications, palette). + * On unload, Worker is terminated — all plugin state destroyed. + */ + +import { appRpc } from './rpc.ts'; + +// ── Types ──────────────────────────────────────────────────────────────────── + +export interface PluginMeta { + id: string; + name: string; + version: string; + description: string; + main: string; + permissions: string[]; +} + +interface LoadedPlugin { + meta: PluginMeta; + worker: Worker; + callbacks: Map void>; + eventSubscriptions: Array<{ event: string; handler: (data: unknown) => void }>; + cleanup: () => void; +} + +type PluginCommandCallback = () => void; + +// ── State ──────────────────────────────────────────────────────────────────── + +const loadedPlugins = new Map(); + +// External command/event registries (set by plugin-store) +let commandRegistry: ((pluginId: string, label: string, callback: PluginCommandCallback) => void) | null = null; +let commandRemover: ((pluginId: string) => void) | null = null; +let eventBus: { on: (event: string, handler: (data: unknown) => void) => void; off: (event: string, handler: (data: unknown) => void) => void } | null = null; + +/** Wire up external registries (called by plugin-store on init). */ +export function setPluginRegistries(opts: { + addCommand: (pluginId: string, label: string, cb: PluginCommandCallback) => void; + removeCommands: (pluginId: string) => void; + eventBus: { on: (e: string, h: (d: unknown) => void) => void; off: (e: string, h: (d: unknown) => void) => void }; +}): void { + commandRegistry = opts.addCommand; + commandRemover = opts.removeCommands; + eventBus = opts.eventBus; +} + +// ── Worker script builder ──────────────────────────────────────────────────── + +function buildWorkerScript(): string { + return ` +"use strict"; + +const _callbacks = new Map(); +let _callbackId = 0; +function _nextCbId() { return '__cb_' + (++_callbackId); } + +const _pending = new Map(); +let _rpcId = 0; +function _rpc(method, args) { + return new Promise((resolve, reject) => { + const id = '__rpc_' + (++_rpcId); + _pending.set(id, { resolve, reject }); + self.postMessage({ type: 'rpc', id, method, args }); + }); +} + +self.onmessage = function(e) { + const msg = e.data; + + if (msg.type === 'init') { + const permissions = msg.permissions || []; + const meta = msg.meta; + const api = { meta: Object.freeze(meta) }; + + if (permissions.includes('palette')) { + api.palette = { + registerCommand(label, callback) { + if (typeof label !== 'string' || !label.trim()) throw new Error('Command label must be non-empty string'); + if (typeof callback !== 'function') throw new Error('Command callback must be a function'); + const cbId = _nextCbId(); + _callbacks.set(cbId, callback); + self.postMessage({ type: 'palette-register', label, callbackId: cbId }); + }, + }; + } + + if (permissions.includes('notifications')) { + api.notifications = { + show(message) { + self.postMessage({ type: 'notification', message: String(message) }); + }, + }; + } + + if (permissions.includes('messages')) { + api.messages = { + list() { return _rpc('messages.list', {}); }, + }; + } + + if (permissions.includes('events')) { + api.events = { + on(event, callback) { + if (typeof event !== 'string' || typeof callback !== 'function') { + throw new Error('events.on requires (string, function)'); + } + const cbId = _nextCbId(); + _callbacks.set(cbId, callback); + self.postMessage({ type: 'event-on', event, callbackId: cbId }); + }, + off(event) { + self.postMessage({ type: 'event-off', event }); + }, + }; + } + + Object.freeze(api); + + try { + const fn = (0, eval)('(function(agor) { "use strict"; ' + msg.code + '\\n})'); + fn(api); + self.postMessage({ type: 'loaded' }); + } catch (err) { + self.postMessage({ type: 'error', message: String(err) }); + } + } + + if (msg.type === 'invoke-callback') { + const cb = _callbacks.get(msg.callbackId); + if (cb) { + try { cb(msg.data); } + catch (err) { self.postMessage({ type: 'callback-error', callbackId: msg.callbackId, message: String(err) }); } + } + } + + if (msg.type === 'rpc-result') { + const pending = _pending.get(msg.id); + if (pending) { + _pending.delete(msg.id); + if (msg.error) pending.reject(new Error(msg.error)); + else pending.resolve(msg.result); + } + } +}; +`; +} + +let workerBlobUrl: string | null = null; +function getWorkerBlobUrl(): string { + if (!workerBlobUrl) { + const blob = new Blob([buildWorkerScript()], { type: 'application/javascript' }); + workerBlobUrl = URL.createObjectURL(blob); + } + return workerBlobUrl; +} + +// ── Public API ─────────────────────────────────────────────────────────────── + +/** + * Load and execute a plugin in a Web Worker sandbox. + * Reads plugin code via RPC from Bun process. + */ +export async function loadPlugin(meta: PluginMeta): Promise { + if (loadedPlugins.has(meta.id)) { + console.warn(`Plugin '${meta.id}' is already loaded`); + return; + } + + // Validate permissions + const validPerms = new Set(['palette', 'notifications', 'messages', 'events']); + for (const p of meta.permissions) { + if (!validPerms.has(p)) { + throw new Error(`Plugin '${meta.id}' requests unknown permission: ${p}`); + } + } + + // Read plugin code via RPC + let code: string; + try { + const res = await appRpc.request['plugin.readFile']({ pluginId: meta.id, filePath: meta.main }); + if (!res.ok) throw new Error(res.error ?? 'Failed to read plugin file'); + code = res.content; + } catch (e) { + throw new Error(`Failed to read plugin '${meta.id}' entry '${meta.main}': ${e}`); + } + + const worker = new Worker(getWorkerBlobUrl(), { type: 'classic' }); + const callbacks = new Map void>(); + const eventSubscriptions: Array<{ event: string; handler: (data: unknown) => void }> = []; + + await new Promise((resolve, reject) => { + worker.onmessage = (e) => { + const msg = e.data; + + switch (msg.type) { + case 'loaded': + resolve(); + break; + + case 'error': + commandRemover?.(meta.id); + for (const sub of eventSubscriptions) eventBus?.off(sub.event, sub.handler); + worker.terminate(); + reject(new Error(`Plugin '${meta.id}' failed: ${msg.message}`)); + break; + + case 'palette-register': { + const cbId = msg.callbackId as string; + const invoke = () => worker.postMessage({ type: 'invoke-callback', callbackId: cbId }); + callbacks.set(cbId, invoke); + commandRegistry?.(meta.id, msg.label, invoke); + break; + } + + case 'notification': + console.log(`[plugin:${meta.id}] notification:`, msg.message); + break; + + case 'event-on': { + const cbId = msg.callbackId as string; + const handler = (data: unknown) => { + worker.postMessage({ type: 'invoke-callback', callbackId: cbId, data }); + }; + eventSubscriptions.push({ event: msg.event, handler }); + eventBus?.on(msg.event, handler); + break; + } + + case 'event-off': { + const idx = eventSubscriptions.findIndex(s => s.event === msg.event); + if (idx >= 0) { + eventBus?.off(eventSubscriptions[idx].event, eventSubscriptions[idx].handler); + eventSubscriptions.splice(idx, 1); + } + break; + } + + case 'callback-error': + console.error(`Plugin '${meta.id}' callback error:`, msg.message); + break; + } + }; + + worker.onerror = (err) => reject(new Error(`Plugin '${meta.id}' worker error: ${err.message}`)); + + worker.postMessage({ + type: 'init', + code, + permissions: meta.permissions, + meta: { id: meta.id, name: meta.name, version: meta.version, description: meta.description }, + }); + }); + + const cleanup = () => { + commandRemover?.(meta.id); + for (const sub of eventSubscriptions) eventBus?.off(sub.event, sub.handler); + eventSubscriptions.length = 0; + callbacks.clear(); + worker.terminate(); + }; + + loadedPlugins.set(meta.id, { meta, worker, callbacks, eventSubscriptions, cleanup }); +} + +/** Unload a plugin. */ +export function unloadPlugin(id: string): void { + const plugin = loadedPlugins.get(id); + if (!plugin) return; + plugin.cleanup(); + loadedPlugins.delete(id); +} + +/** Get all loaded plugin metas. */ +export function getLoadedPlugins(): PluginMeta[] { + return Array.from(loadedPlugins.values()).map(p => p.meta); +} + +/** Unload all plugins. */ +export function unloadAllPlugins(): void { + for (const [id] of loadedPlugins) unloadPlugin(id); +} diff --git a/ui-electrobun/src/mainview/plugin-store.svelte.ts b/ui-electrobun/src/mainview/plugin-store.svelte.ts new file mode 100644 index 0000000..9bfa65b --- /dev/null +++ b/ui-electrobun/src/mainview/plugin-store.svelte.ts @@ -0,0 +1,136 @@ +/** + * Plugin store — Svelte 5 runes. + * + * Discovers plugins from ~/.config/agor/plugins/ via RPC. + * Manages command registry (for palette integration) and event bus. + * Coordinates with plugin-host.ts for Web Worker lifecycle. + */ + +import { appRpc } from './rpc.ts'; +import { + loadPlugin, + unloadPlugin, + unloadAllPlugins, + getLoadedPlugins, + setPluginRegistries, + type PluginMeta, +} from './plugin-host.ts'; + +// ── Types ──────────────────────────────────────────────────────────────────── + +export interface PluginCommand { + pluginId: string; + label: string; + callback: () => void; +} + +// ── State ──────────────────────────────────────────────────────────────────── + +let discovered = $state([]); +let commands = $state([]); +let loaded = $derived(getLoadedPlugins()); + +// ── Event bus (simple pub/sub) ─────────────────────────────────────────────── + +type EventHandler = (data: unknown) => void; +const eventListeners = new Map>(); + +const pluginEventBus = { + on(event: string, handler: EventHandler): void { + let set = eventListeners.get(event); + if (!set) { + set = new Set(); + eventListeners.set(event, set); + } + set.add(handler); + }, + off(event: string, handler: EventHandler): void { + eventListeners.get(event)?.delete(handler); + }, + emit(event: string, data: unknown): void { + const set = eventListeners.get(event); + if (!set) return; + for (const handler of set) { + try { handler(data); } + catch (err) { console.error(`[plugin-event] ${event}:`, err); } + } + }, +}; + +// ── Command registry ───────────────────────────────────────────────────────── + +function addPluginCommand(pluginId: string, label: string, callback: () => void): void { + commands = [...commands, { pluginId, label, callback }]; +} + +function removePluginCommands(pluginId: string): void { + commands = commands.filter(c => c.pluginId !== pluginId); +} + +// Wire up registries to plugin-host +setPluginRegistries({ + addCommand: addPluginCommand, + removeCommands: removePluginCommands, + eventBus: pluginEventBus, +}); + +// ── Public API ─────────────────────────────────────────────────────────────── + +/** Discover plugins from ~/.config/agor/plugins/ via RPC. */ +export async function discoverPlugins(): Promise { + try { + const res = await appRpc.request['plugin.discover']({}); + discovered = res.plugins ?? []; + return discovered; + } catch (err) { + console.error('[plugin-store] discover error:', err); + discovered = []; + return []; + } +} + +/** Load a discovered plugin by id. */ +export async function loadPluginById(pluginId: string): Promise { + const meta = discovered.find(p => p.id === pluginId); + if (!meta) throw new Error(`Plugin not found: ${pluginId}`); + await loadPlugin(meta); +} + +/** Unload a plugin by id. */ +export function unloadPluginById(pluginId: string): void { + unloadPlugin(pluginId); + removePluginCommands(pluginId); +} + +/** Load all discovered plugins. */ +export async function loadAllPlugins(): Promise { + const plugins = await discoverPlugins(); + for (const meta of plugins) { + try { + await loadPlugin(meta); + } catch (err) { + console.error(`[plugin-store] Failed to load '${meta.id}':`, err); + } + } +} + +/** Unload all plugins. */ +export function unloadAll(): void { + unloadAllPlugins(); + commands = []; +} + +/** Get discovered plugins (reactive). */ +export function getDiscoveredPlugins(): PluginMeta[] { + return discovered; +} + +/** Get registered commands (reactive, for palette integration). */ +export function getPluginCommands(): PluginCommand[] { + return commands; +} + +/** Emit an event to all plugins listening for it. */ +export function emitPluginEvent(event: string, data: unknown): void { + pluginEventBus.emit(event, data); +} diff --git a/ui-electrobun/src/shared/pty-rpc-schema.ts b/ui-electrobun/src/shared/pty-rpc-schema.ts index 44c888f..ca256c1 100644 --- a/ui-electrobun/src/shared/pty-rpc-schema.ts +++ b/ui-electrobun/src/shared/pty-rpc-schema.ts @@ -90,6 +90,36 @@ export type PtyRPCRequests = { response: { ok: boolean }; }; + // ── File I/O RPC ────────────────────────────────────────────────────────── + + /** List directory children (files + subdirs). Returns sorted entries. */ + "files.list": { + params: { path: string }; + response: { + entries: Array<{ + name: string; + type: "file" | "dir"; + size: number; + }>; + error?: string; + }; + }; + /** Read a file's content. Returns text for text files, base64 for binary. */ + "files.read": { + params: { path: string }; + response: { + content?: string; + encoding: "utf8" | "base64"; + size: number; + error?: string; + }; + }; + /** Write text content to a file. */ + "files.write": { + params: { path: string; content: string }; + response: { ok: boolean; error?: string }; + }; + // ── Groups RPC ───────────────────────────────────────────────────────────── /** Return all project groups. */ @@ -190,6 +220,268 @@ export type PtyRPCRequests = { }>; }; }; + + // ── Session persistence RPC ──────────────────────────────────────────── + + /** Save/update a session record. */ + "session.save": { + params: { + projectId: string; sessionId: string; provider: string; + status: string; costUsd: number; inputTokens: number; + outputTokens: number; model: string; error?: string; + createdAt: number; updatedAt: number; + }; + response: { ok: boolean }; + }; + /** Load the most recent session for a project. */ + "session.load": { + params: { projectId: string }; + response: { + session: { + projectId: string; sessionId: string; provider: string; + status: string; costUsd: number; inputTokens: number; + outputTokens: number; model: string; error?: string; + createdAt: number; updatedAt: number; + } | null; + }; + }; + /** List sessions for a project (max 20). */ + "session.list": { + params: { projectId: string }; + response: { + sessions: Array<{ + projectId: string; sessionId: string; provider: string; + status: string; costUsd: number; inputTokens: number; + outputTokens: number; model: string; error?: string; + createdAt: number; updatedAt: number; + }>; + }; + }; + /** Save agent messages (batch). */ + "session.messages.save": { + params: { + messages: Array<{ + sessionId: string; msgId: string; role: string; content: string; + toolName?: string; toolInput?: string; timestamp: number; + costUsd?: number; inputTokens?: number; outputTokens?: number; + }>; + }; + response: { ok: boolean }; + }; + /** Load all messages for a session. */ + "session.messages.load": { + params: { sessionId: string }; + response: { + messages: Array<{ + sessionId: string; msgId: string; role: string; content: string; + toolName?: string; toolInput?: string; timestamp: number; + costUsd: number; inputTokens: number; outputTokens: number; + }>; + }; + }; + + // ── btmsg RPC ────────────────────────────────────────────────────────── + + /** Register an agent in btmsg. */ + "btmsg.registerAgent": { + params: { + id: string; name: string; role: string; + groupId: string; tier: number; model?: string; + }; + response: { ok: boolean }; + }; + /** List agents for a group. */ + "btmsg.getAgents": { + params: { groupId: string }; + response: { + agents: Array<{ + id: string; name: string; role: string; groupId: string; + tier: number; model: string | null; status: string; unreadCount: number; + }>; + }; + }; + /** Send a direct message between agents. */ + "btmsg.sendMessage": { + params: { fromAgent: string; toAgent: string; content: string }; + response: { ok: boolean; messageId?: string; error?: string }; + }; + /** Get message history between two agents. */ + "btmsg.listMessages": { + params: { agentId: string; otherId: string; limit?: number }; + response: { + messages: Array<{ + id: string; fromAgent: string; toAgent: string; content: string; + read: boolean; replyTo: string | null; createdAt: string; + senderName: string | null; senderRole: string | null; + }>; + }; + }; + /** Mark messages as read. */ + "btmsg.markRead": { + params: { agentId: string; messageIds: string[] }; + response: { ok: boolean }; + }; + /** List channels for a group. */ + "btmsg.listChannels": { + params: { groupId: string }; + response: { + channels: Array<{ + id: string; name: string; groupId: string; createdBy: string; + memberCount: number; createdAt: string; + }>; + }; + }; + /** Create a channel. */ + "btmsg.createChannel": { + params: { name: string; groupId: string; createdBy: string }; + response: { ok: boolean; channelId?: string }; + }; + /** Get channel messages. */ + "btmsg.getChannelMessages": { + params: { channelId: string; limit?: number }; + response: { + messages: Array<{ + id: string; channelId: string; fromAgent: string; content: string; + createdAt: string; senderName: string; senderRole: string; + }>; + }; + }; + /** Send a channel message. */ + "btmsg.sendChannelMessage": { + params: { channelId: string; fromAgent: string; content: string }; + response: { ok: boolean; messageId?: string }; + }; + /** Record agent heartbeat. */ + "btmsg.heartbeat": { + params: { agentId: string }; + response: { ok: boolean }; + }; + /** Get dead letter queue entries. */ + "btmsg.getDeadLetters": { + params: { limit?: number }; + response: { + letters: Array<{ + id: number; fromAgent: string; toAgent: string; + content: string; error: string; createdAt: string; + }>; + }; + }; + /** Log an audit event. */ + "btmsg.logAudit": { + params: { agentId: string; eventType: string; detail: string }; + response: { ok: boolean }; + }; + /** Get audit log. */ + "btmsg.getAuditLog": { + params: { limit?: number }; + response: { + entries: Array<{ + id: number; agentId: string; eventType: string; + detail: string; createdAt: string; + }>; + }; + }; + + // ── bttask RPC ───────────────────────────────────────────────────────── + + /** List tasks for a group. */ + "bttask.listTasks": { + params: { groupId: string }; + response: { + tasks: Array<{ + id: string; title: string; description: string; status: string; + priority: string; assignedTo: string | null; createdBy: string; + groupId: string; parentTaskId: string | null; sortOrder: number; + createdAt: string; updatedAt: string; version: number; + }>; + }; + }; + /** Create a task. */ + "bttask.createTask": { + params: { + title: string; description: string; priority: string; + groupId: string; createdBy: string; assignedTo?: string; + }; + response: { ok: boolean; taskId?: string; error?: string }; + }; + /** Update task status with optimistic locking. */ + "bttask.updateTaskStatus": { + params: { taskId: string; status: string; expectedVersion: number }; + response: { ok: boolean; newVersion?: number; error?: string }; + }; + /** Delete a task. */ + "bttask.deleteTask": { + params: { taskId: string }; + response: { ok: boolean }; + }; + /** Add a comment to a task. */ + "bttask.addComment": { + params: { taskId: string; agentId: string; content: string }; + response: { ok: boolean; commentId?: string }; + }; + /** List comments for a task. */ + "bttask.listComments": { + params: { taskId: string }; + response: { + comments: Array<{ + id: string; taskId: string; agentId: string; + content: string; createdAt: string; + }>; + }; + }; + /** Count tasks in 'review' status. */ + "bttask.reviewQueueCount": { + params: { groupId: string }; + response: { count: number }; + }; + + // ── Search RPC ────────────────────────────────────────────────────────── + + /** Full-text search across messages, tasks, and btmsg. */ + "search.query": { + params: { query: string; limit?: number }; + response: { + results: Array<{ + resultType: string; + id: string; + title: string; + snippet: string; + score: number; + }>; + }; + }; + /** Index a message for search. */ + "search.indexMessage": { + params: { sessionId: string; role: string; content: string }; + response: { ok: boolean }; + }; + /** Rebuild the entire search index. */ + "search.rebuild": { + params: Record; + response: { ok: boolean }; + }; + + // ── Plugin RPC ────────────────────────────────────────────────────────── + + /** Discover plugins from ~/.config/agor/plugins/. */ + "plugin.discover": { + params: Record; + response: { + plugins: Array<{ + id: string; + name: string; + version: string; + description: string; + main: string; + permissions: string[]; + }>; + }; + }; + /** Read a plugin file (path-traversal-safe). */ + "plugin.readFile": { + params: { pluginId: string; filePath: string }; + response: { ok: boolean; content: string; error?: string }; + }; }; // ── Messages (Bun → WebView, fire-and-forget) ────────────────────────────────