feat: agent brain (ollama) and chat ui

This commit is contained in:
Dave
2025-12-24 17:17:35 +00:00
parent 76e03bc1a2
commit d9cd16601b
18 changed files with 1712 additions and 14 deletions

View File

@@ -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".

View File

@@ -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).

View File

@@ -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.

View File

@@ -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).

View File

@@ -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",

673
pnpm-lock.yaml generated
View File

@@ -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: {}

338
src-tauri/Cargo.lock generated
View File

@@ -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"

View File

@@ -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"] }

View File

@@ -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<String>,
}
const MAX_TURNS: usize = 10;
#[tauri::command]
pub async fn chat(
messages: Vec<Message>,
config: ProviderConfig,
state: State<'_, SessionState>,
) -> Result<Vec<Message>, String> {
// 1. Setup Provider
let provider: Box<dyn ModelProvider> = 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<Message> = 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, &current_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<String> = 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<ToolDefinition> {
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"]
}),
},
},
]
}

View File

@@ -1,3 +1,4 @@
pub mod chat;
pub mod fs;
pub mod search;
pub mod shell;

View File

@@ -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");

2
src-tauri/src/llm/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod ollama;
pub mod types;

170
src-tauri/src/llm/ollama.rs Normal file
View File

@@ -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<OllamaRequestMessage>,
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<Vec<OllamaRequestToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_call_id: Option<String>,
}
#[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<Vec<OllamaResponseToolCall>>,
}
#[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<CompletionResponse, String> {
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<OllamaRequestMessage> = 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,
})
}
}

View File

@@ -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<Vec<ToolCall>>,
// 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<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToolCall {
// ID is required by OpenAI, optional/generated for Ollama depending on version
pub id: Option<String>,
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<String>,
pub tool_calls: Option<Vec<ToolCall>>,
}
/// The abstraction for different LLM providers (Ollama, Anthropic, etc.)
pub trait ModelProvider: Send + Sync {
fn chat(
&self,
model: &str,
messages: &[Message],
tools: &[ToolDefinition],
) -> Result<CompletionResponse, String>;
}

View File

@@ -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() {
<strong>Active Project:</strong> {projectPath}
</div>
<hr style={{ margin: "20px 0" }} />
<p>Project loaded successfully.</p>
<ToolTester />
<Chat />
</div>
)}

126
src/components/Chat.tsx Normal file
View File

@@ -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<Message[]>([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [model, setModel] = useState("llama3.1"); // Default local model
const messagesEndRef = useRef<HTMLDivElement>(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<Message[]>("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 (
<div className="chat-container" style={{ display: "flex", flexDirection: "column", height: "100%", maxWidth: "800px", margin: "0 auto" }}>
{/* Settings Bar */}
<div style={{ padding: "10px", borderBottom: "1px solid #ddd", display: "flex", gap: "10px", alignItems: "center" }}>
<label>Ollama Model:</label>
<input
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder="e.g. llama3, mistral"
style={{ padding: "5px" }}
/>
</div>
{/* Messages Area */}
<div style={{ flex: 1, overflowY: "auto", padding: "20px", display: "flex", flexDirection: "column", gap: "15px" }}>
{messages.map((msg, idx) => (
<div
key={idx}
className={`message ${msg.role}`}
style={{
alignSelf: msg.role === "user" ? "flex-end" : "flex-start",
maxWidth: "80%",
padding: "10px 15px",
borderRadius: "10px",
background: msg.role === "user" ? "#007AFF" : msg.role === "tool" ? "#f0f0f0" : "#E5E5EA",
color: msg.role === "user" ? "white" : "black",
border: msg.role === "tool" ? "1px solid #ccc" : "none",
fontFamily: msg.role === "tool" ? "monospace" : "inherit",
fontSize: msg.role === "tool" ? "0.9em" : "1em",
whiteSpace: msg.role === "tool" ? "pre-wrap" : "normal"
}}
>
<strong>{msg.role === "user" ? "You" : msg.role === "tool" ? "Tool Output" : "Agent"}</strong>
{msg.role === "tool" ? (
<div style={{maxHeight: "200px", overflow: "auto"}}>{msg.content}</div>
) : (
<Markdown>{msg.content}</Markdown>
)}
{/* Show Tool Calls if present */}
{msg.tool_calls && (
<div style={{ marginTop: "10px", fontSize: "0.85em", color: "#666" }}>
{msg.tool_calls.map((tc, i) => (
<div key={i} style={{ background: "rgba(0,0,0,0.05)", padding: "5px", borderRadius: "4px" }}>
🛠 <code>{tc.function.name}({tc.function.arguments})</code>
</div>
))}
</div>
)}
</div>
))}
{loading && <div style={{ alignSelf: "flex-start", color: "#888" }}>Thinking...</div>}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div style={{ padding: "20px", borderTop: "1px solid #ddd", display: "flex", gap: "10px" }}>
<input
value={input}
onChange={(e) => 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}
/>
<button onClick={sendMessage} disabled={loading} style={{ padding: "10px 20px" }}>
Send
</button>
</div>
</div>
);
}

View File

@@ -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;
}