diff --git a/CLAUDE.md b/CLAUDE.md index c5eab9cc..99a3b200 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,9 +17,9 @@ Este projeto é implementação própria. Não copie nem adapte código, testes, ``` crates/botloft-core tipos de domínio, IDs, envelope, tipos do protocolo (exportados via ts-rs) crates/botloft-store SQLite, migrations em migrations/NNNN_nome.sql -crates/botloftd daemon: rpc, supervisor, runtime, terminal, courier, tools, hooks, platform -app/ Tauri v2 + React 19 + TS strict + Tailwind v4 + Zustand + xterm.js -docs/ spec e ADRs +crates/botloftd daemon: rpc, supervisor, runtime, chat, courier, tools, platform +app/ Tauri v2 + React 19 + TS strict + Tailwind v4 + Zustand +docs/ spec e ADRs (docs/adr/0001: bots headless com chat) ``` ## Comandos @@ -50,7 +50,7 @@ Em dev, use `$env:BOTLOFT_HOME = "$PWD\.dev\home"` (caminho absoluto) no daemon - Código, identificadores, comentários, commits e README em **inglês**. Docs em `docs/` podem ser em português. - Arquivos com no máximo **~300 linhas**. Passou disso, divida por responsabilidade. - Erros: `thiserror` nos crates de biblioteca, `anyhow` só no binário. -- IDs são ULID com prefixo (`crw_`, `bot_`, `msg_`, `dlv_`, `tsk_`). Tempo em ms Unix. +- IDs são ULID com prefixo (`crw_`, `bot_`, `msg_`, `dlv_`, `tsk_`, `cht_`, `apr_`, `att_`). Tempo em ms Unix. - Tudo que é específico de Windows fica em `crates/botloftd/src/platform/`. O resto do daemon não chama Win32 direto. - A UI depende só da interface `BotloftApi` (`app/src/lib/api.ts`). Testes de componente usam `FakeBotloft`. - Nunca edite `app/src/lib/protocol.gen.ts` à mão. @@ -58,15 +58,13 @@ Em dev, use `$env:BOTLOFT_HOME = "$PWD\.dev\home"` (caminho absoluto) no daemon ## Windows: armadilhas conhecidas -- O inbox do Claude Code no Windows é um **named pipe** criado pelo próprio Claude Code. O daemon é cliente. A primeira linha tem que ser o auth com `CLAUDE_CODE_MESSAGING_TOKEN`, senão a conexão é descartada. Abra o pipe só com a mensagem pronta. -- Hooks usam **exec form** (`"args": [...]`) chamando `botloftd.exe hook `. Nada de hook em bash ou PowerShell. O subcomando sai sempre com 0 e nunca escreve no stdout. -- Só o `claude.exe` nativo roda. O `claude.cmd` do npm é recusado: passar por `cmd.exe` estraga o quoting na PTY. -- ConPTY não tem fd para `poll`: leitura da PTY em thread dedicada + canal. Ao fim do processo, feche o pseudoconsole, senão a leitura nunca vê EOF. -- O ConPTY do `portable-pty` (com `PSEUDOCONSOLE_INHERIT_CURSOR`) manda `ESC[6n` ao iniciar e trava o filho até a resposta. O daemon responde `ESC[1;1R` à primeira consulta. +- Cada bot é `claude -p` com `stream-json` no stdin e no stdout (spec 7.4, 8, 9.2). O processo fica calado até a primeira mensagem; fechar o stdin o encerra. O formato de entrada não é documentado: qualquer mudança passa por teste real e vai para a seção 19. +- Bots rodam com `--setting-sources project,local` e `--strict-mcp-config`: nada de hooks, skills, modo de permissão ou MCP pessoais do dono. Aprovações passam pela tool `permission_prompt` do nosso MCP. +- Servidor MCP por HTTP: o Claude Code aborta a request em 60 s e a chamada em 5 min sem resposta, a menos que o `mcp.json` tenha `timeout` no servidor. A aprovação depende disso. +- Só o `claude.exe` nativo roda. O `claude.cmd` do npm é recusado: passar por `cmd.exe` estraga o quoting dos argumentos. - O bot recebe o ambiente padrão do usuário (`CreateEnvironmentBlock`), nunca o do daemon: em dev o daemon herda variáveis da sessão do Claude Code (`CLAUDE_CODE_MESSAGING_SOCKET`, `ANTHROPIC_BASE_URL`...). - PATH real pode ter aspas soltas. Separe só por `;`; `std::env::split_paths` trata aspas como agrupamento e engole o resto. -- Numa sessão interativa, os hooks só rodam depois de aceitar a confiança da pasta. Teste manual de bot novo começa pelo diálogo de confiança no terminal. -- Todo processo de bot entra num Job Object com kill-on-close. +- Todo processo de bot entra num Job Object com kill-on-close e roda sem console (`CREATE_NO_WINDOW`). - Permissão de arquivo é ACL (SID do usuário atual), não modo Unix. - Estado em `%LOCALAPPDATA%`, nunca `%APPDATA%` (roaming). - Cuidado com o limite de 260 caracteres em caminhos de workspace. @@ -74,7 +72,7 @@ Em dev, use `$env:BOTLOFT_HOME = "$PWD\.dev\home"` (caminho absoluto) no daemon ## Segurança e privacidade - Daemon só em `127.0.0.1`. -- Nunca logar token, corpo de mensagem ou saída de terminal em nível `info` ou acima. Bots podem lidar com dado pessoal e de saúde. +- Nunca logar token, corpo de mensagem, anexo ou item do chat (resposta, saída de ferramenta) em nível `info` ou acima. Bots podem lidar com dado pessoal e de saúde. - Tokens de bot ficam no banco só como SHA-256. - Nada de telemetria. @@ -83,10 +81,10 @@ Em dev, use `$env:BOTLOFT_HOME = "$PWD\.dev\home"` (caminho absoluto) no daemon Uma tarefa só está pronta quando: 1. `cargo fmt`, `clippy -D warnings` e `cargo test` passam (e `pnpm check`, se mexeu no app); -2. comportamento novo tem teste (supervisor, courier e terminal testados com `FakeRuntime`); +2. comportamento novo tem teste (supervisor, courier e chat testados com `FakeRuntime`); 3. a spec continua verdadeira; 4. o que depende do Claude Code real está listado no PR com o passo de teste manual. ## Ordem de trabalho -Siga os marcos da spec (seção 17): M0 -> M1 -> M2 -> M3 -> M4 -> M5. Não comece rotinas, caixa de perguntas, busca ou acesso remoto antes do M5. +Siga os marcos da spec (seção 17): M0 -> M1 -> M2 -> M3 -> M4 -> M4.1 -> M5. Não comece rotinas, caixa de perguntas, busca ou acesso remoto antes do M5. diff --git a/Cargo.lock b/Cargo.lock index 98ed8371..411b6a79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -292,6 +292,7 @@ version = "0.1.0" dependencies = [ "botloft-core", "rusqlite", + "serde_json", "tempfile", "thiserror 2.0.21", ] @@ -311,7 +312,6 @@ dependencies = [ "futures-util", "getrandom 0.4.3", "hex", - "portable-pty", "serde", "serde_json", "sha2 0.11.0", @@ -494,12 +494,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "chacha20" version = "0.10.2" @@ -946,12 +940,6 @@ dependencies = [ "tendril", ] -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - [[package]] name = "dpi" version = "0.1.2" @@ -999,7 +987,7 @@ dependencies = [ "rustc_version", "toml 1.1.6+spec-1.1.0", "vswhom", - "winreg 0.55.0", + "winreg", ] [[package]] @@ -1072,17 +1060,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] - [[package]] name = "find-msvc-tools" version = "0.1.14" @@ -2255,18 +2232,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags 2.13.2", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2700,27 +2665,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "portable-pty" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "downcast-rs", - "filedescriptor", - "lazy_static", - "libc", - "log", - "nix", - "serial2", - "shared_library", - "shell-words", - "winapi", - "winreg 0.10.1", -] - [[package]] name = "potential_utf" version = "0.1.6" @@ -3308,17 +3252,6 @@ dependencies = [ "syn 3.0.6", ] -[[package]] -name = "serial2" -version = "0.2.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16809bc35793b19ce4e0c53924bc0dce3937f15487997cfdaed936004180730" -dependencies = [ - "cfg-if", - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "serialize-to-javascript" version = "0.1.2" @@ -3403,22 +3336,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shared_library" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" -dependencies = [ - "lazy_static", - "libc", -] - -[[package]] -name = "shell-words" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" - [[package]] name = "shlex" version = "2.0.1" @@ -5078,15 +4995,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "winreg" version = "0.55.0" diff --git a/app/package.json b/app/package.json index 06cbe98d..8099a096 100644 --- a/app/package.json +++ b/app/package.json @@ -19,12 +19,11 @@ "@fontsource/ibm-plex-mono": "^5.3.0", "@fontsource/ibm-plex-sans": "^5.3.0", "@tauri-apps/api": "^2.12.0", - "@xterm/addon-fit": "^0.11.0", - "@xterm/addon-webgl": "^0.19.0", - "@xterm/xterm": "^6.0.0", "lucide-react": "^1.48.0", "react": "^19.3.0", "react-dom": "^19.3.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", "zustand": "^5.0.15" }, "devDependencies": { diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 6da6a843..080c32e9 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -175,15 +175,6 @@ importers: '@tauri-apps/api': specifier: ^2.12.0 version: 2.12.0 - '@xterm/addon-fit': - specifier: ^0.11.0 - version: 0.11.0 - '@xterm/addon-webgl': - specifier: ^0.19.0 - version: 0.19.0 - '@xterm/xterm': - specifier: ^6.0.0 - version: 6.0.0 lucide-react: specifier: ^1.48.0 version: 1.48.0(react@19.3.0) @@ -193,6 +184,12 @@ importers: react-dom: specifier: ^19.3.0 version: 19.3.0(react@19.3.0) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.3.0)(react@19.3.0) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 zustand: specifier: ^5.0.15 version: 5.0.15(@types/react@19.3.0)(react@19.3.0) @@ -690,12 +687,27 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@24.19.0': resolution: {integrity: sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==} @@ -707,6 +719,12 @@ packages: '@types/react@19.3.0': resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} engines: {node: '>=16.20.0'} @@ -827,6 +845,9 @@ packages: cpu: [x64] os: [win32] + '@ungap/structured-clone@1.4.0': + resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} + '@vitejs/plugin-react@6.1.1': resolution: {integrity: sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -857,15 +878,6 @@ packages: '@vitest/spy@5.0.2': resolution: {integrity: sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==} - '@xterm/addon-fit@0.11.0': - resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} - - '@xterm/addon-webgl@0.19.0': - resolution: {integrity: sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A==} - - '@xterm/xterm@6.0.0': - resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -881,13 +893,34 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + bidi-js@1.1.0: resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + css-tree@3.2.1: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -899,9 +932,21 @@ packages: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -910,6 +955,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -924,6 +972,13 @@ packages: es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -931,6 +986,9 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -948,10 +1006,38 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + html-encoding-sniffer@7.0.0: resolution: {integrity: sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==} engines: {node: ^22.13.0 || >=24.0.0} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -1119,6 +1205,9 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lru-cache@11.5.3: resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} engines: {node: 20 || >=22} @@ -1138,9 +1227,147 @@ packages: magic-string@1.4.2: resolution: {integrity: sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.1: + resolution: {integrity: sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.3: + resolution: {integrity: sha512-wgyJtgUkUcdU7zci7uuwc/tzoAhm0TswWhaXuvnWolqug+8jY7bgNkBBL0dYBnHgc/tHL7lCzRdPsdUxUKKBqw==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + micromark-core-commonmark@2.0.4: + resolution: {integrity: sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.2: + resolution: {integrity: sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.1.0: + resolution: {integrity: sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-edit-map@1.0.0: + resolution: {integrity: sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.3: + resolution: {integrity: sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==} + + micromark@4.0.3: + resolution: {integrity: sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.19: resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1150,6 +1377,9 @@ packages: resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} engines: {node: '>=12.20.0'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} @@ -1168,6 +1398,9 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1180,10 +1413,28 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react@19.3.0: resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} engines: {node: '>=0.10.0'} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1204,9 +1455,21 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + tailwindcss@4.3.3: resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} @@ -1241,6 +1504,12 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} @@ -1253,6 +1522,30 @@ packages: resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==} engines: {node: '>=22.19.0'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@8.3.1: resolution: {integrity: sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1387,6 +1680,9 @@ packages: use-sync-external-store: optional: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@asamuzakjp/css-color@7.1.2': @@ -1696,10 +1992,28 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + '@types/estree@1.0.9': {} + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@24.19.0': dependencies: undici-types: 7.24.6 @@ -1712,6 +2026,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -1772,6 +2090,8 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true + '@ungap/structured-clone@1.4.0': {} + '@vitejs/plugin-react@6.1.1(vite@8.3.1(@types/node@24.19.0)(jiti@2.7.0))': dependencies: '@rolldown/pluginutils': 1.0.1 @@ -1788,12 +2108,6 @@ snapshots: '@vitest/spy@5.0.2': {} - '@xterm/addon-fit@0.11.0': {} - - '@xterm/addon-webgl@0.19.0': {} - - '@xterm/xterm@6.0.0': {} - ansi-regex@5.0.1: {} ansi-styles@5.2.0: {} @@ -1804,12 +2118,26 @@ snapshots: assertion-error@2.0.1: {} + bail@2.0.2: {} + bidi-js@1.1.0: dependencies: require-from-string: 2.0.2 + ccount@2.0.1: {} + chai@6.2.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + comma-separated-tokens@2.0.3: {} + css-tree@3.2.1: dependencies: mdn-data: 2.27.1 @@ -1824,12 +2152,24 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + debug@4.4.3: + dependencies: + ms: 2.1.3 + decimal.js@10.6.0: {} + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + dequal@2.0.3: {} detect-libc@2.1.2: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + dom-accessibility-api@0.5.16: {} enhanced-resolve@5.25.1: @@ -1841,12 +2181,18 @@ snapshots: es-module-lexer@2.3.2: {} + escape-string-regexp@5.0.0: {} + + estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 expect-type@1.4.0: {} + extend@3.0.2: {} + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 @@ -1856,12 +2202,53 @@ snapshots: graceful-fs@4.2.11: {} + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + html-encoding-sniffer@7.0.0: dependencies: '@exodus/bytes': 1.16.0 transitivePeerDependencies: - '@noble/hashes' + html-url-attributes@3.0.1: {} + + inline-style-parser@0.2.7: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-decimal@2.0.1: {} + + is-hexadecimal@2.0.1: {} + + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} jiti@2.7.0: {} @@ -1991,6 +2378,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 + longest-streak@3.1.0: {} + lru-cache@11.5.3: {} lucide-react@1.48.0(react@19.3.0): @@ -2007,12 +2396,378 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.3 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.3 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.3 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.3 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.3 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.1 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.3 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.3 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.4.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-character: 2.1.1 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdn-data@2.27.1: {} + micromark-core-commonmark@2.0.4: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.1.0 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-edit-map: 1.0.0 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.4 + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm-table@2.1.2: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.3 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.2 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-factory-space@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.3 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-edit-map@1.0.0: + dependencies: + micromark-util-types: 2.0.3 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.3 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.3: {} + + micromark@4.0.3: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.4 + micromark-factory-space: 2.1.0 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-edit-map: 1.0.0 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.3 + transitivePeerDependencies: + - supports-color + + ms@2.1.3: {} + nanoid@3.3.19: {} obug@2.2.1: {} + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse5@8.0.1: dependencies: entities: 8.1.0 @@ -2033,6 +2788,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + property-information@7.2.0: {} + punycode@2.3.1: {} react-dom@19.3.0(react@19.3.0): @@ -2042,8 +2799,60 @@ snapshots: react-is@17.0.2: {} + react-markdown@10.1.0(@types/react@19.3.0)(react@19.3.0): + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/react': 19.3.0 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.3.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react@19.3.0: {} + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.3 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.3 + unified: 11.0.5 + require-from-string@2.0.2: {} rolldown@1.2.11: @@ -2075,8 +2884,23 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + std-env@4.2.0: {} + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + tailwindcss@4.3.3: {} tapable@2.3.3: {} @@ -2104,6 +2928,10 @@ snapshots: dependencies: punycode: 2.3.1 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -2131,6 +2959,49 @@ snapshots: undici@8.11.2: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite@8.3.1(@types/node@24.19.0)(jiti@2.7.0): dependencies: lightningcss: 1.33.0 @@ -2199,3 +3070,5 @@ snapshots: optionalDependencies: '@types/react': 19.3.0 react: 19.3.0 + + zwitch@2.0.4: {} diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 3797bffd..449c1646 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -51,6 +51,35 @@ async fn open_path(path: String) -> Result<(), String> { .await } +/// Whether `url` is a plain web link, safe to hand to the default browser. +fn is_web_link(url: &str) -> bool { + let rest = url + .strip_prefix("https://") + .or_else(|| url.strip_prefix("http://")); + rest.is_some_and(|rest| !rest.is_empty()) + && url.len() <= 2048 + && !url + .chars() + .any(|ch| ch.is_whitespace() || ch.is_control() || ch == '"') +} + +/// Opens a web link from a bot's reply in the default browser. Only +/// `http` and `https`: anything else could start a program. +#[tauri::command] +async fn open_url(url: String) -> Result<(), String> { + if !is_web_link(&url) { + return Err("only web links can be opened".into()); + } + blocking(move || { + std::process::Command::new("explorer.exe") + .arg(&url) + .spawn() + .map(drop) + .map_err(|err| format!("cannot open the browser: {err}")) + }) + .await +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -58,8 +87,30 @@ pub fn run() { daemon_status, daemon_start, read_owner_token, - open_path + open_path, + open_url ]) .run(tauri::generate_context!()) .expect("error while running the Botloft app"); } + +#[cfg(test)] +mod tests { + use super::is_web_link; + + #[test] + fn only_plain_web_links_open() { + assert!(is_web_link("https://code.claude.com/docs")); + assert!(is_web_link("http://127.0.0.1:3000/page?a=1&b=2")); + for url in [ + "file:///C:/Windows/System32/calc.exe", + r"C:\Windows\notepad.exe", + "https://", + "https://a b", + "https://a\"b", + "ms-settings:privacy", + ] { + assert!(!is_web_link(url), "{url}"); + } + } +} diff --git a/app/src/App.test.tsx b/app/src/App.test.tsx index d989b281..e8aff173 100644 --- a/app/src/App.test.tsx +++ b/app/src/App.test.tsx @@ -1,15 +1,10 @@ import { act, cleanup, fireEvent, render, screen, within } from "@testing-library/react"; -import { afterEach, describe, expect, test, vi } from "vitest"; +import { afterEach, describe, expect, test } from "vitest"; import { App } from "./App"; import type { Client } from "./lib/client"; import { FakeBotloft } from "./lib/fake"; import { FakeHost } from "./lib/fakeHost"; -// xterm.js needs a real browser; the terminal has its own tests. -vi.mock("./features/terminal/TerminalView", () => ({ - TerminalView: ({ botId }: { botId: string }) =>
, -})); - afterEach(cleanup); function renderApp(fake = new FakeBotloft(), host = new FakeHost()) { @@ -55,9 +50,10 @@ describe("app", () => { expect(within(view).getByText("Starting")).toBeDefined(); const bot = [...fake.bots.values()][0]; act(() => fake.setBotState(bot?.id ?? "", "needs_approval")); - // The badge, and a callout that says what to do. - expect(within(view).getAllByText("Needs approval")).toHaveLength(2); - expect(within(view).getByRole("alert").textContent).toContain("permission prompt"); + // The request itself shows in the chat; the header says why it waits. + expect(within(view).getByText("Needs approval")).toBeDefined(); + act(() => fake.setBotState(bot?.id ?? "", "auth_error")); + expect(within(view).getByRole("alert").textContent).toContain("not signed in"); }); test("the daemon's validation errors stay in the form", async () => { @@ -130,51 +126,31 @@ describe("app", () => { act(() => fake.setConnection({ kind: "waiting", retryAt: 0 })); expect(screen.getByText("Reconnecting…")).toBeDefined(); }); - test("a bot's view shows its terminal, and details in a second tab", async () => { + test("a bot opens on its chat, with its details in a side panel", async () => { const fake = new FakeBotloft(); const crew = fake.addCrew("Ops"); const scout = fake.addBot(crew.id, "Scout", "Finds sources"); renderApp(fake); await screen.findByRole("heading", { level: 1, name: "Ops" }); fireEvent.click(within(sidebar()).getByRole("button", { name: /Scout/ })); - expect(screen.getByTestId(`terminal-${scout.id}`)).toBeDefined(); - fireEvent.click(screen.getByRole("tab", { name: "Details" })); - expect(screen.getByText(scout.workspace)).toBeDefined(); - // The terminal stays mounted behind the other tab. - expect(screen.getByTestId(`terminal-${scout.id}`)).toBeDefined(); - }); - - test("a start that takes long points at the trust prompt", async () => { - const fake = new FakeBotloft(); - const crew = fake.addCrew("Ops"); - const scout = fake.addBot(crew.id, "Scout"); - renderApp(fake); - await screen.findByRole("heading", { level: 1, name: "Ops" }); - fireEvent.click(within(sidebar()).getByRole("button", { name: /Scout/ })); - vi.useFakeTimers(); - try { - act(() => fake.setBotState(scout.id, "launching", 2)); - expect(screen.queryByText("Still starting")).toBeNull(); - act(() => vi.advanceTimersByTime(4000)); - expect(screen.getByText("Still starting")).toBeDefined(); - act(() => fake.setBotState(scout.id, "idle")); - expect(screen.queryByText("Still starting")).toBeNull(); - } finally { - vi.useRealTimers(); - } + expect(await screen.findByText("Start a conversation with Scout")).toBeDefined(); + expect(screen.queryByText(scout.workspace)).toBeNull(); + fireEvent.click(screen.getByRole("button", { name: "Show details" })); + const details = screen.getByRole("complementary", { name: "About Scout" }); + expect(within(details).getByText(scout.workspace)).toBeDefined(); + fireEvent.click(within(details).getByRole("button", { name: "Close details" })); + expect(screen.queryByRole("complementary", { name: "About Scout" })).toBeNull(); }); - test("a bot that never started says how to start it", async () => { + test("a paused bot still takes messages, and says they wait", async () => { const fake = new FakeBotloft(); const crew = fake.addCrew("Ops"); await fake.call("crews.setPaused", { crewId: crew.id, paused: true }); - const scout = fake.addBot(crew.id, "Scout"); + fake.addBot(crew.id, "Scout"); renderApp(fake); await screen.findByRole("heading", { level: 1, name: "Ops" }); fireEvent.click(within(sidebar()).getByRole("button", { name: /Scout/ })); - expect(screen.getByText(/has not started since the daemon did/).textContent).toContain( - "Resume it to start it.", - ); - expect(screen.queryByTestId(`terminal-${scout.id}`)).toBeNull(); + expect(await screen.findByText(/What you send waits until it runs again/)).toBeDefined(); + expect(screen.getByLabelText("Message to Scout")).toBeDefined(); }); }); diff --git a/app/src/dev/preview.ts b/app/src/dev/preview.ts index ed2469f9..99a897e9 100644 --- a/app/src/dev/preview.ts +++ b/app/src/dev/preview.ts @@ -10,6 +10,7 @@ import { FakeBotloft } from "../lib/fake"; import { FakeHost } from "../lib/fakeHost"; import type { Host } from "../lib/host"; import { PROTOCOL_VERSION } from "../lib/protocol.gen"; +import { seedChats } from "./seedChats"; export function previewProps(search: string): { host: Host; connect: Connect } { const params = new URLSearchParams(search); @@ -59,9 +60,12 @@ function seed(fake: FakeBotloft): void { fake.setBotState(writer.id, "busy", 2); fake.setBotState(reviewer.id, "needs_approval", 5); fake.setBotState(analyst.id, "rate_limited", 1); - fake.terminals.output(scout.id, SCOUT_SCREEN); - fake.terminals.output(reviewer.id, PROMPT_SCREEN); - conversation(fake, scout.id, writer.id, reviewer.id); + seedChats(fake, { + scout: scout.id, + writer: writer.id, + reviewer: reviewer.id, + analyst: analyst.id, + }); const ops = fake.addCrew("Ops"); const deploy = fake.addBot(ops.id, "Deploy", "Ships the site on Fridays"); const watcher = fake.addBot(ops.id, "Watcher", "Keeps an eye on the error log"); @@ -69,83 +73,3 @@ function seed(fake: FakeBotloft): void { fake.setBotState(deploy.id, "offline"); fake.setBotState(watcher.id, "offline"); } - -/** A morning's worth of messages and tasks between three bots. */ -function conversation(fake: FakeBotloft, scout: string, writer: string, reviewer: string): void { - const talk = fake.conversation; - void fake.call("messages.send", { - botId: scout, - body: "Summarize the three newest papers in shared/inbox.", - }); - const owner = [...talk.deliveries.values()].at(-1); - if (owner) { - talk.deliver(owner.id, "sent"); - } - const summary = talk.task(scout, writer, { - status: "done", - result: "Draft in shared/drafts/week-39.md, 2 pages.", - }); - const asked = talk.say({ - from: scout, - to: writer, - kind: "task", - taskId: summary.id, - body: "Turn shared/summaries/week-39.md into this week's report draft.", - }); - talk.deliver(asked.delivery.id, "sent"); - const done = talk.say({ - from: writer, - to: scout, - kind: "result", - taskId: summary.id, - body: "Draft in shared/drafts/week-39.md, 2 pages.", - }); - talk.deliver(done.delivery.id, "sent"); - const review = talk.task(writer, reviewer, { hops: 2 }); - const check = talk.say({ - from: writer, - to: reviewer, - kind: "task", - taskId: review.id, - body: "Check the numbers in section 2 against the sources.", - }); - talk.deliver(check.delivery.id, "sent"); - const late = talk.say({ - from: scout, - to: reviewer, - body: "Two of the links in the draft moved; the new ones are in shared/links.md.", - }); - talk.deliver(late.delivery.id, "dead", "the bot's inbox did not answer in 10 s"); -} - -const ESC = String.fromCharCode(27); -const NEWLINE = String.fromCharCode(13, 10); -const dim = (text: string) => `${ESC}[2m${text}${ESC}[0m`; -const accent = (text: string) => `${ESC}[38;5;209m${text}${ESC}[0m`; -const bold = (text: string) => `${ESC}[1m${text}${ESC}[0m`; - -const SCOUT_SCREEN = [ - `${bold("scout")} ${dim("· research")}`, - "", - `${accent(">")} Summarize the three newest papers in shared/inbox`, - "", - `${accent("●")} Reading shared/inbox/2026-09-27-retrieval.pdf`, - `${accent("●")} Reading shared/inbox/2026-09-26-agents.pdf`, - `${accent("●")} Wrote shared/summaries/week-39.md ${dim("(42 lines)")}`, - "", - "Done. The summary is in shared/summaries/week-39.md.", - "", - `${accent(">")} `, -].join(NEWLINE); - -const PROMPT_SCREEN = [ - `${bold("revisao")} ${dim("· research")}`, - "", - `${accent("●")} Checking the claims in shared/summaries/week-39.md`, - "", - `${ESC}[33mAllow this command?${ESC}[0m npm run check-links`, - " 1. Yes", - " 2. Yes, and don't ask again for this command", - " 3. No", - "", -].join(NEWLINE); diff --git a/app/src/dev/seedChats.ts b/app/src/dev/seedChats.ts new file mode 100644 index 00000000..047ef34e --- /dev/null +++ b/app/src/dev/seedChats.ts @@ -0,0 +1,128 @@ +// Dev only: a morning's work between four research bots, for the preview. + +import type { FakeBotloft } from "../lib/fake"; +import type { BotId } from "../lib/protocol.gen"; + +/** A small bar chart, as a PNG the owner "attached". */ +const CHART_PNG = + "iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAmElEQVR42u3bQQmAMBiA0RWRtRFb7OxpEQxjFUvYQlgBG4z/MBDZgy/Bu3+ptUedEgJAgAABAvRFZ0nBAAECBAgQIECARpfLFQwQIECAAAECBAgQIECAAAECNAjo3nIwQIAAAQIECBAgQOOAlmMNBggQIECAAAECBAgQIECAAAECBAgQIECAAAGaEajWfdpcz65nQIAA/aoXK4mdiO7oCAwAAAAASUVORK5CYII="; + +const SUMMARY = `Done. The summary is in \`shared/summaries/week-39.md\`. + +**Highlights** + +1. *Retrieval at scale*: hybrid search beats dense-only by 7 points on long documents. +2. *Agents that plan*: writing the plan down first cut tool calls by a third. +3. *Small evals*: fixed test sets drift less than sampled ones. + +| Paper | Pages | Worth a deep read | +|---|---|---| +| Retrieval at scale | 14 | Yes | +| Agents that plan | 22 | Yes | +| Small evals | 9 | Maybe | + +Your chart matches the numbers in the first paper. I asked @writer to turn this into the weekly report.`; + +interface Crew { + scout: BotId; + writer: BotId; + reviewer: BotId; + analyst: BotId; +} + +const MINUTE = 60 * 1000; + +export function seedChats(fake: FakeBotloft, crew: Crew): void { + const { chat, conversation: talk } = fake; + const end = Date.now(); + const at = (minutesAgo: number) => { + fake.now = end - minutesAgo * MINUTE; + }; + + at(95); + void fake.call("messages.send", { + botId: crew.scout, + body: "Summarize the three newest papers in shared/inbox. Here's last week's chart for reference.", + attachments: [{ name: "chart-week-38.png", mediaType: "image/png", data: CHART_PNG }], + }); + const asked = fake.conversation.messages.at(-1); + if (asked) { + talk.read(talk.deliveryOf(asked.id).id); + } + at(94); + const files = [ + ["Glob", "shared/inbox/*.pdf"], + ["Read", "2026-09-27-retrieval.pdf"], + ["Read", "2026-09-26-agents.pdf"], + ["Read", "2026-09-25-evals.pdf"], + ["Write", "week-39.md"], + ] as const; + for (const [name, summary] of files) { + const call = chat.tool(crew.scout, name, { summary, input: JSON.stringify({ path: summary }) }); + chat.finish(call, name === "Write" ? "Wrote 42 lines" : "ok"); + } + at(92); + chat.reply(crew.scout, SUMMARY); + const handoff = chat.tool(crew.scout, "mcp__botloft__send_message", { + summary: "to @writer", + input: JSON.stringify({ to: "writer", kind: "task", body: "Turn week-39.md into the report" }), + }); + chat.finish(handoff, '{"ok":true}'); + + at(91); + const task = talk.task(crew.scout, crew.writer, { hops: 1 }); + const note = talk.say({ + from: crew.scout, + to: crew.writer, + kind: "task", + taskId: task.id, + body: "Turn shared/summaries/week-39.md into this week's report draft.", + }); + talk.read(note.delivery.id); + chat.turn(crew.scout); + at(90); + chat.finish(chat.tool(crew.writer, "Read", { summary: "week-39.md" }), "42 lines"); + chat.reply(crew.writer, "Starting the draft. I'll keep the three highlights as sections."); + chat.tool(crew.writer, "Write", { summary: "week-39-report.md" }); + + at(40); + const review = talk.task(crew.writer, crew.reviewer, { hops: 2 }); + const check = talk.say({ + from: crew.writer, + to: crew.reviewer, + kind: "task", + taskId: review.id, + body: "Check the numbers in section 2 against the sources.", + }); + talk.read(check.delivery.id); + at(39); + chat.reply(crew.reviewer, "I'll run the link checker first, then compare each figure."); + const command = JSON.stringify({ + command: "npm run check-links", + description: "Check the links", + }); + chat.tool(crew.reviewer, "Bash", { summary: "Check the links", input: command }); + chat.ask(crew.reviewer, "Bash", "npm run check-links", command); + at(12); + const late = talk.say({ + from: crew.scout, + to: crew.reviewer, + body: "Two of the links in the draft moved; the new ones are in shared/links.md.", + }); + talk.deliver(late.delivery.id, "dead", "the bot did not start in time"); + + at(30); + void fake.call("messages.send", { + botId: crew.analyst, + body: "Can you check the growth numbers in section 2?", + }); + at(29); + chat.finish(chat.tool(crew.analyst, "Read", { summary: "week-39-report.md" }), "118 lines"); + chat.add(crew.analyst, { + kind: "notice", + level: "warning", + text: "The account reached its usage limit. Messages wait until it resets.", + }); + chat.turn(crew.analyst, "rate_limit"); + fake.now = end; +} diff --git a/app/src/features/bots/BotHeader.tsx b/app/src/features/bots/BotHeader.tsx index 3eae0047..63a5410f 100644 --- a/app/src/features/bots/BotHeader.tsx +++ b/app/src/features/bots/BotHeader.tsx @@ -2,6 +2,7 @@ import { Archive, Ellipsis, FolderOpen, + PanelRight, Pause, Pencil, Play, @@ -22,7 +23,17 @@ import { BotStateBadge } from "./BotStateBadge"; type Open = "edit" | "archive" | "fresh" | null; /** The bot's name, state and actions. */ -export function BotHeader({ bot, crew }: { bot: Bot; crew: Crew }) { +export function BotHeader({ + bot, + crew, + detailsOpen, + onToggleDetails, +}: { + bot: Bot; + crew: Crew; + detailsOpen: boolean; + onToggleDetails(): void; +}) { const api = useApi(); const host = useHost(); const putBot = useApp((state) => state.putBot); @@ -40,11 +51,11 @@ export function BotHeader({ bot, crew }: { bot: Bot; crew: Crew }) { ); return ( -
- +
+
-

{bot.name}

+

{bot.name}

@{bot.handle} @@ -66,6 +77,13 @@ export function BotHeader({ bot, crew }: { bot: Bot; crew: Crew }) { +
+
+
+
Role
+
{bot.role || "No role yet."}
+
+
+
Folder
+
+ {bot.workspace} +
+
+
+
Process
+
+ {bot.generation === null ? "not started" : `generation ${bot.generation}`} +
+
+
+
Instructions
+
+ {bot.instructions || "None yet."} +
+
+
+ ); } diff --git a/app/src/features/chat/ApprovalCard.tsx b/app/src/features/chat/ApprovalCard.tsx new file mode 100644 index 00000000..58b93344 --- /dev/null +++ b/app/src/features/chat/ApprovalCard.tsx @@ -0,0 +1,109 @@ +// A permission request (spec 10.1): what the bot wants to do, with Allow +// and Deny. Once answered it shrinks to one line. + +import { Ban, Check, Hand, TimerOff } from "lucide-react"; +import { useId, useState } from "react"; +import type { ApprovalItem, Bot } from "../../lib/protocol.gen"; +import { useApi } from "../../store/context"; +import { Button } from "../../ui/Button"; +import { attempt } from "../../ui/toast"; +import { pretty, toolLabel } from "./ToolLines"; + +function Answered({ approval }: { approval: ApprovalItem }) { + const label = toolLabel(approval.toolName); + const line = "flex min-w-0 items-center gap-2 px-1.5 py-1 text-sm"; + switch (approval.status) { + case "allowed": + return ( +

+ + You allowed {label} + {approval.summary} +

+ ); + case "denied": + return ( +

+ + You denied {label} + + {approval.note ? `“${approval.note}”` : approval.summary} + +

+ ); + default: + return ( +

+ + {label} was not approved in time +

+ ); + } +} + +export function ApprovalCard({ approval, bot }: { approval: ApprovalItem; bot: Bot }) { + const api = useApi(); + const [note, setNote] = useState(""); + const [busy, setBusy] = useState(false); + const noteId = useId(); + if (approval.status !== "pending") { + return ; + } + const label = toolLabel(approval.toolName); + const answer = async (allow: boolean) => { + setBusy(true); + const trimmed = note.trim(); + await attempt(allow ? "Could not allow it" : "Could not deny it", () => + api.call("approvals.answer", { + approvalId: approval.approvalId, + allow, + ...(allow || !trimmed ? {} : { note: trimmed }), + }), + ); + setBusy(false); + }; + + return ( +
+

+ + {bot.name} wants to use {label} +

+ {approval.summary && ( +

+ {approval.summary} +

+ )} +
+ Full input +
+          {pretty(approval.input)}
+        
+
+ + setNote(event.target.value)} + placeholder="Why not? Sent to the bot if you deny (optional)" + className="mt-3 h-8 w-full rounded-[3px] border border-line-strong bg-canvas px-2.5 text-sm outline-none placeholder:text-muted focus:border-accent" + /> +
+ + +
+
+ ); +} diff --git a/app/src/features/chat/AttachmentList.tsx b/app/src/features/chat/AttachmentList.tsx new file mode 100644 index 00000000..134030c4 --- /dev/null +++ b/app/src/features/chat/AttachmentList.tsx @@ -0,0 +1,151 @@ +// Files sent with a message: images as thumbnails that open larger, the +// rest as cards with name, type and size (spec 15.3). + +import { File, FileImage, FileText, FolderOpen, LoaderCircle } from "lucide-react"; +import { useState } from "react"; +import { fileSize } from "../../lib/format"; +import type { Attachment } from "../../lib/protocol.gen"; +import { useHost } from "../../store/context"; +import { Dialog } from "../../ui/Dialog"; +import { attempt } from "../../ui/toast"; +import { useImage } from "./images"; + +/** The folder an attachment was saved in, inside the bot's workspace. */ +export function attachmentFolder(workspace: string, attachment: Attachment): string { + const parts = attachment.path.split("/").slice(0, -1); + return [workspace, ...parts].join("\\"); +} + +function kindLabel(mediaType: string): string { + const [kind, sub] = mediaType.split("/"); + if (!sub || mediaType === "application/octet-stream") { + return "File"; + } + return (sub.split(/[.+-]/).at(-1) ?? kind ?? "file").toUpperCase(); +} + +function FileCard({ + attachment, + workspace, + note, +}: { + attachment: Attachment; + workspace: string | null; + note?: string; +}) { + const host = useHost(); + const Icon = attachment.mediaType.startsWith("image/") + ? FileImage + : attachment.mediaType.startsWith("text/") || attachment.mediaType.endsWith("pdf") + ? FileText + : File; + return ( +
+ +
+

+ {attachment.name} +

+

+ {note ?? `${kindLabel(attachment.mediaType)} · ${fileSize(attachment.size)}`} +

+
+ {workspace && ( + + )} +
+ ); +} + +function Thumbnail({ + attachment, + workspace, +}: { + attachment: Attachment; + workspace: string | null; +}) { + const source = useImage(attachment); + const [open, setOpen] = useState(false); + if (source.kind === "none") { + return ; + } + if (source.kind === "missing") { + return ( + + ); + } + if (source.kind === "loading") { + return ( +
+ +
+ ); + } + return ( + <> + + {open && ( + setOpen(false)}> + {attachment.name} + + )} + + ); +} + +export function AttachmentList({ + attachments, + workspace, + align = "start", +}: { + attachments: Attachment[]; + /** The bot's folder, to show a file in it; `null` when unknown. */ + workspace: string | null; + align?: "start" | "end"; +}) { + if (attachments.length === 0) { + return null; + } + return ( +
    + {attachments.map((attachment) => ( +
  • + +
  • + ))} +
+ ); +} diff --git a/app/src/features/chat/BotRun.tsx b/app/src/features/chat/BotRun.tsx new file mode 100644 index 00000000..63f02cfd --- /dev/null +++ b/app/src/features/chat/BotRun.tsx @@ -0,0 +1,98 @@ +// What the bot did in one turn, under its avatar: replies in markdown, +// tool calls together, approvals, and how the turn ended. The last run +// also shows the reply being written. + +import { CircleAlert } from "lucide-react"; +import { duration, when } from "../../lib/format"; +import type { Bot, ChatItem, TurnItem } from "../../lib/protocol.gen"; +import { BotAvatar } from "../bots/BotAvatar"; +import { ApprovalCard } from "./ApprovalCard"; +import { Markdown } from "./Markdown"; +import { runParts } from "./rows"; +import { ToolLines } from "./ToolLines"; + +export interface Live { + /** Text of the reply being written. */ + draft: string; + /** The bot is in a turn. */ + working: boolean; +} + +function TurnEnd({ turn }: { turn: TurnItem }) { + if (turn.error) { + return ( +

+ + The turn stopped: {turn.error.replaceAll("_", " ")} +

+ ); + } + const cost = turn.costUsd === null ? "" : ` · about $${turn.costUsd.toFixed(2)} of usage`; + return ( +

+ Done in {duration(turn.durationMs)} +

+ ); +} + +function Working() { + return ( +

+ {[0, 150, 300].map((delay) => ( + + ))} +

+ ); +} + +export function BotRun({ + items, + bot, + live, +}: { + items: ChatItem[]; + bot: Bot; + live?: Live | undefined; +}) { + const first = items[0]; + return ( +
  • + +
    +
    + {bot.name} + {first && ( + + )} +
    + {runParts(items).map((part) => { + const head = part[0] as ChatItem; + switch (head.body.kind) { + case "tool": + return ; + case "reply": + return ; + case "approval": + return ; + case "turn": + return ; + default: + return null; + } + })} + {live?.draft && ( +
    + +
    + )} + {live && !live.draft && live.working && } +
    +
  • + ); +} diff --git a/app/src/features/chat/ChatComposer.tsx b/app/src/features/chat/ChatComposer.tsx new file mode 100644 index 00000000..22f06958 --- /dev/null +++ b/app/src/features/chat/ChatComposer.tsx @@ -0,0 +1,201 @@ +// Writing to the bot: text, and files picked, pasted or dropped on the +// chat. Enter sends and Shift+Enter starts a new line (spec 15.1). + +import { ArrowUp, File as FileIcon, Paperclip, X } from "lucide-react"; +import { + type ClipboardEvent, + type FormEvent, + type KeyboardEvent, + useId, + useLayoutEffect, + useRef, + useState, +} from "react"; +import { errorText } from "../../lib/api"; +import { fileSize } from "../../lib/format"; +import { type Bot, FIELD_LIMITS } from "../../lib/protocol.gen"; +import { useApi } from "../../store/context"; +import { rememberImage } from "./images"; +import type { Files, PendingFile } from "./useFiles"; + +const MAX_HEIGHT_PX = 240; + +function Chip({ file, onRemove }: { file: PendingFile; onRemove(): void }) { + return ( +
  • + {file.preview ? ( + + ) : ( + + )} +
    +

    + {file.name} +

    +

    {fileSize(file.size)}

    +
    + +
  • + ); +} + +export function ChatComposer({ + bot, + files, + stopped, + onSent, +}: { + bot: Bot; + files: Files; + /** The bot is paused or its crew is. */ + stopped: boolean; + onSent(): void; +}) { + const api = useApi(); + const [text, setText] = useState(""); + const [busy, setBusy] = useState(false); + const field = useRef(null); + const picker = useRef(null); + const fieldId = useId(); + const tooLong = text.length > FIELD_LIMITS.message; + const empty = !text.trim() && files.files.length === 0; + + // Grows with the text up to a limit, then scrolls. + useLayoutEffect(() => { + const element = field.current; + if (element) { + element.style.height = "auto"; + if (text) { + element.style.height = `${Math.min(element.scrollHeight, MAX_HEIGHT_PX)}px`; + } + } + }, [text]); + + const send = async (event?: FormEvent) => { + event?.preventDefault(); + if (empty || tooLong || busy) { + return; + } + setBusy(true); + files.setError(null); + const pending = files.files; + try { + const attachments = pending.map(({ name, mediaType, data }) => ({ name, mediaType, data })); + const message = await api.call("messages.send", { + botId: bot.id, + body: text, + ...(attachments.length > 0 ? { attachments } : {}), + }); + // The daemon keeps the order it was given. + message.attachments.forEach((attachment, index) => { + const preview = pending[index]?.preview; + if (preview) { + rememberImage(attachment.id, preview); + } + }); + setText(""); + files.clear(); + onSent(); + } catch (failure) { + files.setError(errorText(failure)); + } finally { + setBusy(false); + field.current?.focus(); + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { + event.preventDefault(); + void send(); + } + }; + + const onPaste = (event: ClipboardEvent) => { + const pasted = [...event.clipboardData.files]; + if (pasted.length > 0) { + event.preventDefault(); + void files.add(pasted); + } + }; + + return ( +
    + {stopped && ( +

    + {bot.name} is paused. What you send waits until it runs again. +

    + )} +
    + {files.files.length > 0 && ( +
      + {files.files.map((file) => ( + files.remove(file.key)} /> + ))} +
    + )} + +