From d9cd16601be197fb22c49e60490d53a17a03fcde Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 24 Dec 2025 17:17:35 +0000 Subject: [PATCH] feat: agent brain (ollama) and chat ui --- .living_spec/README.md | 9 +- .../specs/functional/AI_INTEGRATION.md | 29 + .living_spec/specs/tech/STACK.md | 2 + .../{ => archive}/02_core_agent_tools.md | 0 .living_spec/stories/archive/03_llm_ollama.md | 22 + package.json | 3 +- pnpm-lock.yaml | 673 ++++++++++++++++++ src-tauri/Cargo.lock | 338 ++++++++- src-tauri/Cargo.toml | 3 + src-tauri/src/commands/chat.rs | 243 +++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/lib.rs | 4 +- src-tauri/src/llm/mod.rs | 2 + src-tauri/src/llm/ollama.rs | 170 +++++ src-tauri/src/llm/types.rs | 72 ++ src/App.tsx | 5 +- src/components/Chat.tsx | 126 ++++ src/types.ts | 24 + 18 files changed, 1712 insertions(+), 14 deletions(-) create mode 100644 .living_spec/specs/functional/AI_INTEGRATION.md rename .living_spec/stories/{ => archive}/02_core_agent_tools.md (100%) create mode 100644 .living_spec/stories/archive/03_llm_ollama.md create mode 100644 src-tauri/src/commands/chat.rs create mode 100644 src-tauri/src/llm/mod.rs create mode 100644 src-tauri/src/llm/ollama.rs create mode 100644 src-tauri/src/llm/types.rs create mode 100644 src/components/Chat.tsx diff --git a/.living_spec/README.md b/.living_spec/README.md index 07930e3..30aab55 100644 --- a/.living_spec/README.md +++ b/.living_spec/README.md @@ -87,7 +87,8 @@ When the LLM context window fills up (or the chat gets slow/confused): If a user hands you this document and says "Apply this process to my project": 1. **Analyze the Request:** Ask for the high-level goal ("What are we building?") and the tech preferences ("Rust or Python?"). -2. **Scaffold:** Run commands to create the `specs/` and `stories/` folders. -3. **Draft Context:** Write `specs/00_CONTEXT.md` based on the user's answer. -4. **Draft Stack:** Write `specs/tech/STACK.md` based on best practices for that language. -5. **Wait:** Ask the user for "Story #1". +2. **Git Check:** Check if the directory is a git repository (`git status`). If not, run `git init`. +3. **Scaffold:** Run commands to create the `specs/` and `stories/` folders. +4. **Draft Context:** Write `specs/00_CONTEXT.md` based on the user's answer. +5. **Draft Stack:** Write `specs/tech/STACK.md` based on best practices for that language. +6. **Wait:** Ask the user for "Story #1". diff --git a/.living_spec/specs/functional/AI_INTEGRATION.md b/.living_spec/specs/functional/AI_INTEGRATION.md new file mode 100644 index 0000000..40a8e91 --- /dev/null +++ b/.living_spec/specs/functional/AI_INTEGRATION.md @@ -0,0 +1,29 @@ +# Functional Spec: AI Integration + +## 1. Provider Abstraction +The system uses a pluggable architecture for LLMs. The `ModelProvider` interface abstracts: +* **Generation:** Sending prompt + history + tools to the model. +* **Parsing:** Extracting text content vs. tool calls from the raw response. + +## 2. Ollama Implementation +* **Endpoint:** `http://localhost:11434/api/chat` +* **JSON Protocol:** + * Request: `{ model: "name", messages: [...], stream: false, tools: [...] }` + * Response: Standard Ollama JSON with `message.tool_calls`. +* **Fallback:** If the specific local model doesn't support native tool calling, we may need a fallback system prompt approach, but for this story, we assume a tool-capable model (like `llama3.1` or `mistral-nemo`). + +## 3. Chat Loop (Backend) +The `chat` command acts as the **Agent Loop**: +1. Frontend sends: `User Message`. +2. Backend appends to `SessionState.history`. +3. Backend calls `OllamaProvider`. +4. **If Text Response:** Return text to Frontend. +5. **If Tool Call:** + * Backend executes the Tool (using the Core Tools from Story #2). + * Backend appends `ToolResult` to history. + * Backend *re-prompts* Ollama with the new history (recursion). + * Repeat until Text Response or Max Turns reached. + +## 4. Frontend State +* **Settings:** Store `llm_provider` ("ollama"), `ollama_model` ("llama3.2"), `ollama_base_url`. +* **Chat:** Display the conversation. Tool calls should be visible as "System Events" (e.g., collapsed accordions). diff --git a/.living_spec/specs/tech/STACK.md b/.living_spec/specs/tech/STACK.md index fd26e11..fa1aae8 100644 --- a/.living_spec/specs/tech/STACK.md +++ b/.living_spec/specs/tech/STACK.md @@ -78,6 +78,8 @@ To support both Remote and Local models, the system implements a `ModelProvider` * `walkdir`: Simple directory traversal. * `tokio`: Async runtime. * `reqwest`: For LLM API calls (if backend-initiated). + * `uuid`: For unique message IDs. + * `chrono`: For timestamps. * `tauri-plugin-dialog`: Native system dialogs. * **JavaScript:** * `@tauri-apps/api`: Tauri Bridge. diff --git a/.living_spec/stories/02_core_agent_tools.md b/.living_spec/stories/archive/02_core_agent_tools.md similarity index 100% rename from .living_spec/stories/02_core_agent_tools.md rename to .living_spec/stories/archive/02_core_agent_tools.md diff --git a/.living_spec/stories/archive/03_llm_ollama.md b/.living_spec/stories/archive/03_llm_ollama.md new file mode 100644 index 0000000..579480c --- /dev/null +++ b/.living_spec/stories/archive/03_llm_ollama.md @@ -0,0 +1,22 @@ +# Story: The Agent Brain (Ollama Integration) + +## User Story +**As a** User +**I want to** connect the Assistant to a local Ollama instance +**So that** I can chat with the Agent and have it execute tools without sending data to the cloud. + +## Acceptance Criteria +* [ ] Backend: Implement `ModelProvider` trait/interface. +* [ ] Backend: Implement `OllamaProvider` (POST /api/chat). +* [ ] Backend: Implement `chat(message, history, provider_config)` command. + * [ ] Must support passing Tool Definitions to Ollama (if model supports it) or System Prompt instructions. + * [ ] Must parse Tool Calls from the response. +* [ ] Frontend: Settings Screen to toggle "Ollama" and set Model Name (default: `llama3`). +* [ ] Frontend: Chat Interface. + * [ ] Message History (User/Assistant). + * [ ] Tool Call visualization (e.g., "Running git status..."). + +## Out of Scope +* Remote Providers (Anthropic/OpenAI) - Future Story. +* Streaming responses (wait for full completion for MVP). +* Complex context window management (just send full history for now). diff --git a/package.json b/package.json index ca9c192..1b54b9b 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "@tauri-apps/plugin-dialog": "^2.4.2", "@tauri-apps/plugin-opener": "^2", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-markdown": "^10.1.0" }, "devDependencies": { "@tauri-apps/cli": "^2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d49f4fb..bdc81d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: react-dom: specifier: ^19.1.0 version: 19.2.3(react@19.2.3) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.7)(react@19.2.3) devDependencies: '@tauri-apps/cli': specifier: ^2 @@ -505,9 +508,24 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -516,12 +534,24 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@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==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + baseline-browser-mapping@2.9.11: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true @@ -534,6 +564,24 @@ packages: caniuse-lite@1.0.30001761: resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + 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==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -549,6 +597,16 @@ packages: supports-color: optional: true + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -561,6 +619,12 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + 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'} @@ -579,6 +643,34 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + 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-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'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -592,9 +684,99 @@ packages: engines: {node: '>=6'} hasBin: true + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + 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.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + 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.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + 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-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.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -606,6 +788,9 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -617,11 +802,20 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + react-dom@19.2.3: resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: react: ^19.2.3 + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -630,6 +824,12 @@ packages: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + rollup@4.54.0: resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -646,21 +846,63 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + 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==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + 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@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true + 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.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@7.3.0: resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -704,6 +946,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@babel/code-frame@7.27.1': @@ -1061,8 +1306,26 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -1071,6 +1334,12 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.0': {} + '@vitejs/plugin-react@4.7.0(vite@7.3.0)': dependencies: '@babel/core': 7.28.5 @@ -1083,6 +1352,8 @@ snapshots: transitivePeerDependencies: - supports-color + bail@2.0.2: {} + baseline-browser-mapping@2.9.11: {} browserslist@4.28.1: @@ -1095,6 +1366,18 @@ snapshots: caniuse-lite@1.0.30001761: {} + ccount@2.0.1: {} + + 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: {} + convert-source-map@2.0.0: {} csstype@3.2.3: {} @@ -1103,6 +1386,16 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + electron-to-chromium@1.5.267: {} esbuild@0.27.2: @@ -1136,6 +1429,10 @@ snapshots: escalade@3.2.0: {} + estree-util-is-identifier-name@3.0.0: {} + + extend@3.0.2: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -1145,22 +1442,297 @@ snapshots: gensync@1.0.0-beta.2: {} + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@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.1.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.4 + + 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: {} + js-tokens@4.0.0: {} jsesc@3.1.0: {} json5@2.2.3: {} + longest-streak@3.1.0: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + 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.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + 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.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + 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.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.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.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + 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-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + 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-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.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + 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.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + 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.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + 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.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + 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.2 + + 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.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + 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-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.2 + transitivePeerDependencies: + - supports-color + ms@2.1.3: {} nanoid@3.3.11: {} node-releases@2.0.27: {} + 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.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + picocolors@1.1.1: {} picomatch@4.0.3: {} @@ -1171,15 +1743,52 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + property-information@7.1.0: {} + react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 scheduler: 0.27.0 + react-markdown@10.1.0(@types/react@19.2.7)(react@19.2.3): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.7 + 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.2.3 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-refresh@0.17.0: {} react@19.2.3: {} + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + rollup@4.54.0: dependencies: '@types/estree': 1.0.8 @@ -1214,19 +1823,81 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + + 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 + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + typescript@5.8.3: {} + 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.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 + 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@7.3.0: dependencies: esbuild: 0.27.2 @@ -1239,3 +1910,5 @@ snapshots: fsevents: 2.3.3 yallist@3.1.1: {} + + zwitch@2.0.4: {} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index bb2c246..5fcde05 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -473,8 +473,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -513,6 +515,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -536,9 +548,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -549,7 +561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -864,6 +876,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.1" @@ -986,6 +1007,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -993,7 +1023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -1007,6 +1037,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1039,6 +1075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1430,6 +1467,25 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1527,6 +1583,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1538,6 +1595,38 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.19" @@ -1557,9 +1646,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1965,13 +2056,16 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" name = "living-spec-standalone" version = "0.1.0" dependencies = [ + "chrono", "ignore", + "reqwest", "serde", "serde_json", "tauri", "tauri-build", "tauri-plugin-dialog", "tauri-plugin-opener", + "uuid", "walkdir", ] @@ -2090,6 +2184,23 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2412,6 +2523,50 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -3021,22 +3176,31 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", + "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", + "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", + "mime", + "native-tls", "percent-encoding", "pin-project-lite", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-util", "tower", "tower-http", @@ -3073,6 +3237,20 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -3095,6 +3273,39 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3116,6 +3327,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3179,6 +3399,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3534,6 +3777,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3587,6 +3836,27 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3608,7 +3878,7 @@ checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" dependencies = [ "bitflags 2.10.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dispatch", @@ -4072,6 +4342,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -4361,6 +4651,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.7" @@ -4409,6 +4705,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.1" @@ -4851,6 +5153,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -4896,6 +5209,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -5359,6 +5681,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1276a68..0c2eb57 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,4 +25,7 @@ serde_json = "1" tauri-plugin-dialog = "2.4.2" ignore = "0.4.25" walkdir = "2.5.0" +reqwest = { version = "0.12.28", features = ["json", "blocking"] } +uuid = { version = "1.19.0", features = ["v4", "serde"] } +chrono = { version = "0.4.42", features = ["serde"] } diff --git a/src-tauri/src/commands/chat.rs b/src-tauri/src/commands/chat.rs new file mode 100644 index 0000000..9942963 --- /dev/null +++ b/src-tauri/src/commands/chat.rs @@ -0,0 +1,243 @@ +use crate::commands::{fs, search, shell}; +use crate::llm::ollama::OllamaProvider; +use crate::llm::types::{ + Message, ModelProvider, Role, ToolCall, ToolDefinition, ToolFunctionDefinition, +}; +use crate::state::SessionState; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use tauri::State; + +#[derive(Deserialize)] +pub struct ProviderConfig { + pub provider: String, // "ollama" + pub model: String, + pub base_url: Option, +} + +const MAX_TURNS: usize = 10; + +#[tauri::command] +pub async fn chat( + messages: Vec, + config: ProviderConfig, + state: State<'_, SessionState>, +) -> Result, String> { + // 1. Setup Provider + let provider: Box = match config.provider.as_str() { + "ollama" => Box::new(OllamaProvider::new( + config + .base_url + .unwrap_or_else(|| "http://localhost:11434".to_string()), + )), + _ => return Err(format!("Unsupported provider: {}", config.provider)), + }; + + // 2. Define Tools + let tools = get_tool_definitions(); + + // 3. Agent Loop + let mut current_history = messages.clone(); + let mut new_messages: Vec = Vec::new(); + let mut turn_count = 0; + + loop { + if turn_count >= MAX_TURNS { + return Err("Max conversation turns reached.".to_string()); + } + turn_count += 1; + + // Call LLM + let response = provider + .chat(&config.model, ¤t_history, &tools) + .map_err(|e| format!("LLM Error: {}", e))?; + + // Process Response + if let Some(tool_calls) = response.tool_calls { + // The Assistant wants to run tools + let assistant_msg = Message { + role: Role::Assistant, + content: response.content.unwrap_or_default(), + tool_calls: Some(tool_calls.clone()), + tool_call_id: None, + }; + + current_history.push(assistant_msg.clone()); + new_messages.push(assistant_msg); + + // Execute Tools + for call in tool_calls { + let output = execute_tool(&call, &state).await; + + let tool_msg = Message { + role: Role::Tool, + content: output, + tool_calls: None, + // For Ollama/Simple flow, we just append. + // For OpenAI strict, this needs to match call.id. + tool_call_id: call.id, + }; + + current_history.push(tool_msg.clone()); + new_messages.push(tool_msg); + } + } else { + // Final text response + let assistant_msg = Message { + role: Role::Assistant, + content: response.content.unwrap_or_default(), + tool_calls: None, + tool_call_id: None, + }; + + // We don't push to current_history needed for next loop, because we are done. + new_messages.push(assistant_msg); + break; + } + } + + Ok(new_messages) +} + +async fn execute_tool(call: &ToolCall, state: &State<'_, SessionState>) -> String { + let name = call.function.name.as_str(); + // Parse arguments. They come as a JSON string from the LLM abstraction. + let args: serde_json::Value = match serde_json::from_str(&call.function.arguments) { + Ok(v) => v, + Err(e) => return format!("Error parsing arguments: {}", e), + }; + + match name { + "read_file" => { + let path = args["path"].as_str().unwrap_or("").to_string(); + match fs::read_file(path, state.clone()).await { + Ok(content) => content, + Err(e) => format!("Error: {}", e), + } + } + "write_file" => { + let path = args["path"].as_str().unwrap_or("").to_string(); + let content = args["content"].as_str().unwrap_or("").to_string(); + match fs::write_file(path, content, state.clone()).await { + Ok(_) => "File written successfully.".to_string(), + Err(e) => format!("Error: {}", e), + } + } + "list_directory" => { + let path = args["path"].as_str().unwrap_or("").to_string(); + match fs::list_directory(path, state.clone()).await { + Ok(entries) => serde_json::to_string(&entries).unwrap_or_default(), + Err(e) => format!("Error: {}", e), + } + } + "search_files" => { + let query = args["query"].as_str().unwrap_or("").to_string(); + match search::search_files(query, state.clone()).await { + Ok(results) => serde_json::to_string(&results).unwrap_or_default(), + Err(e) => format!("Error: {}", e), + } + } + "exec_shell" => { + let command = args["command"].as_str().unwrap_or("").to_string(); + let args_vec: Vec = args["args"] + .as_array() + .map(|arr| { + arr.iter() + .map(|v| v.as_str().unwrap_or("").to_string()) + .collect() + }) + .unwrap_or_default(); + + match shell::exec_shell(command, args_vec, state.clone()).await { + Ok(output) => serde_json::to_string(&output).unwrap_or_default(), + Err(e) => format!("Error: {}", e), + } + } + _ => format!("Unknown tool: {}", name), + } +} + +fn get_tool_definitions() -> Vec { + vec![ + ToolDefinition { + kind: "function".to_string(), + function: ToolFunctionDefinition { + name: "read_file".to_string(), + description: "Reads the content of a file in the project.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "path": { "type": "string", "description": "Relative path to the file" } + }, + "required": ["path"] + }), + }, + }, + ToolDefinition { + kind: "function".to_string(), + function: ToolFunctionDefinition { + name: "write_file".to_string(), + description: "Writes content to a file. Overwrites if exists.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "path": { "type": "string", "description": "Relative path to the file" }, + "content": { "type": "string", "description": "The full content to write" } + }, + "required": ["path", "content"] + }), + }, + }, + ToolDefinition { + kind: "function".to_string(), + function: ToolFunctionDefinition { + name: "list_directory".to_string(), + description: "Lists files and directories at a path.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "path": { "type": "string", "description": "Relative path to list (use '.' for root)" } + }, + "required": ["path"] + }), + }, + }, + ToolDefinition { + kind: "function".to_string(), + function: ToolFunctionDefinition { + name: "search_files".to_string(), + description: "Searches for text content across all files in the project." + .to_string(), + parameters: json!({ + "type": "object", + "properties": { + "query": { "type": "string", "description": "The string to search for" } + }, + "required": ["query"] + }), + }, + }, + ToolDefinition { + kind: "function".to_string(), + function: ToolFunctionDefinition { + name: "exec_shell".to_string(), + description: "Executes a shell command in the project root.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The command to run (e.g., 'git', 'cargo', 'ls')" + }, + "args": { + "type": "array", + "items": { "type": "string" }, + "description": "Arguments for the command" + } + }, + "required": ["command", "args"] + }), + }, + }, + ] +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 71e6bf5..943956d 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod chat; pub mod fs; pub mod search; pub mod shell; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 880d78d..d4ecf33 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ mod commands; +mod llm; mod state; use state::SessionState; @@ -15,7 +16,8 @@ pub fn run() { commands::fs::write_file, commands::fs::list_directory, commands::search::search_files, - commands::shell::exec_shell + commands::shell::exec_shell, + commands::chat::chat ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/llm/mod.rs b/src-tauri/src/llm/mod.rs new file mode 100644 index 0000000..16f013b --- /dev/null +++ b/src-tauri/src/llm/mod.rs @@ -0,0 +1,2 @@ +pub mod ollama; +pub mod types; diff --git a/src-tauri/src/llm/ollama.rs b/src-tauri/src/llm/ollama.rs new file mode 100644 index 0000000..65b38b1 --- /dev/null +++ b/src-tauri/src/llm/ollama.rs @@ -0,0 +1,170 @@ +use crate::llm::types::{ + CompletionResponse, FunctionCall, Message, ModelProvider, Role, ToolCall, ToolDefinition, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +pub struct OllamaProvider { + base_url: String, +} + +impl OllamaProvider { + pub fn new(base_url: String) -> Self { + Self { base_url } + } +} + +// --- Request Types --- + +#[derive(Serialize)] +struct OllamaRequest<'a> { + model: &'a str, + messages: Vec, + stream: bool, + #[serde(skip_serializing_if = "is_empty_tools")] + tools: &'a [ToolDefinition], +} + +fn is_empty_tools(tools: &&[ToolDefinition]) -> bool { + tools.is_empty() +} + +#[derive(Serialize)] +struct OllamaRequestMessage { + role: Role, + content: String, + #[serde(skip_serializing_if = "Option::is_none")] + tool_calls: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_call_id: Option, +} + +#[derive(Serialize)] +struct OllamaRequestToolCall { + function: OllamaRequestFunctionCall, + #[serde(rename = "type")] + kind: String, +} + +#[derive(Serialize)] +struct OllamaRequestFunctionCall { + name: String, + arguments: Value, +} + +// --- Response Types --- + +#[derive(Deserialize)] +struct OllamaResponse { + message: OllamaResponseMessage, +} + +#[derive(Deserialize)] +struct OllamaResponseMessage { + content: String, + tool_calls: Option>, +} + +#[derive(Deserialize)] +struct OllamaResponseToolCall { + function: OllamaResponseFunctionCall, +} + +#[derive(Deserialize)] +struct OllamaResponseFunctionCall { + name: String, + arguments: Value, // Ollama returns Object, we convert to String for internal storage +} + +impl ModelProvider for OllamaProvider { + fn chat( + &self, + model: &str, + messages: &[Message], + tools: &[ToolDefinition], + ) -> Result { + let client = reqwest::blocking::Client::new(); + let url = format!("{}/api/chat", self.base_url.trim_end_matches('/')); + + // Convert domain Messages to Ollama Messages (handling String -> Object args mismatch) + let ollama_messages: Vec = messages + .iter() + .map(|m| { + let tool_calls = m.tool_calls.as_ref().map(|calls| { + calls + .iter() + .map(|tc| { + // Try to parse string args as JSON, fallback to string value if fails + let args_val: Value = serde_json::from_str(&tc.function.arguments) + .unwrap_or(Value::String(tc.function.arguments.clone())); + + OllamaRequestToolCall { + kind: tc.kind.clone(), + function: OllamaRequestFunctionCall { + name: tc.function.name.clone(), + arguments: args_val, + }, + } + }) + .collect() + }); + + OllamaRequestMessage { + role: m.role.clone(), + content: m.content.clone(), + tool_calls, + tool_call_id: m.tool_call_id.clone(), + } + }) + .collect(); + + let request_body = OllamaRequest { + model, + messages: ollama_messages, + stream: false, + tools, + }; + + let res = client + .post(&url) + .json(&request_body) + .send() + .map_err(|e| format!("Request failed: {}", e))?; + + if !res.status().is_success() { + let status = res.status(); + let text = res.text().unwrap_or_default(); + return Err(format!("Ollama API error {}: {}", status, text)); + } + + let response_body: OllamaResponse = res + .json() + .map_err(|e| format!("Failed to parse response: {}", e))?; + + // Convert Response back to Domain types + let content = if response_body.message.content.is_empty() { + None + } else { + Some(response_body.message.content) + }; + + let tool_calls = response_body.message.tool_calls.map(|calls| { + calls + .into_iter() + .map(|tc| ToolCall { + id: None, // Ollama doesn't typically send IDs + kind: "function".to_string(), + function: FunctionCall { + name: tc.function.name, + arguments: tc.function.arguments.to_string(), // Convert Object -> String + }, + }) + .collect() + }); + + Ok(CompletionResponse { + content, + tool_calls, + }) + } +} diff --git a/src-tauri/src/llm/types.rs b/src-tauri/src/llm/types.rs new file mode 100644 index 0000000..07dd2c4 --- /dev/null +++ b/src-tauri/src/llm/types.rs @@ -0,0 +1,72 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Role { + System, + User, + Assistant, + Tool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Message { + pub role: Role, + pub content: String, + + // For assistant messages that request tool execution + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, + + // For tool output messages, we need to link back to the call ID + // Note: OpenAI uses 'tool_call_id', Ollama sometimes just relies on sequence. + // We will include it for compatibility. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ToolCall { + // ID is required by OpenAI, optional/generated for Ollama depending on version + pub id: Option, + pub function: FunctionCall, + #[serde(rename = "type")] + pub kind: String, // usually "function" +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FunctionCall { + pub name: String, + pub arguments: String, // JSON string of arguments +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ToolDefinition { + #[serde(rename = "type")] + pub kind: String, // "function" + pub function: ToolFunctionDefinition, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ToolFunctionDefinition { + pub name: String, + pub description: String, + pub parameters: serde_json::Value, // JSON Schema object +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CompletionResponse { + pub content: Option, + pub tool_calls: Option>, +} + +/// The abstraction for different LLM providers (Ollama, Anthropic, etc.) +pub trait ModelProvider: Send + Sync { + fn chat( + &self, + model: &str, + messages: &[Message], + tools: &[ToolDefinition], + ) -> Result; +} diff --git a/src/App.tsx b/src/App.tsx index c703388..622350f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { open } from "@tauri-apps/plugin-dialog"; -import { ToolTester } from "./components/ToolTester"; +import { Chat } from "./components/Chat"; import "./App.css"; function App() { @@ -62,8 +62,7 @@ function App() { Active Project: {projectPath}
-

Project loaded successfully.

- + )} diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx new file mode 100644 index 0000000..7d94863 --- /dev/null +++ b/src/components/Chat.tsx @@ -0,0 +1,126 @@ +import { useState, useRef, useEffect } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import Markdown from "react-markdown"; +import { Message, ProviderConfig } from "../types"; + +export function Chat() { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const [model, setModel] = useState("llama3.1"); // Default local model + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(scrollToBottom, [messages]); + + const sendMessage = async () => { + if (!input.trim() || loading) return; + + const userMsg: Message = { role: "user", content: input }; + const newHistory = [...messages, userMsg]; + + setMessages(newHistory); + setInput(""); + setLoading(true); + + try { + const config: ProviderConfig = { + provider: "ollama", + model: model, + base_url: "http://localhost:11434", + }; + + // Invoke backend chat command + // The backend returns the *new* messages (assistant response + tool outputs) + const response = await invoke("chat", { + messages: newHistory, + config: config, + }); + + setMessages((prev) => [...prev, ...response]); + } catch (e) { + console.error(e); + setMessages((prev) => [ + ...prev, + { role: "assistant", content: `**Error:** ${e}` }, + ]); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* Settings Bar */} +
+ + setModel(e.target.value)} + placeholder="e.g. llama3, mistral" + style={{ padding: "5px" }} + /> +
+ + {/* Messages Area */} +
+ {messages.map((msg, idx) => ( +
+ {msg.role === "user" ? "You" : msg.role === "tool" ? "Tool Output" : "Agent"} + {msg.role === "tool" ? ( +
{msg.content}
+ ) : ( + {msg.content} + )} + + {/* Show Tool Calls if present */} + {msg.tool_calls && ( +
+ {msg.tool_calls.map((tc, i) => ( +
+ 🛠 {tc.function.name}({tc.function.arguments}) +
+ ))} +
+ )} +
+ ))} + {loading &&
Thinking...
} +
+
+ + {/* Input Area */} +
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && sendMessage()} + placeholder="Ask the agent to do something..." + style={{ flex: 1, padding: "10px", borderRadius: "4px", border: "1px solid #ccc" }} + disabled={loading} + /> + +
+
+ ); +} diff --git a/src/types.ts b/src/types.ts index 5d60ad4..f9d3ede 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,3 +13,27 @@ export interface CommandOutput { stderr: string; exit_code: number; } + +export type Role = "system" | "user" | "assistant" | "tool"; + +export interface ToolCall { + id?: string; + type: string; + function: { + name: string; + arguments: string; + }; +} + +export interface Message { + role: Role; + content: string; + tool_calls?: ToolCall[]; + tool_call_id?: string; +} + +export interface ProviderConfig { + provider: string; + model: string; + base_url?: string; +}