diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 00000000..d7f90c58
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,2 @@
+[build]
+target-dir = "C:/Users/julie/AppData/Local/Temp/sinew-cargo-target"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 936ba43a..d86e0934 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -132,6 +132,9 @@ jobs:
- name: Install npm dependencies
run: npm ci
+ - name: Prepare Tauri sidecars
+ run: npm run prepare-sidecars
+
# Workaround for the well-known `bundle_dmg.sh` hang on GitHub macOS
# runners: Finder + DiskImageMounter aren't warmed up in the headless
# session, so the first AppleScript call inside bundle_dmg.sh races and
@@ -238,6 +241,16 @@ jobs:
echo "No .deb produced - skipping .deb alias."
fi
+ - name: Build & upload Linux Daemon (for Super SSH)
+ if: runner.os == 'Linux'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ set -e
+ cargo build --release -p sinew-agent-daemon
+ cp target/release/sinew-agent-daemon sinew-agent-daemon-linux
+ gh release upload "${{ needs.create-release.outputs.tag_name }}" sinew-agent-daemon-linux --clobber
+
finalize-release:
name: Finalize release
needs: [create-release, build-tauri]
diff --git a/.gitignore b/.gitignore
index 6b113801..98159dd8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
/target
/dist
/node_modules
+sinew-chrome-bridge/node_modules/
+scripts/agent-bridge/node_modules/
/FEATURES.md
/EDIT_FILE_HARNESS_COMPARISON.md
/GLOB_HARNESS_COMPARISON.md
@@ -18,3 +20,17 @@
# Tauri updater signing keys — NEVER commit private keys
/.tauri-keys/
*.key
+
+/installers
+.fastembed_cache/
+desktop-state.sqlite3
+desktop-state.sqlite3-wal
+desktop-state.sqlite3-shm
+
+/build
+
+
+
+rust-live-*.txt
+scripts/agent-bridge/_node_*.txt
+scripts/agent-bridge/_payload*.json
diff --git a/AGENTS.md b/AGENTS.md
index d772c748..c1db6dc8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,282 +1,430 @@
-Code map:
-- L'agent doit garder à jour cette carte simple des fichiers à chaque création, suppression, renommage, déplacement ou modification.
-
-.
-├── .gitignore
-├── AGENTS.md
-├── Cargo.lock
-├── Cargo.toml
-├── EDIT_FILE_HARNESS_COMPARISON.md
-├── FEATURES.md
-├── GLOB_HARNESS_COMPARISON.md
-├── GREP_HARNESS_COMPARISON.md
-├── index.html
-├── LICENSE
-├── package-lock.json
-├── package.json
-├── README.md
-├── test-stop.md
-├── scripts
-│ └── prepare-sidecars.mjs
-├── tsconfig.json
-├── tsconfig.node.json
-├── vite.config.ts
-├── .github
-│ ├── assets
-│ │ ├── architecture.png
-│ │ ├── harness.png
-│ │ ├── hero.png
-│ │ ├── modes.png
-│ │ ├── screenshot.png
-│ │ └── swarm.png
-│ └── workflows
-│ └── release.yml
-├── crates
-│ ├── sinew-anthropic
-│ │ ├── Cargo.toml
-│ │ └── src
-│ │ ├── auth.rs
-│ │ ├── client.rs
-│ │ ├── lib.rs
-│ │ ├── model_info.rs
-│ │ ├── stream.rs
-│ │ └── wire.rs
-│ ├── sinew-app
-│ │ ├── Cargo.toml
-│ │ └── src
-│ │ ├── agent.rs
-│ │ ├── agent
-│ │ │ ├── assistant_message.rs
-│ │ │ ├── cancel.rs
-│ │ │ ├── clean_context.rs
-│ │ │ ├── compaction.rs
-│ │ │ ├── context.rs
-│ │ │ ├── events.rs
-│ │ │ ├── history.rs
-│ │ │ ├── mode.rs
-│ │ │ ├── tests.rs
-│ │ │ ├── tool_dispatch.rs
-│ │ │ ├── tool_summary.rs
-│ │ │ └── turn.rs
-│ │ ├── bash.rs
-│ │ ├── compact.rs
-│ │ ├── edit.rs
-│ │ ├── glob.rs
-│ │ ├── grep.rs
-│ │ ├── image.rs
-│ │ ├── lib.rs
-│ │ ├── mcp.rs
-│ │ ├── question.rs
-│ │ ├── read.rs
-│ │ ├── skill.rs
-│ │ ├── store.rs
-│ │ ├── subagent.rs
-│ │ ├── team.rs
-│ │ ├── team
-│ │ │ ├── agent_turns.rs
-│ │ │ ├── context.rs
-│ │ │ ├── descriptors.rs
-│ │ │ ├── launch.rs
-│ │ │ ├── live.rs
-│ │ │ ├── messaging.rs
-│ │ │ ├── model.rs
-│ │ │ ├── render.rs
-│ │ │ ├── session.rs
-│ │ │ ├── status_stop.rs
-│ │ │ ├── task_board.rs
-│ │ │ └── tests.rs
-│ │ ├── text.rs
-│ │ ├── todo.rs
-│ │ ├── tool_names.rs
-│ │ ├── tool_run.rs
-│ │ ├── web.rs
-│ │ ├── write.rs
-│ │ └── workspace.rs
-│ ├── sinew-core
-│ │ ├── Cargo.toml
-│ │ └── src
-│ │ ├── error.rs
-│ │ ├── lib.rs
-│ │ ├── message.rs
-│ │ ├── model.rs
-│ │ ├── provider.rs
-│ │ ├── stream.rs
-│ │ └── tool.rs
-│ ├── sinew-google
-│ │ ├── Cargo.toml
-│ │ └── src
-│ │ ├── auth.rs
-│ │ ├── client.rs
-│ │ ├── lib.rs
-│ │ ├── model_info.rs
-│ │ ├── stream.rs
-│ │ └── wire.rs
-│ ├── sinew-kimi
-│ │ ├── Cargo.toml
-│ │ └── src
-│ │ ├── auth.rs
-│ │ ├── client.rs
-│ │ ├── lib.rs
-│ │ ├── model_info.rs
-│ │ ├── stream.rs
-│ │ └── wire.rs
-│ ├── sinew-openai
-│ │ ├── Cargo.toml
-│ │ └── src
-│ │ ├── auth.rs
-│ │ ├── client.rs
-│ │ ├── lib.rs
-│ │ ├── model_info.rs
-│ │ ├── responses_stream.rs
-│ │ ├── stream.rs
-│ │ ├── websocket.rs
-│ │ └── wire.rs
-│ └── sinew-openrouter
-│ ├── Cargo.toml
-│ └── src
-│ ├── auth.rs
-│ ├── client.rs
-│ ├── lib.rs
-│ ├── model_info.rs
-│ ├── stream.rs
-│ └── wire.rs
-├── src-tauri
-│ ├── Cargo.toml
-│ ├── binaries
-│ │ └── .gitkeep
-│ ├── build.rs
-│ ├── tauri.sidecars.conf.json
-│ ├── tauri.conf.json
-│ ├── tauri.windows.conf.json
-│ ├── capabilities
-│ │ └── default.json
-│ ├── gen
-│ │ └── schemas
-│ │ ├── acl-manifests.json
-│ │ ├── capabilities.json
-│ │ ├── desktop-schema.json
-│ │ └── macOS-schema.json
-│ ├── icons
-│ │ ├── 128x128.png
-│ │ ├── 128x128@2x.png
-│ │ ├── 32x32.png
-│ │ ├── 64x64.png
-│ │ ├── Square107x107Logo.png
-│ │ ├── Square142x142Logo.png
-│ │ ├── Square150x150Logo.png
-│ │ ├── Square284x284Logo.png
-│ │ ├── Square30x30Logo.png
-│ │ ├── Square310x310Logo.png
-│ │ ├── Square44x44Logo.png
-│ │ ├── Square71x71Logo.png
-│ │ ├── Square89x89Logo.png
-│ │ ├── StoreLogo.png
-│ │ ├── icon.icns
-│ │ ├── icon.ico
-│ │ ├── icon.png
-│ │ ├── nsis-sidebar.bmp
-│ │ ├── source.svg
-│ │ ├── android
-│ │ │ ├── mipmap-anydpi-v26
-│ │ │ │ └── ic_launcher.xml
-│ │ │ ├── mipmap-hdpi
-│ │ │ │ ├── ic_launcher.png
-│ │ │ │ ├── ic_launcher_foreground.png
-│ │ │ │ └── ic_launcher_round.png
-│ │ │ ├── mipmap-mdpi
-│ │ │ │ ├── ic_launcher.png
-│ │ │ │ ├── ic_launcher_foreground.png
-│ │ │ │ └── ic_launcher_round.png
-│ │ │ ├── mipmap-xhdpi
-│ │ │ │ ├── ic_launcher.png
-│ │ │ │ ├── ic_launcher_foreground.png
-│ │ │ │ └── ic_launcher_round.png
-│ │ │ ├── mipmap-xxhdpi
-│ │ │ │ ├── ic_launcher.png
-│ │ │ │ ├── ic_launcher_foreground.png
-│ │ │ │ └── ic_launcher_round.png
-│ │ │ ├── mipmap-xxxhdpi
-│ │ │ │ ├── ic_launcher.png
-│ │ │ │ ├── ic_launcher_foreground.png
-│ │ │ │ └── ic_launcher_round.png
-│ │ │ └── values
-│ │ │ └── ic_launcher_background.xml
-│ │ └── ios
-│ │ ├── AppIcon-20x20@1x.png
-│ │ ├── AppIcon-20x20@2x-1.png
-│ │ ├── AppIcon-20x20@2x.png
-│ │ ├── AppIcon-20x20@3x.png
-│ │ ├── AppIcon-29x29@1x.png
-│ │ ├── AppIcon-29x29@2x-1.png
-│ │ ├── AppIcon-29x29@2x.png
-│ │ ├── AppIcon-29x29@3x.png
-│ │ ├── AppIcon-40x40@1x.png
-│ │ ├── AppIcon-40x40@2x-1.png
-│ │ ├── AppIcon-40x40@2x.png
-│ │ ├── AppIcon-40x40@3x.png
-│ │ ├── AppIcon-512@2x.png
-│ │ ├── AppIcon-60x60@2x.png
-│ │ ├── AppIcon-60x60@3x.png
-│ │ ├── AppIcon-76x76@1x.png
-│ │ ├── AppIcon-76x76@2x.png
-│ │ └── AppIcon-83.5x83.5@2x.png
-│ └── src
-│ ├── context.rs
-│ ├── conversations.rs
-│ ├── git.rs
-│ ├── lib.rs
-│ ├── main.rs
-│ ├── models.rs
-│ ├── platform.rs
-│ ├── providers.rs
-│ ├── state.rs
-│ ├── swarm.rs
-│ ├── terminal.rs
-│ ├── tests.rs
-│ ├── turns.rs
-│ ├── updater.rs
-│ ├── workflow.rs
-│ └── workspace.rs
-└── src
- ├── App.tsx
- ├── main.tsx
- ├── styles.css
- ├── types.ts
- ├── vite-env.d.ts
- ├── components
- │ ├── ConversationList.tsx
- │ ├── EditorPane.tsx
- │ ├── FileTree.tsx
- │ ├── GitPanel.tsx
- │ ├── SearchPane.tsx
- │ ├── SettingsPane.tsx
- │ ├── SinewMark.tsx
- │ ├── Splitter.tsx
- │ ├── TerminalPanel.tsx
- │ ├── UpdateBadge.tsx
- │ ├── UpdaterLockScreen.tsx
- │ ├── Welcome.tsx
- │ ├── WindowControls.tsx
- │ ├── Workspace.tsx
- │ └── chat
- │ ├── AIThinkingBlock.tsx
- │ ├── ChatPane.tsx
- │ ├── DotmSquare2.tsx
- │ ├── DotmSquare5.tsx
- │ ├── FileChangeBlock.tsx
- │ ├── Markdown.tsx
- │ ├── MermaidDiagram.tsx
- │ ├── PlanningNextMoveBlock.tsx
- │ ├── Questionnaire.tsx
- │ ├── TodoStrip.tsx
- │ ├── ToolCard.tsx
- │ ├── dotmatrix-core.tsx
- │ ├── dotmatrix-hooks.ts
- │ └── stream.ts
- ├── lib
- │ ├── fileIcon.ts
- │ ├── ipc.ts
- │ ├── language.ts
- │ ├── models.ts
- │ ├── recents.ts
- │ └── tools.ts
+# R�gles anti-boucle locales
+
+- **Dossier de travail des commandes** : **ATTENTION EXCEPTION** : Contrairement aux outils `read`/`write` qui exigent des chemins absolus, l'outil terminal (bash) ne supporte **PAS** les chemins absolus ni `cwd: "."`. Vous devez obligatoirement utiliser `cwd: ""` pour rester � la racine sous peine d'erreur de snapshot `path escapes workspace`.
+- **CHANGELOG avant modification** : juste avant toute modification de `CHANGELOG.md`, toujours relire `C:\dev\sinew\CHANGELOG.md` avec l'outil `read`, puis modifier `CHANGELOG.md` dans le m�me lot que les autres fichiers touch�s.
+- **Chemins de fichiers** : pour `read`, `edit_file` et `write_file`, utiliser les chemins absolus Windows du workspace, par exemple `C:\dev\sinew\...`.
+- **Grep Limit** : le param�tre `limit` est strictement obligatoire pour les outils `grep` et `glob`. Ne jamais l'omettre.
+- **Fichiers fant�mes** : avant d'appeler `grep` ou `read` sur un chemin sp�cifique, s'assurer que le fichier existe r�ellement sur le disque (ex: via `bash` avec `Test-Path`).
+# Code map
+- L'agent doit garder à jour cette carte simple des fichiers à chaque création, suppression, renommage, déplacement ou modification.
+
+.
+�S���� .gitignore
+�S���� AGENTS.md
+�S���� CHANGELOG.md
+�S���� Cargo.lock
+�S���� Cargo.toml
+�S���� index.html
+�S���� launch-sinew-dev.bat
+�S���� LICENSE
+�S���� package-lock.json
+�S���� package.json
+�S���� README.md
+�S���� .sinew
+�S���� scripts
+� �S���� check.ps1
+� �S���� compil.ps1
+� �S���� export-agent-descriptor.mjs
+� �S���� prepare-agent-bridge.mjs
+� �S���� prepare-sidecars.mjs
+� �S���� agent-bridge
+� � �S���� exec-handlers.mjs
+� � �S���� export-agent-fds-prost.mjs
+� � �S���� h2-bridge.mjs
+� � �S���� install-proto.mjs
+� � �S���� package-lock.json
+� � �S���� package.json
+� � �S���� run-stream.mjs
+� � �S���� test-live-rust.ps1
+� � �S���� test-live.ps1
+� � ����� vendor
+� � ����� agent_pb.ts
+� ����� mitm
+� �S���� check-mitm.ps1
+� �S���� install-mitmproxy.ps1
+� �S���� README.md
+� ����� start-mitmweb.ps1
+�S���� tsconfig.json
+�S���� tsconfig.node.json
+�S���� vite.config.ts
+�S���� .github
+� �S���� assets
+� � �S���� architecture.png
+� � �S���� harness.png
+� � �S���� hero.png
+� � �S���� modes.png
+� � �S���� screenshot.png
+� � ����� swarm.png
+� ����� workflows
+� �S���� release.yml
+� ����� security.yml
+�S���� crates
+� �S���� sinew-anthropic
+� � �S���� Cargo.toml
+� � ����� src
+� � �S���� auth.rs
+� � �S���� client.rs
+� � �S���� lib.rs
+� � �S���� model_info.rs
+� � �S���� stream.rs
+� � ����� wire.rs
+� �S���� sinew-app
+� � �S���� Cargo.toml
+� � ����� src
+� � �S���� agent.rs
+� � �S���� agent
+� � � �S���� assistant_message.rs
+� � � �S���� cancel.rs
+� � � �S���� clean_context.rs
+� � � �S���� compaction.rs
+� � � �S���� context.rs
+� � � �S���� events.rs
+� � � �S���� history.rs
+� � � �S���� mode.rs
+� � � �S���� tests.rs
+� � � �S���� tool_dispatch.rs
+� � � �S���� tool_summary.rs
+� � � ����� turn.rs
+� � �S���� bash.rs
+� � �S���� check_sota.rs
+� � �S���� codebase_search.rs
+� � �S���� compact.rs
+� � �S���� computer_use.rs
+� � �S���� delete_file.rs
+� � �S���� edit.rs
+� � �S���� editor_diagnostics.rs
+� � �S���� glob.rs
+� � �S���� grep.rs
+� � �S���� image.rs
+� � �S���� lib.rs
+� � �S���� list_dir.rs
+� � �S���� mcp.rs
+� � �S���� question.rs
+� � �S���� read.rs
+� � �S���� read_lints.rs
+� � �S���� ripgrep.rs
+� � �S���� skill.rs
+� � �S���� store.rs
+� � �S���� subagent.rs
+� � �S���� team.rs
+� � �S���� team
+� � � �S���� agent_turns.rs
+� � � �S���� context.rs
+� � � �S���� descriptors.rs
+� � � �S���� launch.rs
+� � � �S���� live.rs
+� � � �S���� messaging.rs
+� � � �S���� model.rs
+� � � �S���� render.rs
+� � � �S���� session.rs
+� � � �S���� status_stop.rs
+� � � �S���� task_board.rs
+� � � ����� tests.rs
+� � �S���� text.rs
+� � �S���� todo.rs
+� � �S���� tool_names.rs
+� � �S���� tool_run.rs
+� � �S���� web.rs
+� � �S���� workspace.rs
+� � ����� write.rs
+� �S���� sinew-core
+� � �S���� Cargo.toml
+� � ����� src
+� � �S���� error.rs
+� � �S���� lib.rs
+� � �S���� message.rs
+� � �S���� model.rs
+� � �S���� provider.rs
+� � �S���� stream.rs
+� � ����� tool.rs
+� �S���� sinew-deepseek
+� � �S���� Cargo.toml
+� � ����� src
+� � �S���� auth.rs
+� � �S���� client.rs
+� � �S���� lib.rs
+� � �S���� model_info.rs
+� � �S���� stream.rs
+� � ����� wire.rs
+� �S���� sinew-google
+� � �S���� Cargo.toml
+� � ����� src
+� � �S���� auth.rs
+� � �S���� client.rs
+� � �S���� lib.rs
+� � �S���� model_info.rs
+� � �S���� stream.rs
+� � ����� wire.rs
+� �S���� sinew-index
+� � �S���� Cargo.toml
+� � ����� src
+� � �S���� background.rs
+� � �S���� chunk.rs
+� � �S���� embeddings.rs
+� � �S���� indexer.rs
+� � �S���� lib.rs
+� � �S���� process.rs
+� � �S���� search.rs
+� � ����� store.rs
+� �S���� sinew-kimi
+� � �S���� Cargo.toml
+� � ����� src
+� � �S���� auth.rs
+� � �S���� client.rs
+� � �S���� lib.rs
+� � �S���� model_info.rs
+� � �S���� stream.rs
+� � ����� wire.rs
+� �S���� sinew-openai
+� � �S���� Cargo.toml
+� � ����� src
+� � �S���� auth.rs
+� � �S���� client.rs
+� � �S���� lib.rs
+� � �S���� model_info.rs
+� � �S���� responses_stream.rs
+� � �S���� stream.rs
+� � �S���� websocket.rs
+� � ����� wire.rs
+� ����� sinew-openrouter
+� �S���� Cargo.toml
+� ����� src
+� �S���� auth.rs
+� �S���� client.rs
+� �S���� lib.rs
+� �S���� model_info.rs
+� �S���� stream.rs
+� ����� wire.rs
+� ����� sinew-cursor
+� ����� sinew-ollama
+� �S���� Cargo.toml
+� ����� src
+� �S���� auth.rs
+� �S���� client.rs
+� �S���� lib.rs
+� �S���� model_info.rs
+� �S���� stream.rs
+� ����� wire.rs
+� ����� sinew-cursor
+� �S���� Cargo.toml
+� ����� src
+� �S���� agent
+� � �S���� bridge.rs
+� � �S���� client_proto.rs
+� � �S���� connect_proto.rs
+� � �S���� conversation_id.rs
+� � �S���� exec_handler.rs
+� � �S���� h2_client.rs
+� � �S���� mod.rs
+� � �S���� models.rs
+� � �S���� proto_dynamic.rs
+� � �S���� proto_pool.rs
+� � �S���� retry.rs
+� � �S���� run_h2.rs
+� � �S���� run_request.rs
+� � �S���� rust_bridge.rs
+� � �S���� server_decode.rs
+� � �S���� setup.rs
+� � �S���� state.rs
+� � �S���� tools.rs
+� � �S���� transcript.rs
+� � ����� transport.rs
+� �S���� auth
+� � �S���� composer.rs
+� � �S���� mod.rs
+� � ����� oauth.rs
+� �S���� proto
+� � �S���� agent.fds
+� � �S���� agent.pb
+� � ����� README.md
+� �S���� client.rs
+� �S���� connect.rs
+� �S���� context_injection.rs
+� �S���� conversation.rs
+� �S���� encryption.rs
+� �S���� identity.rs
+� �S���� images.rs
+� �S���� lib.rs
+� �S���� model_info.rs
+� �S���� sanitize.rs
+� �S���� stream_state.rs
+� �S���� tests.rs
+� �S���� tools.rs
+� �S���� usage.rs
+� ����� workspace.rs
+�S���� src-tauri
+� �S���� Cargo.toml
+� �S���� PROVIDERS.md
+� �S���� binaries
+� � ����� .gitkeep
+� �S���� build.rs
+� �S���� tauri.sidecars.conf.json
+� �S���� tauri.conf.json
+� �S���� tauri.windows.conf.json
+� �S���� capabilities
+� � ����� default.json
+� �S���� gen
+� � ����� schemas
+� � �S���� acl-manifests.json
+� � �S���� capabilities.json
+� � �S���� desktop-schema.json
+� � ����� macOS-schema.json
+� �S���� icons
+� � �S���� 128x128.png
+� � �S���� 128x128@2x.png
+� � �S���� 32x32.png
+� � �S���� 64x64.png
+� � �S���� Square107x107Logo.png
+� � �S���� Square142x142Logo.png
+� � �S���� Square150x150Logo.png
+� � �S���� Square284x284Logo.png
+� � �S���� Square30x30Logo.png
+� � �S���� Square310x310Logo.png
+� � �S���� Square44x44Logo.png
+� � �S���� Square71x71Logo.png
+� � �S���� Square89x89Logo.png
+� � �S���� StoreLogo.png
+� � �S���� icon.icns
+� � �S���� icon.ico
+� � �S���� icon.png
+� � �S���� nsis-sidebar.bmp
+� � �S���� source.svg
+� � �S���� android
+� � � �S���� mipmap-anydpi-v26
+� � � � ����� ic_launcher.xml
+� � � �S���� mipmap-hdpi
+� � � � �S���� ic_launcher.png
+� � � � �S���� ic_launcher_foreground.png
+� � � � ����� ic_launcher_round.png
+� � � �S���� mipmap-mdpi
+� � � � �S���� ic_launcher.png
+� � � � �S���� ic_launcher_foreground.png
+� � � � ����� ic_launcher_round.png
+� � � �S���� mipmap-xhdpi
+� � � � �S���� ic_launcher.png
+� � � � �S���� ic_launcher_foreground.png
+� � � � ����� ic_launcher_round.png
+� � � �S���� mipmap-xxhdpi
+� � � � �S���� ic_launcher.png
+� � � � �S���� ic_launcher_foreground.png
+� � � � ����� ic_launcher_round.png
+� � � �S���� mipmap-xxxhdpi
+� � � � �S���� ic_launcher.png
+� � � � �S���� ic_launcher_foreground.png
+� � � � ����� ic_launcher_round.png
+� � � ����� values
+� � � ����� ic_launcher_background.xml
+� � ����� ios
+� � �S���� AppIcon-20x20@1x.png
+� � �S���� AppIcon-20x20@2x-1.png
+� � �S���� AppIcon-20x20@2x.png
+� � �S���� AppIcon-20x20@3x.png
+� � �S���� AppIcon-29x29@1x.png
+� � �S���� AppIcon-29x29@2x-1.png
+� � �S���� AppIcon-29x29@2x.png
+� � �S���� AppIcon-29x29@3x.png
+� � �S���� AppIcon-40x40@1x.png
+� � �S���� AppIcon-40x40@2x-1.png
+� � �S���� AppIcon-40x40@2x.png
+� � �S���� AppIcon-40x40@3x.png
+� � �S���� AppIcon-512@2x.png
+� � �S���� AppIcon-60x60@2x.png
+� � �S���� AppIcon-60x60@3x.png
+� � �S���� AppIcon-76x76@1x.png
+� � �S���� AppIcon-76x76@2x.png
+� � ����� AppIcon-83.5x83.5@2x.png
+� ����� src
+� �S���� context.rs
+� �S���� conversations.rs
+� �S���� git.rs
+� �S���� lib.rs
+� �S���� main.rs
+� �S���� models.rs
+� �S���� platform.rs
+� �S���� providers.rs
+� �S���� state.rs
+� �S���� swarm.rs
+� �S���� terminal.rs
+� �S���� tests.rs
+� �S���� turns.rs
+� �S���� updater.rs
+� �S���� workflow.rs
+� ����� workspace.rs
+����� src
+ �S���� App.tsx
+ �S���� main.tsx
+ �S���� styles.css
+ �S���� types.ts
+ �S���� vite-env.d.ts
+ �S���� components
+ � �S���� ConversationList.tsx
+ � �S���� EditorPane.tsx
+ � �S���� FileTree.tsx
+ � �S���� GitPanel.tsx
+ � �S���� ImageContextMenu.tsx
+ � �S���� SearchPane.tsx
+ � �S���� SettingsPane.tsx
+ � �S���� SinewMark.tsx
+ � �S���� Splitter.tsx
+ � �S���� TerminalPanel.tsx
+ � �S���� UpdateBadge.tsx
+ � �S���� UpdaterLockScreen.tsx
+ � �S���� Welcome.tsx
+ � �S���� WindowControls.tsx
+ � �S���� Workspace.tsx
+ � ����� chat
+ � �S���� AIThinkingBlock.tsx
+ � �S���� ChatPane.tsx
+ � �S���� DotmSquare2.tsx
+ � �S���� DotmSquare5.tsx
+ � �S���� FileChangeBlock.tsx
+ � �S���� Markdown.tsx
+ � �S���� MermaidDiagram.tsx
+ � �S���� PlanningNextMoveBlock.tsx
+ � �S���� Questionnaire.tsx
+ � �S���� TodoStrip.tsx
+ � �S���� ToolCard.tsx
+ � �S���� dotmatrix-core.tsx
+ � �S���� dotmatrix-hooks.ts
+ � ����� stream.ts
+ �S���� lib
+ � �S���� customIcons.ts
+ � �S���� fileIcon.ts
+ � �S���� frRuntime.ts
+ � �S���� ipc.ts
+ � �S���� language.ts
+ � �S���� locale.ts
+ � �S���� models.ts
+ � �S���� quotas.ts
+ � �S���� recents.ts
+ � ����� tools.ts
+����� sinew-chrome-bridge
+ �S���� add_to_sinew.py
+ �S���� background.js
+ �S���� com.sinew.chrome_bridge.json
+ �S���� e2e-local.mjs
+ �S���� e2e-structured.mjs
+ �S���� icon-128.png
+ �S���� icon-32.png
+ �S���� icon-64.png
+ �S���� icon.jpg
+ �S���� interact_chrome.js
+ �S���� launch_chrome_silent.bat
+ �S���� manifest.json
+ �S���� mcp_server.js
+ �S���� native-host-wrapper.exe
+ �S���� native_host.bat
+ �S���� package-lock.json
+ �S���� package.json
+ �S���� popup.html
+ �S���� popup.js
+ �S���� register.ps1
+ �S���� run_bridge.bat
+ �S���� run_sinew_bridge.bat
+ �S���� server.js
+ �S���� sinew_cursor.js
+ ����� native-host-wrapper
+ �S���� Cargo.toml
+ ����� src
+ ����� main.rs
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..3cc8e76b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,647 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [2026-05-30 22:25:25]
+- `src/styles.css` : Harmonisation des barres de séparation (diviseurs et bordures) entre le lecteur de document (éditeur/preview) et le panneau de chat. Uniformisation de la hauteur de l'en-tête du chat (`.chat-head`) à 40px pour l'aligner parfaitement avec les onglets de l'éditeur (`.tabs`), et ajout d'une bordure inférieure de séparation nette pour ces deux en-têtes. Mise à jour de la couleur de fond des séparateurs (`.gutter` et `.gutter-h`) avec la couleur standard de bordure (`var(--line)`) et ajout d'un effet de surbrillance réactif lors du survol ou du glissement. Simplification et correction des bordures d'arrière-plan du thème IA en ciblant uniquement les colonnes principales et en supprimant les bordures internes redondantes des sous-composants.
+
+## [2026-05-30 22:27:43]
+- `README.md` : Réintégration du contenu officiel en anglais (les modes, les outils, le swarm, etc.) et retrait de toutes les instructions et sections relatives à l'installation et à la compilation.
+
+## [2026-05-30 22:24:17]
+- `README.md` : Condensation du fichier et intégration de la présentation des fonctionnalités majeures du fork premium.
+
+## [2026-05-30 22:19:45]
+- `crates/sinew-app/src/store.rs` : Ajout d'une table SQLite `provider_states` pour gérer l'archivage/restauration des fournisseurs avec les méthodes correspondantes `set_provider_status`, `get_provider_status` et `list_archived_providers`. Passage à la version 12 du schéma. Ajout du test unitaire `provider_archiving_and_restoring` pour valider le bon fonctionnement de l'archivage/restauration en base SQLite.
+- `src-tauri/src/providers.rs` : Enregistrement des commandes Tauri `archive_provider`, `restore_provider`, `list_archived_providers` et filtrage des fournisseurs archivés dans `list_configured_model_providers`.
+- `src-tauri/src/lib.rs` : Exposition des nouvelles commandes d'archivage et de restauration des fournisseurs.
+- `src/lib/ipc.ts` : Ajout des liaisons IPC pour les méthodes `archiveProvider`, `restoreProvider` et `listArchivedProviders`.
+- `src/components/SettingsPane.tsx` : Intégration de la gestion de l'archivage et de la restauration dans les cartes de fournisseurs, masquage des éléments archivés et ajout d'un filtre d'affichage (Actifs / Archivés). Ajout des boutons d'archivage/restauration dans les en-têtes de toutes les cartes de fournisseurs (DeepSeek, OpenRouter, Ollama, ProviderCard).
+
+## [2026-05-30 22:17:59]
+- `src-tauri/src/turns.rs` : Prise en charge du niveau de réflexion (thinking/effort) dans les paramètres d'optimisation de prompt en utilisant `model_with_optional_selection`.
+- `src/lib/ipc.ts` : Ajout de l'état `autoOptimizeThinking` dans la configuration active des options d'optimisation magique.
+- `src/components/SettingsPane.tsx` : Intégration d'un sélecteur de niveau de réflexion (dropdown) dans l'interface d'Optimisation Magique Auto, avec alignement automatique du niveau lors du changement de modèle.
+- `src/components/chat/ChatPane.tsx` : Transmission du niveau de réflexion sélectionné lors des appels à l'API d'optimisation magique automatique et manuelle.
+
+## [2026-05-30 22:15:36]
+- `src/lib/ipc.ts` : Ajout de la clé de configuration `autoOptimizeMode` avec prise en charge des trois options d'optimisation ("auto", "manual", "disabled").
+- `src/components/SettingsPane.tsx` : Remplacement du commutateur activé/désactivé par un choix explicite à trois options : "Automatique", "Actif à la demande", "Désactivé".
+- `src/components/chat/ChatPane.tsx` : Affichage permanent du bouton d'optimisation (baguette magique) en bas dans la barre de saisie, avec une désactivation et un titre adaptés au mode choisi.
+- `src/styles.css` : Ajustement de l'opacité et du curseur du bouton d'optimisation lorsqu'il est rendu inactif.
+
+## [2026-05-30 22:09:30]
+- `src/components/chat/ChatPane.tsx` : Intégration de l'effet visuel et du blocage de l'interface lors de l'Optimisation Magique. Toutes les commandes de saisie et de validation sont désactivées durant l'optimisation, et un scroll automatique amène le squelette d'optimisation en vue.
+- `src/styles.css` : Ajout des animations de reflets mouvants (effet shimmer) et du style de squelette de chargement pour la carte d'optimisation, ainsi que le style semi-opaque inactif pour le conteneur du composeur.
+
+## [2026-05-30 22:08:04]
+- `src/components/SettingsPane.tsx` : Correction d'une balise div non fermée dans la section "Power User". Le manque de fermeture pour le conteneur grid (Ligne 4) imbriquait indûment la Ligne 5 et le choix du mode d'affichage, provoquant un alignement incorrect dans la colonne gauche et un vide dans la colonne droite. Le positionnement et l'alignement des options sont désormais rétablis.
+
+## [2026-05-30 22:06:13]
+- `src/styles.css` : Restriction des halos lumineux d'arrière-plan du chat et de leurs animations au seul mode IA (`data-theme="ai"`). En modes normaux (jour, nuit et système), ces animations et halos sont désormais désactivés pour conserver une interface sobre et sans distractions.
+
+## [2026-05-30 21:45:24]
+- `C:\Users\julie\OneDrive\Bureau\Sinew_0.1.27_x64-setup.exe` : Recompilation de l'installateur Windows et copie sur le Bureau pour fournir une version installable fraîche.
+
+## [2026-05-30 21:38:12]
+- `src/lib/quotas.ts` : Ajustement de la pastille de quota OpenAI/Codex. La pastille suit maintenant la fenêtre courte 5h tant que la semaine n'est pas totalement vide, et ne passe rouge à cause de la semaine que lorsqu'il ne reste vraiment plus rien.
+
+## [2026-05-30 21:15:44]
+- `.gitignore` : Exclusion des fichiers temporaires `_node_*.txt` et `_payload*.json` produits par les tests locaux du pont agent, afin d'éviter d'envoyer des journaux ou jetons de session par erreur.
+- `scripts/agent-bridge/_node_err.txt`, `scripts/agent-bridge/_node_out.txt`, `scripts/agent-bridge/_payload25.json` : Suppression des traces de test locales non nécessaires au produit.
+
+## [2026-05-30 21:09:14]
+- `crates/sinew-cursor/src/agent/proto_dynamic.rs` : Correction de la lecture des messages Cursor pour ignorer les enveloppes vides. Composer 2.5 Fast n'était pas refusé : Sinew ouvrait la mauvaise enveloppe, comme un courrier trié dans la mauvaise boîte, et ne répondait donc pas aux demandes de blobs de Cursor.
+- `crates/sinew-cursor/src/agent/run_h2.rs` : Conservation de la correction du lecteur HTTP/2 qui empêchait la boucle de rester bloquée après le premier battement de vie du serveur, lecture correcte des blobs enregistrés par Cursor, et retrait des traces de diagnostic temporaires.
+- `crates/sinew-cursor/src/tests.rs` : Ajout d'un choix de modèle par variable de test et remise en forme du test live pour vérifier explicitement `composer-2.5-fast`.
+- `crates/sinew-cursor/src/identity.rs` : Conservation de l'option de version Cursor configurable pour comparer facilement le comportement avec le client officiel sans retoucher le code.
+
+## [2026-05-30 21:09:49]
+- `src-tauri/src/state.rs` : Ajout du chemin de mémoire persistante `.sinew/memory.md` pour conserver un carnet de projet entre sessions.
+- `src-tauri/src/turns.rs` : Injection automatique de `.sinew/memory.md` dans le prompt système quand le fichier existe, afin que l'agent retrouve les décisions, l'état du projet et les pièges connus dès le début d'une session.
+- `.sinew/memory.md` : Création du carnet local initial avec les décisions sur le rôle réel du GPU, la recherche hybride et la mémoire persistante.
+## [2026-05-30 21:05:00]
+- `src-tauri/src/tests.rs` : Ajout d'un test de latence réel (ignoré par défaut, `flash_optimizer_race`) qui met en course DeepSeek V4 Flash et Gemini 3.5 Flash sur la tâche d'optimisation de prompt et mesure le temps jusqu'au 1er mot et le temps de réponse complète. Résultat sur 4 runs : 1er mot quasi à égalité (DeepSeek légèrement devant), mais Gemini 3.5 Flash termine la réponse complète plus vite et de manière plus régulière — meilleur choix pour l'optimiseur.
+
+## [2026-05-30 20:50:00]
+- `src-tauri/src/turns.rs` : Correction de l'Optimisation Magique Auto qui ne réécrivait jamais le prompt. Cause racine : on demandait au modèle un JSON strict, mais un prompt réécrit est souvent multi-lignes, ce qui produit un JSON invalide (sauts de ligne bruts) et faisait échouer l'analyse en silence — le brouillon d'origine était alors envoyé tel quel. Nouveau format texte délimité robuste (`MODE:` + `===PROMPT===`), avec repli sur l'ancien JSON puis sur le texte brut, et normalisation du mode (act/plan/goal). L'option reste en envoi automatique invisible à l'Entrée (choix utilisateur).
+
+## [2026-05-30 20:41:00]
+- `crates/sinew-app/src/agent/boost_distill.rs` (nouveau) : Auto-distillation des grosses sorties d'outils dans la boucle de l'agent. Quand le Boost Local est actif (drapeau `SINEW_BOOST_DISTILLER`), les sorties volumineuses de `bash`/`bash_input`/`web_fetch` (> ~6 000 jetons) sont résumées par le modèle local avant d'entrer dans le contexte du modèle principal. Garde-fous SOTA : jamais sur `read`/`grep`/`codebase_search` (texte exact requis pour les éditions), jamais sur une sortie en erreur, fin de sortie conservée brute (codes d'erreur), repli sur la sortie brute si le distillateur échoue.
+- `crates/sinew-app/src/agent.rs` : Déclaration du module `boost_distill`.
+- `crates/sinew-app/src/agent/turn.rs` : Branchement de la distillation sur le contenu envoyé au modèle (l'interface continue d'afficher la sortie complète).
+- `src-tauri/src/boost.rs` : Pose/retrait du drapeau `SINEW_BOOST_DISTILLER` au démarrage/arrêt du Boost Local (même mécanisme que la sémantique).
+
+## [2026-05-30 20:24:01]
+- `src-tauri/src/boost.rs` (nouveau) : Ajout de la fonctionnalité « Boost Local ». Un seul interrupteur démarre le serveur Ollama si besoin, charge un petit modèle distillateur (`qwen2.5:3b` par défaut) et le garde en mémoire toute la session (`keep_alive = -1`), et active la recherche sémantique vectorielle. Expose 4 commandes : `boost_local_status`, `boost_local_start`, `boost_local_stop`, `boost_local_distill` (cette dernière compresse un gros texte — log, fichier, sortie d'outil — en faits utiles pour économiser les jetons des IA).
+- `src-tauri/src/lib.rs` : Déclaration du module `boost` et enregistrement des 4 commandes dans le handler Tauri.
+- `src/lib/ipc.ts` : Ajout du type `BoostStatus` et des appels IPC `boostLocalStatus`, `boostLocalStart`, `boostLocalStop`, `boostLocalDistill`.
+- `src/components/SettingsPane.tsx` : Ajout de la carte/bouton « Boost Local » (toggle Activé/Désactivé) avec voyants d'état (Ollama, distillateur chargé, sémantique) ; l'activation aligne aussi le toggle de recherche sémantique existant.
+- `scripts/boost-proof.ps1` (nouveau) : Script de preuve mesurée — démontre l'économie de jetons (recherche ciblée + distillation locale) sur un vrai fichier du projet (99,3% de jetons en moins, réponse correcte).
+## [2026-05-30 19:41:23]
+- `crates/sinew-ollama/*` (nouveau crate) : Ajout d'un fournisseur Ollama complet pour utiliser les modèles installés localement. Détecte automatiquement tous les modèles via l'API locale d'Ollama (`/api/tags` + `/api/show` pour le contexte et les capacités outils/vision/raisonnement), discute en streaming via l'endpoint compatible OpenAI, et stocke l'adresse du serveur (par défaut `http://localhost:11434`) au lieu d'une clé.
+- `Cargo.toml`, `src-tauri/Cargo.toml` : Déclaration et branchement du nouveau crate `sinew-ollama`.
+- `crates/sinew-app/src/store.rs` : Ajout du stockage de la liste des modèles Ollama (`load_ollama_models` / `save_ollama_models`).
+- `src-tauri/src/models.rs` : Ajout des structures `OllamaProviderStatus` et `ConnectOllamaInput`.
+- `src-tauri/src/providers.rs` : Ajout des fonctions d'installation/retrait du fournisseur et des commandes `get_ollama_provider_status`, `connect_ollama_provider`, `refresh_ollama_models`, `list_ollama_models`, `disconnect_ollama_provider`.
+- `src-tauri/src/lib.rs` : Enregistrement du fournisseur Ollama au démarrage, dans le modèle par défaut et dans la liste des commandes exposées au frontend.
+- `src/types.ts`, `src/lib/ipc.ts`, `src/lib/models.ts` : Ajout du type/carte Ollama, des appels IPC et de l'intégration des modèles Ollama dans les listes de choix de modèle.
+- `src/components/SettingsPane.tsx`, `src/styles.css`, `src/lib/frRuntime.ts` : Ajout de la carte Ollama dans les paramètres (connexion par adresse, détection auto, actualisation, déconnexion) avec son style et ses traductions françaises.
+- `src/components/chat/ChatPane.tsx` : Chargement des modèles Ollama et affichage dans le sélecteur de modèle.
+
+## [2026-05-30 19:42:00]
+- `src-tauri/src/rules.rs` : Ajout d'un filet de sécurité à la consolidation par IA. Avant d'écraser `instructions_consolidated.md`, l'ancienne version est sauvegardée (`instructions_consolidated.bak.md`), et la réécriture est refusée si le résultat de l'IA est anormalement court (moins de la moitié de l'actuel) afin d'éviter toute perte silencieuse de règles en cas de réponse tronquée du modèle.
+
+## [2026-05-30 19:17:08]
+- `crates/sinew-app/src/agent/repeat_guard.rs` (nouveau) : Ajout du maillon « Capture » manquant du système d'auto-amélioration. Détecte quand l'agent rejoue la même commande shell ou le même appel d'outil en erreur sans progresser : injecte un rappel fort à 3 répétitions, puis enregistre l'incident dans `errors_raw.json` et coupe le tour à 4. Ces incidents alimentent désormais automatiquement la consolidation en règles globales (avant, `errors_raw.json` n'était jamais rempli par le code).
+- `crates/sinew-app/src/agent/turn.rs` : Branchement du détecteur de boucle dans la boucle d'outils (observation de chaque résultat, injection du rappel dans le prompt système, enregistrement + arrêt propre du tour avec événement d'erreur quand une boucle est avérée).
+- `crates/sinew-app/src/agent.rs` : Déclaration du module `repeat_guard`.
+- `crates/sinew-app/Cargo.toml` : Ajout de la dépendance `chrono` (horodatage des incidents).
+## [2026-05-30 18:22:51]
+- `src/components/SettingsPane.tsx` : Déplacement de "Optimisation Magique Auto" en tête de l'onglet Power User sous forme de carte pleine largeur responsive. Rétablissement de l'accès au menu de choix de modèle d'analyse indépendamment de l'état d'activation de l'optimisation. Transfert de l'option "Recherche de mise à jour automatique" vers l'onglet Système (Diagnostics) pour une cohérence thématique parfaite.
+- `src/styles.css` : Ajout de la classe `.settings-pane__select` pour habiller proprement les listes déroulantes de paramètres.
+
+## [2026-05-30 18:21:34]
+- `crates/sinew-anthropic/src/client.rs` : Ajout de la méthode `get_usage` pour récupérer les limites de quota Anthropic via l'API OAuth `/api/oauth/usage` avec gestion du rafraîchissement des jetons.
+- `src-tauri/src/providers.rs` : Ajout de la commande Tauri `get_anthropic_usage` et définition du cache associé pour récupérer l'utilisation de l'abonnement Anthropic.
+- `src-tauri/src/lib.rs` : Exposition de la commande Tauri `get_anthropic_usage` au processus frontal.
+- `src/lib/ipc.ts` : Déclaration de la méthode IPC `getAnthropicUsage`.
+- `src/lib/quotas.ts` : Implémentation de la récupération et du parsing du quota pour le fournisseur Anthropic en utilisant les données de l'API d'abonnement Anthropic.
+
+## [2026-05-30 18:18:38]
+- `src/components/SettingsPane.tsx` : Correction des problèmes d'encodage pour le bouton "Jour" et "Système" dans l'onglet Apparence, remplacement par des emojis corrects. Remplacement des réglages de taille numérique par une structure de classe CSS unifiée et correction de l'icône de titre pour DeepSeek.
+- `src/styles.css` : Ajout de la classe `.settings-pane__number-adjuster` pour uniformiser la disposition des ajusteurs numériques. Conversion de la règle responsive `@media` en `@container` query sur les lignes à deux colonnes pour qu'elles se comportent correctement suivant la taille du panneau de réglages, et élargissement de la bascule adaptative des cartes de 500px à 600px pour une meilleure lisibilité mobile.
+
+## [2026-05-30 17:51:00]
+- `src/components/chat/chatUtils.ts` : Ajout de la fonction `playNotificationSound` utilisant l'API Web Audio pour jouer un carillon agréable à la fin d'un chat.
+- `src/components/chat/ChatPane.tsx` : Importation et déclenchement de la sonnerie de notification lors de la réception de l'événement `turn_finished`.
+- `src/components/SettingsPane.tsx` : Ajout de l'option de configuration pour activer/désactiver la sonnerie de fin de chat sous l'onglet Apparence, avec synchronisation de l'état dans le stockage local.
+
+## [2026-05-30 16:58:12]
+- `src/components/Workspace.tsx` : Correction du comportement des menus de fermeture d'onglets (Fermer les autres, Fermer à droite, Fermer tous) pour qu'ils ferment également l'onglet "Paramètres" s'il est ouvert et actif, évitant ainsi le rendu d'un écran noir vide.
+- `src/components/EditorPane.tsx` : Mise à jour du composant `EditorTabContextMenu` pour accepter la propriété `settingsOpen`. Le bouton "Fermer les onglets à droite" reste désormais cliquable même sur le dernier onglet fichier si l'onglet Paramètres est ouvert (puisqu'il est situé à sa droite).
+
+## [2026-05-30 16:55:47]
+- `REFACTORING_PLAN.md` : Création du rapport d'audit global. Identification et priorisation des 3 meilleures cibles de rationalisation : unification des providers LLM (Rust), découpage des composants monolithiques SettingsPane/ChatPane (React), et standardisation du pipeline d'outils (Rust).
+
+## [2026-05-30 16:17:12]
+- `src-tauri/src/workspace.rs` : Automatisation du déploiement du daemon Linux "Super SSH". Si le binaire local est absent, la commande de démarrage télécharge automatiquement la dernière release stable du daemon `sinew-agent-daemon-linux` via `curl` sur le serveur distant depuis GitHub, garantissant une utilisation sans configuration pour les utilisateurs.
+- `.github/workflows/release.yml` : Ajout d'une étape de compilation native du daemon (`cargo build --release -p sinew-agent-daemon`) sur les runners Linux de l'intégration continue. Le binaire est ensuite publié en tant qu'asset additionnel (`sinew-agent-daemon-linux`) sur les releases GitHub.
+
+
+
+## [2026-05-30 16:01:00]
+- `src-tauri/src/workspace.rs` : Création de la commande Tauri `mount_super_ssh_workspace` qui orchestre la connexion "Super SSH". Cette commande déploie dynamiquement le daemon natif compilé sur le serveur distant via SCP, le démarre en arrière-plan, puis met en place un port forwarding SSH local (47990 -> 127.0.0.1:47990) sans bloquer l'interface.
+- `src-tauri/src/workspace.rs` : Ajout de la méthode `proxy_to_daemon` et interception des requêtes de fichiers (`list_workspace_entries_command`, `list_workspace_files_command`, `read_workspace_file_command`, `write_workspace_file_command`, `search_workspace_files_command`). Si l'espace de travail est préfixé par `super-ssh://`, les commandes ne lisent pas le disque local mais encapsulent l'appel en JSON et le transmettent au daemon distant sur le port 47990 pour une latence nulle.
+- `src-tauri/src/turns.rs` : Modification du routeur d'agents (`run_turn_via_daemon`). En mode Super SSH, la communication ne s'établit plus sur le pipe nommé Windows `\\.\pipe\sinew-agent-ipc` mais via une connexion TCP directe (`127.0.0.1:47990`) vers le daemon Linux du serveur.
+- `crates/sinew-app/src/workspace.rs` : Dérivation et exposition complète des traits de désérialisation (`Deserialize`) sur toutes les structures liées aux résultats de requêtes de l'espace de travail (`WorkspaceEntry`, `FileDocument`, `WorkspaceSearchResult`, etc.) pour supporter le parsing des données distantes renvoyées par le proxy.
+- `src-tauri/src/lib.rs` : Exposition de la commande système `mount_super_ssh_workspace` au processus IPC Tauri pour permettre au client React de l'invoquer depuis `Welcome.tsx`.
+
+## [2026-05-30 16:09:40]
+- `src-tauri/src/turns.rs` : Assouplissement du prompt système de la règle "Maquettes Visuelles Automatiques" suite aux retours utilisateurs. L'agent est désormais encouragé à générer proactivement des diagrammes Mermaid pour illustrer ses explications ou son architecture, au lieu de s'en priver par peur de bloquer l'édition de fichiers.
+- `src/components/SettingsPane.tsx` : Mise à jour de la description UI de la règle des maquettes visuelles pour refléter le changement (passage d'un comportement passif "uniquement si demandé" à un comportement proactif "Génère spontanément des schémas").
+
+## [2026-05-30 16:05:00]
+- `crates/sinew-agent-daemon/src/protocol.rs` & `crates/sinew-agent-daemon/src/main.rs` : Ajout des requêtes `ListEntries`, `ListAllFiles`, `ReadFile` et `WriteFile` au protocole IPC du démon. Cela permet au frontend de lire et écrire des fichiers distants via le proxy TCP en mode Super SSH, sans nécessiter de point de montage SSHFS, garantissant une latence nulle.
+- `src/components/Welcome.tsx` : Ajout de l'option de sélection "Super SSH (Native Agent)" dans le formulaire de connexion SSH pour utiliser le nouveau mode de connexion proxy distant SOTA.
+- `src/lib/ipc.ts` : Ajout de la méthode `mountSuperSshWorkspace` qui appelle la commande Tauri `connect_super_ssh` de `@backend_ssh` pour gérer la connexion "Super SSH".
+
+## [2026-05-30 16:01:00]
+- `crates/sinew-agent-daemon/src/main.rs` : Adaptation du démon persistant pour écouter sur le port TCP `127.0.0.1:47990` sur les environnements Linux (en plus de l'écoute sur le Named Pipe sous Windows), permettant la création de l'architecture "Super SSH" et la connexion proxy du frontend vers le serveur distant.
+
+
+- `src/components/SettingsPane.tsx` : Amélioration de "Optimisation Magique Auto" qui s'active de manière totalement invisible lors de l'appui sur "Entrée" dans le panneau de chat.
+- `src/components/chat/ChatPane.tsx` : Interception de l'envoi de message pour appeler l'API d'optimisation, basculer le mode (Action, Plan, Objectif) en temps réel, puis relayer automatiquement le prompt structuré à l'agent sans aucune friction.
+- `src-tauri/src/turns.rs` : Remplacement complet de la méthode d'extraction du JSON par une recherche textuelle du bloc JSON au lieu du pattern `trim` pour garantir que le prompt de retour soit correctement identifié même s'il est noyé dans le texte.
+
+## [2026-05-30 16:02:15]
+- `src/components/SettingsPane.tsx` : Ajout de deux nouvelles options avancées (Power User) dans l'interface : "Résolution Stricte des Problèmes" (pour interdire à l'agent de contourner les erreurs) et "Implémentation Complète" (pour interdire les faux blocs de code ou les commentaires TODOs).
+- `src/lib/ipc.ts` : Transmission des nouvelles préférences `strictProblemSolving` et `fullImplementation` via les payloads IPC d'estimation et d'envoi de messages.
+- `src-tauri/src/state.rs` : Création des constantes `DEFAULT_STRICT_PROBLEM_SOLVING_PROMPT` et `DEFAULT_FULL_IMPLEMENTATION_PROMPT` définissant les consignes à injecter dans le contexte de l'IA.
+- `src-tauri/src/models.rs` & `src-tauri/src/turns.rs` & `src-tauri/src/context.rs` : Injection de ces nouvelles instructions système aux prompts racines du moteur Rust.
+
+## [2026-05-30 16:01:20]
+- `src-tauri/src/state.rs` & `src-tauri/src/turns.rs` : Ajout d'une consigne système permanente (`DEFAULT_SSH_OPTIMIZATION_PROMPT`) pour les agents. Lorsqu'ils détectent être sur un espace de travail distant monté en SSHFS, les agents utiliseront désormais leurs outils MCP natifs (`ssh_exec`) pour installer des utilitaires (SOTA) et exécuter des scripts directement sur le serveur, afin de contourner la latence réseau des outils de recherche de fichiers en local.
+
+
+
+## [2026-05-30 15:44:34]
+- `src/components/Workspace.tsx` : Suppression du composant de connexion SSH dans la barre latérale inférieure de l'espace de travail. Le bouton "Se connecter" et ses états associés ont été retirés pour éviter toute confusion avec le changement de projet, réservant ainsi l'accès SSH à l'écran de bienvenue.
+
+
+
+## [2026-05-30 15:44:10]
+- `src-tauri/src/turns.rs` : Masquage de l'avertissement répétitif (`WARN`) signalant l'absence du démon d'agent (qui bascule silencieusement sur le moteur local) en abaissant le niveau de log à `DEBUG` pour ne plus polluer la console. Arrêt propre de la tentative de création de processus si le binaire `sinew-agent-daemon.exe` n'est pas compilé.
+
+## [2026-05-30 15:43:00]
+- `src-tauri/src/turns.rs` & `src/lib/ipc.ts` : Ajout d'une commande Tauri `optimize_prompt` qui fait appel au modèle sélectionné pour agir comme "Prompt Engineer". Le modèle analyse le brouillon de l'utilisateur, détermine le mode optimal (`act`, `plan`, `goal`), et retourne une consigne réécrite de qualité professionnelle.
+- `src/components/chat/ChatPane.tsx` : Intégration d'un bouton "Baguette magique" (Optimiser) directement dans le panneau de saisie. Un clic envoie le brouillon à l'IA d'optimisation, remplace le texte par la consigne SOTA structurée, et bascule le menu déroulant sur le mode recommandé automatiquement.
+
+## [2026-05-30 15:35:10]
+- `src-tauri/src/models.rs` : Neutralisation de warnings du compilateur Rust (`dead_code`) sur les structures `OptimizePromptInput` et `OptimizePromptOutput`.
+
+## [2026-05-30 15:27:26]
+- `src-tauri/src/tray.rs` : Nettoyage du code Rust. Suppression des imports de modules inutilisés (`DesktopState`, `Runtime`, `Manager`) et des variables mortes (`handle`, `tray`) signalés par le compilateur pour maintenir une base de code propre et sans avertissements.
+
+## [2026-05-30 15:25:37]
+- `Cargo.lock`, `Cargo.toml`, `package.json`, `src-tauri/tauri.conf.json` : Mise à jour de la version vers la `0.1.27` via la fusion avec le dépôt upstream.
+- `crates/sinew-app/src/store.rs` : Amélioration SOTA du mode Plan. L'IA a interdiction de détailler l'implémentation (code, shell), mais doit impérativement conserver les choix de design, les technologies et les paramètres décidés lors de la discussion pour un plan plus précis et actionnable.
+- `src-tauri/src/state.rs` : Simplification du prompt système pour supprimer l'obligation fastidieuse des mises à jour utilisateur toutes les 30 secondes en mode exploration, rendant l'IA plus silencieuse et efficace.
+
+## [2026-05-30 15:21:45]
+- `src/components/SettingsPane.tsx` : Rétablissement intégral des emojis et des caractères accentués suite à une corruption d'encodage (passage de UTF-8 à ISO-8859-1). Remplacement par regex de toutes les entités corrompues (ex: soleil, lune, ordinateur, etc.) pour garantir un affichage propre du panneau de paramètres.
+
+## [2026-05-30 15:14:38]
+- `src/components/SettingsPane.tsx` : Correction des erreurs d'encodage (mojibake) où les caractères accentués français apparaissaient sous la forme de caractères corrompus à cause d'une précédente écriture dans un format de texte incorrect. Le fichier a été restauré en UTF-8 pur.
+- `AGENTS.md` : Correction identique de l'encodage pour restaurer les caractères français.
+
+## [2026-05-30 15:10:00]
+- `src-tauri/Cargo.toml` & `Cargo.toml` : Activation du plugin natif `tray-icon` pour afficher l'icône de Sinew dans la zone de notification Windows.
+- `src-tauri/src/tray.rs` : Création du module de gestion de l'icône système (Tray Icon) pour proposer le menu des projets récents lors d'un clic droit sur l'icône Sinew (en bas à droite).
+- `src-tauri/src/lib.rs` : Intégration et exposition des commandes Tauri (`get_recent_workspaces_command`, `record_recent_workspace_command`, `clear_recent_workspaces_command`) pour que les projets récents soient sauvegardés directement sur le disque (en Rust) plutôt que dans le `localStorage` volatile du navigateur.
+- `src/lib/recents.ts` : Réécriture de la gestion des projets récents pour s'interfacer avec le backend Rust de manière asynchrone, garantissant la persistance des projets même si les données du navigateur sont effacées.
+- `src/App.tsx` : Modification du processus de démarrage de l'application pour utiliser le dernier projet enregistré via le backend Rust, garantissant que Sinew s'ouvre toujours sur le dernier espace de travail de façon très fiable et ne demande plus le dossier à chaque ouverture.
+
+## [2026-05-30 15:07:15]
+- `src/lib/recents.ts` & `src/components/Welcome.tsx` : Amélioration SOTA pour le "Sans dossier" (Sandbox). Au lieu de le cacher ou de l'afficher comme un dossier système brut, il est désormais intégré à l'historique avec une interface dédiée.
+- src/components/chat/TodoStrip.tsx : Implémentation d'une vue Kanban temps réel pour le mode Swarm (Essaim d'agents), remplaçant la liste plate par des colonnes 'À faire', 'En cours', 'Bloqué' et 'Terminé'.
+- crates/sinew-app/src/agent/tool_dispatch.rs : Implémentation du mode Auto-Lint Ghost-Loop ! Les appels à 'edit_file' et 'write_file' incluent désormais instantanément les retours des linteurs (cargo, eslint, etc.) s'ils ont échoué, forçant l'agent à s'auto-corriger dans la foulée.
+- src/components/chat/ChatPane.tsx : Extraction chirurgicale des fonctions utilitaires pures (formatBytes, formatTurnDuration, etc.) vers 'chatUtils.ts' pour alléger le composant monolithique sans risque de conflits.: icône de boîte distinctive (`solar:box-bold-duotone`), nom "Brouillon actif (Sandbox)", et un sous-titre clair ("Dernier espace de travail temporaire") masquant le chemin technique.
+
+## [2026-05-30 15:04:00]
+- src/components/SettingsPane.tsx : Déplacement des barres de quotas sous les boutons d'action (Se déconnecter/Se connecter) afin de gagner de l'espace horizontal.
+- src/components/SettingsPane.tsx : Découpage de l'onglet massif 'Options' en 3 nouveaux onglets dédiés dans la navigation principale ('Apparence', 'Power User', et 'Système') pour aérer l'interface.
+- src/components/SettingsPane.tsx : Déplacement de la 'Synchronisation Multi-PC', 'Recherche Sémantique', et 'Apprentissage Automatique IA' vers le nouvel onglet 'Système'. Harmonisation de leur apparence dans la grille dédiée.
+- src/components/chat/TodoStrip.tsx : Implémentation d'une vue Kanban temps réel pour le mode Swarm (Essaim d'agents), remplaçant la liste plate par des colonnes 'À faire', 'En cours', 'Bloqué' et 'Terminé'.
+- crates/sinew-app/src/agent/tool_dispatch.rs : Implémentation du mode Auto-Lint Ghost-Loop ! Les appels à 'edit_file' et 'write_file' incluent désormais instantanément les retours des linteurs (cargo, eslint, etc.) s'ils ont échoué, forçant l'agent à s'auto-corriger dans la foulée.
+- src/components/chat/ChatPane.tsx : Extraction chirurgicale des fonctions utilitaires pures (formatBytes, formatTurnDuration, etc.) vers 'chatUtils.ts' pour alléger le composant monolithique sans risque de conflits.sur les cartes fournisseurs et éviter qu'elles ne soient écrasées sur de petites résolutions.
+- src/components/SettingsPane.tsx : Retrait des mentions explicites de numéros de version (ex: V3 & R1) pour la description de DeepSeek, car ces informations évoluent vite.
+- src/lib/frRuntime.ts : Raccourcissement de "Limite atteinte" en "Limite" pour optimiser l'affichage.
+- src/components/SettingsPane.tsx : Masquage automatique de la description du fournisseur une fois connecté pour gagner de la place verticalement.
+- src/components/SettingsPane.tsx : Correction de l'affichage interne des sous-cartes de fournisseurs pour que les barres de quotas ne détruisent plus l'alignement des emails.
+- src/components/SettingsPane.tsx : Optimisation extrême des sous-cartes : les quotas sont maintenant empilés proprement sur toute la largeur, et les détails (emails, plan, etc.) sont fusionnés sur une seule ligne condensée.
+- src/components/SettingsPane.tsx : Transformation des quotas des sous-cartes en blocs pleine largeur avec l'info au-dessus de la barre pour exploiter tout l'espace horizontal.
+- src/components/SettingsPane.tsx : Découpage de l'onglet massif 'Options' en 3 nouveaux onglets dédiés dans la navigation principale ('Apparence', 'Power User', et 'Système') pour aérer l'interface.
+- src/components/SettingsPane.tsx : Déplacement de la 'Synchronisation Multi-PC', 'Recherche Sémantique', et 'Apprentissage Automatique IA' vers le nouvel onglet 'Système'. Harmonisation de leur apparence dans la grille dédiée.
+- src/components/chat/TodoStrip.tsx : Implémentation d'une vue Kanban temps réel pour le mode Swarm (Essaim d'agents), remplaçant la liste plate par des colonnes 'À faire', 'En cours', 'Bloqué' et 'Terminé'.
+- crates/sinew-app/src/agent/tool_dispatch.rs : Implémentation du mode Auto-Lint Ghost-Loop ! Les appels à 'edit_file' et 'write_file' incluent désormais instantanément les retours des linteurs (cargo, eslint, etc.) s'ils ont échoué, forçant l'agent à s'auto-corriger dans la foulée.
+- src/components/chat/ChatPane.tsx : Extraction chirurgicale des fonctions utilitaires pures (formatBytes, formatTurnDuration, etc.) vers 'chatUtils.ts' pour alléger le composant monolithique sans risque de conflits.
+## [2026-05-30 15:02:20]
+- `src/lib/recents.ts` : Exclusion automatique du dossier temporaire (`.sinew-sandbox` ou "Sans dossier") de la liste des projets récents affichés sur la page d'accueil pour éviter de polluer l'historique de l'utilisateur, tout en conservant la possibilité de le rouvrir automatiquement au prochain lancement si c'était le dernier projet actif.
+
+## [2026-05-30 14:57:30]
+- `src/components/SettingsPane.tsx` : Ajout d'une fonctionnalité permettant de masquer les fournisseurs de modèles non utilisés (bouton œil barré sur les fournisseurs non connectés) et de les réafficher depuis une nouvelle section "Ajouter un fournisseur" en bas de page. Seuls OpenAI, Google et DeepSeek sont affichés par défaut (ainsi que les fournisseurs ayant déjà des identifiants/comptes).
+
+
+## [2026-05-30 14:56:52]
+- `src/components/SettingsPane.tsx` : Correction d'un problème d'affichage où le composant `QuotaBar` en mode inline dépassait du bord de la carte pour les fournisseurs ayant de longs libellés de quotas (ex: "Claude & GPT-OSS"). Ajout de `maxWidth: "100%"`, `overflow: "hidden"` et `textOverflow: "ellipsis"` pour tronquer proprement le texte à l'intérieur.
+
+## [2026-05-30 14:55:00]
+- `src/components/chat/ToolCard.tsx` : Suppression complète du bouton "Auto" / "Auto-fix", car les relances et correctifs automatiques doivent être gérés par l'agent de manière autonome sans nécessiter d'intervention manuelle.
+
+## [2026-05-30 14:54:12]
+- `src/components/SettingsPane.tsx` : Ajout de paliers de couleurs supplémentaires pour le solde DeepSeek : Rouge (<10$), Orange (<20$), Jaune (<40$), Vert clair (<60$), Émeraude (<80$), Cyan (<100$) et Bleu Tech (>=100$).
+
+## [2026-05-30 14:51:15]
+- `src/components/chat/ToolCard.tsx` : Modification du bouton "Auto" en "Auto-fix" avec une nouvelle icône baguette magique, car l'ancienne icône de sliders ressemblait à un caractère chinois et son utilité n'était pas claire.
+
+## [2026-05-30 14:52:10]
+- `src/components/SettingsPane.tsx` : Ajout d'un code couleur dynamique pour l'affichage du solde (ex: DeepSeek) : rouge si le solde est inférieur à 10$, orange s'il est entre 10$ et 20$, et vert au-delà.
+
+## [2026-05-30 14:48:45]
+- `src/components/SettingsPane.tsx` : Agrandissement de la police (de 11px à 13px) et changement de la couleur en vert émeraude (`#10b981`) pour le texte du solde restant DeepSeek lorsque le pourcentage est nul, afin qu'il soit bien plus visible et lisible ("plus gros").
+
+## [2026-05-30 14:46:07]
+- `src/lib/quotas.ts` : Suppression du calcul inutile du pourcentage pour DeepSeek car le point de terminaison de l'API rendait toujours `100%` (le dénominateur est souvent égal au reste), renvoyant `null` à la place.
+- `src/components/SettingsPane.tsx` : Modification de l'affichage en ligne du composant `QuotaBar` pour masquer la barre de progression complète et afficher uniquement le texte centré et agrandi lorsque le pourcentage restant est nul (`null`), ce qui permet d'afficher la ligne de crédit DeepSeek ("Restant $17.08") plus clairement sans une barre `100%` trompeuse.
+
+
+- `src-tauri/src/turns.rs` : Implémentation de la lecture "Hot-Reload" en temps réel des règles d'IA (Cerveau Cloud via OneDrive). L'application lit désormais dynamiquement `instructions_consolidated.md` directement depuis OneDrive (avant de basculer sur LocalAppData) à chaque nouveau message, permettant des améliorations SOTA immédiates sans recharger ni recompiler l'application.
+
+## [2026-05-30 14:21:14]
+- `AGENTS.md` : Clarification de la règle sur `cwd` dans le terminal pour éviter la confusion avec la règle d'utilisation des chemins absolus (les chemins absolus sont interdits pour l'outil bash mais requis pour read/write).
+
+## [2026-05-30 14:14:00]
+- `crates/sinew-cursor/src/agent/run_h2.rs` : Remplacement de l'appel `hyper::body::to_bytes(resp.into_body()).await` par `resp.into_body().collect().await.map(|c| c.to_bytes())` pour s'adapter à l'API de Hyper 1.x et corriger l'erreur de compilation `E0425: cannot find function to_bytes in module hyper::body` remontée dans `build-error.txt`.
+
+## [2026-05-30 14:10:18]
+- `AGENTS.md` : Ajout d'une règle d'auto-amélioration globale pour documenter l'obligation de fournir le paramètre `limit` avec `grep`/`glob` et de vérifier l'existence des fichiers avant l'appel (via `Test-Path`), suite aux erreurs de l'agent.
+
+## [2026-05-30 14:06:32]
+- `src-tauri/capabilities/default.json` : Extension des fenêtres autorisées à toutes (`*` au lieu de `main, sinew-window-*`) et ajout explicite de la permission `dialog:allow-message` pour s'assurer que le frontend puisse exécuter `window.confirm` sur n'importe quelle fenêtre Tauri sans blocage ACL.
+
+## [2026-05-30 14:04:09]
+- `src-tauri/capabilities/default.json` : Ajout des permissions `dialog:allow-confirm` et `dialog:allow-ask` pour autoriser le frontend à utiliser les boîtes de dialogue de confirmation natives via `window.confirm`, résolvant l'erreur d'autorisation ACL `Command plugin:dialog|confirm not allowed by ACL` identifiée dans le fichier de log `frontend-error.log`.
+
+## [2026-05-30 11:26:12]
+- `src/components/chat/ToolCard.tsx` : Remplacement du libellé du bouton de dépannage de "Auto-réparer" par "Auto" pour plus de clarté.
+
+## [2026-05-30 11:17:33]
+- `src/styles.css` : Ajustement des styles de l'encart SSH pour forcer l'affichage de l'état de connexion et du bouton sur une seule ligne (sans retour à la ligne) avec troncature automatique du texte en cas de manque d'espace.
+
+## [2026-05-30 11:15:47]
+- src/components/SettingsPane.tsx : Ajout de la sérialisation du champ id dans settingsToJson pour préserver les identifiants uniques des serveurs MCP (tels que sinew-chrome ou mcp_ssh_mcp) lors de leur affichage et de leur édition dans la configuration avancée du frontend, résolvant le problème d'outils manquants dû à des ID non concordants entre le backend et le frontend.
+- crates/sinew-app/src/edit.rs : Normalisation en minuscules des chemins relatifs sous Windows lors de la modification de fichiers pour assurer la cohérence avec l'outil de lecture et éviter le blocage de sécurité (read-before-write) dû aux différences de casse.
+
+## [2026-05-30 11:14:58]
+- `src-tauri/src/rules.rs` : Suppression automatique du marqueur de début de fichier UTF-8 (BOM `\u{FEFF}`) lors de la lecture de `errors_raw.json` pour éviter l'erreur d'analyse JSON `Format errors_raw.json invalide: expected value at line 1 column 1` qui bloquait la consolidation des règles par l'IA.
+
+## [2026-05-30 04:01:40]
+- `src/components/Welcome.tsx` : Enregistrement de l'hôte SSH connecté dans le stockage local du navigateur (`localStorage`) pour pouvoir l'identifier plus tard.
+- `src/components/Workspace.tsx` : Ajout d'un encart de connexion SSH au bas de la colonne de gauche (barre latérale). Il affiche un indicateur vert avec le nom du serveur si connecté, ainsi qu'un bouton de déconnexion. Sinon, il propose un bouton "Se connecter" qui ouvre un petit formulaire intégré avec gestion des connexions rapides.
+- `src/styles.css` : Ajout des styles graphiques pour le nouvel encart de connexion SSH en bas à gauche de l'interface.
+
+## [2026-05-30 03:57:31]
+- `src/lib/quotas.ts` : Correction du calcul du pourcentage DeepSeek — la barre incluait seulement le solde rechargé (`toppedUpBalance`) comme dénominateur, ce qui donnait toujours ≥100% tant que des crédits gratuits (`grantedBalance`) étaient disponibles. Le dénominateur devient `toppedUpBalance + grantedBalance` pour refléter le total réel.
+
+## [2026-05-30 03:55:30]
+- **Icônes globales — 12 fichiers, ~50 icônes modernisées** :
+ - `circle` → `square` : `close` (20 occurrences), `add` (8), `minus` (3) — style carré plus net et cohérent.
+ - `trash-bin-trash` → `trash-bin-minimalistic` : (6 occurrences) icône poubelle plus moderne.
+ - `linear` → `bold` : toutes les flèches (`alt-arrow-right/down/up`, `square-alt-arrow-up/down`) pour une meilleure visibilité.
+ - Spécifiques : `wrench` → `tuning`, `download-linear` → `download-square`, `play-linear` → `play-circle`, `rewind-back` → `undo-left`, `clock-circle` → `history`.
+
+## [2026-05-30 03:53:56]
+- `src/components/chat/ChatPane.tsx` : 12 icônes modernisées — zoom lightbox (`magnifer-zoom` → `minimize/maximize-square`), pièce jointe (`paperclip-bold` → `paperclip-rounded-bold`), retour (`alt-arrow-left` → `arrow-left`), scroll question (`arrow-up` → `arrow-to-top-left`), téléchargement (`download-linear` → `download-square`), retour arrière (`rewind-back` → `undo-left`), archives `linear` → `bold` (3 occurrences), historique (`clock-circle` → `history`).
+- `src/components/chat/ToolCard.tsx` : Icône clé à molette (`wrench` → `tuning`) plus moderne, stop (`stop-circle-linear` → `stop-bold`).
+- `src/components/chat/MermaidDiagram.tsx` : Zoom (`minus-circle`/`add-circle` → `minimize/maximize-square`) plus cohérent avec la lightbox.
+- `src/components/chat/TodoStrip.tsx` : Flèches expand/collapse (`alt-arrow-down/up-linear` → `bold`) plus visibles.
+
+## [2026-05-30 03:53:45]
+- `crates/sinew-app/src/agent/turn.rs` : Passage de `info!` à `debug!` pour compaction et turn_finished (uniformité totale).
+
+## [2026-05-30 03:51:25]
+- `src-tauri/src/main.rs` : Ajout d'un panic hook global qui capture toutes les panics Rust dans `logs/panic.log` avant le crash.
+- `src-tauri/src/lib.rs` : Ajout de la commande `log_frontend_error` qui écrit les erreurs du frontend dans `logs/frontend-error.log`.
+- `src/main.tsx` : Ajout de `window.onerror` and `window.onunhandledrejection` qui capturent toutes les erreurs JS/React et les envoient au backend.
+- `src/lib/ipc.ts` : Ajout de la méthode `logFrontendError`.
+- **Couverture erreurs totale** : panics Rust + erreurs React/JS + erreurs bridge = tout dans `logs/`.
+
+## [2026-05-30 03:48:15]
+- `scripts/agent-bridge/run-stream.mjs` : Ajout logger JSON fichier vers `logs/agent-bridge.log` + timers (bridge_start, h2_connected, mcp_tool_exec, bridge_end).
+- `scripts/agent-bridge/h2-bridge.mjs` : Ajout logger JSON fichier vers `logs/h2-bridge.log` + timers (bridge_start, h2_stream_end/error).
+- `%LOCALAPPDATA%/sinew/ChromeBridge/server.js` : Mirror des logs vers `logs/chrome-bridge.log`.
+- `%LOCALAPPDATA%/sinew/ChromeBridge/mcp_server.js` : Ajout log fichier vers `logs/chrome-mcp.log`.
+- **Centralisation totale** : tous les logs (Rust + Node.js bridges) convergent maintenant dans `%LOCALAPPDATA%/dev/hyrak/sinew/data/logs/`.
+
+## [2026-05-30 03:41:07]
+- `crates/sinew-app/src/web.rs` : Ajout timers web_search + web_fetch.
+- `crates/sinew-app/src/store.rs` : Ajout timer load_conversation.
+- `crates/sinew-cursor/src/agent/run_h2.rs` : Ajout timer Cursor bridge h2 (durée totale + output tokens).
+- `crates/sinew-index/src/search.rs` : Ajout timer workspace search.
+- Couverture complète atteinte : tous les points d'entrée/sortie majeurs sont maintenant tracés avec durée.
+
+## [2026-05-30 03:35:24]
+- `src-tauri/src/rules.rs` : Refonte du prompt système de `ai_consolidate_rules()` avec un système de confiance à 3 niveaux (🟢 ACTIVE / 🟡 CANDIDATE / 🔴 OBSOLÈTE), traçabilité complète (origine des erreurs, dates, règles remplacées), et dégradation automatique des règles obsolètes (2+ mois sans mise à jour ou contredites par une règle plus récente).
+
+## [2026-05-30 03:24:31]
+- `src-tauri/src/lib.rs` : Changement du filtre de log par défaut de `info` à `trace` pour tous les crates Sinew (`sinew_app`, `sinew_cursor`, `sinew_openai`, `sinew_anthropic`, `sinew_google`, `sinew_kimi`, `sinew_deepseek`, `sinew_openrouter`, `sinew_index`, `sinew_core`). Les libs externes restent à `warn`/`debug` pour éviter le bruit. Le fichier de log passe de `desktop-app.log` à `logs/sinew.log` avec rotation à 64 Mo.
+- `crates/sinew-app/src/agent/turn.rs` : Ajout de timers de précision (stream setup, premier token, exécution de chaque outil, compaction automatique, durée totale du tour) avec `tracing::debug!` et `tracing::info!`.
+- `crates/sinew-app/src/store.rs` : Ajout d'un timer sur `save_conversation` (temps SQLite + sérialisation).
+- `crates/sinew-anthropic/src/client.rs` : Ajout d'un timer HTTP round-trip sur le stream Anthropic.
+- `crates/sinew-openai/src/client.rs` : Ajout d'un timer HTTP round-trip sur le stream SSE OpenAI.
+- `crates/sinew-google/src/client.rs` : Ajout d'un timer HTTP round-trip sur le stream Google Antigravity.
+- `crates/sinew-deepseek/src/client.rs` : Ajout d'un timer HTTP round-trip dans `send_json`.
+- `crates/sinew-kimi/src/client.rs` : Ajout d'un timer HTTP round-trip dans `send_json` (inclut les retries 401).
+- `crates/sinew-openrouter/src/client.rs` : Ajout d'un timer HTTP round-trip sur le stream OpenRouter.
+- **Centralisation logs** : Tous les logs sont maintenant dans `%LOCALAPPDATA%/dev/hyrak/sinew/data/logs/sinew.log`.
+
+## [2026-05-30 03:33:19]
+- `src/components/Workspace.tsx` : Ajout d'un timer automatique (toutes les 5 minutes) qui vérifie si l'apprentissage IA est activé et, si oui, déclenche la consolidation IA des erreurs. Premier déclenchement après 30 secondes au démarrage.
+
+## [2026-05-30 03:20:13]
+- `src-tauri/src/rules.rs` : Ajout de la fonction `ai_consolidate_rules()` qui lit les erreurs brutes (`errors_raw.json`) et les règles existantes (`instructions_consolidated.md`), les envoie à un fournisseur IA (DeepSeek par défaut) pour analyse, dédoublonnage et fusion intelligente des règles similaires, puis écrit le fichier optimisé.
+- `src-tauri/src/lib.rs` : Ajout de la commande Tauri `trigger_ai_rule_consolidation` pour déclencher manuellement l'analyse IA depuis l'interface.
+- `src/lib/ipc.ts` : Ajout de la méthode `triggerAiRuleConsolidation(providerId)` au bridge IPC.
+- `src/components/SettingsPane.tsx` : Ajout d'une carte "Apprentissage Automatique IA" dans la section Diagnostics, avec bouton ON/OFF, sélecteur de fournisseur IA, bouton d'analyse manuelle, et affichage du statut. Cette IA remplace le script de consolidation simple pour fusionner les règles redondantes.
+
+## [2026-05-30 03:22:45]
+- `src/components/SettingsPane.tsx` : Ajout d'un bouton "Refresh" global dans l'en-tête de la section MCP pour reconnecter et rafraîchir tous les serveurs MCP en un clic.
+
+## [2026-05-30 03:21:11]
+- `.sinew/skills/browser/` & `.sinew/skills/computer_use/` : Déplacement des compétences `browser` et `computer_use` du workspace vers le dossier global utilisateur `~/.agents/skills/`, afin qu'elles soient disponibles pour tous les workspaces et non uniquement pour celui de Sinew.
+
+## [2026-05-30 03:13:06]
+- `src-tauri/src/workspace.rs` : Création de la commande `list_ssh_hosts` pour extraire automatiquement les serveurs/alias configurés dans le fichier `~/.ssh/config` de l'utilisateur.
+- `src-tauri/src/lib.rs` : Enregistrement de la commande `list_ssh_hosts` dans le gestionnaire Tauri.
+- `src/lib/ipc.ts` : Exposition de la méthode API `listSshHosts`.
+- `src/components/Welcome.tsx` : Intégration des boutons de connexion rapide ("Quick Connect") basés sur la liste des serveurs configurés pour une connexion instantanée en un clic.
+
+## [2026-05-30 03:06:42]
+- `src-tauri/src/turns.rs` : Correction d'une erreur d'emprunt de valeur déplacée (borrow of moved value) en clonant les paramètres de configuration dans la fonction d'orchestration des turns.
+
+## [2026-05-30 03:03:29]
+- `crates/sinew-app/src/lib.rs` : Re-exportation de la structure `TurnOutput` pour la rendre accessible par l'application Tauri.
+- `crates/sinew-app/src/agent/events.rs` : Dérivation du trait `Deserialize` pour la structure `AgentEvent`, permettant au client de désérialiser les évènements de l'agent.
+- `src-tauri/src/turns.rs` : Implémentation du pont Named Pipe IPC client (`run_turn_via_daemon`) qui envoie la commande `StartTurn` au démon Windows, écoute les réponses en temps réel, redirige les évènements vers le moteur principal de l'UI, et démarre automatiquement le binaire detached (`spawn_daemon`) en cas d'absence.
+
+
+## [2026-05-30 03:06:42]
+- `src-tauri/src/turns.rs` : Utilisation explicite des types ré-exportés par `sinew_app` dans le proxy du démon de fond (AgentEvent, TurnOutput, McpSettings, etc.) pour résoudre les conflits de types et d'importations.
+- `src-tauri/src/workspace.rs` : Ajout de l'installation automatique et silencieuse en tâche de fond de WinFsp et SSHFS-Win via Winget s'ils sont absents lors de la connexion.
+- `sinew-chrome-bridge/native-host-wrapper/src/main.rs` : Correction d'une erreur de syntaxe (accolades fermantes superflues) empêchant la compilation du binaire natif du pont Chrome.
+
+## [2026-05-30 03:04:09]
+- `src-tauri/src/lib.rs` : Restauration des imports accidentellement supprimés par la session précédente (`DeleteFileTool`, `EditFileTool`, `GlobTool`, `GoalWorkflowState`, `GrepTool`), résolvant les erreurs de compilation du projet principal.
+- `sinew-chrome-bridge/native-host-wrapper/Cargo.toml` : Ajout de la dépendance `base64` pour permettre la compilation du binaire natif du pont Chrome.
+
+## [2026-05-30 03:06:42]
+- `crates/sinew-agent-daemon/src/main.rs` : Remplacement du chargement incorrect de `all_auth_files` par le constructeur standard `from_default_sources` pour le fournisseur Google, résolvant les erreurs de compilation du daemon.
+
+## [2026-05-30 03:04:09]
+- `sinew-chrome-bridge/native-host-wrapper/Cargo.toml` : Ajout de la dépendance `base64` pour permettre la compilation du binaire natif du pont Chrome.
+
+## [2026-05-30 03:06:42]
+- `src-tauri/src/workspace.rs` : Ajout de l'installation automatique et silencieuse en tâche de fond de WinFsp et SSHFS-Win via Winget s'ils sont absents lors de la connexion.
+- `sinew-chrome-bridge/native-host-wrapper/src/main.rs` : Correction d'une erreur de syntaxe (accolades fermantes superflues) empêchant la compilation du binaire natif du pont Chrome.
+
+## [2026-05-30 03:00:17]
+- `AGENTS.md` : Mise à jour de la carte des fichiers (code map) avec `computer_use.rs` et la nouvelle Skill.
+- `.sinew/skills/computer_use/SKILL.md` : Création de la compétence (Skill) documentant le pilotage Windows pour l'agent.
+- `sinew-chrome-bridge/native-host-wrapper/src/main.rs` : Exposition de la commande MCP `computer_use` et implémentation Windows native correspondante.
+- `sinew-chrome-bridge/native-host-wrapper/Cargo.toml` : Ajout de la dépendance `image` pour le wrapper MCP.
+- `crates/sinew-app/src/subagent.rs`, `crates/sinew-app/src/team.rs`, `crates/sinew-app/src/team/agent_turns.rs` : Instanciation de l'outil `ComputerUseTool` pour les sous-agents et les agents d'équipe.
+- `src-tauri/src/lib.rs` : Import de `ComputerUseTool` dans le binaire principal de Tauri.
+- `src-tauri/src/turns.rs`, `src-tauri/src/swarm.rs` : Instanciation de l'outil `ComputerUseTool` dans les contextes de turn.
+- `crates/sinew-app/src/agent/turn.rs` : Intégration de l'appel et des descripteurs de `ComputerUseTool` dans le flux principal de discussion de l'agent.
+- `crates/sinew-app/src/agent/tool_dispatch.rs` : Routage dynamique de la commande `computer_use` vers la simulation système correspondante.
+- `crates/sinew-app/src/agent/context.rs` : Intégration de `ComputerUseTool` dans le contexte de discussion de l'agent.
+- `crates/sinew-app/src/lib.rs` : Exportation du nouvel outil `ComputerUseTool`.
+- `crates/sinew-app/src/tool_names.rs` : Définition de la constante d'outil `computer_use` et prise en charge de sa résolution canonique.
+- `crates/sinew-app/Cargo.toml` : Ajout de la dépendance `image` pour compresser les captures d'écran du Computer Use.
+- `crates/sinew-app/src/computer_use.rs` : Création du module d'automatisation et de pilotage d'ordinateur (Computer Use) natif pour Windows (GDI screenshots, simulation clavier/souris).
+
+
+
+
+
+
+
+
+
+
+## [2026-05-30 03:01:48]
+- `CHANGELOG.md` : Enregistrement de la suppression des fichiers temporaires et rapports d'analyse obsolètes.
+- `AGENTS.md` : Mise à jour de la carte des fichiers (code map) suite au retrait des fichiers inutiles du projet.
+- `afaire.md`, `AMELIORATION_SSH.md`, `COMPARAISON_ARCHITECTURE.md`, `Rapport_Analyse_Composer_2.5.md`, `RAPPORT_ANTIGRAVITY.md`, `Rapport_Codex_Analyse.md`, `RAPPORT_DAEMON_PERSISTANT.md`, `RAPPORT_DECOMPILE_CURSOR.md`, `Rapport_SSH_Analyse.md`, `untitled.txt` : Suppression des fichiers de rapports temporaires et documents d'analyse obsolètes pour nettoyer le projet.
+- `sinew-chrome-bridge/bridge.log`, `sinew-chrome-bridge/bridge_err.log` : Nettoyage des journaux de logs locaux inutiles.
+
+
+## [2026-05-30 03:02:04]
+- `crates/sinew-agent-daemon/src/protocol.rs` : Création de la structure du protocole d'échange JSON IPC (Requêtes de turn, d'annulation, de statut et Réponses d'événements et d'erreurs).
+- `crates/sinew-agent-daemon/src/main.rs` : Implémentation du serveur d'écoute asynchrone multithread gérant les connexions entrantes sur le Named Pipe et le traitement des messages JSON-RPC de limités par des retours à la ligne (`\n`).
+
+
+## [2026-05-30 02:57:00]
+- `src-tauri/src/workspace.rs` : Création de la commande `mount_ssh_workspace` pour automatiser la détection de lettre libre, le lancement d'SSHFS-Win et l'ouverture automatique du lecteur.
+- `src-tauri/src/lib.rs` : Enregistrement du gestionnaire Tauri `mount_ssh_workspace`.
+- `src/lib/ipc.ts` : Exposition de la méthode API `mountSshWorkspace`.
+- `src/components/Welcome.tsx` : Intégration d'un formulaire et bouton de connexion SSH directe dans l'interface d'accueil (Switch) permettant de connecter n'importe quelle VM à la volée.
+
+## [2026-05-30 03:00:40]
+- `Cargo.toml` : Ajout du sous-projet `crates/sinew-agent-daemon` à la liste des membres du workspace Cargo.
+- `crates/sinew-agent-daemon/Cargo.toml` : Création du fichier de configuration Cargo avec ses dépendances (tokio, anyhow, serde, etc.).
+- `crates/sinew-agent-daemon/src/main.rs` : Implémentation du squelette du démon de fond persistant Windows (écriture de PID, configuration de serveur Named Pipe).
+
+
+## [2026-05-30 02:57:59]
+- `RAPPORT_DAEMON_PERSISTANT.md` : Création du rapport de conception SOTA détaillant le découplage du moteur de discussion en démon d'arrière-plan Windows persistant (Named Pipes, cycle de vie detached, persistance SQLite, stream de reconnexion).
+
+
+## [2026-05-30 02:52:05]
+- `sinew-chrome-bridge/mcp_server.js`, `sinew-chrome-bridge/server.js` : Suppression définitive des anciens serveurs Node.js obsolètes après la réécriture totale du pont Chrome en Rust.
+- `sinew-chrome-bridge/register.ps1` : Nettoyage et suppression complète des dépendances et lanceurs Node.js (`ws`, `npm install`, fichiers `.bat`) pour un déploiement 100% natif.
+
+## [2026-05-30 02:54:37]
+- `src/components/Welcome.tsx` : Retrait du bouton d'accès SSH/Sandbox de la page d'accueil pour respecter la préférence de l'utilisateur de travailler exclusivement dans un dossier projet monté.
+
+## [2026-05-30 02:51:02]
+- `Cargo.toml` : Ajout de la dépendance `ignore` au niveau de l'espace de travail.
+- `crates/sinew-index/Cargo.toml` : Ajout de la dépendance `ignore`.
+- `crates/sinew-index/src/indexer.rs` : Intégration de la gestion dynamique des fichiers `.gitignore`, `.cursorignore` et `.sinewignore` dans l'indexeur de base de code.
+- `crates/sinew-app/src/workspace.rs` : Ajout de `.sinew` dans la liste des répertoires exclus de l'exploration de l'espace de travail, masquant ainsi `.sinew/worktrees`.
+- `sinew-chrome-bridge/sinew_cursor.js` : Implémentation du système d'étiquetage d'interface (injection visuelle des badges `@ref1`, `@ref2` etc.) et résolution automatique des sélecteurs de référence par l'assistant.
+- `crates/sinew-app/src/agent/turn.rs` : Ajout d'une boucle d'auto-correction (Forced Reflection system reminder) en cas de tours d'outils répétés pour éviter les boucles infinies de l'IA.
+
+
+- `sinew-chrome-bridge/native-host-wrapper/Cargo.toml` : Ajout de la dépendance chrono pour l'analyse de performance.
+- `sinew-chrome-bridge/native-host-wrapper/src/main.rs` : Ajout des outils de diagnostic et d'émulation Chrome restants (emulate_experience, lighthouse_audit et analyze_memory_leaks) en Rust natif pour atteindre 100% de parité fonctionnelle et supprimer la dépendance à Node.js.
+
+## [2026-05-30 02:48:23]
+- `src-tauri/src/cli.rs` : Enregistrement du serveur MCP natif Rust (native-host-wrapper.exe) s'il existe, avec repli automatique sur Node.js (mcp_server.js).
+- `crates/sinew-openai/src/stream.rs` : Correction d'avertissement clippy sur un bloc match pliable.
+- `crates/sinew-app/src/edit.rs` : Correction d'avertissements clippy sur l'indexation de boucles et les tris personnalisés.
+- `crates/sinew-app/src/agent/cancel.rs` : Correction d'avertissement clippy sur le retour d'un type d'erreur unitaire.
+- `sinew-chrome-bridge/native-host-wrapper/src/main.rs` : Suppression des avertissements clippy de conversions redondantes dans l'affichage JSON.
+- `sinew-chrome-bridge/native-host-wrapper.exe` : Recompilation en mode release sans avertissements clippy.
+
+## [2026-05-30 02:45:07]
+- `AMELIORATION_SSH.md` : Création du plan d'action d'amélioration SSH surpassant le SOTA (filtrage des clés, persistance des connexions en tâche de fond et découplage des configurations).
+
+## [2026-05-30 02:44:34]
+- `Cargo.toml` : Ajout de la dépendance chrono dans le workspace pour la consolidation des règles en Rust.
+- `src-tauri/Cargo.toml` : Ajout des dépendances regex, chrono et futures.
+- `src-tauri/src/rules.rs` : Création de l'implémentation native en Rust de la consolidation des règles d'apprentissage.
+- `src-tauri/src/cli.rs` : Création du CLI natif en Rust pour synchroniser le projet et enregistrer les extensions MCP sans dépendance Python (et nettoyage des imports inutilisés), ajout de l'outil de diagnostic de connexion (--probe) avec gestion des événements de flux.
+- `src-tauri/src/main.rs` : Interception des paramètres en ligne de commande pour le CLI de synchronisation et de configuration.
+- `src-tauri/src/lib.rs` : Exposition des fonctions internes de base de données et de copie de fichiers pour le CLI.
+- `consolidate_rules.py`, `sync_now.py`, `sinew-chrome-bridge/add_to_sinew.py`, `scripts/probe_*.py` : Suppression de tous les scripts Python obsolètes suite à leur réécriture native en Rust.
+- `sinew-chrome-bridge/register.ps1` : Utilisation de la commande native Rust `Sinew.exe --register-chrome` au lieu du script Python.
+- `crates/sinew-cursor`, `crates/sinew-app` : Application de corrections automatiques Clippy et résolution manuelle de warnings de syntaxe.
+
+## [2026-05-30 02:44:22]
+- `src/components/Welcome.tsx` : Ajout d'un bouton d'accès direct SSH/Sandbox sur la page d'accueil (Switch) pour utiliser le serveur MCP SSH.
+
+## [2026-05-30 02:43:27]
+- `COMPARAISON_ARCHITECTURE.md` : Création du document d'analyse comparative entre l'architecture de Cursor et les fonctionnalités actuelles de Sinew, évaluant le niveau d'opportunité d'intégration (Shadow Workspace, Indexation/Ignore, MCP Navigateur, Boucle d'agent, Commits).
+
+
+## [2026-05-30 02:36:45]
+- `RAPPORT_DECOMPILE_CURSOR.md` : Mise à jour et enrichissement en profondeur du rapport d'analyse de l'architecture de Cursor (gRPC, sockets locaux, indexation Merkle native, daemon autonome d'agent, plomberie Git temporaire, automatisation de navigateur par WebView injectée et réduction de contexte).
+
+
+## [2026-05-30 02:33:05]
+- `sinew-chrome-bridge/native-host-wrapper/Cargo.toml` : Ajout des dépendances tokio, tokio-tungstenite, serde, serde_json, anyhow, futures-util, directories, uuid et reqwest pour réécrire le pont Chrome natif en Rust.
+- `sinew-chrome-bridge/native-host-wrapper/src/main.rs` : Réécriture complète du pont Chrome et du serveur MCP en Rust (SOTA zero-install) permettant de supprimer la dépendance à Node.js.
+- `sinew-chrome-bridge/add_to_sinew.py` : Enregistrement du nouveau binaire natif Rust MCP dans la base de données SQLite de Sinew à la place de l'ancien script Node.js.
+
+
+## [2026-05-30 02:39:37]
+- `mcp_settings` : Intégration du serveur MCP SOTA `slepp-ssh-mcp` dans la base SQLite locale pour donner aux agents un accès SSH complet aux machines distantes.
+
+## [2026-05-30 02:38:31]
+- `src/components/chat/ToolCard.tsx` : Ajout du bouton "Auto-réparer" sur les cartes de commande bash en cas d'erreur.
+- `src/components/chat/ChatPane.tsx` : Implémentation du callback de réparation `handleFixCommand` et passage du prop à `ToolCard`.
+- `src/styles.css` : Ajout des styles pour le bouton d'auto-réparation `.tool-card__fix-action`.
+
+## [2026-05-30 02:36:45]
+- `search_decompiled.py` : Créé puis supprimé après avoir servi à analyser en profondeur les extensions décompilées de Cursor.
+- `RAPPORT_DECOMPILE_CURSOR.md` : Rapport complet d'analyse de l'architecture de Cursor (Shadow Workspace, Retrieval, MCP Navigateur, Boucle d'agent, Commits) enrichi avec les détails bas niveau (Délégation CDP, sockets gRPC, synchronisations Merkle, simhash, correctifs OAuth MCP SDK) rédigé en français simple.
+
+## [2026-05-30 02:37:33]
+- `Rapport_Codex_Analyse.md` : Détail complet du fonctionnement SOTA du bouton "Auto-réparer" (boucle d'auto-correction via sous-agents et vérification de build).
+
+## [2026-05-30 02:35:40]
+- `Rapport_Codex_Analyse.md` : Ajout des sections d'analyse sur le pilotage d'ordinateur (Computer Use) et la télécommande par téléphone (Remote Control).
+- Confirmé la présence native du rendu de diagrammes Mermaid dans Sinew.
+
+## [2026-05-30 02:26:13]
+- `crates/sinew-cursor/src/agent/run_h2.rs` : Redirection du point de contact de l'agent NAL vers le serveur de production express de Cursor (`agent.api5.cursor.sh` au lieu de `api2.cursor.sh`).
+- `scripts/agent-bridge/run-stream.mjs` : Alignement de l'endpoint du pont Node pour utiliser le serveur express `agent.api5.cursor.sh`.
+- `scripts/agent-bridge/h2-bridge.mjs` : Alignement de l'endpoint par défaut du pont HTTP/2 Node pour utiliser `agent.api5.cursor.sh`.
+
+## [2026-05-30 02:26:13]
+- `Rapport_Analyse_Composer_2.5.md` : Ajout du rapport d'analyse synthétique sur le support de Composer 2.5 standalone, les clés de sécurité et la migration vers la ligne express agent.api5.
+
+## [2026-05-30 02:31:00]
+- `Rapport_Codex_Analyse.md` : Enrichissement du rapport avec les analyses d'interface utilisateur et de fonctionnalités frontend (Mini-apps MCP, planificateur d'automatisations RRule, auto-réparation des espaces temporaires Git et régulateur de débit d'affichage).
+
+## [2026-05-30 02:26:01]
+- `Rapport_Codex_Analyse.md` : Ajout des analyses détaillées sur la sécurité de Codex (relocalisation de binaires hors WindowsApps, filtres réseau WFP persistants pour Windows Sandbox et jetons AppContainer/Capability SIDs pour le Command Runner).
+
+## [2026-05-30 02:29:00]
+- `Rapport_Codex_Analyse.md` : Création du rapport de synthèse de Codex analysant son architecture, son intégration avec le clavier Work Louder, son isolation d'exécutables (staging) et ses politiques de bac à sable (sandbox).
+
+
+## [2026-05-30 02:26:42]
+- `src/components/SettingsPane.tsx` : Ajout d'une option de configuration pour agrandir la taille de la boîte de saisie (boîte de chat) en mode normal ou agrandi.
+- `src/App.tsx` : Initialisation au démarrage de l'attribut `data-large-chat-box` sur le document HTML à partir des paramètres persistés de l'utilisateur.
+- `src/styles.css` : Utilisation de variables CSS pour la hauteur minimale/maximale du composer de messages et doublement automatique de ces dimensions en mode agrandi.
+
+
+## [2026-05-30 02:23:29]
+- `RAPPORT_ANTIGRAVITY.md` : Création et simplification complète du rapport d'analyse pour supprimer le jargon technique et utiliser des métaphores faciles à comprendre (Téléviseur et Décodeur).
+
+## [2026-05-30 02:20:52]
+- `Rapport_SSH_Analyse.md` : Création du rapport détaillé d'analyse de l'implémentation SSH dans Antigravity, Codexx et Cursor en utilisant les perspectives des 4 sous-agents.
+
+## [2026-05-30 02:18:39]
+- `crates/sinew-app/src/write.rs` : Résolution d'un bug critique bloquant l'écriture de nouveaux fichiers sur Windows en harmonisant la comparaison insensible à la casse et la suppression des préfixes UNC (`\\?\`).
+- `crates/sinew-app/src/read.rs` : Harmonisation de la fonction `relative_from_root` pour nettoyer correctement les préfixes UNC sous Windows et éviter les fausses alertes d'accès hors espace de travail.
+
+## [2026-05-30 02:16:06]
+- `consolidate_rules.py` : Correction d'un bug cosmétique de double point final lors de la génération de règles d'auto-apprentissage si la description d'erreur contenait déjà un point.
+- `test_consolidation.py` : Ajout puis suppression du script temporaire de test de validation du système d'auto-apprentissage des erreurs.
+
+## [2026-05-30 02:13:43]
+- `C:\Users\julie\.agents\skills` : Restauration de la compétence de recherche globale `find-skills` pour permettre la découverte et l'installation de compétences à la demande.
+
+## [2026-05-30 02:15:11]
+- `crates/sinew-cursor/src/identity.rs` : Cache de la détection du fuseau horaire via OnceLock pour éviter le spawn répétitif de PowerShell sur chaque requête.
+- `crates/sinew-index/src/store.rs` : Optimisation majeure des performances SQLite. Mise en cache du profil de puissance machine (OnceLock), détection SSD/NVMe Windows améliorée via le PNPDeviceID et Caption, augmentation dynamique de la taille du cache SQLite (limité à ~3.1% de la mémoire vive pour rester bien en dessous du plafond de 40% demandé par l'utilisateur, max 1 Go) et de la taille de mmap (max 4 Go), et activation de PRAGMA threads multi-cœurs.
+
+## [2026-05-30 02:12:16]
+- `crates/sinew-index/src/process.rs` : Limitation de la mémoire des sous-processus de l'indexeur (recherche codebase et watch) à 12 Go maximum sur Windows via les API de Job Object, afin d'éviter tout blocage ou fuite de mémoire excessive.
+
+## [2026-05-30 02:10:30]
+- `C:\Users\julie\.agents\skills` : Suppression des dossiers de compétences globales pré-installés superflus pour ne conserver que la compétence Chrome locale (`browser`) de l'espace de travail.
+
+## [2026-05-30 02:08:33]
+- `src/components/SettingsPane.tsx` : Suppression du bouton de synchronisation manuelle ("Synchroniser maintenant") et de la section de détection/liaison des conversations d'autres projets ("Détection de conversations d'autres projets / PC") pour simplifier l'interface utilisateur.
+
+
+## 🚀 Présentation des Fonctionnalités Majeures (Fork Premium julienpiron.fr)
+
+Cette version a été optimisée en profondeur pour offrir une expérience utilisateur haut de gamme (SOTA), une autonomie maximale en arrière-plan, et des intégrations d'intelligence artificielle inégalées.
+
+### 🎨 Interface, Confort & Ergonomie (Premium UI)
+* **Animation de démarrage premium :** Une animation de boot moderne, fluide et élégante à l'ouverture de l'application.
+* **3 niveaux de réflexion :** Choix entre Détaillé, Compact ou Très compact pour configurer précisément la verbosité de l'IA et le masquage des détails techniques dans le chat.
+* **Question collante (Sticky Question) :** La question en cours de traitement reste épinglée en haut de l'écran pendant que vous faites défiler le fil de discussion.
+* **Menu clic droit interactif sur les onglets de l'éditeur :** Clic droit (ou `F10`) sur les onglets pour fermer l'onglet (raccourci `Ctrl+F4`), les autres, à sa droite ou tous, copier le chemin (absolu ou relatif) et révéler dans le Finder/Explorateur.
+* **Menu clic droit d'exécution :** Clic droit sur les fichiers dans le chat et l'arbre des fichiers pour les ouvrir, les révéler ou les exécuter directement.
+* **Polices dynamiques ajustables :** Boutons tactiles réactifs (`+` et `-`) dans les options pour ajuster instantanément à chaud la taille du texte de l'éditeur Monaco et du chat.
+* **Version française complète :** L'interface entière et toutes les actions de l'application s'adaptent automatiquement en français ou en anglais.
+* **Sélection et copie libre :** Déblocage de la sélection et copie de texte directement dans le fil de discussion du chat.
+* **Démarcation visuelle :** Ligne de séparation verticale élégante à gauche du panneau de configuration des paramètres.
+* **Découpage du bundle Vite (-80% de taille) :** Monaco Editor et xterm.js sont isolés dans des sous-lots séparés pour un chargement instantanél'interface utilisateur.
+* **Visualisation du plan d'action (Planning Board) :** Intégration d'un bloc dynamique interactif (`PlanningNextMoveBlock`) montrant en temps réel les prochaines étapes planifiées by le Swarm d'agents.
+* **Aperçu d'image immersif (Lightbox) :** Visionneuse d'images de discussion immersive avec zoom à la molette de souris, déplacement panoramique, rotation, téléchargement et fermeture par clic extérieur.
+
+### 💾 Autonomie, Sauvegarde & Robustesse Système
+* **Sauvegarde automatique (Auto-Save SOTA) :** Enregistrement automatique et transparent en arrière-plan 1,5 seconde après l'arrêt de la frappe. Activable ou désactivable d'un clic dans vos options.
+* **Mode Sandbox :** Lancement de l'application en un clic sans aucun projet ouvert pour tester l'IA ou utiliser les outils MCP de manière isolée.
+* **Synchro OneDrive & SQLite automatique :** Synchronisation transparente de vos conversations, configurations de projets, jetons de connexion/clés d'authentification (`*-auth.json`, `*-device.json`, `*-stream-state.json`), fichiers d'apprentissage globaux (`errors_raw.json` et `instructions_consolidated.md`), et bases de données SQLite entre vos différents ordinateurs.
+* **Zéro popup console Windows :** Lancement asynchrone et silencieux de tous les outils, serveurs MCP, commandes Git et diagnostics SOTA en arrière-plan sans aucune ouverture de fenêtres d'invite de commandes.
+* **Préfixe PC réel automatique :** Identification automatique du nom de la machine physique pour typer et sécuriser les configurations de conversation multi-PC.
+* **Diagnostic Windows OAuth résilient :** Capture robuste de l'erreur réseau typique sous Windows (code 10013) et conseils clairs pour débloquer la connexion (WinNAT/HNS).
+* **Diagnostic SOTA :** Vérification en un clic de l'état de santé, du PATH et des versions de tous vos outils de développement (Git, Python, Node, Cargo, etc.).
+* **Écran de mises à jour sécurisé (`UpdaterLockScreen`) :** Verrouillage de l'interface pendant l'application des correctifs système pour éviter tout conflit de fichiers ou corruption de base de données.
+* **Script de compilation OneDrive (`compil.ps1`) :** Automatisation de la génération de l'application et copie immédiate sur OneDrive pour un déploiement instantané sur vos PC.
+* **Active Turn Registry :** Moteur intelligent Rust qui suit les turns de l'agent en cours et assure une reprise instantanée du streaming.
+* **Fiche de transmission structurée (Compaction d'IA) :** Compactage automatique du contexte lors du changement de fournisseur d'IA dans une fiche structurée reprenant le statut des fichiers modifiés, le relais des tâches et les diagnostics du linter.
+* **Mode plein gaz adaptatif (`crates/sinew-index/src/store.rs`) :** Optimisation dynamique des performances de l'indexeur augmentant le cache et la lecture en mémoire lorsque la machine dispose d'un stockage SSD/NVMe.
+* **Indexation locale parallèle SOTA :** Préparation et analyse des fichiers en parallèle répartie sur tous les coeurs de CPU disponibles via Rayon, avec détection immédiate et saut des fichiers inchangés grâce à leurs empreintes de taille et date.
+* **Identification de projet universelle :** Association automatique des conversations au dépôt Git distant (remote origin URL) ou via un fichier d'identifiant unique `.sinew/project_id.txt` pour lier instantanément vos conversations d'un PC à un autre sans aucune action manuelle, avec détection, liaison et rafraîchissement dynamique des conversations provenant de PC alternatifs depuis les paramètres.
+* **Gestion des mises à jour configurables :** Option à 3 choix (Bloquant, Notification, Désactivé) pour décider précisément du niveau de vérification des nouvelles versions de Sinew et empêcher l'écrasement de vos modifications.
+* **Script de contrôle qualité unifié (`scripts/check.ps1`) :** Commande unique `npm run check` exécutant le build frontend, `cargo check`, les tests, `clippy` et les audits de dépendances en une seule opération.
+* **Système d'apprentissage global transparent :** Chargement et injection automatique de la base d'instructions centralisées de l'utilisateur (`%LOCALAPPDATA%\Sinew\instructions_consolidated.md`) dans le prompt système de tous les agents pour l'ensemble des projets ouverts sur la machine.
+* **Consolidation automatique de la mémoire :** Mécanisme au démarrage transformant automatiquement les erreurs répétées enregistrées dans `errors_raw.json` en règles d'apprentissage globales permanentes dans `instructions_consolidated.md` avec nettoyage du compteur d'erreurs.
+* **Bouton de synchronisation forcée :** Ajout d'un bouton de synchronisation immédiate à la demande dans les paramètres pour déclencher manuellement la synchronisation du dossier OneDrive local.
+
+### 🤖 Modèles d'IA, Comptes & Furtivité (AI Engine)
+* **Gestion Multi-comptes OpenAI & Google Gemini :** Connexion simultanée de plusieurs profils OpenAI et Google Gemini secondaires avec bascule instantanée entre vos différentes clés, comptes et abonnements.
+* **Quotas en temps réel :** Visualisation dynamique de votre consommation (crédits / balance restante) sous forme de barres de progression colorées adaptatives dans les options, et pastille live dans le chat.
+* **Routage & Résilience Google Antigravity SOTA :** Réparation, de-surcharge réseau (erreur 503), routeurs de secours et transition transparente entre modèles avec résolution dynamique des identifiants d'appels d'outils (tool_call_id).
+* **Optimisation de vitesse Gemini :** Streaming et requêtes ultra-rapides pour les modèles Gemini.
+* **Incorporation de Claude Opus 4.8 & 4.6 :** Intégration complète de Claude Opus 4.8 (contexte 1M natif) et Claude Opus 4.6 via les abonnements professionnels Google.
+* **Système Pending/Steering pour Influencer :** Un vrai système d'interception et de guidage pour orienter, corriger ou ajouter des instructions en temps réel sans blocage du flux de l'IA.
+* **Indexation sémantique locale vectorielle :** Indexation et recherche vectorielle haute-performance effectuée localement sur votre machine avec commutateur d'activation directe (BETA) dans le panneau d'options.
+* **Intégration de DeepSeek R1 & V3 :** Support complet de **DeepSeek V3** et de **DeepSeek R1** avec capture et rendu en temps réel du bloc de réflexion (*reasoning*) grâce à l'extraction du champ `reasoning_content` dans le chat.
+* **Pont Cursor Composer 2.5 (agent.v1) :** Moteur haute-performance autonome sur connexions HTTP/2 persistantes gérant toutes les modifications chirurgicales de fichiers, avec installation automatique et invisible en arrière-plan, et masquage du sélecteur d'intelligence inutile.
+* **Sécurité & Furtivité WebSocket :** Spoofing d'empreinte réseau avancé pour éliminer tout risque de détection ou de blocage sur les flux de ChatGPT.
+* **WebSocket OpenAI :** Transport temps-réel haute performance basé sur WebSocket pour des réponses fluides et à latence minimale avec OpenAI.
+
+### 🔌 Extensions & Ponts locaux (MCP & Bridge)
+* **Extension Chrome nouvelle génération :** Pilotage d'actions de navigation ultra-stables en natif Rust avec mouvements et clics à vitesse humaine (mouvements Beziers, physique fluide) et mode silencieux.
+* **Réparation Chrome en un clic :** Bouton bleu de configuration automatique si le pont Chrome ne répond pas sur un nouveau PC.
+* **Empaquetage des ressources Tauri :** Le pont local et l'extension Chrome sont intégrés directement au sein de l'installateur compilé (MSI/EXE).
+* **Outils Rust & ripgrep Sidecar :** Intégration de Ripgrep en binaire natif sidecar et de nouveaux outils (`list_dir`, `delete_file`) pour accélérer la recherche et la gestion des fichiers par 10x.
+* **Diagnostics Monaco en temps réel :** Remontée automatique des lints et erreurs de compilation de l'éditeur de code à l'IA en temps réel.
+* **Logs ultra-compacts :** Nettoyage automatique du contexte de discussion pour éliminer le bruit et optimiser la consommation de jetons.
+* **Laboratoire réseau MITM :** Outils de débogage et d'ingénierie inverse intégrés pour inspecter le trafic chiffrés des outils IA.
+* **Moteur de remplacement intelligent (Search/Replace) :** Système d'auto-correction à 8 couches (Unicode, indentations, etc.) garantissant que les modifications de l'IA s'insèrent correctement dans vos fichiers même en cas de légères erreurs d'espaces.
+* **Outils MCP de diagnostics Chrome avancés :** Intégration de nouveaux outils d'audit (`emulate_experience`, `lighthouse_audit`, `analyze_memory_leaks`) basés sur l'API CDP pour tester les performances, diagnostics Lighthouse et fuites mémoire en local.
+
+---
+
diff --git a/Cargo.lock b/Cargo.lock
index e2cb00a0..a6558e8b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,19 +8,28 @@ version = "0.1.27"
dependencies = [
"anyhow",
"base64 0.22.1",
+ "chrono",
+ "directories",
+ "futures",
"notify",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"portable-pty",
+ "regex",
"reqwest 0.12.28",
+ "rusqlite",
"serde",
"serde_json",
"sinew-anthropic",
"sinew-app",
"sinew-core",
+ "sinew-cursor",
+ "sinew-deepseek",
"sinew-google",
+ "sinew-index",
"sinew-kimi",
+ "sinew-ollama",
"sinew-openai",
"sinew-openrouter",
"tauri",
@@ -31,6 +40,7 @@ dependencies = [
"tracing",
"tracing-subscriber",
"url",
+ "uuid",
]
[[package]]
@@ -46,7 +56,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
+ "getrandom 0.3.4",
"once_cell",
+ "serde",
"version_check",
"zerocopy",
]
@@ -60,6 +72,24 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "aligned"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
+dependencies = [
+ "as-slice",
+]
+
+[[package]]
+name = "aligned-vec"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
+dependencies = [
+ "equator",
+]
+
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
@@ -99,6 +129,54 @@ dependencies = [
"derive_arbitrary",
]
+[[package]]
+name = "arg_enum_proc_macro"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "as-slice"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -145,6 +223,77 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+[[package]]
+name = "av-scenechange"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
+dependencies = [
+ "aligned",
+ "anyhow",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "log",
+ "num-rational",
+ "num-traits",
+ "pastey",
+ "rayon",
+ "thiserror 2.0.18",
+ "v_frame",
+ "y4m",
+]
+
+[[package]]
+name = "av1-grain"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
+dependencies = [
+ "anyhow",
+ "arrayvec",
+ "log",
+ "nom 8.0.0",
+ "num-rational",
+ "v_frame",
+]
+
+[[package]]
+name = "avif-serialize"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
+dependencies = [
+ "aws-lc-sys",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+]
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
[[package]]
name = "base64"
version = "0.21.7"
@@ -172,6 +321,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+[[package]]
+name = "bit_field"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -187,6 +342,15 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "bitstream-io"
+version = "4.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f"
+dependencies = [
+ "no_std_io2",
+]
+
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -226,6 +390,22 @@ dependencies = [
"alloc-stdlib",
]
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "built"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9"
+
[[package]]
name = "bumpalo"
version = "3.20.2"
@@ -244,6 +424,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
[[package]]
name = "bytes"
version = "1.11.1"
@@ -320,6 +506,15 @@ dependencies = [
"toml 0.9.12+spec-1.1.0",
]
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
[[package]]
name = "cc"
version = "1.2.60"
@@ -327,6 +522,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
"find-msvc-tools",
+ "jobserver",
+ "libc",
"shlex",
]
@@ -393,11 +590,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
+ "js-sys",
"num-traits",
"serde",
+ "wasm-bindgen",
"windows-link 0.2.1",
]
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
[[package]]
name = "combine"
version = "4.6.7"
@@ -408,6 +622,34 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "compact_str"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "serde",
+ "static_assertions",
+]
+
+[[package]]
+name = "console"
+version = "0.15.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "once_cell",
+ "unicode-width",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "convert_case"
version = "0.4.0"
@@ -424,6 +666,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"
@@ -447,9 +699,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
dependencies = [
"bitflags 2.11.1",
- "core-foundation",
+ "core-foundation 0.10.1",
"core-graphics-types",
- "foreign-types",
+ "foreign-types 0.5.0",
"libc",
]
@@ -460,7 +712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.11.1",
- "core-foundation",
+ "core-foundation 0.10.1",
"libc",
]
@@ -500,12 +752,37 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -566,14 +843,38 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core 0.20.11",
+ "darling_macro 0.20.11",
+]
+
[[package]]
name = "darling"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
- "darling_core",
- "darling_macro",
+ "darling_core 0.23.0",
+ "darling_macro 0.23.0",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.117",
]
[[package]]
@@ -589,17 +890,37 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core 0.20.11",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
- "darling_core",
+ "darling_core 0.23.0",
"quote",
"syn 2.0.117",
]
+[[package]]
+name = "dary_heap"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "data-encoding"
version = "2.11.0"
@@ -627,6 +948,37 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "derive_builder"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
+dependencies = [
+ "darling 0.20.11",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
+dependencies = [
+ "derive_builder_core",
+ "syn 2.0.117",
+]
+
[[package]]
name = "derive_more"
version = "0.99.20"
@@ -816,6 +1168,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
[[package]]
name = "embed-resource"
version = "3.0.8"
@@ -836,6 +1194,41 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[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 = "equator"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
+dependencies = [
+ "equator-macro",
+]
+
+[[package]]
+name = "equator-macro"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -863,6 +1256,12 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "esaxx-rs"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6"
+
[[package]]
name = "eventsource-stream"
version = "0.2.3"
@@ -870,10 +1269,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab"
dependencies = [
"futures-core",
- "nom",
+ "nom 7.1.3",
"pin-project-lite",
]
+[[package]]
+name = "exr"
+version = "1.74.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
+dependencies = [
+ "bit_field",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
[[package]]
name = "fallible-iterator"
version = "0.3.0"
@@ -886,12 +1300,35 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+[[package]]
+name = "fastembed"
+version = "4.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04c269a76bfc6cea69553b7d040acb16c793119cebd97c756d21e08d0f075ff8"
+dependencies = [
+ "anyhow",
+ "hf-hub",
+ "image",
+ "ndarray",
+ "ort",
+ "ort-sys",
+ "rayon",
+ "serde_json",
+ "tokenizers",
+]
+
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+[[package]]
+name = "fax"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
+
[[package]]
name = "fdeflate"
version = "0.3.7"
@@ -967,6 +1404,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+[[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"
@@ -974,7 +1420,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]]
@@ -988,6 +1434,12 @@ dependencies = [
"syn 2.0.117",
]
+[[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"
@@ -1003,6 +1455,12 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
[[package]]
name = "fsevent-sys"
version = "4.1.0"
@@ -1280,6 +1738,16 @@ dependencies = [
"wasip3",
]
+[[package]]
+name = "gif"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
[[package]]
name = "gio"
version = "0.18.4"
@@ -1365,6 +1833,19 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+[[package]]
+name = "globset"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
[[package]]
name = "gobject-sys"
version = "0.18.0"
@@ -1428,6 +1909,36 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "h2"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.14.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1485,6 +1996,27 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+[[package]]
+name = "hf-hub"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97"
+dependencies = [
+ "dirs",
+ "http",
+ "indicatif",
+ "libc",
+ "log",
+ "native-tls",
+ "rand 0.9.4",
+ "reqwest 0.12.28",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "ureq",
+ "windows-sys 0.60.2",
+]
+
[[package]]
name = "html5ever"
version = "0.29.1"
@@ -1538,6 +2070,7 @@ dependencies = [
"http",
"http-body",
"pin-project-lite",
+ "tokio",
]
[[package]]
@@ -1562,6 +2095,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
+ "h2",
"http",
"http-body",
"httparse",
@@ -1581,13 +2115,31 @@ dependencies = [
"http",
"hyper",
"hyper-util",
+ "log",
"rustls",
+ "rustls-native-certs",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots 1.0.7",
]
+[[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.20"
@@ -1606,9 +2158,11 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2",
+ "system-configuration",
"tokio",
"tower-service",
"tracing",
+ "windows-registry",
]
[[package]]
@@ -1642,7 +2196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
dependencies = [
"byteorder",
- "png",
+ "png 0.17.16",
]
[[package]]
@@ -1760,6 +2314,62 @@ dependencies = [
"icu_properties",
]
+[[package]]
+name = "ignore"
+version = "0.4.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "color_quant",
+ "exr",
+ "gif",
+ "image-webp",
+ "moxcms",
+ "num-traits",
+ "png 0.18.1",
+ "qoi",
+ "ravif",
+ "rayon",
+ "rgb",
+ "tiff",
+ "zune-core",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "image-webp"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
+dependencies = [
+ "byteorder-lite",
+ "quick-error",
+]
+
+[[package]]
+name = "imgref"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2"
+
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -1783,6 +2393,19 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "indicatif"
+version = "0.17.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
+dependencies = [
+ "console",
+ "number_prefix",
+ "portable-atomic",
+ "unicode-width",
+ "web-time",
+]
+
[[package]]
name = "infer"
version = "0.19.0"
@@ -1812,6 +2435,17 @@ dependencies = [
"libc",
]
+[[package]]
+name = "interpolate_name"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "ipnet"
version = "2.12.0"
@@ -1828,6 +2462,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "1.0.18"
@@ -1901,6 +2544,16 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
[[package]]
name = "js-sys"
version = "0.3.95"
@@ -1990,6 +2643,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+[[package]]
+name = "lebe"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
+
[[package]]
name = "libappindicator"
version = "0.9.0"
@@ -2020,6 +2679,16 @@ version = "0.2.185"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
+dependencies = [
+ "arbitrary",
+ "cc",
+]
+
[[package]]
name = "libloading"
version = "0.7.4"
@@ -2080,6 +2749,15 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "loop9"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
+dependencies = [
+ "imgref",
+]
+
[[package]]
name = "lru-slab"
version = "0.1.2"
@@ -2092,6 +2770,22 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+[[package]]
+name = "macro_rules_attribute"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520"
+dependencies = [
+ "macro_rules_attribute-proc_macro",
+ "paste",
+]
+
+[[package]]
+name = "macro_rules_attribute-proc_macro"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30"
+
[[package]]
name = "markup5ever"
version = "0.14.1"
@@ -2143,6 +2837,26 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+[[package]]
+name = "matrixmultiply"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
+dependencies = [
+ "autocfg",
+ "rawpointer",
+]
+
+[[package]]
+name = "maybe-rayon"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
+dependencies = [
+ "cfg-if",
+ "rayon",
+]
+
[[package]]
name = "memchr"
version = "2.8.0"
@@ -2210,24 +2924,106 @@ dependencies = [
]
[[package]]
-name = "muda"
-version = "0.17.2"
+name = "monostate"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67"
+dependencies = [
+ "monostate-impl",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "monostate-impl"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "moxcms"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
+[[package]]
+name = "muda"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177"
+dependencies = [
+ "crossbeam-channel",
+ "dpi",
+ "gtk",
+ "keyboard-types",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "once_cell",
+ "png 0.17.16",
+ "serde",
+ "thiserror 2.0.18",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "native-host-wrapper"
+version = "0.1.27"
+dependencies = [
+ "anyhow",
+ "base64 0.22.1",
+ "chrono",
+ "directories",
+ "futures-util",
+ "image",
+ "reqwest 0.12.28",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tokio-tungstenite",
+ "uuid",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "ndarray"
+version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177"
+checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841"
dependencies = [
- "crossbeam-channel",
- "dpi",
- "gtk",
- "keyboard-types",
- "objc2",
- "objc2-app-kit",
- "objc2-core-foundation",
- "objc2-foundation",
- "once_cell",
- "png",
- "serde",
- "thiserror 2.0.18",
- "windows-sys 0.60.2",
+ "matrixmultiply",
+ "num-complex",
+ "num-integer",
+ "num-traits",
+ "portable-atomic",
+ "portable-atomic-util",
+ "rawpointer",
]
[[package]]
@@ -2278,6 +3074,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "no_std_io2"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "nodrop"
version = "0.1.14"
@@ -2294,6 +3099,21 @@ dependencies = [
"minimal-lexical",
]
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "noop_proc_macro"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+
[[package]]
name = "notify"
version = "6.1.1"
@@ -2322,12 +3142,62 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "num-conv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -2359,6 +3229,12 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "number_prefix"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
+
[[package]]
name = "objc2"
version = "0.6.4"
@@ -2501,18 +3377,101 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+[[package]]
+name = "onig"
+version = "6.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2"
+dependencies = [
+ "bitflags 2.11.1",
+ "libc",
+ "once_cell",
+ "onig_sys",
+]
+
+[[package]]
+name = "onig_sys"
+version = "69.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "openssl"
+version = "0.10.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
+dependencies = [
+ "bitflags 2.11.1",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "libc",
+ "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.117",
+]
+
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+[[package]]
+name = "openssl-sys"
+version = "0.9.116"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+[[package]]
+name = "ort"
+version = "2.0.0-rc.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52afb44b6b0cffa9bf45e4d37e5a4935b0334a51570658e279e9e3e6cf324aa5"
+dependencies = [
+ "ndarray",
+ "ort-sys",
+ "tracing",
+]
+
+[[package]]
+name = "ort-sys"
+version = "2.0.0-rc.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41d7757331aef2d04b9cb09b45583a59217628beaf91895b7e76187b6e8c088"
+dependencies = [
+ "flate2",
+ "pkg-config",
+ "sha2",
+ "tar",
+ "ureq",
+]
+
[[package]]
name = "osakit"
version = "0.3.1"
@@ -2575,6 +3534,18 @@ dependencies = [
"windows-link 0.2.1",
]
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -2812,6 +3783,34 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags 2.11.1",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
+dependencies = [
+ "portable-atomic",
+]
+
[[package]]
name = "portable-pty"
version = "0.9.0"
@@ -2941,6 +3940,89 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "profiling"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
+dependencies = [
+ "profiling-procmacros",
+]
+
+[[package]]
+name = "profiling-procmacros"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "prost"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "prost-reflect"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5edd582b62f5cde844716e66d92565d7faf7ab1445c8cebce6e00fba83ddb2"
+dependencies = [
+ "once_cell",
+ "prost",
+ "prost-types",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "pxfm"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
+
+[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
[[package]]
name = "quick-xml"
version = "0.38.4"
@@ -3153,11 +4235,98 @@ dependencies = [
"rand_core 0.5.1",
]
+[[package]]
+name = "rav1e"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
+dependencies = [
+ "aligned-vec",
+ "arbitrary",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "av-scenechange",
+ "av1-grain",
+ "bitstream-io",
+ "built",
+ "cfg-if",
+ "interpolate_name",
+ "itertools",
+ "libc",
+ "libfuzzer-sys",
+ "log",
+ "maybe-rayon",
+ "new_debug_unreachable",
+ "noop_proc_macro",
+ "num-derive",
+ "num-traits",
+ "paste",
+ "profiling",
+ "rand 0.9.4",
+ "rand_chacha 0.9.0",
+ "simd_helpers",
+ "thiserror 2.0.18",
+ "v_frame",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ravif"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
+dependencies = [
+ "avif-serialize",
+ "imgref",
+ "loop9",
+ "quick-error",
+ "rav1e",
+ "rayon",
+ "rgb",
+]
+
[[package]]
name = "raw-window-handle"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "rawpointer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-cond"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f"
+dependencies = [
+ "either",
+ "itertools",
+ "rayon",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
[[package]]
name = "redox_syscall"
@@ -3256,16 +4425,21 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
+ "encoding_rs",
"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",
"quinn",
@@ -3276,6 +4450,7 @@ dependencies = [
"serde_urlencoded",
"sync_wrapper",
"tokio",
+ "tokio-native-tls",
"tokio-rustls",
"tokio-util",
"tower",
@@ -3352,6 +4527,12 @@ dependencies = [
"windows-sys 0.60.2",
]
+[[package]]
+name = "rgb"
+version = "0.8.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
+
[[package]]
name = "ring"
version = "0.17.14"
@@ -3414,6 +4595,8 @@ version = "0.23.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
dependencies = [
+ "aws-lc-rs",
+ "log",
"once_cell",
"ring",
"rustls-pki-types",
@@ -3450,7 +4633,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
- "core-foundation",
+ "core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
@@ -3477,6 +4660,7 @@ version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
+ "aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
@@ -3576,7 +4760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.11.1",
- "core-foundation",
+ "core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -3771,7 +4955,7 @@ version = "3.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65"
dependencies = [
- "darling",
+ "darling 0.23.0",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -3898,12 +5082,43 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+[[package]]
+name = "simd_helpers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
+dependencies = [
+ "quote",
+]
+
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
+[[package]]
+name = "sinew-agent-daemon"
+version = "0.1.27"
+dependencies = [
+ "anyhow",
+ "serde",
+ "serde_json",
+ "sinew-anthropic",
+ "sinew-app",
+ "sinew-core",
+ "sinew-cursor",
+ "sinew-deepseek",
+ "sinew-google",
+ "sinew-kimi",
+ "sinew-openai",
+ "sinew-openrouter",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+ "uuid",
+]
+
[[package]]
name = "sinew-anthropic"
version = "0.1.27"
@@ -3932,12 +5147,15 @@ version = "0.1.27"
dependencies = [
"anyhow",
"base64 0.22.1",
+ "chrono",
"directories",
"eventsource-stream",
"futures-util",
+ "image",
"kuchikiki",
"portable-pty",
"rand 0.9.4",
+ "rayon",
"regex",
"reqwest 0.12.28",
"rusqlite",
@@ -3946,6 +5164,8 @@ dependencies = [
"sha2",
"similar",
"sinew-core",
+ "sinew-google",
+ "sinew-index",
"sinew-openai",
"tokio",
"tracing",
@@ -3964,6 +5184,59 @@ dependencies = [
"thiserror 2.0.18",
]
+[[package]]
+name = "sinew-cursor"
+version = "0.1.27"
+dependencies = [
+ "anyhow",
+ "async-stream",
+ "async-trait",
+ "base64 0.22.1",
+ "bytes",
+ "directories",
+ "futures",
+ "hex",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "prost",
+ "prost-reflect",
+ "prost-types",
+ "rand 0.9.4",
+ "reqwest 0.12.28",
+ "rustls",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sinew-core",
+ "sinew-index",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "sinew-deepseek"
+version = "0.1.27"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "directories",
+ "eventsource-stream",
+ "futures",
+ "reqwest 0.12.28",
+ "serde",
+ "serde_json",
+ "sinew-core",
+ "tokio",
+ "tracing",
+]
+
[[package]]
name = "sinew-google"
version = "0.1.27"
@@ -3986,6 +5259,25 @@ dependencies = [
"url",
]
+[[package]]
+name = "sinew-index"
+version = "0.1.27"
+dependencies = [
+ "anyhow",
+ "directories",
+ "fastembed",
+ "ignore",
+ "notify",
+ "rayon",
+ "regex",
+ "rusqlite",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tracing",
+ "walkdir",
+]
+
[[package]]
name = "sinew-kimi"
version = "0.1.27"
@@ -4005,6 +5297,23 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "sinew-ollama"
+version = "0.1.27"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "directories",
+ "eventsource-stream",
+ "futures",
+ "reqwest 0.12.28",
+ "serde",
+ "serde_json",
+ "sinew-core",
+ "tokio",
+ "tracing",
+]
+
[[package]]
name = "sinew-openai"
version = "0.1.27"
@@ -4078,6 +5387,17 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "socks"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b"
+dependencies = [
+ "byteorder",
+ "libc",
+ "winapi",
+]
+
[[package]]
name = "softbuffer"
version = "0.4.8"
@@ -4126,12 +5446,30 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "spm_precompiled"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326"
+dependencies = [
+ "base64 0.13.1",
+ "nom 7.1.3",
+ "serde",
+ "unicode-segmentation",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
[[package]]
name = "string_cache"
version = "0.8.9"
@@ -4246,6 +5584,27 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "system-configuration"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
+dependencies = [
+ "bitflags 2.11.1",
+ "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"
@@ -4267,7 +5626,7 @@ checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20"
dependencies = [
"bitflags 2.11.1",
"block2",
- "core-foundation",
+ "core-foundation 0.10.1",
"core-graphics",
"crossbeam-channel",
"dispatch2",
@@ -4410,7 +5769,7 @@ dependencies = [
"ico",
"json-patch",
"plist",
- "png",
+ "png 0.17.16",
"proc-macro2",
"quote",
"semver",
@@ -4715,6 +6074,20 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "tiff"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
+dependencies = [
+ "fax",
+ "flate2",
+ "half",
+ "quick-error",
+ "weezl",
+ "zune-jpeg",
+]
+
[[package]]
name = "time"
version = "0.3.47"
@@ -4771,6 +6144,39 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+[[package]]
+name = "tokenizers"
+version = "0.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a620b996116a59e184c2fa2dfd8251ea34a36d0a514758c6f966386bd2e03476"
+dependencies = [
+ "ahash",
+ "aho-corasick",
+ "compact_str",
+ "dary_heap",
+ "derive_builder",
+ "esaxx-rs",
+ "getrandom 0.3.4",
+ "itertools",
+ "log",
+ "macro_rules_attribute",
+ "monostate",
+ "onig",
+ "paste",
+ "rand 0.9.4",
+ "rayon",
+ "rayon-cond",
+ "regex",
+ "regex-syntax",
+ "serde",
+ "serde_json",
+ "spm_precompiled",
+ "thiserror 2.0.18",
+ "unicode-normalization-alignments",
+ "unicode-segmentation",
+ "unicode_categories",
+]
+
[[package]]
name = "tokio"
version = "1.52.1"
@@ -4799,6 +6205,16 @@ dependencies = [
"syn 2.0.117",
]
+[[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"
@@ -4809,6 +6225,17 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "tokio-stream"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
@@ -5065,7 +6492,7 @@ dependencies = [
"objc2-core-graphics",
"objc2-foundation",
"once_cell",
- "png",
+ "png 0.17.16",
"serde",
"thiserror 2.0.18",
"windows-sys 0.60.2",
@@ -5155,24 +6582,65 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+[[package]]
+name = "unicode-normalization-alignments"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de"
+dependencies = [
+ "smallvec",
+]
+
[[package]]
name = "unicode-segmentation"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+[[package]]
+name = "ureq"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
+dependencies = [
+ "base64 0.22.1",
+ "flate2",
+ "log",
+ "native-tls",
+ "once_cell",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "socks",
+ "url",
+ "webpki-roots 0.26.11",
+]
+
[[package]]
name = "url"
version = "2.5.8"
@@ -5223,6 +6691,17 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "v_frame"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
+dependencies = [
+ "aligned-vec",
+ "num-traits",
+ "wasm-bindgen",
+]
+
[[package]]
name = "valuable"
version = "0.1.1"
@@ -5570,6 +7049,12 @@ dependencies = [
"windows-core 0.61.2",
]
+[[package]]
+name = "weezl"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+
[[package]]
name = "winapi"
version = "0.3.9"
@@ -5719,6 +7204,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"
@@ -6282,6 +7778,12 @@ dependencies = [
"rustix",
]
+[[package]]
+name = "y4m"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
+
[[package]]
name = "yoke"
version = "0.8.2"
@@ -6402,3 +7904,27 @@ name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zune-core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
+
+[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "zune-jpeg"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
+dependencies = [
+ "zune-core",
+]
diff --git a/Cargo.toml b/Cargo.toml
index a662a3f8..d6cbd037 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,8 +7,13 @@ members = [
"crates/sinew-kimi",
"crates/sinew-openai",
"crates/sinew-openrouter",
+ "crates/sinew-deepseek",
+ "crates/sinew-ollama",
+ "crates/sinew-cursor",
+ "crates/sinew-index",
"crates/sinew-app",
- "src-tauri",
+ "crates/sinew-agent-daemon",
+ "src-tauri", "sinew-chrome-bridge/native-host-wrapper",
]
[workspace.package]
@@ -27,6 +32,10 @@ sinew-google = { path = "crates/sinew-google" }
sinew-kimi = { path = "crates/sinew-kimi" }
sinew-openai = { path = "crates/sinew-openai" }
sinew-openrouter = { path = "crates/sinew-openrouter" }
+sinew-deepseek = { path = "crates/sinew-deepseek" }
+sinew-ollama = { path = "crates/sinew-ollama" }
+sinew-cursor = { path = "crates/sinew-cursor" }
+sinew-index = { path = "crates/sinew-index" }
sinew-app = { path = "crates/sinew-app" }
tokio = { version = "1.40", features = ["full"] }
@@ -40,12 +49,15 @@ eventsource-stream = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
regex = "1"
+chrono = { version = "0.4.44", features = ["serde"] }
+rayon = "1.12"
-reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }
+reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls", "http2"] }
rusqlite = { version = "0.32", features = ["bundled"] }
directories = "5"
uuid = { version = "1", features = ["v4", "fast-rng"] }
notify = "6.1"
+ignore = "0.4"
rand = "0.9"
sha2 = "0.10"
url = "2.5"
@@ -56,8 +68,16 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
similar = "2.6"
base64 = "0.22"
-tauri = { version = "2.1", features = ["protocol-asset"] }
+tauri = { version = "2.1", features = ["protocol-asset", "tray-icon"] }
tauri-build = { version = "2.1" }
tauri-plugin-dialog = "2.0"
tauri-plugin-updater = "2.0"
walkdir = "2.5"
+
+[profile.release]
+opt-level = 3 # Optimisation maximale du code
+lto = "thin" # LTO "thin" pour compiler 10 fois plus vite tout en optimisant
+codegen-units = 16 # Utilise plusieurs processeurs en parallèle pour la génération de code
+panic = "abort" # Supprime la surcharge du déroulement de pile
+strip = true # Retire les symboles de débogage (gains de 10 à 30 Mo sur le binaire)
+
diff --git a/README.md b/README.md
index 4c26caa7..d06ea7a7 100644
--- a/README.md
+++ b/README.md
@@ -36,11 +36,77 @@
---
+## 🚀 Présentation des Fonctionnalités Majeures (Fork Premium julienpiron.fr)
+
+Cette version a été optimisée en profondeur pour offrir une expérience utilisateur haut de gamme (SOTA), une autonomie maximale en arrière-plan, et des intégrations d'intelligence artificielle inégalées.
+
+### 🎨 Interface, Confort & Ergonomie (Premium UI)
+* **Animation de démarrage premium :** Une animation de boot moderne, fluide et élégante à l'ouverture de l'application.
+* **3 niveaux de réflexion :** Choix entre Détaillé, Compact ou Très compact pour configurer précisément la verbosité de l'IA et le masquage des détails techniques dans le chat.
+* **Question collante (Sticky Question) :** La question en cours de traitement reste épinglée en haut de l'écran pendant que vous faites défiler le fil de discussion.
+* **Menu clic droit interactif sur les onglets de l'éditeur :** Clic droit (ou `F10`) sur les onglets pour fermer l'onglet (raccourci `Ctrl+F4`), les autres, à sa droite ou tous, copier le chemin (absolu ou relatif) et révéler dans le Finder/Explorateur.
+* **Menu clic droit d'exécution :** Clic droit sur les fichiers dans le chat et l'arbre des fichiers pour les ouvrir, les révéler ou les exécuter directement.
+* **Polices dynamiques ajustables :** Boutons tactiles réactifs (`+` et `-`) dans les options pour ajuster instantanément à chaud la taille du texte de l'éditeur Monaco et du chat.
+* **Version française complète :** L'interface entière et toutes les actions de l'application s'adaptent automatiquement en français ou en anglais.
+* **Sélection et copie libre :** Déblocage de la sélection et copie de texte directement dans le fil de discussion du chat.
+* **Démarcation visuelle :** Ligne de séparation verticale élégante à gauche du panneau de configuration des paramètres.
+* **Découpage du bundle Vite (-80% de taille) :** Monaco Editor et xterm.js sont isolés dans des sous-lots séparés pour un chargement instantané de l'interface utilisateur.
+* **Visualisation du plan d'action (Planning Board) :** Intégration d'un bloc dynamique interactif (`PlanningNextMoveBlock`) montrant en temps réel les prochaines étapes planifiées par le Swarm d'agents.
+* **Aperçu d'image immersif (Lightbox) :** Visionneuse d'images de discussion immersive avec zoom à la molette de souris, déplacement panoramique, rotation, téléchargement et fermeture par clic extérieur.
+
+### 💾 Autonomie, Sauvegarde & Robustesse Système
+* **Sauvegarde automatique (Auto-Save SOTA) :** Enregistrement automatique et transparent en arrière-plan 1,5 seconde après l'arrêt de la frappe. Activable ou désactivable d'un clic dans vos options.
+* **Mode Sandbox :** Lancement de l'application en un clic sans aucun projet ouvert pour tester l'IA ou utiliser les outils MCP de manière isolée.
+* **Synchro OneDrive & SQLite automatique :** Synchronisation transparente de vos conversations, configurations de projets, jetons de connexion/clés d'authentification (`*-auth.json`, `*-device.json`, `*-stream-state.json`), fichiers d'apprentissage globaux (`errors_raw.json` et `instructions_consolidated.md`), et bases de données SQLite entre vos différents ordinateurs.
+* **Zéro popup console Windows :** Lancement asynchrone et silencieux de tous les outils, serveurs MCP, commandes Git et diagnostics SOTA en arrière-plan sans aucune ouverture de fenêtres d'invite de commandes.
+* **Préfixe PC réel automatique :** Identification automatique du nom de la machine physique pour typer et sécuriser les configurations de conversation multi-PC.
+* **Diagnostic Windows OAuth résilient :** Capture robuste de l'erreur réseau typique sous Windows (code 10013) et conseils clairs pour débloquer la connexion (WinNAT/HNS).
+* **Diagnostic SOTA :** Vérification en un clic de l'état de santé, du PATH et des versions de tous vos outils de développement (Git, Python, Node, Cargo, etc.).
+* **Écran de mises à jour sécurisé (`UpdaterLockScreen`) :** Verrouillage de l'interface pendant l'application des correctifs système pour éviter tout conflit de fichiers ou corruption de base de données.
+* **Script de compilation OneDrive (`compil.ps1`) :** Automatisation de la génération de l'application et copie immédiate sur OneDrive pour un déploiement instantané sur vos PC.
+* **Active Turn Registry :** Moteur intelligent Rust qui suit les turns de l'agent en cours et assure une reprise instantanée du streaming.
+* **Fiche de transmission structurée (Compaction d'IA) :** Compactage automatique du contexte lors du changement de fournisseur d'IA dans une fiche structurée reprenant le statut des fichiers modifiés, le relais des tâches et les diagnostics du linter.
+* **Mode plein gaz adaptatif (`crates/sinew-index/src/store.rs`) :** Optimisation dynamique des performances de l'indexeur augmentant le cache et la lecture en mémoire lorsque la machine dispose d'un stockage SSD/NVMe.
+* **Indexation locale parallèle SOTA :** Préparation et analyse des fichiers en parallèle répartie sur tous les coeurs de CPU disponibles via Rayon, avec détection immédiate et saut des fichiers inchangés grâce à leurs empreintes de taille et date.
+* **Identification de projet universelle :** Association automatique des conversations au dépôt Git distant (remote origin URL) ou via un fichier d'identifiant unique `.sinew/project_id.txt` pour lier instantanément vos conversations d'un PC à un autre sans aucune action manuelle, avec détection, liaison et rafraîchissement dynamique des conversations provenant de PC alternatifs depuis les paramètres.
+* **Gestion des mises à jour configurables :** Option à 3 choix (Bloquant, Notification, Désactivé) pour décider précisément du niveau de vérification des nouvelles versions de Sinew et empêcher l'écrasement de vos modifications.
+* **Script de contrôle qualité unifié (`scripts/check.ps1`) :** Commande unique `npm run check` exécutant le build frontend, `cargo check`, les tests, `clippy` et les audits de dépendances en une seule opération.
+* **Système d'apprentissage global transparent :** Chargement et injection automatique de la base d'instructions centralisées de l'utilisateur (`%LOCALAPPDATA%\Sinew\instructions_consolidated.md`) dans le prompt système de tous les agents pour l'ensemble des projets ouverts sur la machine.
+* **Consolidation automatique de la mémoire :** Mécanisme au démarrage transformant automatiquement les erreurs répétées enregistrées dans `errors_raw.json` en règles d'apprentissage globales permanentes dans `instructions_consolidated.md` avec nettoyage du compteur d'erreurs.
+* **Bouton de synchronisation forcée :** Ajout d'un bouton de synchronisation immédiate à la demande dans les paramètres pour déclencher manuellement la synchronisation du dossier OneDrive local.
+
+### 🤖 Modèles d'IA, Comptes & Furtivité (AI Engine)
+* **Gestion Multi-comptes OpenAI & Google Gemini :** Connexion simultanée de plusieurs profils OpenAI et Google Gemini secondaires avec bascule instantanée entre vos différentes clés, comptes et abonnements.
+* **Quotas en temps réel :** Visualisation dynamique de votre consommation (crédits / balance restante) sous forme de barres de progression colorées adaptatives dans les options, et pastille live dans le chat.
+* **Routage & Résilience Google Antigravity SOTA :** Réparation, de-surcharge réseau (erreur 503), routeurs de secours et transition transparente entre modèles avec résolution dynamique des identifiants d'appels d'outils (tool_call_id).
+* **Optimisation de vitesse Gemini :** Streaming et requêtes ultra-rapides pour les modèles Gemini.
+* **Incorporation de Claude Opus 4.8 & 4.6 :** Intégration complète de Claude Opus 4.8 (contexte 1M natif) et Claude Opus 4.6 via les abonnements professionnels Google.
+* **Système Pending/Steering pour Influencer :** Un vrai système d'interception et de guidage pour orienter, corriger ou ajouter des instructions en temps réel sans blocage du flux de l'IA.
+* **Indexation sémantique locale vectorielle :** Indexation et recherche vectorielle haute-performance effectuée localement sur votre machine avec commutateur d'activation directe (BETA) dans le panneau d'options.
+* **Intégration de DeepSeek R1 & V3 :** Support complet de **DeepSeek V3** et de **DeepSeek R1** avec capture et rendu en temps réel du bloc de réflexion (*reasoning*) grâce à l'extraction du champ `reasoning_content` dans le chat.
+* **Pont Cursor Composer 2.5 (agent.v1) :** Moteur haute-performance autonome sur connexions HTTP/2 persistantes gérant toutes les modifications chirurgicales de fichiers, avec installation automatique et invisible en arrière-plan, et masquage du sélecteur d'intelligence inutile.
+* **Sécurité & Furtivité WebSocket :** Spoofing d'empreinte réseau avancé pour éliminer tout risque de détection ou de blocage sur les flux de ChatGPT.
+* **WebSocket OpenAI :** Transport temps-réel haute performance basé sur WebSocket pour des réponses fluides et à latence minimale avec OpenAI.
+
+### 🔌 Extensions & Ponts locaux (MCP & Bridge)
+* **Extension Chrome nouvelle génération :** Pilotage d'actions de navigation ultra-stables en natif Rust avec mouvements et clics à vitesse humaine (mouvements Beziers, physique fluide) et mode silencieux.
+* **Réparation Chrome en un clic :** Bouton bleu de configuration automatique si le pont Chrome ne répond pas sur un nouveau PC.
+* **Empaquetage des ressources Tauri :** Le pont local et l'extension Chrome sont intégrés directement au sein de l'installateur compilé (MSI/EXE).
+* **Outils Rust & ripgrep Sidecar :** Intégration de Ripgrep en binaire natif sidecar et de nouveaux outils (`list_dir`, `delete_file`) pour accélérer la recherche et la gestion des fichiers par 10x.
+* **Diagnostics Monaco en temps réel :** Remontée automatique des lints et erreurs de compilation de l'éditeur de code à l'IA en temps réel.
+* **Logs ultra-compacts :** Nettoyage automatique du contexte de discussion pour éliminer le bruit et optimiser la consommation de jetons.
+* **Laboratoire réseau MITM :** Outils de débogage et d'ingénierie inverse intégrés pour inspecter le trafic chiffrés des outils IA.
+* **Moteur de remplacement intelligent (Search/Replace) :** Système d'auto-correction à 8 couches (Unicode, indentations, etc.) garantissant que les modifications de l'IA s'insèrent correctement dans vos fichiers même en cas de légères erreurs d'espaces.
+* **Outils MCP de diagnostics Chrome avancés :** Intégration de nouveaux outils d'audit (`emulate_experience`, `lighthouse_audit`, `analyze_memory_leaks`) basés sur l'API CDP pour tester les performances, diagnostics Lighthouse et fuites mémoire en local.
+
+---
+
## Contents
- [The three modes](#the-three-modes) — Act, Goal, Plan
- [`AGENTS.md` & `DESIGN.md`](#agentsmd--designmd) — system prompt injection
-- [Multi-provider, one harness](#multi-provider-one-harness) — Anthropic, OpenAI, Google, Kimi, OpenRouter
+- [Multi-provider, one harness](#multi-provider-one-harness) — Anthropic, OpenAI, Google, Kimi, OpenRouter, Ollama
+- [Real-time Quota Monitoring](#real-time-quota-monitoring) — OpenAI Codex, Antigravity, OpenRouter
- [Tools](#tools) — the agent's toolset
- [`clean_context`](#clean_context) — the model cleans its own context
- [`bash` / `bash_input`](#bash--bash_input) — PTY-backed shell sessions
@@ -53,7 +119,13 @@
- [Agent swarm](#agent-swarm) — peer-to-peer team of 2–8 agents
- [Compaction](#compaction) — auto and manual
- [Rollback](#rollback) — checkpointed conversation
-- [Architecture](#architecture) · [Install](#install) · [Build from source](#build-from-source)
+- [Display modes](#display-modes) — technical reasoning density
+- [SOTA System Diagnostics](#sota-system-diagnostics) — real-time local dependency audit
+- [Architecture](#architecture)
+- [Screenshot](#screenshot)
+- [OAuth credentials](#oauth-credentials)
+- [Community](#community)
+- [License](#license)
---
@@ -107,6 +179,7 @@ Sinew supports five model providers, each with its own connection method:
| **OpenAI** | subscription |
| **Google** | subscription |
| **Kimi** | subscription |
+| **Ollama** | local server |
| **OpenRouter** | API key |
### OAuth mode — the real differentiator
@@ -127,6 +200,25 @@ Model selection happens at three levels:
---
+## Real-time Quota Monitoring
+
+Sinew is equipped with a built-in real-time quota tracking engine, letting you monitor your usage and limits directly inside the Settings and connection panels without leaving the harness.
+
+Depending on the provider, three distinct tracking systems are implemented:
+
+- **OpenAI Codex Quotas (ChatGPT Subscription)**: When connected via OpenAI OAuth, Sinew automatically pulls your active ChatGPT Plus/Pro rate limits directly from the official backend APIs. It tracks remaining requests in your primary (short) and secondary (long) windows, complete with exact reset timelines.
+- **Antigravity Quotas (Google Cloud Code / Gemini)**: When authenticated via Google OAuth, Sinew interfaces with Gemini's active developer platform quotas, mapping active rate-limit groups and returning precise remaining request/token percentages and reset times.
+- **OpenRouter Credits**: For API-key-based OpenRouter connections, the harness queries the `/auth/key` endpoint to extract your total credit limit, credits used, and exact remaining USD balance (or indicators for unlimited keys).
+
+### Dynamic Visual Cues
+To ensure you are never surprised by rate-limiting, progress bars in the Settings panel are dynamically color-coded based on your remaining percentage:
+- **Green (>80% remaining):** Comfortable headroom.
+- **Blue (>50% remaining):** Stable usage.
+- **Pink (>20% remaining):** Reaching limits soon.
+- **Red (<20% remaining):** Critically low; throttle imminent.
+
+---
+
## Tools
The agent has access to a full set of tools:
@@ -222,7 +314,7 @@ total: 124
A header with the path and the total line count, then the requested lines, numbered.
-And that's Sinew's whole philosophy: give the **minimum useful information**, no repetition. Just the path, the total line count (so the agent knows where it is and can paginate), and each line numbered. Nothing more. The model figures out the rest — target, cross-reference, widen if needed.
+And that's Sinew's whole philosophy: give the **minimum useful information**, no redundancy. Just the path, the total line count (so the agent knows where it is and can paginate), and each line numbered. Nothing more. The model figures out the rest — target, cross-reference, widen if needed.
---
@@ -527,6 +619,31 @@ Under the hood, each turn records a *checkpoint* that captures the before / afte
---
+## Display modes
+
+Sinew features a customizable display mode selector (*Mode d'affichage*) in the General Options, allowing you to configure the density of **AI thinking processes** (reasoning blocks) and **tool execution details** (bash output, file reads/writes, grep searches) displayed in the chat.
+
+The interface offers three density levels:
+
+- **Detailed (*Détaillé*)** — Full visibility. AI reasoning blocks remain open after streaming. Tool calls are rendered in full detail. File modifications (`edit_file`, `write_file`) bypass the standard card wrappers and render their complete, raw line-by-line diffs (`FileChangeBlock`s) directly in the chat stream for maximum technical auditability.
+- **Compact** — A clean, balanced view. AI reasoning blocks automatically collapse once streaming is finished, showing only a small summary header (e.g. *Thinking (5.2s)*). All successful tool executions, **including file modifications**, are collapsed into single-line clickable card headers. Diffs and code outputs are hidden by default, but can be expanded manually with a single click. Long outputs inside cards are capped to a max-height of 180px with a scrollbar to keep the chat tidy.
+- **Very Compact (*Très compact*)** — The cleanest layout. AI reasoning blocks are visible during generation, then **disappear completely** once finished. All successful, completed tool executions (including file edits and reads) are **filtered out of the chat entirely** (0 pixels used). Only errors, questions from the agent, and the final text responses from the AI remain visible, allowing you to focus purely on the results.
+
+---
+
+## SOTA System Diagnostics
+
+Sinew features a built-in real-time **SOTA System Diagnostics** utility in the Options panel to ensure your environment is fully configured for state-of-the-art agent operations.
+
+- **How it works** — Clicking the **Refresh (*Actualiser*)** button queries the local operating system, verifies execution health, resolves absolute system paths, and extracts version details of all crucial development tools (Git, Node.js, Npm, Cargo, Rustc, Python, Pip, and Ripgrep).
+- **Why it matters** — These dependencies are directly used by Sinew:
+ - **Git** powers the conversation rollback and automatic background Git commits.
+ - **Ripgrep (`rg`)** provides blisteringly fast codebase indexing and regex searches.
+ - **Python & Pip** run advanced Model Context Protocol (MCP) servers and AI-assisted scripts.
+ - **Node.js, Npm, Rustc & Cargo** handle application runtime, sidecar compilation, and build pipelines.
+
+---
+
## Architecture
@@ -549,34 +666,6 @@ Under the hood, each turn records a *checkpoint* that captures the before / afte
---
-## Install
-
-Grab the latest build for your OS from the
-[releases page](https://github.com/Paseru/sinew/releases/latest).
-
-- **macOS** — `.dmg`
-- **Windows** — `.msi` / `.exe`
-- **Linux** — `.AppImage` / `.deb`
-
-The app self-updates from GitHub releases.
-
----
-
-## Build from source
-
-```bash
-# requires Rust 1.80+ and Node 20+
-# see https://tauri.app/start/prerequisites/ for platform deps
-
-npm install
-npm run tauri dev # development
-npm run tauri build # release bundle
-```
-
-The repo is a Cargo workspace (`crates/*` + `src-tauri/`) plus a Vite + React frontend (`src/`).
-
----
-
## OAuth credentials
Provider OAuth client IDs (and Google's client secret) are embedded in the source. This follows the standard practice for "installed applications" — the same approach used by tools like `gcloud`. These credentials are not treated as secret in this context.
diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md
new file mode 100644
index 00000000..fcfcbc72
--- /dev/null
+++ b/REFACTORING_PLAN.md
@@ -0,0 +1,20 @@
+# Rapport d'Audit & Plan de Rationalisation (Refactoring)
+
+Ce rapport consolide l'analyse globale de la base de code (Frontend React & Backend Rust) pour identifier les meilleures opportunités de rationalisation (DRY) et de refactoring.
+
+## Top 3 des Cibles Prioritaires de Refactoring
+
+### 1. Unification des Providers LLM (Backend Rust)
+- **Problème** : L'authentification OAuth, les appels réseaux et le formatage des requêtes sont massivement dupliqués à travers 6 crates dédiés (`sinew-anthropic`, `sinew-openai`, `sinew-google`, `sinew-deepseek`, `sinew-kimi`, `sinew-openrouter`) ainsi que dans `providers.rs`.
+- **Action de Refactoring** : Créer un crate ou module core pour centraliser le client HTTP, la gestion des tokens OAuth et la standardisation des requêtes/réponses. Chaque crate fournisseur deviendra une simple surcouche de mapping de payload (API-specific).
+
+### 2. Découpage des Composants Monolithiques (Frontend React)
+- **Problème** : Les composants principaux sont devenus des monolithes ingérables, notamment `SettingsPane.tsx` (plus de 8000 lignes) et `ChatPane.tsx` (plus de 6500 lignes). L'interface contient également de nombreux composants UI dupliqués (ex: icônes, formatBytes, MenuItem) répartis entre `EditorPane`, `FileTree` et `ImageContextMenu`.
+- **Action de Refactoring** :
+ - Extraire les fonctions utilitaires de `ChatPane.tsx` vers un module `chatUtils.ts`.
+ - Découper `SettingsPane.tsx` en sous-composants abstraits (ex: `ProviderCard.tsx`, `SystemSettings.tsx`).
+ - Centraliser les composants UI partagés dans un dossier `src/components/ui`.
+
+### 3. Standardisation du Pipeline d'Outils et d'Exécution (Backend Rust)
+- **Problème** : Le passage d'arguments et le dispatch des outils se fait manuellement dans `tool_dispatch.rs` via des match/if complexes. De plus, la logique d'exécution des tours de l'agent est éclatée entre `turn.rs` et `turns.rs`.
+- **Action de Refactoring** : Implémenter un système de `Trait` commun pour l'ensemble des outils de l'agent, avec un registre d'outils dynamique. Consolider le moteur d'exécution en une pipeline d'actions modulaire pour simplifier la boucle principale.
diff --git a/build-error.txt b/build-error.txt
new file mode 100644
index 00000000..963fe1d2
--- /dev/null
+++ b/build-error.txt
@@ -0,0 +1,6 @@
+ Info `tauri-build` dependency has workspace inheritance enabled. The features array won't be automatically rewritten. Expected features: []
+ Info `tauri` dependency has workspace inheritance enabled. The features array won't be automatically rewritten. Expected features: [protocol-asset]
+ Info Looking up installed tauri packages to check mismatched versions...
+ Running beforeBuildCommand `npm run prepare-sidecars && npm run prepare-agent-bridge && npm run build`
+ Compiling Sinew v0.1.26 (C:\Dev\Sinew\src-tauri)
+failed to build app
\ No newline at end of file
diff --git a/build-log.txt b/build-log.txt
new file mode 100644
index 00000000..1302ad96
--- /dev/null
+++ b/build-log.txt
@@ -0,0 +1,40 @@
+=== 1. Lancement de la compilation Tauri (NSIS uniquement) ===
+
+> sinew@0.1.26 prepare-sidecars
+> node scripts/prepare-sidecars.mjs
+
+ripgrep sidecar already present: src-tauri\binaries\rg-x86_64-pc-windows-msvc.exe
+
+> sinew@0.1.26 prepare-agent-bridge
+> node scripts/prepare-agent-bridge.mjs
+
+agent-bridge deps already present: agent-bridge
+agent-bridge ready for bundle/runtime.
+Chrome bridge dependencies already present.
+
+> sinew@0.1.26 build
+> tsc --noEmit && vite build
+
+vite v5.4.21 building for production...
+transforming...
+✓ 5347 modules transformed.
+rendering chunks...
+computing gzip size...
+dist/index.html 1.48 kB │ gzip: 0.61 kB
+dist/assets/codicon-DCmgc-ay.ttf 80.34 kB
+dist/assets/editor.worker-wFIaQSWA.js 230.61 kB
+dist/assets/json.worker-CdYhYu-b.js 362.27 kB
+dist/assets/html.worker-MgVplfgg.js 668.17 kB
+dist/assets/css.worker-zQUO28Un.js 1,009.37 kB
+dist/assets/ts.worker-BQHvlPb0.js 6,013.96 kB
+dist/assets/xterm-CgzjseLH.css 5.89 kB │ gzip: 2.10 kB
+dist/assets/monaco-DIPB3Vbg.css 130.51 kB │ gzip: 21.01 kB
+dist/assets/index-tY5BIKa2.css 185.17 kB │ gzip: 27.61 kB
+dist/assets/icons-DR2osktw.js 32.83 kB │ gzip: 11.06 kB
+dist/assets/katex-HP8lGamR.js 258.47 kB │ gzip: 77.57 kB
+dist/assets/markdown-DaqiLpSk.js 334.56 kB │ gzip: 101.44 kB
+dist/assets/xterm-Ca-2fTbv.js 602.68 kB │ gzip: 156.70 kB
+dist/assets/index-BdiB8aQN.js 693.57 kB │ gzip: 193.00 kB
+dist/assets/mermaid-BpOho62B.js 2,881.75 kB │ gzip: 782.58 kB
+dist/assets/monaco-CI5vmIO4.js 3,904.22 kB │ gzip: 987.77 kB
+✓ built in 27.97s
diff --git a/cleanup.js b/cleanup.js
new file mode 100644
index 00000000..0d8e96c0
Binary files /dev/null and b/cleanup.js differ
diff --git a/crates/sinew-agent-daemon/Cargo.toml b/crates/sinew-agent-daemon/Cargo.toml
new file mode 100644
index 00000000..a8418f56
--- /dev/null
+++ b/crates/sinew-agent-daemon/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "sinew-agent-daemon"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+description = "Persistent background daemon for Sinew agent loops"
+
+[dependencies]
+anyhow = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+tokio = { workspace = true }
+uuid = { workspace = true }
+tracing = { workspace = true }
+tracing-subscriber = { workspace = true }
+sinew-core = { path = "../sinew-core" }
+sinew-app = { path = "../sinew-app" }
+sinew-cursor = { path = "../sinew-cursor" }
+sinew-anthropic = { workspace = true }
+sinew-google = { workspace = true }
+sinew-kimi = { workspace = true }
+sinew-openai = { workspace = true }
+sinew-openrouter = { workspace = true }
+sinew-deepseek = { workspace = true }
+
+[lints]
+workspace = true
diff --git a/crates/sinew-agent-daemon/src/main.rs b/crates/sinew-agent-daemon/src/main.rs
new file mode 100644
index 00000000..4df85449
--- /dev/null
+++ b/crates/sinew-agent-daemon/src/main.rs
@@ -0,0 +1,502 @@
+use std::fs;
+use std::path::PathBuf;
+use std::collections::HashMap;
+use std::sync::Arc;
+use anyhow::{Context, Result};
+use tracing::{info, error};
+
+mod protocol;
+
+#[cfg(windows)]
+use tokio::net::windows::named_pipe::{ServerOptions, NamedPipeServer};
+
+use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
+#[cfg(not(windows))]
+use tokio::net::{TcpListener, TcpStream};
+
+fn load_all_providers() -> HashMap> {
+ let mut providers: HashMap> = HashMap::new();
+
+ // Load OpenAI
+ if let Ok(files) = sinew_openai::all_auth_files() {
+ for (key, path) in files {
+ if let Ok(p) = sinew_openai::OpenAiProvider::from_file(&path) {
+ providers.insert(key, Arc::new(p));
+ }
+ }
+ }
+
+ // Load Anthropic
+ if let Ok(p) = sinew_anthropic::AnthropicProvider::from_default_sources() {
+ let arc = Arc::new(p);
+ providers.insert("anthropic".to_string(), arc.clone());
+ providers.insert("anthropic:1".to_string(), arc);
+ }
+
+ // Load Google
+ if let Ok(p) = sinew_google::GoogleProvider::from_default_sources() {
+ let arc = Arc::new(p);
+ providers.insert("google".to_string(), arc.clone());
+ providers.insert("google:1".to_string(), arc);
+ }
+
+ // Load Kimi
+ if let Ok(p) = sinew_kimi::KimiProvider::from_default_sources() {
+ let arc = Arc::new(p);
+ providers.insert("kimi".to_string(), arc.clone());
+ providers.insert("kimi:1".to_string(), arc);
+ }
+
+ // Load OpenRouter
+ if let Ok(p) = sinew_openrouter::OpenRouterProvider::from_default_sources(Vec::new()) {
+ let arc = Arc::new(p);
+ providers.insert("openrouter".to_string(), arc.clone());
+ providers.insert("openrouter:1".to_string(), arc);
+ }
+
+ // Load DeepSeek
+ if let Ok(p) = sinew_deepseek::DeepSeekProvider::from_default_sources() {
+ let arc = Arc::new(p);
+ providers.insert("deepseek".to_string(), arc.clone());
+ providers.insert("deepseek:1".to_string(), arc);
+ }
+
+ // Load Cursor
+ if let Ok(p) = sinew_cursor::CursorProvider::from_default_sources() {
+ let arc = Arc::new(p);
+ providers.insert("cursor:1".to_string(), arc.clone());
+ providers.insert("cursor".to_string(), arc);
+ }
+
+ providers
+}
+
+#[allow(unreachable_code)]
+#[tokio::main]
+async fn main() -> Result<()> {
+ // Initialize tracing
+ tracing_subscriber::fmt::init();
+ info!("Starting Sinew Agent Daemon...");
+
+ // Write PID file
+ let local_app_data = std::env::var("LOCALAPPDATA")
+ .map(PathBuf::from)
+ .unwrap_or_else(|_| PathBuf::from("."));
+ let daemon_dir = local_app_data.join("Sinew").join("Daemon");
+ fs::create_dir_all(&daemon_dir).context("Failed to create daemon state directory")?;
+ let pid_path = daemon_dir.join("daemon.pid");
+ fs::write(&pid_path, std::process::id().to_string().as_bytes())
+ .context("Failed to write PID file")?;
+ info!("PID written to {}", pid_path.display());
+
+ let providers = Arc::new(tokio::sync::RwLock::new(load_all_providers()));
+
+ // Windows Named Pipe setup
+ #[cfg(windows)]
+ {
+ let pipe_name = r"\\.\pipe\sinew-agent-ipc";
+ info!("Listening on Windows Named Pipe: {}", pipe_name);
+
+ let mut first = true;
+ loop {
+ let server = ServerOptions::new()
+ .first_pipe_instance(first)
+ .create(pipe_name)?;
+ first = false;
+
+ // Wait for client connection
+ server.connect().await?;
+ info!("Client connected via Named Pipe!");
+
+ let providers_clone = providers.clone();
+ tokio::spawn(async move {
+ if let Err(e) = handle_client(server, providers_clone).await {
+ error!("Error handling Named Pipe client: {:?}", e);
+ }
+ });
+ }
+ }
+
+ #[cfg(not(windows))]
+ {
+ let addr = "127.0.0.1:47990";
+ info!("Listening on TCP: {}", addr);
+ let listener = TcpListener::bind(addr).await.context("Failed to bind TCP listener")?;
+
+ loop {
+ match listener.accept().await {
+ Ok((stream, _)) => {
+ info!("Client connected via TCP!");
+ let providers_clone = providers.clone();
+ tokio::spawn(async move {
+ if let Err(e) = handle_client_tcp(stream, providers_clone).await {
+ error!("Error handling TCP client: {:?}", e);
+ }
+ });
+ }
+ Err(e) => {
+ error!("Error accepting TCP connection: {}", e);
+ }
+ }
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(windows)]
+async fn handle_client(
+ stream: NamedPipeServer,
+ providers: Arc>>>,
+) -> Result<()> {
+ let (reader, writer) = tokio::io::split(stream);
+ handle_client_inner(reader, writer, providers).await
+}
+
+#[cfg(not(windows))]
+async fn handle_client_tcp(
+ stream: TcpStream,
+ providers: Arc>>>,
+) -> Result<()> {
+ let (reader, writer) = tokio::io::split(stream);
+ handle_client_inner(reader, writer, providers).await
+}
+
+async fn handle_client_inner(
+ reader: R,
+ mut writer: W,
+ providers: Arc>>>,
+) -> Result<()>
+where
+ R: tokio::io::AsyncRead + Unpin,
+ W: tokio::io::AsyncWrite + Unpin + Send + 'static,
+{
+ let mut reader = BufReader::new(reader);
+ let mut line = String::new();
+
+ loop {
+ line.clear();
+ let bytes_read = reader.read_line(&mut line).await?;
+ if bytes_read == 0 {
+ break; // Connection closed
+ }
+
+ let request: protocol::DaemonRequest = match serde_json::from_str(&line) {
+ Ok(req) => req,
+ Err(e) => {
+ let err_resp = protocol::DaemonResponse::Error {
+ message: format!("Invalid JSON request: {}", e),
+ };
+ let mut resp_str = serde_json::to_string(&err_resp)?;
+ resp_str.push('\n');
+ writer.write_all(resp_str.as_bytes()).await?;
+ continue;
+ }
+ };
+
+ // Process request
+ info!("Received request: {:?}", request);
+ match request {
+ protocol::DaemonRequest::GetStatus => {
+ let resp = protocol::DaemonResponse::Status {
+ is_busy: false,
+ active_conversation_id: None,
+ };
+ let mut resp_str = serde_json::to_string(&resp)?;
+ resp_str.push('\n');
+ writer.write_all(resp_str.as_bytes()).await?;
+ }
+ protocol::DaemonRequest::CancelTurn { conversation_id } => {
+ info!("Cancel turn requested for: {}", conversation_id);
+ }
+ protocol::DaemonRequest::ListEntries { workspace_path, relative_path } => {
+ info!("ListEntries requested for: {} {:?}", workspace_path, relative_path);
+ let root = PathBuf::from(&workspace_path);
+ let entries = sinew_app::workspace::list_workspace_entries(&root, relative_path.as_deref()).unwrap_or_default();
+ let resp = protocol::DaemonResponse::EntriesList {
+ entries: serde_json::to_value(&entries).unwrap_or(serde_json::Value::Null),
+ };
+ let mut resp_str = serde_json::to_string(&resp)?;
+ resp_str.push('\n');
+ writer.write_all(resp_str.as_bytes()).await?;
+ }
+ protocol::DaemonRequest::ListAllFiles { workspace_path } => {
+ info!("ListAllFiles requested for: {}", workspace_path);
+ let root = PathBuf::from(&workspace_path);
+ let entries = sinew_app::workspace::list_workspace_files(&root).unwrap_or_default();
+ let resp = protocol::DaemonResponse::EntriesList {
+ entries: serde_json::to_value(&entries).unwrap_or(serde_json::Value::Null),
+ };
+ let mut resp_str = serde_json::to_string(&resp)?;
+ resp_str.push('\n');
+ writer.write_all(resp_str.as_bytes()).await?;
+ }
+ protocol::DaemonRequest::ReadFile { workspace_path, relative_path } => {
+ info!("ReadFile requested for: {}/{}", workspace_path, relative_path);
+ let root = PathBuf::from(&workspace_path);
+ match sinew_app::workspace::read_workspace_file(&root, &relative_path) {
+ Ok(doc) => {
+ let resp = protocol::DaemonResponse::FileContent {
+ content: doc.content.unwrap_or_default(),
+ };
+ let mut resp_str = serde_json::to_string(&resp)?;
+ resp_str.push('\n');
+ writer.write_all(resp_str.as_bytes()).await?;
+ }
+ Err(e) => {
+ let resp = protocol::DaemonResponse::Error {
+ message: format!("Failed to read file: {}", e),
+ };
+ let mut resp_str = serde_json::to_string(&resp)?;
+ resp_str.push('\n');
+ writer.write_all(resp_str.as_bytes()).await?;
+ }
+ }
+ }
+ protocol::DaemonRequest::WriteFile { workspace_path, relative_path, content } => {
+ info!("WriteFile requested for: {}/{}", workspace_path, relative_path);
+ let root = PathBuf::from(&workspace_path);
+ match sinew_app::workspace::write_workspace_file(&root, &relative_path, &content) {
+ Ok(_) => {
+ let resp = protocol::DaemonResponse::FileWritten;
+ let mut resp_str = serde_json::to_string(&resp)?;
+ resp_str.push('\n');
+ writer.write_all(resp_str.as_bytes()).await?;
+ }
+ Err(e) => {
+ let resp = protocol::DaemonResponse::Error {
+ message: format!("Failed to write file: {}", e),
+ };
+ let mut resp_str = serde_json::to_string(&resp)?;
+ resp_str.push('\n');
+ writer.write_all(resp_str.as_bytes()).await?;
+ }
+ }
+ }
+ protocol::DaemonRequest::StartTurn {
+ conversation_id,
+ workspace_path,
+ system_prompt,
+ model_name,
+ provider: provider_id,
+ history,
+ todo_list,
+ goal_workflow,
+ mcp_settings,
+ tool_settings,
+ skill_settings,
+ sub_agent_settings,
+ } => {
+ info!("Start turn requested for: {}", conversation_id);
+
+ // Get provider from registry
+ let provider_instance = {
+ let lock = providers.read().await;
+ lock.get(&provider_id).cloned()
+ };
+
+ let provider_instance = match provider_instance {
+ Some(p) => p,
+ None => {
+ let err_resp = protocol::DaemonResponse::Error {
+ message: format!("provider `{}` is not configured or missing credentials", provider_id),
+ };
+ let mut resp_str = serde_json::to_string(&err_resp)?;
+ resp_str.push('\n');
+ writer.write_all(resp_str.as_bytes()).await?;
+ continue;
+ }
+ };
+
+ // Write TurnStarted response
+ let resp = protocol::DaemonResponse::TurnStarted {
+ conversation_id: conversation_id.clone(),
+ };
+ let mut resp_str = serde_json::to_string(&resp)?;
+ resp_str.push('\n');
+ writer.write_all(resp_str.as_bytes()).await?;
+
+ // Prepare TurnContext parameters
+ let workspace_root = PathBuf::from(&workspace_path);
+ let model = sinew_core::ModelRef {
+ provider: provider_id.clone(),
+ name: model_name.clone(),
+ effort: None,
+ };
+
+ let tool_settings = tool_settings
+ .and_then(|v| serde_json::from_value::(v).ok())
+ .unwrap_or_default();
+ let mcp_settings = mcp_settings
+ .and_then(|v| serde_json::from_value::(v).ok())
+ .unwrap_or_default();
+ let skill_settings = skill_settings
+ .and_then(|v| serde_json::from_value::(v).ok())
+ .unwrap_or_default();
+ let _sub_agent_settings = sub_agent_settings
+ .and_then(|v| serde_json::from_value::(v).ok())
+ .unwrap_or_default();
+
+ let history = serde_json::from_value::>(history).unwrap_or_default();
+ let todo_list = serde_json::from_value::(todo_list).unwrap_or_default();
+ let goal_workflow = serde_json::from_value::(goal_workflow).unwrap_or_default();
+
+ // Setup tools
+ let bash = Arc::new(sinew_app::bash::BashTool::new(workspace_root.clone()));
+ let glob = Arc::new(sinew_app::glob::GlobTool::new(workspace_root.clone()));
+ let list_dir = Arc::new(sinew_app::list_dir::ListDirTool::new(workspace_root.clone()));
+ let grep = Arc::new(sinew_app::grep::GrepTool::new(workspace_root.clone()));
+ let codebase_search = Arc::new(sinew_app::codebase_search::CodebaseSearchTool::new(workspace_root.clone()));
+ let check_sota = Arc::new(sinew_app::check_sota::CheckSotaTool::new());
+ let computer_use = Arc::new(sinew_app::ComputerUseTool::new());
+ let read = Arc::new(sinew_app::read::ReadTool::new(workspace_root.clone()));
+ let edit_file = Arc::new(sinew_app::edit::EditFileTool::new(workspace_root.clone()));
+ let write_file = Arc::new(sinew_app::write::WriteFileTool::new(workspace_root.clone()));
+ let delete_file = Arc::new(sinew_app::delete_file::DeleteFileTool::new(workspace_root.clone()));
+
+ let editor_diagnostics = sinew_app::editor_diagnostics::new_editor_diagnostics_store();
+ let read_lints = Arc::new(sinew_app::read_lints::ReadLintsTool::new(workspace_root.clone(), editor_diagnostics.clone()));
+
+ let create_image = Arc::new(sinew_app::image::CreateImageTool::with_settings(
+ workspace_root.clone(),
+ tool_settings.image_provider,
+ tool_settings.openai_image_use_subscription,
+ tool_settings.gemini_image_use_subscription,
+ Some(tool_settings.openai_image_model.clone()),
+ Some(tool_settings.gemini_image_model.clone()),
+ tool_settings.openai_image_api_key(),
+ tool_settings.nano_banana_api_key(),
+ ));
+
+ let todo_list_tool = Some(Arc::new(sinew_app::todo::ToDoListTool::new()));
+ let question = Some(Arc::new(sinew_app::question::QuestionTool::new()));
+ let web_search = Arc::new(sinew_app::web::WebSearchTool::with_settings(
+ tool_settings.web_search_provider,
+ tool_settings.linkup_api_key(),
+ ));
+ let web_fetch = Arc::new(sinew_app::web::WebFetchTool::new());
+ let skill = Arc::new(sinew_app::skill::SkillTool::with_settings(
+ workspace_root.clone(),
+ skill_settings.clone(),
+ ));
+ let mcp_registry = Arc::new(sinew_app::mcp::McpToolRegistry::new(mcp_settings.clone()));
+
+ let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel();
+ let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
+ let cancel = sinew_app::TurnCancel::with_steering(cmd_tx, tokio::sync::mpsc::unbounded_channel().0);
+
+ let context = sinew_app::TurnContext {
+ provider: provider_instance,
+ workspace_root,
+ model,
+ cache_key: Some(conversation_id.clone()),
+ cache_stable_message_count: history.len(),
+ service_tier: None,
+ auto_compact: true,
+ mode: sinew_app::AgentMode::Act,
+ stop_questions: false,
+ system_prompt,
+ history,
+ todo_list,
+ goal_workflow,
+ bash,
+ glob,
+ list_dir,
+ grep,
+ codebase_search,
+ check_sota,
+ computer_use,
+ read,
+ edit_file,
+ write_file,
+ delete_file,
+ read_lints,
+ create_image,
+ todo_list_tool,
+ question,
+ web_search,
+ web_fetch,
+ skill,
+ mcp: mcp_registry,
+ subagents: None,
+ teams: None,
+ tool_settings,
+ event_scope: None,
+ max_tool_rounds: 30,
+ event_tx,
+ cancel,
+ cmd_rx,
+ steering_rx: None,
+ };
+
+ // Spawn the turn loop in the daemon
+ let conversation_id_clone = conversation_id.clone();
+ let event_task = tokio::spawn(async move {
+ sinew_app::run_turn(context).await
+ });
+
+ // Forward events back to the client
+ let _writer_providers = providers.clone();
+ let mut writer_clone = writer;
+ let conversation_id_events = conversation_id.clone();
+
+ let event_forwarder = tokio::spawn(async move {
+ while let Some(event) = event_rx.recv().await {
+ let event_val = serde_json::to_value(&event).unwrap_or(serde_json::Value::Null);
+ let resp = protocol::DaemonResponse::Event {
+ conversation_id: conversation_id_events.clone(),
+ event: event_val,
+ };
+ if let Ok(mut resp_str) = serde_json::to_string(&resp) {
+ resp_str.push('\n');
+ if writer_clone.write_all(resp_str.as_bytes()).await.is_err() {
+ break;
+ }
+ }
+ }
+ writer_clone
+ });
+
+ // Wait for the turn loop to finish
+ let turn_result = event_task.await;
+ let mut writer_recovered = event_forwarder.await?;
+
+ match turn_result {
+ Ok(output) => {
+ let output_val = serde_json::json!({
+ "history": output.history,
+ "todo_list": output.todo_list,
+ "goal_workflow": output.goal_workflow,
+ "interrupted": output.interrupted,
+ "compacted": output.compacted,
+ });
+ let resp = protocol::DaemonResponse::TurnFinished {
+ conversation_id: conversation_id_clone,
+ success: true,
+ error: None,
+ output: Some(output_val),
+ };
+ let mut resp_str = serde_json::to_string(&resp)?;
+ resp_str.push('\n');
+ writer_recovered.write_all(resp_str.as_bytes()).await?;
+ }
+ Err(e) => {
+ let resp = protocol::DaemonResponse::TurnFinished {
+ conversation_id: conversation_id_clone,
+ success: false,
+ error: Some(format!("Turn panicked or aborted: {:?}", e)),
+ output: None,
+ };
+ let mut resp_str = serde_json::to_string(&resp)?;
+ resp_str.push('\n');
+ writer_recovered.write_all(resp_str.as_bytes()).await?;
+ }
+ }
+
+ // Recover the writer back to the loop
+ writer = writer_recovered;
+ }
+ }
+ }
+ Ok(())
+}
diff --git a/crates/sinew-agent-daemon/src/protocol.rs b/crates/sinew-agent-daemon/src/protocol.rs
new file mode 100644
index 00000000..352bf2bb
--- /dev/null
+++ b/crates/sinew-agent-daemon/src/protocol.rs
@@ -0,0 +1,72 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "type", rename_all = "snake_case")]
+pub enum DaemonRequest {
+ StartTurn {
+ conversation_id: String,
+ workspace_path: String,
+ system_prompt: String,
+ model_name: String,
+ provider: String,
+ history: serde_json::Value,
+ todo_list: serde_json::Value,
+ goal_workflow: serde_json::Value,
+ mcp_settings: Option,
+ tool_settings: Option,
+ skill_settings: Option,
+ sub_agent_settings: Option,
+ },
+ CancelTurn {
+ conversation_id: String,
+ },
+ GetStatus,
+ ListEntries {
+ workspace_path: String,
+ relative_path: Option,
+ },
+ ListAllFiles {
+ workspace_path: String,
+ },
+ ReadFile {
+ workspace_path: String,
+ relative_path: String,
+ },
+ WriteFile {
+ workspace_path: String,
+ relative_path: String,
+ content: String,
+ },
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "type", rename_all = "snake_case")]
+pub enum DaemonResponse {
+ Status {
+ is_busy: bool,
+ active_conversation_id: Option,
+ },
+ TurnStarted {
+ conversation_id: String,
+ },
+ Event {
+ conversation_id: String,
+ event: serde_json::Value,
+ },
+ TurnFinished {
+ conversation_id: String,
+ success: bool,
+ error: Option,
+ output: Option,
+ },
+ Error {
+ message: String,
+ },
+ EntriesList {
+ entries: serde_json::Value,
+ },
+ FileContent {
+ content: String,
+ },
+ FileWritten,
+}
diff --git a/crates/sinew-anthropic/src/client.rs b/crates/sinew-anthropic/src/client.rs
index 118fc6a4..2fc974e0 100644
--- a/crates/sinew-anthropic/src/client.rs
+++ b/crates/sinew-anthropic/src/client.rs
@@ -1,5 +1,6 @@
use async_trait::async_trait;
use serde::Serialize;
+use std::time::Instant;
use sinew_core::{
AppError, ChatMessage, Effort, ModelCapabilities, ModelRef, Part, Provider, ProviderRequest,
ProviderStream, Result, Role, TokenEstimate, ToolDescriptor,
@@ -162,6 +163,80 @@ impl AnthropicProvider {
}
values.join(",")
}
+
+ pub async fn get_usage(&self) -> Result {
+ let token = self.config.credential.bearer_or_key(&self.http).await?;
+ let is_oauth = self.config.credential.is_oauth();
+ if !is_oauth {
+ return Err(AppError::Auth("Usage limits are only available for Claude OAuth accounts".into()));
+ }
+
+ let url = format!(
+ "{}/api/oauth/usage",
+ self.config.base_url.trim_end_matches('/')
+ );
+
+ let response = self
+ .http
+ .get(&url)
+ .header("authorization", format!("Bearer {token}"))
+ .header("anthropic-beta", "oauth-2025-04-20")
+ .header("content-type", "application/json")
+ .header("accept", "application/json")
+ .header("user-agent", USER_AGENT)
+ .send()
+ .await
+ .map_err(|err| AppError::Network(err.to_string()))?;
+
+ if response.status() == reqwest::StatusCode::UNAUTHORIZED {
+ let refreshed_token = self
+ .config
+ .credential
+ .force_refresh(&self.http, &token)
+ .await
+ .map_err(|err| AppError::Auth(format!("Token refresh failed: {err}")))?;
+
+ let response2 = self
+ .http
+ .get(&url)
+ .header("authorization", format!("Bearer {refreshed_token}"))
+ .header("anthropic-beta", "oauth-2025-04-20")
+ .header("content-type", "application/json")
+ .header("accept", "application/json")
+ .header("user-agent", USER_AGENT)
+ .send()
+ .await
+ .map_err(|err| AppError::Network(err.to_string()))?;
+
+ if !response2.status().is_success() {
+ let status = response2.status();
+ let text = response2.text().await.unwrap_or_default();
+ return Err(AppError::Network(format!(
+ "Failed to fetch Anthropic usage after token refresh: HTTP {status} - {text}"
+ )));
+ }
+
+ let data = response2
+ .json::()
+ .await
+ .map_err(|err| AppError::Decode(err.to_string()))?;
+ return Ok(data);
+ }
+
+ if !response.status().is_success() {
+ let status = response.status();
+ let text = response.text().await.unwrap_or_default();
+ return Err(AppError::Network(format!(
+ "Failed to fetch Anthropic usage: HTTP {status} - {text}"
+ )));
+ }
+
+ let data = response
+ .json::()
+ .await
+ .map_err(|err| AppError::Decode(err.to_string()))?;
+ Ok(data)
+ }
}
#[async_trait]
@@ -262,10 +337,21 @@ impl Provider for AnthropicProvider {
stream: true,
};
+ let model_name = request.model.name.clone();
+ let provider_name = "anthropic".to_string();
+ let req_start = Instant::now();
let response = self
.send_json_accept("/v1/messages", &body, "text/event-stream")
.await?;
+ let http_ms = req_start.elapsed().as_millis();
+ tracing::debug!(
+ provider = provider_name,
+ model = model_name,
+ http_ms,
+ "Anthropic HTTP round-trip (stream setup)"
+ );
+
if !response.status().is_success() {
return Err(read_http_error(response, true).await);
}
diff --git a/crates/sinew-app/Cargo.toml b/crates/sinew-app/Cargo.toml
index 7880af31..176c79ab 100644
--- a/crates/sinew-app/Cargo.toml
+++ b/crates/sinew-app/Cargo.toml
@@ -8,14 +8,18 @@ description = "Headless desktop runtime for sinew"
[dependencies]
sinew-core = { workspace = true }
+sinew-index = { workspace = true }
sinew-openai = { workspace = true }
+sinew-google = { workspace = true }
anyhow = { workspace = true }
base64 = { workspace = true }
+chrono = { workspace = true }
directories = { workspace = true }
eventsource-stream = { workspace = true }
futures-util = { workspace = true }
kuchikiki = "0.8.8-speedreader"
+image = { version = "0.25.10", default-features = false, features = ["png", "jpeg"] }
reqwest = { workspace = true }
regex = { workspace = true }
sha2 = { workspace = true }
@@ -29,6 +33,7 @@ tokio = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
walkdir = { workspace = true }
+rayon = { workspace = true }
[lints]
workspace = true
diff --git a/crates/sinew-app/src/agent.rs b/crates/sinew-app/src/agent.rs
index 7dec7f36..fd060f63 100644
--- a/crates/sinew-app/src/agent.rs
+++ b/crates/sinew-app/src/agent.rs
@@ -1,4 +1,5 @@
mod assistant_message;
+mod boost_distill;
mod cancel;
mod clean_context;
mod compaction;
@@ -6,6 +7,7 @@ mod context;
mod events;
mod history;
mod mode;
+mod repeat_guard;
#[cfg(test)]
mod tests;
mod tool_dispatch;
diff --git a/crates/sinew-app/src/agent/boost_distill.rs b/crates/sinew-app/src/agent/boost_distill.rs
new file mode 100644
index 00000000..d01f1b15
--- /dev/null
+++ b/crates/sinew-app/src/agent/boost_distill.rs
@@ -0,0 +1,107 @@
+//! Boost Local — distillation automatique des grosses sorties d'outils.
+//!
+//! Quand le Boost Local est actif (variable d'environnement `SINEW_BOOST_DISTILLER`
+//! posée par le backend), les sorties **volumineuses** de `bash` et `web_fetch`
+//! sont résumées par le petit modèle local avant d'entrer dans le contexte du
+//! modèle principal. On économise ainsi des milliers de jetons par tour.
+//!
+//! Règles de sûreté (SOTA, ne jamais casser l'agent) :
+//! * jamais sur `read`/`grep`/`codebase_search` : le modèle a besoin du texte
+//! EXACT pour éditer (correspondance `oldContent`) ;
+//! * jamais sur une sortie en erreur : le modèle doit voir l'erreur brute ;
+//! * uniquement au-dessus d'un seuil élevé (les petites sorties restent brutes) ;
+//! * la **fin** de la sortie est conservée telle quelle (codes de sortie,
+//! erreurs finales) ;
+//! * en cas d'échec/timeout du distillateur, on renvoie la sortie brute.
+
+use std::time::Duration;
+
+use serde_json::json;
+
+use crate::tool_names;
+
+const OLLAMA_URL: &str = "http://127.0.0.1:11434";
+/// Seuil de déclenchement (~6 000 jetons). En dessous, aucune distillation.
+const DISTILL_MIN_CHARS: usize = 24_000;
+/// Nombre de caractères de fin conservés bruts (codes d'erreur, exit codes…).
+const RAW_TAIL_CHARS: usize = 1_000;
+
+/// Le distillateur local est-il actif ? (drapeau posé par le backend)
+fn distiller_model() -> Option {
+ std::env::var("SINEW_BOOST_DISTILLER")
+ .ok()
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+}
+
+/// Seul `bash` et `web_fetch` produisent de gros textes non requis à l'identique.
+fn is_distillable_tool(tool: &str) -> bool {
+ tool == tool_names::BASH || tool == tool_names::BASH_INPUT || tool == tool_names::WEB_FETCH
+}
+
+/// Renvoie une version distillée du contenu pour le contexte du modèle, ou `None`
+/// si rien ne doit changer (la sortie brute sera alors utilisée).
+pub(super) async fn maybe_distill_tool_output(
+ tool: &str,
+ content: &str,
+ is_error: bool,
+) -> Option {
+ if is_error || !is_distillable_tool(tool) || content.len() < DISTILL_MIN_CHARS {
+ return None;
+ }
+ let model = distiller_model()?;
+
+ let orig_tokens = content.len() / 4;
+
+ // On distille le gros du texte mais on garde la fin brute intacte.
+ let split = content.len().saturating_sub(RAW_TAIL_CHARS);
+ let head = &content[..split];
+ let tail = &content[split..];
+
+ let prompt = format!(
+ "You are a context distiller for an AI coding agent. The agent ran a tool and got verbose \
+ output. Summarize the ESSENTIAL facts it needs: key results, file paths, names, numbers, \
+ warnings, and especially any errors. Be terse and factual, no filler, no code fences. \
+ Max 200 words.\n\nTOOL: {tool}\n\nOUTPUT:\n{head}"
+ );
+
+ let body = json!({
+ "model": model,
+ "prompt": prompt,
+ "stream": false,
+ "keep_alive": -1,
+ "options": { "num_ctx": 8192, "temperature": 0.1 }
+ });
+
+ let client = reqwest::Client::builder()
+ .timeout(Duration::from_secs(60))
+ .build()
+ .ok()?;
+
+ let resp = client
+ .post(format!("{OLLAMA_URL}/api/generate"))
+ .json(&body)
+ .send()
+ .await
+ .ok()?;
+ if !resp.status().is_success() {
+ return None;
+ }
+ let value: serde_json::Value = resp.json().await.ok()?;
+ let summary = value.get("response").and_then(|r| r.as_str())?.trim();
+ if summary.is_empty() {
+ return None;
+ }
+
+ let new_tokens = (summary.len() + tail.len()) / 4;
+ let saved = if orig_tokens > 0 {
+ 100 - (new_tokens * 100 / orig_tokens).min(100)
+ } else {
+ 0
+ };
+
+ Some(format!(
+ "[Boost Local — sortie distillée localement : ~{orig_tokens} → ~{new_tokens} jetons (-{saved}%). \
+ Sortie complète visible dans l'interface.]\n\n{summary}\n\n[Fin de sortie brute conservée :]\n{tail}"
+ ))
+}
diff --git a/crates/sinew-app/src/agent/cancel.rs b/crates/sinew-app/src/agent/cancel.rs
index 23a6aa32..c3f76f4b 100644
--- a/crates/sinew-app/src/agent/cancel.rs
+++ b/crates/sinew-app/src/agent/cancel.rs
@@ -5,11 +5,19 @@ use std::{
use tokio::sync::{mpsc, oneshot};
+use sinew_core::ChatMessage;
+
#[derive(Debug)]
pub enum EngineCommand {
Cancel,
}
+#[derive(Debug, Clone)]
+pub struct SteeringCommand {
+ pub id: String,
+ pub message: ChatMessage,
+}
+
#[derive(Debug)]
pub enum QuestionReply {
Answer {
@@ -26,6 +34,7 @@ pub struct TurnCancel {
#[derive(Debug, Default)]
struct TurnCancelState {
+ root_steering: Option>,
senders: Vec>,
questions: HashMap>,
cancelled: bool,
@@ -38,15 +47,52 @@ impl TurnCancel {
group
}
+ pub fn with_steering(
+ root: mpsc::UnboundedSender,
+ steering: mpsc::UnboundedSender,
+ ) -> Self {
+ let group = Self::new(root);
+ if let Ok(mut state) = group.state.lock() {
+ state.root_steering = Some(steering);
+ }
+ group
+ }
+
pub fn empty() -> Self {
Self::default()
}
+ pub fn steer(&self, id: impl Into, message: ChatMessage) -> bool {
+ let sender = self
+ .state
+ .lock()
+ .ok()
+ .and_then(|state| (!state.cancelled).then(|| state.root_steering.clone()))
+ .flatten();
+ sender
+ .map(|sender| {
+ sender
+ .send(SteeringCommand {
+ id: id.into(),
+ message,
+ })
+ .is_ok()
+ })
+ .unwrap_or(false)
+ }
+
+ pub fn close_steering(&self) {
+ if let Ok(mut state) = self.state.lock() {
+ state.root_steering = None;
+ }
+ }
+
pub fn register(&self, sender: mpsc::UnboundedSender) {
if let Ok(mut state) = self.state.lock() {
if state.cancelled {
let _ = sender.send(EngineCommand::Cancel);
}
+ state.senders.retain(|sender| !sender.is_closed());
state.senders.push(sender);
}
}
@@ -57,6 +103,7 @@ impl TurnCancel {
.lock()
.map(|mut state| {
state.cancelled = true;
+ state.senders.retain(|sender| !sender.is_closed());
let questions = state
.questions
.drain()
@@ -75,6 +122,7 @@ impl TurnCancel {
sent
}
+ #[allow(clippy::result_unit_err)]
pub fn register_question(
&self,
tool_call_id: impl Into,
diff --git a/crates/sinew-app/src/agent/compaction.rs b/crates/sinew-app/src/agent/compaction.rs
index c5fccf22..bf553fde 100644
--- a/crates/sinew-app/src/agent/compaction.rs
+++ b/crates/sinew-app/src/agent/compaction.rs
@@ -5,10 +5,10 @@ use tokio::sync::mpsc;
use uuid::Uuid;
use sinew_core::{
- AppError, ChatMessage, ModelRef, Part, Provider, ProviderRequest, ServiceTier, ToolDescriptor,
+ AppError, ChatMessage, ModelRef, Part, Provider, ProviderRequest, Role, ServiceTier, ToolDescriptor,
};
-use crate::compact_conversation_history;
+use crate::{compact_conversation_history, ReadLintsTool, TodoListState};
use super::{
cancel::EngineCommand,
@@ -34,6 +34,9 @@ pub(super) async fn maybe_auto_compact_history(
event_scope: Option<&AgentEventScope>,
cmd_rx: &mut mpsc::UnboundedReceiver,
auto_compaction_attempts: &mut usize,
+ workspace_root: &std::path::Path,
+ read_lints: &Arc,
+ todo_list: &TodoListState,
) -> std::result::Result {
if !can_auto_compact_history(history, *auto_compaction_attempts) {
return Ok(false);
@@ -46,26 +49,85 @@ pub(super) async fn maybe_auto_compact_history(
return Ok(false);
}
- let request_history =
- history_with_current_tool_result_ids(history, current_turn_tool_result_ids);
- let mut request = ProviderRequest::new(model.clone(), request_history)
- .with_system(system_prompt.to_string())
- .with_tools(tool_descriptors.to_vec())
- .with_cache_stable_message_count(*cache_stable_message_count);
- if let Some(cache_key) = cache_key {
- request = request.with_cache_key(cache_key.clone());
- }
- if let Some(service_tier) = service_tier {
- request = request.with_service_tier(service_tier);
- }
+ let current_provider_name = provider.name();
+ let last_assistant_provider = history
+ .iter()
+ .rev()
+ .filter(|msg| msg.role == Role::Assistant)
+ .flat_map(|msg| &msg.parts)
+ .find_map(|part| {
+ let meta = match part {
+ Part::Text { meta, .. } => meta,
+ Part::Thinking { meta, .. } => meta,
+ Part::ToolCall { meta, .. } => meta,
+ _ => &None,
+ };
+ meta.as_ref()
+ .and_then(|m| m.get("provider"))
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string())
+ });
+
+ let provider_changed = if let Some(prev) = last_assistant_provider {
+ prev != current_provider_name
+ } else {
+ false
+ };
- let should_compact = match provider.estimate_tokens(request).await {
- Ok(estimate) => {
- let threshold = auto_compact_threshold(caps.context_window, caps.max_output_tokens);
- estimate.input_tokens >= threshold
+ let (should_compact, user_instruction) = if provider_changed {
+ let mut custom_instruction = String::from("We are transitioning from another AI model to you. Here is the structural state of the workspace to help you resume the task cleanly:\n\n");
+
+ if let Ok(git_output) = run_git_status(workspace_root) {
+ let trimmed = git_output.trim();
+ if !trimmed.is_empty() {
+ custom_instruction.push_str("### 📂 Modified Files (Git Status):\n");
+ custom_instruction.push_str(trimmed);
+ custom_instruction.push_str("\n\n");
+ }
+ }
+
+ if let Some(todo_str) = todo_list.system_block() {
+ let trimmed = todo_str.trim();
+ if !trimmed.is_empty() {
+ custom_instruction.push_str("### 📋 Relay of Checklist/Tasks:\n");
+ custom_instruction.push_str(trimmed);
+ custom_instruction.push_str("\n\n");
+ }
}
- Err(err) if is_context_length_error(&err) => true,
- Err(_) => false,
+
+ if let Ok(lints_output) = run_linter_checks(read_lints).await {
+ let trimmed = lints_output.trim();
+ if !trimmed.is_empty() {
+ custom_instruction.push_str("### 🛡️ Code Health (Linter diagnostics):\n");
+ custom_instruction.push_str(trimmed);
+ custom_instruction.push_str("\n\n");
+ }
+ }
+
+ (true, Some(custom_instruction))
+ } else {
+ let request_history =
+ history_with_current_tool_result_ids(history, current_turn_tool_result_ids);
+ let mut request = ProviderRequest::new(model.clone(), request_history)
+ .with_system(system_prompt.to_string())
+ .with_tools(tool_descriptors.to_vec())
+ .with_cache_stable_message_count(*cache_stable_message_count);
+ if let Some(cache_key) = cache_key {
+ request = request.with_cache_key(cache_key.clone());
+ }
+ if let Some(service_tier) = service_tier {
+ request = request.with_service_tier(service_tier);
+ }
+
+ let threshold_exceeded = match provider.estimate_tokens(request).await {
+ Ok(estimate) => {
+ let threshold = auto_compact_threshold(caps.context_window, caps.max_output_tokens);
+ estimate.input_tokens >= threshold
+ }
+ Err(err) if is_context_length_error(&err) => true,
+ Err(_) => false,
+ };
+ (threshold_exceeded, None)
};
if !should_compact {
@@ -85,6 +147,10 @@ pub(super) async fn maybe_auto_compact_history(
event_scope,
cmd_rx,
auto_compaction_attempts,
+ workspace_root,
+ read_lints,
+ todo_list,
+ user_instruction,
)
.await?;
Ok(true)
@@ -103,6 +169,10 @@ pub(super) async fn run_auto_compaction(
event_scope: Option<&AgentEventScope>,
cmd_rx: &mut mpsc::UnboundedReceiver,
auto_compaction_attempts: &mut usize,
+ _workspace_root: &std::path::Path,
+ _read_lints: &Arc,
+ _todo_list: &TodoListState,
+ user_instruction: Option,
) -> std::result::Result<(), String> {
if !can_auto_compact_history(history, *auto_compaction_attempts) {
return Err("context is still too large, but there is no new content to compact".into());
@@ -152,7 +222,7 @@ pub(super) async fn run_auto_compaction(
cache_key.cloned(),
*cache_stable_message_count,
service_tier,
- None,
+ user_instruction,
cmd_rx,
Some(summary_delta_tx),
)
@@ -284,3 +354,31 @@ fn part_meta(part: &Part) -> Option<&Value> {
| Part::ToolResult { meta, .. } => meta.as_ref(),
}
}
+
+
+fn run_git_status(workspace_root: &std::path::Path) -> std::result::Result {
+ use std::process::Command;
+ let mut cmd = Command::new("git");
+ cmd.arg("status").arg("--short");
+ cmd.current_dir(workspace_root);
+ #[cfg(windows)]
+ {
+ use std::os::windows::process::CommandExt;
+ cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
+ }
+ let output = cmd.output().map_err(|err| err.to_string())?;
+ if output.status.success() {
+ Ok(String::from_utf8_lossy(&output.stdout).into_owned())
+ } else {
+ Err(String::from_utf8_lossy(&output.stderr).into_owned())
+ }
+}
+
+async fn run_linter_checks(read_lints: &Arc) -> std::result::Result {
+ let result = read_lints.run(serde_json::json!({})).await;
+ if result.is_error {
+ Err(result.content)
+ } else {
+ Ok(result.content)
+ }
+}
diff --git a/crates/sinew-app/src/agent/context.rs b/crates/sinew-app/src/agent/context.rs
index 4ffb0088..8889d23f 100644
--- a/crates/sinew-app/src/agent/context.rs
+++ b/crates/sinew-app/src/agent/context.rs
@@ -1,17 +1,18 @@
-use std::sync::Arc;
+use std::{path::PathBuf, sync::Arc};
use tokio::sync::mpsc;
use sinew_core::{ChatMessage, Provider, ServiceTier};
use crate::{
- BashTool, CreateImageTool, EditFileTool, GlobTool, GoalWorkflowState, GrepTool,
- McpToolRegistry, QuestionTool, ReadTool, SkillTool, SubAgentTool, TeamTool, ToDoListTool,
- TodoListState, ToolSettings, WebFetchTool, WebSearchTool, WriteFileTool,
+ BashTool, CheckSotaTool, CodebaseSearchTool, ComputerUseTool, CreateImageTool, DeleteFileTool, EditFileTool,
+ GlobTool, GoalWorkflowState, GrepTool, ListDirTool, McpToolRegistry, QuestionTool, ReadLintsTool,
+ ReadTool, SkillTool, SubAgentTool, TeamTool, ToDoListTool, TodoListState, ToolSettings, WebFetchTool,
+ WebSearchTool, WriteFileTool,
};
use super::{
- cancel::{EngineCommand, TurnCancel},
+ cancel::{EngineCommand, SteeringCommand, TurnCancel},
events::{AgentEvent, AgentEventScope},
};
@@ -25,6 +26,7 @@ pub enum AgentMode {
pub struct TurnContext {
pub provider: Arc,
+ pub workspace_root: PathBuf,
pub model: sinew_core::ModelRef,
pub cache_key: Option,
pub cache_stable_message_count: usize,
@@ -38,10 +40,16 @@ pub struct TurnContext {
pub goal_workflow: GoalWorkflowState,
pub bash: Arc,
pub glob: Arc,
+ pub list_dir: Arc,
pub grep: Arc,
+ pub codebase_search: Arc,
+ pub check_sota: Arc,
+ pub computer_use: Arc,
pub read: Arc,
pub edit_file: Arc,
pub write_file: Arc,
+ pub delete_file: Arc,
+ pub read_lints: Arc,
pub create_image: Arc,
pub todo_list_tool: Option>,
pub question: Option>,
@@ -57,6 +65,7 @@ pub struct TurnContext {
pub event_tx: mpsc::UnboundedSender,
pub cancel: TurnCancel,
pub cmd_rx: mpsc::UnboundedReceiver,
+ pub steering_rx: Option>,
}
pub struct TurnOutput {
diff --git a/crates/sinew-app/src/agent/events.rs b/crates/sinew-app/src/agent/events.rs
index 13ff15d8..9b552266 100644
--- a/crates/sinew-app/src/agent/events.rs
+++ b/crates/sinew-app/src/agent/events.rs
@@ -1,6 +1,6 @@
use std::sync::Arc;
-use serde::Serialize;
+use serde::{Serialize, Deserialize};
use serde_json::{json, Map, Value};
use tokio::sync::mpsc;
@@ -8,7 +8,7 @@ use sinew_core::{ChatMessage, ModelRef, Part, Provider, Usage};
use crate::tool_run::{FileChange, ToolRunImage};
-#[derive(Debug, Clone, Serialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentEvent {
TurnStarted,
@@ -56,6 +56,10 @@ pub enum AgentEvent {
max_output_tokens: u32,
usage: Usage,
},
+ SteeringApplied {
+ id: String,
+ message: ChatMessage,
+ },
Interrupted,
Error {
message: String,
diff --git a/crates/sinew-app/src/agent/history.rs b/crates/sinew-app/src/agent/history.rs
index 1143cee8..8e39a80b 100644
--- a/crates/sinew-app/src/agent/history.rs
+++ b/crates/sinew-app/src/agent/history.rs
@@ -228,14 +228,15 @@ pub(super) fn successful_read_fingerprints(
else {
continue;
};
- if name == "read" {
+ let name_without_prefix = name.strip_prefix("default_api:").unwrap_or(name);
+ if name_without_prefix == "read" {
let Some(path) = input.get("path").and_then(|value| value.as_str()) else {
continue;
};
if let Ok(normalized) = read.normalize_path(path) {
pending_reads.insert(id.clone(), Some(normalized));
}
- } else if name == "edit_file" || name == "write_file" {
+ } else if name_without_prefix == "edit_file" || name_without_prefix == "write_file" {
pending_reads.insert(id.clone(), None);
}
}
diff --git a/crates/sinew-app/src/agent/repeat_guard.rs b/crates/sinew-app/src/agent/repeat_guard.rs
new file mode 100644
index 00000000..de4c4837
--- /dev/null
+++ b/crates/sinew-app/src/agent/repeat_guard.rs
@@ -0,0 +1,202 @@
+//! Détection automatique des boucles d'outils (le maillon "Capture" du système
+//! d'auto-amélioration). Quand l'agent rejoue la même commande / le même appel
+//! d'outil sans progresser, on injecte un rappel ciblé, puis on enregistre
+//! l'incident dans `errors_raw.json` et on coupe la boucle. La consolidation
+//! (`rules.rs`) transformera ensuite ces incidents répétés en règles globales.
+
+use std::collections::{HashMap, HashSet};
+use std::fs;
+use std::path::PathBuf;
+
+use chrono::Local;
+use serde_json::Value;
+
+use crate::tool_names;
+
+/// Nombre de répétitions identiques avant d'injecter un rappel fort.
+const WARN_THRESHOLD: u32 = 3;
+/// Nombre de répétitions identiques avant d'enregistrer l'erreur et de couper.
+const BREAK_THRESHOLD: u32 = 4;
+/// Longueur maximale du détail de commande conservé dans la signature/description.
+const DETAIL_MAX: usize = 160;
+
+/// Incident de boucle détecté, prêt à être enregistré et à arrêter le tour.
+pub struct RepeatHit {
+ pub id: String,
+ pub description: String,
+ pub count: u32,
+}
+
+#[derive(Default)]
+pub struct RepeatGuard {
+ counts: HashMap,
+ warned: HashSet,
+ pending_reminder: Option,
+ break_requested: Option,
+}
+
+impl RepeatGuard {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Observe le résultat d'un appel d'outil et met à jour les compteurs.
+ /// On ne suit que les actions à risque de boucle : commandes shell (`bash`)
+ /// et tout appel d'outil qui termine en erreur.
+ pub fn observe(&mut self, canonical_name: &str, input: &Value, is_error: bool) {
+ let is_bash = canonical_name == tool_names::BASH;
+ if !is_bash && !is_error {
+ return;
+ }
+
+ let detail = signature_detail(canonical_name, input);
+ if detail.is_empty() {
+ return;
+ }
+ let kind = if is_error { "err" } else { "cmd" };
+ let signature = format!("{kind}:{canonical_name}:{detail}");
+
+ let count = self.counts.entry(signature.clone()).or_insert(0);
+ *count += 1;
+ let count = *count;
+
+ if count >= WARN_THRESHOLD && self.warned.insert(signature.clone()) {
+ self.pending_reminder = Some(format!(
+ "\n\n\n\
+ BOUCLE DÉTECTÉE : vous venez de répéter {count} fois la même action sans progresser :\n\
+ « {short} »\n\
+ ARRÊTEZ de relancer cette commande/outil à l'identique. Identifiez la cause racine, \
+ changez d'approche, ou demandez une précision si vous êtes bloqué. \
+ Répéter encore sera enregistré comme une erreur et interrompra le tour.\n\
+ ",
+ count = count,
+ short = truncate(&human_detail(canonical_name, input), DETAIL_MAX),
+ ));
+ }
+
+ if count >= BREAK_THRESHOLD && self.break_requested.is_none() {
+ let human = human_detail(canonical_name, input);
+ self.break_requested = Some(RepeatHit {
+ id: error_id(canonical_name, &detail),
+ description: format!(
+ "L'agent a répété {count} fois la même action sans progresser : `{detail}`. \
+ Détecté automatiquement (boucle d'outil {canonical_name}).",
+ count = count,
+ detail = truncate(&human, DETAIL_MAX),
+ canonical_name = canonical_name,
+ ),
+ count,
+ });
+ }
+ }
+
+ /// Renvoie (et consomme) le rappel à injecter dans le prompt système.
+ pub fn take_reminder(&mut self) -> Option {
+ self.pending_reminder.take()
+ }
+
+ /// Renvoie (et consomme) une demande d'arrêt si une boucle est avérée.
+ pub fn take_break(&mut self) -> Option {
+ self.break_requested.take()
+ }
+}
+
+/// Détail normalisé utilisé pour la signature (insensible à la casse/espaces).
+fn signature_detail(canonical_name: &str, input: &Value) -> String {
+ let raw = human_detail(canonical_name, input);
+ let lowered = raw.to_lowercase();
+ let collapsed: String = lowered.split_whitespace().collect::>().join(" ");
+ truncate(&collapsed, DETAIL_MAX)
+}
+
+/// Détail lisible : pour bash, la commande ; sinon, l'input compact.
+fn human_detail(canonical_name: &str, input: &Value) -> String {
+ if canonical_name == tool_names::BASH {
+ if let Some(cmd) = input.get("command").and_then(Value::as_str) {
+ return cmd.trim().to_string();
+ }
+ }
+ match input {
+ Value::Object(_) | Value::Array(_) => {
+ serde_json::to_string(input).unwrap_or_default()
+ }
+ Value::String(s) => s.trim().to_string(),
+ other => other.to_string(),
+ }
+}
+
+/// Génère un identifiant stable pour `errors_raw.json` à partir de la signature.
+fn error_id(canonical_name: &str, detail: &str) -> String {
+ let token: String = detail
+ .chars()
+ .map(|c| if c.is_ascii_alphanumeric() { c } else { ' ' })
+ .collect::()
+ .split_whitespace()
+ .take(3)
+ .collect::>()
+ .join("_");
+ let token = if token.is_empty() {
+ "call".to_string()
+ } else {
+ token.to_lowercase()
+ };
+ format!("repeated_{canonical_name}_{token}")
+}
+
+fn truncate(s: &str, max: usize) -> String {
+ if s.chars().count() <= max {
+ return s.to_string();
+ }
+ let mut out: String = s.chars().take(max).collect();
+ out.push('…');
+ out
+}
+
+/// Enregistre (ou incrémente) un incident de boucle dans `errors_raw.json`.
+/// Crée le fichier au besoin. Tolérant aux erreurs : ne panique jamais.
+pub fn record_repeated_error(hit: &RepeatHit) {
+ let Ok(local_app_data) = std::env::var("LOCALAPPDATA") else {
+ return;
+ };
+ let sinew_dir = PathBuf::from(local_app_data).join("Sinew");
+ let _ = fs::create_dir_all(&sinew_dir);
+ let errors_path = sinew_dir.join("errors_raw.json");
+
+ let mut errors: Vec = match fs::read_to_string(&errors_path) {
+ Ok(data) => {
+ let clean = data.strip_prefix('\u{FEFF}').unwrap_or(&data);
+ serde_json::from_str(clean).unwrap_or_default()
+ }
+ Err(_) => Vec::new(),
+ };
+
+ let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
+ let mut found = false;
+ for entry in errors.iter_mut() {
+ if entry.get("id").and_then(Value::as_str) == Some(hit.id.as_str()) {
+ let count = entry.get("count").and_then(Value::as_i64).unwrap_or(0) + 1;
+ if let Value::Object(map) = entry {
+ map.insert("count".into(), Value::from(count));
+ map.insert("description".into(), Value::from(hit.description.clone()));
+ map.insert("last_occurrence".into(), Value::from(now.clone()));
+ // Une nouvelle occurrence rouvre l'erreur pour reconsolidation.
+ map.insert("consolidated_at".into(), Value::Null);
+ }
+ found = true;
+ break;
+ }
+ }
+
+ if !found {
+ errors.push(serde_json::json!({
+ "id": hit.id,
+ "description": hit.description,
+ "count": 1,
+ "last_occurrence": now,
+ }));
+ }
+
+ if let Ok(serialized) = serde_json::to_string_pretty(&errors) {
+ let _ = fs::write(&errors_path, format!("{serialized}\n"));
+ }
+}
diff --git a/crates/sinew-app/src/agent/tool_dispatch.rs b/crates/sinew-app/src/agent/tool_dispatch.rs
index 3b4741fa..c6c5fc1e 100644
--- a/crates/sinew-app/src/agent/tool_dispatch.rs
+++ b/crates/sinew-app/src/agent/tool_dispatch.rs
@@ -4,9 +4,10 @@ use serde_json::Value;
use tokio::sync::mpsc;
use crate::{
- tool_names, BashTool, CreateImageTool, EditFileTool, GlobTool, GrepTool, McpToolRegistry,
- QuestionTool, ReadFingerprint, ReadTool, SkillTool, SubAgentTool, TeamTool, ToDoListTool,
- TodoListState, ToolRunResult, ToolSettings, WebFetchTool, WebSearchTool, WriteFileTool,
+ tool_names, BashTool, CheckSotaTool, CodebaseSearchTool, ComputerUseTool, CreateImageTool, DeleteFileTool,
+ EditFileTool, GlobTool, GrepTool, ListDirTool, McpToolRegistry, QuestionTool, ReadFingerprint,
+ ReadLintsTool, ReadTool, SkillTool, SubAgentTool, TeamTool, ToDoListTool, TodoListState, ToolRunResult,
+ ToolSettings, WebFetchTool, WebSearchTool, WriteFileTool,
};
use super::{cancel::TurnCancel, context::AgentMode, events::AgentEvent};
@@ -28,10 +29,16 @@ pub(super) fn should_wait_for_cooperative_cancel(
pub(super) async fn run_tool(
bash: &BashTool,
glob: &GlobTool,
+ list_dir: &ListDirTool,
grep: &GrepTool,
+ codebase_search: &CodebaseSearchTool,
+ check_sota: &CheckSotaTool,
+ computer_use: &ComputerUseTool,
read: &ReadTool,
edit_file: &EditFileTool,
write_file: &WriteFileTool,
+ delete_file: &DeleteFileTool,
+ read_lints: &ReadLintsTool,
create_image: &CreateImageTool,
todo_list_tool: Option<&ToDoListTool>,
question: Option<&QuestionTool>,
@@ -52,6 +59,13 @@ pub(super) async fn run_tool(
input: Value,
) -> ToolRunResult {
let canonical_name = tool_names::canonical_tool_name(name);
+ if name == "composer_unsupported_tool" {
+ let message = input
+ .get("message")
+ .and_then(Value::as_str)
+ .unwrap_or("Unsupported Composer tool");
+ return ToolRunResult::err(message.to_string(), Vec::new());
+ }
if !tool_settings.is_enabled(canonical_name) {
return ToolRunResult::err(
format!("{canonical_name} is disabled in Settings"),
@@ -64,20 +78,51 @@ pub(super) async fn run_tool(
bash.run_input(input).await
} else if canonical_name == tool_names::GLOB {
glob.run(input).await
+ } else if canonical_name == tool_names::LIST_DIR {
+ list_dir.run(input).await
} else if canonical_name == tool_names::GREP {
grep.run(input).await
+ } else if canonical_name == tool_names::CODEBASE_SEARCH {
+ codebase_search.run(input).await
+ } else if canonical_name == tool_names::CHECK_SOTA {
+ check_sota.run(input).await
+ } else if canonical_name == tool_names::COMPUTER_USE {
+ computer_use.run(input).await
} else if canonical_name == tool_names::READ {
read.run(input).await
} else if canonical_name == tool_names::EDIT_FILE {
if mode == AgentMode::Plan {
return ToolRunResult::err("edit_file is unavailable in Plan mode", Vec::new());
}
- edit_file.run(input, read_fingerprints).await
+ let mut result = edit_file.run(input, read_fingerprints).await;
+ if !result.is_error {
+ let lints = read_lints.run(serde_json::json!({})).await;
+ if !lints.is_error && !lints.content.trim().is_empty() && !lints.content.contains("No linter errors found") {
+ result.content.push_str("\n\n[Auto-Lint Diagnostics (Self-Healing)]:\n");
+ result.content.push_str(&lints.content);
+ }
+ }
+ result
} else if canonical_name == tool_names::WRITE_FILE {
if mode == AgentMode::Plan {
return ToolRunResult::err("write_file is unavailable in Plan mode", Vec::new());
}
- write_file.run(input, read_fingerprints).await
+ let mut result = write_file.run(input, read_fingerprints).await;
+ if !result.is_error {
+ let lints = read_lints.run(serde_json::json!({})).await;
+ if !lints.is_error && !lints.content.trim().is_empty() && !lints.content.contains("No linter errors found") {
+ result.content.push_str("\n\n[Auto-Lint Diagnostics (Self-Healing)]:\n");
+ result.content.push_str(&lints.content);
+ }
+ }
+ result
+ } else if canonical_name == tool_names::DELETE_FILE {
+ if mode == AgentMode::Plan {
+ return ToolRunResult::err("delete_file is unavailable in Plan mode", Vec::new());
+ }
+ delete_file.run(input).await
+ } else if canonical_name == tool_names::READ_LINTS {
+ read_lints.run(input).await
} else if canonical_name == tool_names::CREATE_IMAGE {
if mode == AgentMode::Plan {
return ToolRunResult::err("create_image is unavailable in Plan mode", Vec::new());
diff --git a/crates/sinew-app/src/agent/tool_summary.rs b/crates/sinew-app/src/agent/tool_summary.rs
index 770a2db5..a2cde059 100644
--- a/crates/sinew-app/src/agent/tool_summary.rs
+++ b/crates/sinew-app/src/agent/tool_summary.rs
@@ -44,6 +44,44 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
return format!("Read {path}");
}
}
+ if name == tool_names::READ_LINTS {
+ if let Some(scope) = lint_paths_scope(input) {
+ return format!("Read lints in {scope}");
+ }
+ return "Read lints".to_string();
+ }
+ if name == tool_names::LIST_DIR {
+ if let Some(path) = input
+ .get("path")
+ .and_then(|value| value.as_str())
+ .map(str::trim)
+ .filter(|value| !value.is_empty() && *value != ".")
+ {
+ return format!("List {path}");
+ }
+ return "List directory".to_string();
+ }
+ if name == tool_names::DELETE_FILE {
+ if let Some(path) = input.get("path").and_then(|value| value.as_str()) {
+ return format!("Delete {path}");
+ }
+ return "Delete file".to_string();
+ }
+ if name == tool_names::CODEBASE_SEARCH {
+ if let Some(query) = input
+ .get("query")
+ .and_then(|value| value.as_str())
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ {
+ let mut clipped = query.chars().take(48).collect::();
+ if query.chars().count() > 48 {
+ clipped.push_str("...");
+ }
+ return format!("Search codebase: {clipped}");
+ }
+ return "Search codebase".to_string();
+ }
if name == tool_names::GREP {
let scope = input
.get("path")
@@ -91,20 +129,20 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
if file_count == 1 {
if let Some(path) = groups[0].get("path").and_then(Value::as_str) {
if replacement_count > 1 {
- return format!("Edit {path} · {replacement_count} replacements");
+ return format!("Edit {path} - {replacement_count} replacements");
}
return format!("Edit {path}");
}
return if replacement_count > 1 {
- format!("Edit file · {replacement_count} replacements")
+ format!("Edit file - {replacement_count} replacements")
} else {
"Edit file".to_string()
};
}
return if replacement_count > 0 {
- format!("Edit files · {file_count} files · {replacement_count} replacements")
+ format!("Edit files - {file_count} files - {replacement_count} replacements")
} else {
- format!("Edit files · {file_count} files")
+ format!("Edit files - {file_count} files")
};
}
return "Edit file".to_string();
@@ -125,7 +163,7 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
return if count == 0 {
"Clean context".to_string()
} else {
- format!("Clean context · {count} results")
+ format!("Clean context - {count} results")
};
}
if name == tool_names::UPDATE_GOAL {
@@ -198,7 +236,7 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
.map(str::trim)
.filter(|value| !value.is_empty());
if let (Some(server), Some(tool)) = (server, tool) {
- return format!("Load {server} · {tool}");
+ return format!("Load {server} - {tool}");
}
return "Load MCP tool".to_string();
}
@@ -209,7 +247,7 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
.map(str::trim)
.filter(|value| !value.is_empty())
{
- return format!("Load skill · {skill}");
+ return format!("Load skill - {skill}");
}
return "Load skill".to_string();
}
@@ -220,7 +258,7 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
.map(str::trim)
.filter(|value| !value.is_empty())
{
- return format!("Sub-agent · {task}");
+ return format!("Sub-agent - {task}");
}
return "Sub-agent".to_string();
}
@@ -241,11 +279,11 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
.map(str::trim)
.filter(|value| !value.is_empty());
return match (team, agent, objective) {
- (Some(team), Some(agent), _) => format!("Agent Swarm · restart @{agent} · {team}"),
- (None, Some(agent), _) => format!("Agent Swarm · restart @{agent}"),
- (Some(team), None, Some(objective)) => format!("Agent Swarm · {team} · {objective}"),
- (Some(team), None, None) => format!("Agent Swarm · {team}"),
- (None, None, Some(objective)) => format!("Agent Swarm · {objective}"),
+ (Some(team), Some(agent), _) => format!("Agent Swarm - restart @{agent} - {team}"),
+ (None, Some(agent), _) => format!("Agent Swarm - restart @{agent}"),
+ (Some(team), None, Some(objective)) => format!("Agent Swarm - {team} - {objective}"),
+ (Some(team), None, None) => format!("Agent Swarm - {team}"),
+ (None, None, Some(objective)) => format!("Agent Swarm - {objective}"),
_ => "Agent Swarm".to_string(),
};
}
@@ -256,7 +294,7 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
.map(str::trim)
.filter(|value| !value.is_empty())
{
- return format!("Agent Swarm · {team}");
+ return format!("Agent Swarm - {team}");
}
return "Create Agent Swarm".to_string();
}
@@ -273,8 +311,8 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
.map(str::trim)
.filter(|value| !value.is_empty());
return match (teammate, task) {
- (Some(teammate), Some(task)) => format!("Agent · @{teammate} · {task}"),
- (Some(teammate), None) => format!("Agent · @{teammate}"),
+ (Some(teammate), Some(task)) => format!("Agent - @{teammate} - {task}"),
+ (Some(teammate), None) => format!("Agent - @{teammate}"),
_ => "Agent teammate".to_string(),
};
}
@@ -285,7 +323,7 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
.map(str::trim)
.filter(|value| !value.is_empty())
{
- return format!("Message · {to}");
+ return format!("Message - {to}");
}
return "Send Agent Swarm message".to_string();
}
@@ -296,7 +334,7 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
.map(str::trim)
.filter(|value| !value.is_empty())
{
- return format!("Task · create · {subject}");
+ return format!("Task - create - {subject}");
}
return "Create task".to_string();
}
@@ -321,11 +359,11 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
.map(str::trim)
.filter(|value| !value.is_empty());
return match (action, task_id, subject) {
- (Some("create"), _, Some(subject)) => format!("Task · create · {subject}"),
+ (Some("create"), _, Some(subject)) => format!("Task - create - {subject}"),
(Some(action @ ("update" | "claim" | "delete")), Some(task_id), _) => {
- format!("Task · {action} · #{task_id}")
+ format!("Task - {action} - #{task_id}")
}
- (Some(action), _, _) => format!("Task · {action}"),
+ (Some(action), _, _) => format!("Task - {action}"),
_ => "Task list".to_string(),
};
}
@@ -345,8 +383,8 @@ pub(super) fn summarize_tool(name: &str, input: &Value) -> String {
.map(str::trim)
.filter(|value| !value.is_empty());
return match (task_id, status) {
- (Some(task_id), Some(status)) => format!("Task · #{task_id} · {status}"),
- (Some(task_id), None) => format!("Task · #{task_id}"),
+ (Some(task_id), Some(status)) => format!("Task - #{task_id} - {status}"),
+ (Some(task_id), None) => format!("Task - #{task_id}"),
_ => "Update task".to_string(),
};
}
@@ -424,6 +462,13 @@ fn grep_path_scope(value: &Value) -> Option {
}
}
+fn lint_paths_scope(input: &Value) -> Option {
+ input
+ .get("paths")
+ .or_else(|| input.get("path"))
+ .and_then(grep_path_scope)
+}
+
pub(super) fn display_mcp_server_name(value: &str) -> String {
let trimmed = value.trim();
let Some(rest) = trimmed.get(3..) else {
diff --git a/crates/sinew-app/src/agent/turn.rs b/crates/sinew-app/src/agent/turn.rs
index c9266acd..f015f94f 100644
--- a/crates/sinew-app/src/agent/turn.rs
+++ b/crates/sinew-app/src/agent/turn.rs
@@ -1,11 +1,12 @@
use std::{
collections::{BTreeMap, BTreeSet},
- time::Duration,
+ time::{Duration, Instant},
};
use futures_util::StreamExt;
use rand::Rng;
use serde_json::{json, Map, Value};
+use tokio::sync::mpsc::error::TryRecvError;
use tokio::{sync::mpsc, task::JoinHandle};
use uuid::Uuid;
@@ -16,7 +17,7 @@ use sinew_core::{
use super::{
assistant_message::AssistantMessageBuilder,
- cancel::EngineCommand,
+ cancel::{EngineCommand, SteeringCommand},
clean_context::{clean_context_descriptor, run_clean_context},
compaction::{
can_auto_compact_history, is_context_length_error, maybe_auto_compact_history,
@@ -30,6 +31,8 @@ use super::{
successful_read_fingerprints,
},
mode::{run_update_goal, system_prompt_for_turn, update_goal_descriptor},
+ repeat_guard::{record_repeated_error, RepeatGuard},
+ boost_distill::maybe_distill_tool_output,
tool_dispatch::{run_tool, should_wait_for_cooperative_cancel},
tool_summary::{display_mcp_server_name, pretty_json, should_stream_tool_args, summarize_tool},
};
@@ -38,9 +41,65 @@ use crate::{system_prompt_with_todo, tool_names, ReadFingerprint, ToolRunResult}
const SAFE_STREAM_MAX_RETRIES: usize = 5;
+fn tool_result_from_composer_bridge_meta(meta: &Option) -> Option {
+ let Value::Object(map) = meta.as_ref()? else {
+ return None;
+ };
+ let Value::Object(bridge) = map.get("composer_bridge")? else {
+ return None;
+ };
+ let content = bridge
+ .get("content")
+ .and_then(|v| v.as_str())
+ .unwrap_or("")
+ .to_string();
+ let is_error = bridge
+ .get("is_error")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+ Some(if is_error {
+ ToolRunResult::err(content, Vec::new())
+ } else {
+ ToolRunResult::ok(content, Vec::new())
+ })
+}
+
+fn cursor_system_prompt(
+ base: &str,
+ mode: AgentMode,
+ goal_workflow: &crate::GoalWorkflowState,
+ plan_mode_prompt: &str,
+) -> String {
+ let base = base.trim();
+ match mode {
+ AgentMode::Act => format!(
+ "{base}\n\nWhen the user asks to generate or create an image, use the generate image tool."
+ ),
+ AgentMode::Plan => {
+ let plan = plan_mode_prompt.trim();
+ if plan.is_empty() {
+ format!("{base}\n\nYou are in planning mode. Explore the codebase and propose a plan before making changes.")
+ } else {
+ format!("{base}\n\nYou are in planning mode.\n{plan}")
+ }
+ }
+ AgentMode::Goal => {
+ let mut prompt = format!(
+ "{base}\n\nWork autonomously toward the objective across multiple turns."
+ );
+ if let Some(objective) = super::mode::goal_objective(goal_workflow) {
+ prompt.push_str("\n\nCurrent objective:\n");
+ prompt.push_str(objective);
+ }
+ prompt
+ }
+ }
+}
+
pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
let TurnContext {
provider,
+ workspace_root,
model,
cache_key,
mut cache_stable_message_count,
@@ -54,10 +113,16 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
mut goal_workflow,
bash,
glob,
+ list_dir,
grep,
+ codebase_search,
+ check_sota,
+ computer_use,
read,
edit_file,
write_file,
+ delete_file,
+ read_lints,
create_image,
todo_list_tool,
question,
@@ -73,24 +138,62 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
event_tx,
cancel,
mut cmd_rx,
+ mut steering_rx,
} = ctx;
send_event(&event_tx, event_scope.as_ref(), AgentEvent::TurnStarted);
+ let turn_start = Instant::now();
strip_all_visible_tool_result_ids(&mut history);
normalize_tool_call_inputs(&mut history);
repair_missing_tool_results(&mut history);
- mcp.refresh_catalog(&history).await;
-
let mut cancelled = false;
+ tokio::select! {
+ biased;
+ command = cmd_rx.recv() => {
+ if matches!(command, Some(EngineCommand::Cancel)) {
+ cancelled = true;
+ }
+ }
+ _ = mcp.refresh_catalog(&history) => {}
+ }
+
+ let root_accepts_steering = steering_rx.is_some();
let mut compacted = false;
let mut loops = 0usize;
let mut auto_compaction_attempts = 0usize;
let mut current_turn_tool_result_ids = BTreeSet::new();
+ let mut repeat_guard = RepeatGuard::new();
let mut eager_tool_results = BTreeMap::>::new();
let mut read_fingerprints = successful_read_fingerprints(&history, &read);
todo_list.normalize();
+ if cancelled {
+ if root_accepts_steering {
+ cancel.close_steering();
+ }
+ send_event(&event_tx, event_scope.as_ref(), AgentEvent::Interrupted);
+ send_event(
+ &event_tx,
+ event_scope.as_ref(),
+ AgentEvent::TurnFinished { duration_ms: None },
+ );
+ todo_list.normalize();
+ return TurnOutput {
+ history,
+ todo_list,
+ goal_workflow,
+ interrupted: true,
+ compacted: false,
+ };
+ }
+
'conversation: loop {
+ drain_steering_commands(
+ &mut steering_rx,
+ &mut history,
+ &event_tx,
+ event_scope.as_ref(),
+ );
if let Some(teams) = &teams {
if let Some(messages_prompt) = teams.drain_current_agent_messages_prompt().await {
history.push(ChatMessage {
@@ -108,7 +211,11 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
bash.input_descriptor(),
glob.descriptor(),
grep.descriptor(),
+ codebase_search.descriptor(),
+ check_sota.descriptor(),
+ computer_use.descriptor(),
read.descriptor(),
+ read_lints.descriptor(),
clean_context_descriptor(),
web_search.descriptor(),
web_fetch.descriptor(),
@@ -137,6 +244,18 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
if let Some(teams) = &teams {
tool_descriptors.extend(teams.descriptors());
}
+ let tool_descriptors = if model.provider == "cursor" {
+ let mut filtered = tool_descriptors
+ .into_iter()
+ .filter(|tool| tool_names::is_cursor_compatible_tool(&tool.name))
+ .collect::>();
+ if mode != AgentMode::Plan && tool_settings.is_enabled(tool_names::CREATE_IMAGE) {
+ filtered.push(crate::composer_mcp_descriptor(&create_image.descriptor()));
+ }
+ filtered
+ } else {
+ tool_descriptors
+ };
let tool_descriptors = tool_settings.apply_to_descriptors(tool_descriptors);
let question_enabled = question.is_some() && tool_settings.is_enabled(tool_names::QUESTION);
@@ -147,14 +266,61 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
current_system_prompt.push_str(&team_reminder);
}
}
- let current_system_prompt = system_prompt_for_turn(
- ¤t_system_prompt,
- mode,
- &goal_workflow,
- tool_settings.plan_mode_prompt(),
- );
+ if loops >= 5 {
+ current_system_prompt.push_str(
+ "\n\n\n\
+ Attention : Vous avez effectué plusieurs tours d'outils consécutifs. \
+ Avant d'exécuter d'autres outils, vous DEVEZ prendre du recul, réfléchir sur vos tentatives précédentes \
+ et expliquer précisément ce qui bloque et comment vous allez ajuster votre méthode.\n\
+ "
+ );
+ }
+ if let Some(reminder) = repeat_guard.take_reminder() {
+ current_system_prompt.push_str(&reminder);
+ }
+ let current_system_prompt = if model.provider == "cursor" {
+ cursor_system_prompt(
+ ¤t_system_prompt,
+ mode,
+ &goal_workflow,
+ tool_settings.plan_mode_prompt(),
+ )
+ } else {
+ system_prompt_for_turn(
+ ¤t_system_prompt,
+ mode,
+ &goal_workflow,
+ tool_settings.plan_mode_prompt(),
+ )
+ };
+
+ let current_provider_name = provider.name();
+ let last_assistant_provider = history
+ .iter()
+ .rev()
+ .filter(|msg| msg.role == Role::Assistant)
+ .flat_map(|msg| &msg.parts)
+ .find_map(|part| {
+ let meta = match part {
+ Part::Text { meta, .. } => meta,
+ Part::Thinking { meta, .. } => meta,
+ Part::ToolCall { meta, .. } => meta,
+ _ => &None,
+ };
+ meta.as_ref()
+ .and_then(|m| m.get("provider"))
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string())
+ });
+
+ let provider_changed = if let Some(prev) = last_assistant_provider {
+ prev != current_provider_name
+ } else {
+ false
+ };
- if auto_compact {
+ if auto_compact || provider_changed {
+ let compaction_start = Instant::now();
match maybe_auto_compact_history(
&provider,
&model,
@@ -169,14 +335,32 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
event_scope.as_ref(),
&mut cmd_rx,
&mut auto_compaction_attempts,
+ &workspace_root,
+ &read_lints,
+ &todo_list,
)
.await
{
Ok(true) => {
+ let compaction_ms = compaction_start.elapsed().as_millis();
+ tracing::debug!(
+ provider = provider.name(),
+ compaction_ms,
+ "auto compaction completed before turn"
+ );
compacted = true;
continue;
}
- Ok(false) => {}
+ Ok(false) => {
+ let compaction_ms = compaction_start.elapsed().as_millis();
+ if compaction_ms > 10 {
+ tracing::debug!(
+ provider = provider.name(),
+ compaction_ms,
+ "auto compaction check (no compaction needed)"
+ );
+ }
+ }
Err(err) => {
send_event(
&event_tx,
@@ -188,12 +372,19 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
}
}
+ drain_steering_commands(
+ &mut steering_rx,
+ &mut history,
+ &event_tx,
+ event_scope.as_ref(),
+ );
let request_history =
history_with_current_tool_result_ids(&history, ¤t_turn_tool_result_ids);
let request = ProviderRequest::new(model.clone(), request_history)
.with_system(current_system_prompt.clone())
.with_tools(tool_descriptors.clone())
- .with_cache_stable_message_count(cache_stable_message_count);
+ .with_cache_stable_message_count(cache_stable_message_count)
+ .with_workspace_root(workspace_root.display().to_string());
let request = match &cache_key {
Some(cache_key) => request.with_cache_key(cache_key.clone()),
None => request,
@@ -203,10 +394,35 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
None => request,
};
+ let loop_start = Instant::now();
let mut stream_retry_attempts = 0usize;
let (message_builder, mut stop_reason, response_usage) = 'stream_attempt: loop {
- let mut stream = match provider.stream(request.clone()).await {
- Ok(stream) => stream,
+ let stream_setup_start = Instant::now();
+ let stream_res = tokio::select! {
+ biased;
+ res = provider.stream(request.clone()) => Some(res),
+ command = cmd_rx.recv() => {
+ if matches!(command, Some(EngineCommand::Cancel)) {
+ cancelled = true;
+ break 'conversation;
+ }
+ None
+ }
+ };
+ let Some(stream_res) = stream_res else {
+ continue 'stream_attempt;
+ };
+ let mut stream = match stream_res {
+ Ok(stream) => {
+ let stream_setup_ms = stream_setup_start.elapsed().as_millis();
+ tracing::debug!(
+ provider = provider.name(),
+ model = model.name,
+ stream_setup_ms,
+ "provider stream setup completed"
+ );
+ stream
+ }
Err(err) => {
if should_retry_stream(&err, stream_retry_attempts) {
stream_retry_attempts += 1;
@@ -217,7 +433,16 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
error = %err,
"retrying provider stream setup"
);
- tokio::time::sleep(stream_retry_delay(stream_retry_attempts)).await;
+ tokio::select! {
+ biased;
+ command = cmd_rx.recv() => {
+ if matches!(command, Some(EngineCommand::Cancel)) {
+ cancelled = true;
+ break 'conversation;
+ }
+ }
+ _ = tokio::time::sleep(stream_retry_delay(stream_retry_attempts)) => {}
+ }
continue 'stream_attempt;
}
@@ -225,6 +450,7 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
&& is_context_length_error(&err)
&& can_auto_compact_history(&history, auto_compaction_attempts)
{
+ let compaction_start = Instant::now();
match run_auto_compaction(
&provider,
&model,
@@ -238,10 +464,20 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
event_scope.as_ref(),
&mut cmd_rx,
&mut auto_compaction_attempts,
+ &workspace_root,
+ &read_lints,
+ &todo_list,
+ None,
)
.await
{
Ok(()) => {
+ let compaction_ms = compaction_start.elapsed().as_millis();
+ tracing::debug!(
+ provider = provider.name(),
+ compaction_ms,
+ "auto compaction completed after stream error"
+ );
compacted = true;
continue 'conversation;
}
@@ -276,6 +512,7 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
let mut stream_error = None;
let mut saw_message_stop = false;
let mut finalized_tool_calls = 0usize;
+ let mut first_token = true;
loop {
tokio::select! {
@@ -290,7 +527,19 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
event = stream.next() => {
let Some(event) = event else { break; };
let event = match event {
- Ok(event) => event,
+ Ok(event) => {
+ if first_token {
+ first_token = false;
+ let first_token_ms = stream_setup_start.elapsed().as_millis();
+ tracing::debug!(
+ provider = provider.name(),
+ model = model.name,
+ first_token_ms,
+ "first token received"
+ );
+ }
+ event
+ }
Err(err) => {
stream_error = Some(err);
break;
@@ -409,7 +658,7 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
// Detect a silent stream close: the underlying SSE source returned `None` (or yielded
// its last item) without ever emitting a `MessageStop`. This is the classic "OpenAI
- // just stops without an error" symptom — usually a connection drop on the provider /
+ // just stops without an error" symptom — usually a connection drop on the provider /
// edge proxy side. Surface it as an explicit stream error so the user gets feedback
// and the normal recovery path (auto-compaction, etc.) is given a chance to run.
if !cancelled && stream_error.is_none() && !saw_message_stop {
@@ -439,7 +688,16 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
error = %err,
"retrying provider stream"
);
- tokio::time::sleep(stream_retry_delay(stream_retry_attempts)).await;
+ tokio::select! {
+ biased;
+ command = cmd_rx.recv() => {
+ if matches!(command, Some(EngineCommand::Cancel)) {
+ cancelled = true;
+ break 'conversation;
+ }
+ }
+ _ = tokio::time::sleep(stream_retry_delay(stream_retry_attempts)) => {}
+ }
continue 'stream_attempt;
}
@@ -461,6 +719,10 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
event_scope.as_ref(),
&mut cmd_rx,
&mut auto_compaction_attempts,
+ &workspace_root,
+ &read_lints,
+ &todo_list,
+ None,
)
.await
{
@@ -524,15 +786,37 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
history.push(assistant.clone());
}
+ let steering_after_assistant = if !matches!(stop_reason, StopReason::ToolUse) {
+ drain_steering_commands(
+ &mut steering_rx,
+ &mut history,
+ &event_tx,
+ event_scope.as_ref(),
+ )
+ } else {
+ false
+ };
+
if !matches!(stop_reason, StopReason::ToolUse) && !eager_tool_results.is_empty() {
stop_reason = StopReason::ToolUse;
}
if !matches!(stop_reason, StopReason::ToolUse) {
+ if steering_after_assistant {
+ continue 'conversation;
+ }
break;
}
if loops >= max_tool_rounds {
abort_eager_tool_results(&mut eager_tool_results);
+ tracing::warn!(
+ provider = provider.name(),
+ model = model.name,
+ loops,
+ max_tool_rounds,
+ loop_elapsed_ms = loop_start.elapsed().as_millis(),
+ "tool loop limit reached"
+ );
send_event(
&event_tx,
event_scope.as_ref(),
@@ -547,32 +831,56 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
let mut tool_results = Vec::new();
for part in &assistant.parts {
if let Part::ToolCall {
- id, name, input, ..
+ id,
+ name,
+ input,
+ meta,
} = part
{
- let result = if name == "clean_context" {
- run_clean_context(&mut history, input.clone(), ¤t_turn_tool_result_ids)
+ let result = if let Some(result) = tool_result_from_composer_bridge_meta(meta) {
+ result
+ } else if name == "clean_context" {
+ let tool_start = Instant::now();
+ let result = run_clean_context(&mut history, input.clone(), ¤t_turn_tool_result_ids);
+ let tool_ms = tool_start.elapsed().as_millis();
+ tracing::debug!(tool = name, tool_ms, "tool executed");
+ result
} else if name == "update_goal" {
- run_update_goal(&mut goal_workflow, input.clone())
+ let tool_start = Instant::now();
+ let result = run_update_goal(&mut goal_workflow, input.clone());
+ let tool_ms = tool_start.elapsed().as_millis();
+ tracing::debug!(tool = name, tool_ms, "tool executed");
+ result
} else if let Some(handle) = eager_tool_results.remove(id) {
- match handle.await {
+ let tool_start = Instant::now();
+ let result = match handle.await {
Ok(result) => result,
Err(err) => {
ToolRunResult::err(format!("write_file task failed: {err}"), Vec::new())
}
- }
+ };
+ let tool_ms = tool_start.elapsed().as_millis();
+ tracing::debug!(tool = name, tool_ms, eager = true, "tool executed");
+ result
} else if should_wait_for_cooperative_cancel(
name,
subagents.as_ref(),
teams.as_ref(),
) {
+ let tool_start = Instant::now();
let result = run_tool(
&bash,
&glob,
+ &list_dir,
&grep,
+ &codebase_search,
+ &check_sota,
+ &computer_use,
&read,
&edit_file,
&write_file,
+ &delete_file,
+ &read_lints,
&create_image,
todo_list_tool.as_deref(),
question.as_deref(),
@@ -593,13 +901,23 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
input.clone(),
)
.await;
+ let tool_ms = tool_start.elapsed().as_millis();
+ tracing::debug!(tool = name, tool_ms, "tool executed");
if matches!(cmd_rx.try_recv(), Ok(EngineCommand::Cancel)) {
cancelled = true;
abort_eager_tool_results(&mut eager_tool_results);
+ } else {
+ drain_steering_commands(
+ &mut steering_rx,
+ &mut history,
+ &event_tx,
+ event_scope.as_ref(),
+ );
}
result
} else {
- tokio::select! {
+ let tool_start = Instant::now();
+ let result = tokio::select! {
biased;
command = cmd_rx.recv() => {
if matches!(command, Some(EngineCommand::Cancel)) {
@@ -613,10 +931,16 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
result = run_tool(
&bash,
&glob,
+ &list_dir,
&grep,
+ &codebase_search,
+ &check_sota,
+ &computer_use,
&read,
&edit_file,
&write_file,
+ &delete_file,
+ &read_lints,
&create_image,
todo_list_tool.as_deref(),
question.as_deref(),
@@ -636,9 +960,13 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
name,
input.clone(),
) => result,
- }
+ };
+ let tool_ms = tool_start.elapsed().as_millis();
+ tracing::debug!(tool = name, tool_ms, "tool executed");
+ result
};
let canonical_name = tool_names::canonical_tool_name(name);
+ repeat_guard.observe(canonical_name, input, result.is_error);
if (canonical_name == tool_names::READ
|| canonical_name == tool_names::EDIT_FILE
|| canonical_name == tool_names::WRITE_FILE)
@@ -687,14 +1015,25 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
if canonical_name != tool_names::CLEAN_CONTEXT {
current_turn_tool_result_ids.insert(id.clone());
}
+ // Boost Local : si actif, distille les grosses sorties (bash/web_fetch)
+ // pour le contexte du modèle. L'UI a déjà reçu la sortie complète.
+ let history_content =
+ match maybe_distill_tool_output(canonical_name, &result_content, result.is_error)
+ .await
+ {
+ Some(distilled) => distilled,
+ None => result_content.clone(),
+ };
tool_results.push(Part::ToolResult {
tool_call_id: id.clone(),
- content: result_content,
+ content: history_content,
images: result_images
.into_iter()
.map(|image| ToolResultImage {
media_type: image.media_type,
- data: if canonical_name == tool_names::CREATE_IMAGE {
+ data: if canonical_name == tool_names::CREATE_IMAGE
+ && model.provider != "cursor"
+ {
String::new()
} else {
image.data
@@ -720,6 +1059,14 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
}
if tool_results.is_empty() {
+ if drain_steering_commands(
+ &mut steering_rx,
+ &mut history,
+ &event_tx,
+ event_scope.as_ref(),
+ ) {
+ continue 'conversation;
+ }
break;
}
@@ -734,9 +1081,35 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
role: Role::User,
parts: tool_results,
});
+ let steering_after_tools = drain_steering_commands(
+ &mut steering_rx,
+ &mut history,
+ &event_tx,
+ event_scope.as_ref(),
+ );
if cancelled {
break 'conversation;
}
+ if let Some(hit) = repeat_guard.take_break() {
+ record_repeated_error(&hit);
+ tracing::warn!(
+ error_id = %hit.id,
+ count = hit.count,
+ "repeated tool loop detected, recorded and stopping turn"
+ );
+ send_event(
+ &event_tx,
+ event_scope.as_ref(),
+ AgentEvent::Error {
+ message: format!(
+ "Boucle d'outil détectée et stoppée : la même action a été répétée {} fois. \
+ Incident enregistré pour auto-amélioration ({}).",
+ hit.count, hit.id
+ ),
+ },
+ );
+ break 'conversation;
+ }
if stop_after_question_result {
stop_questions = true;
history.push(ChatMessage {
@@ -748,15 +1121,27 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
});
continue 'conversation;
}
+ if steering_after_tools {
+ continue 'conversation;
+ }
}
+ if root_accepts_steering {
+ cancel.close_steering();
+ }
if cancelled {
send_event(&event_tx, event_scope.as_ref(), AgentEvent::Interrupted);
}
send_event(
&event_tx,
event_scope.as_ref(),
- AgentEvent::TurnFinished { duration_ms: None },
+ AgentEvent::TurnFinished { duration_ms: Some(turn_start.elapsed().as_millis() as i64) },
+ );
+ tracing::debug!(
+ total_turn_ms = turn_start.elapsed().as_millis(),
+ cancelled,
+ compacted,
+ "turn finished"
);
todo_list.normalize();
TurnOutput {
@@ -768,6 +1153,39 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput {
}
}
+fn drain_steering_commands(
+ steering_rx: &mut Option>,
+ history: &mut Vec,
+ event_tx: &mpsc::UnboundedSender,
+ event_scope: Option<&AgentEventScope>,
+) -> bool {
+ let Some(rx) = steering_rx.as_mut() else {
+ return false;
+ };
+ let mut changed = false;
+ loop {
+ match rx.try_recv() {
+ Ok(command) => {
+ let id = command.id;
+ let message = command.message;
+ history.push(message.clone());
+ send_event(
+ event_tx,
+ event_scope,
+ AgentEvent::SteeringApplied { id, message },
+ );
+ changed = true;
+ }
+ Err(TryRecvError::Empty) => break,
+ Err(TryRecvError::Disconnected) => {
+ *steering_rx = None;
+ break;
+ }
+ }
+ }
+ changed
+}
+
pub(super) fn retain_cancelled_eager_parts(
message: &mut ChatMessage,
eager_tool_results: &BTreeMap>,
@@ -824,8 +1242,24 @@ fn update_read_fingerprint_cache(
}
}
+fn is_non_retryable_stream_err(err: &AppError) -> bool {
+ let msg = match err {
+ AppError::Auth(m) | AppError::Network(m) | AppError::Stream(m) => m.to_ascii_lowercase(),
+ AppError::Decode(m) => m.to_ascii_lowercase(),
+ AppError::RetryableStream { message, .. } => message.to_ascii_lowercase(),
+ _ => return false,
+ };
+ msg.contains("unauthenticated")
+ || msg.contains("non authentifié")
+ || msg.contains("oauth")
+ || msg.contains("réglages → fournisseurs")
+ || msg.contains("not connected")
+ || msg.contains("n'est pas connecté")
+}
+
fn should_retry_stream(err: &AppError, attempts: usize) -> bool {
attempts < SAFE_STREAM_MAX_RETRIES
+ && !is_non_retryable_stream_err(err)
&& matches!(
err,
AppError::Network(_)
diff --git a/crates/sinew-app/src/bash.rs b/crates/sinew-app/src/bash.rs
index 2e1b90ff..bd77beb0 100644
--- a/crates/sinew-app/src/bash.rs
+++ b/crates/sinew-app/src/bash.rs
@@ -313,9 +313,9 @@ impl BashTool {
) -> Result {
#[cfg(windows)]
{
- return spawn_windows_piped_session(command, cwd, max_lifetime, before, || {
+ spawn_windows_piped_session(command, cwd, max_lifetime, before, || {
self.next_session_id.fetch_add(1, Ordering::Relaxed)
- });
+ })
}
#[cfg(not(windows))]
@@ -819,9 +819,15 @@ mod tests {
let root = temp_workspace("interactive_session_accepts_input");
let tool = BashTool::new(&root);
+ let command = if cfg!(windows) {
+ "Write-Host -NoNewline 'Name: '; $name = [Console]::ReadLine(); Write-Host \"Hello $name\""
+ } else {
+ "printf 'Name: '; read name; printf 'Hello %s\\n' \"$name\""
+ };
+
let started = tool
.run(json!({
- "command": "printf 'Name: '; read name; printf 'Hello %s\\n' \"$name\"",
+ "command": command,
"yield_time_ms": 500
}))
.await;
@@ -830,14 +836,26 @@ mod tests {
assert!(started.content.contains("process still running"));
let session_id = parse_session_id(&started.content);
- let finished = tool
+ let mut finished = tool
.run_input(json!({
"session_id": session_id,
"input": "Ada\n",
- "yield_time_ms": 1_000
+ "yield_time_ms": 3_000
}))
.await;
+ for _ in 0..5 {
+ if finished.is_error || finished.content.contains("[exit status:") {
+ break;
+ }
+ finished = tool
+ .run_input(json!({
+ "session_id": session_id,
+ "yield_time_ms": 1_000
+ }))
+ .await;
+ }
+
assert!(!finished.is_error, "{}", finished.content);
assert!(
finished.content.contains("Hello Ada"),
diff --git a/crates/sinew-app/src/check_sota.rs b/crates/sinew-app/src/check_sota.rs
new file mode 100644
index 00000000..b12f19b3
--- /dev/null
+++ b/crates/sinew-app/src/check_sota.rs
@@ -0,0 +1,305 @@
+use serde_json::{json, Value};
+use sinew_core::ToolDescriptor;
+use std::path::PathBuf;
+use std::process::Command as StdCommand;
+
+use crate::{tool_names, tool_run::ToolRunResult};
+
+#[derive(Debug, Clone, Default)]
+pub struct CheckSotaTool;
+
+impl CheckSotaTool {
+ pub fn new() -> Self {
+ Self
+ }
+
+ pub fn descriptor(&self) -> ToolDescriptor {
+ ToolDescriptor {
+ name: tool_names::CHECK_SOTA.into(),
+ description: "Check the status of SOTA (State-of-the-Art) development tools and system dependencies on this machine (e.g. ripgrep/rg, git, python, cargo, node, npm). Use this to verify if everything is installed and working correctly.".into(),
+ input_schema: json!({
+ "type": "object",
+ "properties": {},
+ "additionalProperties": false
+ }),
+ }
+ }
+
+ pub async fn run(&self, _input: Value) -> ToolRunResult {
+ let mut results = serde_json::Map::new();
+
+ // 1. Ripgrep (rg)
+ let rg_status = check_binary("rg");
+ results.insert("ripgrep".into(), rg_status);
+
+ // 2. Git
+ let git_status = check_binary("git");
+ results.insert("git".into(), git_status);
+
+ // 3. Python
+ let python_status = check_binary("python");
+ results.insert("python".into(), python_status);
+
+ // 3b. Pip (pip / pip3)
+ let mut pip_status = check_binary("pip");
+ if !pip_status
+ .get("available")
+ .and_then(|a| a.as_bool())
+ .unwrap_or(false)
+ {
+ let pip3_status = check_binary("pip3");
+ if pip3_status
+ .get("available")
+ .and_then(|a| a.as_bool())
+ .unwrap_or(false)
+ {
+ pip_status = pip3_status;
+ }
+ }
+ results.insert("pip".into(), pip_status);
+
+ // 4. Cargo / Rust
+ let cargo_status = check_binary("cargo");
+ results.insert("cargo".into(), cargo_status);
+
+ // 4b. Rust Compiler (rustc)
+ let rustc_status = check_binary("rustc");
+ results.insert("rustc".into(), rustc_status);
+
+ // 5. Node
+ let node_status = check_binary("node");
+ results.insert("node".into(), node_status);
+
+ // 6. Npm
+ let npm_status = check_binary("npm");
+ results.insert("npm".into(), npm_status);
+
+ // 7. Sinew Browser Extension
+ let extension_status = check_sinew_chrome_bridge();
+ results.insert("sinew-extension".into(), extension_status);
+
+ // Compute overall SOTA status
+ let mut overall_ok = true;
+ for (key, val) in results.iter() {
+ if key == "sinew-extension" {
+ continue;
+ }
+ if let Some(available) = val.get("available").and_then(|a| a.as_bool()) {
+ if !available {
+ overall_ok = false;
+ }
+ }
+ }
+
+ let output = json!({
+ "status": if overall_ok { "ok" } else { "warning" },
+ "message": if overall_ok {
+ "All SOTA development tools are fully installed and configured."
+ } else {
+ "Some SOTA development tools or dependencies are missing or could not be run."
+ },
+ "tools": results
+ });
+
+ ToolRunResult::ok(
+ serde_json::to_string_pretty(&output).unwrap_or_default(),
+ Vec::new(),
+ )
+ }
+}
+
+fn check_sinew_chrome_bridge() -> Value {
+ let mut available = false;
+ let mut path = None;
+ let mut version = None;
+ let mut error = None;
+
+ #[cfg(windows)]
+ {
+ use std::os::windows::process::CommandExt;
+ let mut cmd = StdCommand::new("reg");
+ cmd.args(["query", "HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.sinew.chrome_bridge", "/ve"]);
+ cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
+ match cmd.output() {
+ Ok(output) => {
+ if output.status.success() {
+ let out_str = String::from_utf8_lossy(&output.stdout);
+ if let Some(pos) = out_str.find("REG_SZ") {
+ let p = out_str[pos + 6..].trim().to_string();
+ let p_buf = PathBuf::from(&p);
+ path = Some(p.clone());
+ if p_buf.is_file() {
+ available = true;
+ // Check package.json in the same directory for version
+ let pkg_json_path = p_buf.parent().map(|parent| parent.join("package.json"));
+ if let Some(pkg_path) = pkg_json_path {
+ if pkg_path.is_file() {
+ if let Ok(content) = std::fs::read_to_string(pkg_path) {
+ if let Ok(pkg) = serde_json::from_str::(&content) {
+ if let Some(v) = pkg.get("version").and_then(|v| v.as_str()) {
+ version = Some(v.to_string());
+ }
+ }
+ }
+ }
+ }
+ if version.is_none() {
+ version = Some("1.0.0".into());
+ }
+ } else {
+ error = Some("Fichier manifeste de l'extension introuvable".to_string());
+ }
+ } else {
+ error = Some("Impossible de lire le chemin dans la base de registre".to_string());
+ }
+ } else {
+ error = Some("Extension Sinew non enregistrée ou non installée".to_string());
+ }
+ }
+ Err(e) => {
+ error = Some(e.to_string());
+ }
+ }
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ let home = std::env::var("HOME").unwrap_or_default();
+ let p_buf = PathBuf::from(home).join("Library/Application Support/Google/Chrome/NativeMessagingHosts/com.sinew.chrome_bridge.json");
+ if p_buf.is_file() {
+ available = true;
+ path = Some(p_buf.display().to_string());
+ // Try package.json in same directory
+ let pkg_json_path = p_buf.parent().map(|parent| parent.join("package.json"));
+ if let Some(pkg_path) = pkg_json_path {
+ if pkg_path.is_file() {
+ if let Ok(content) = std::fs::read_to_string(pkg_path) {
+ if let Ok(pkg) = serde_json::from_str::(&content) {
+ if let Some(v) = pkg.get("version").and_then(|v| v.as_str()) {
+ version = Some(v.to_string());
+ }
+ }
+ }
+ }
+ }
+ if version.is_none() {
+ version = Some("1.0.0".into());
+ }
+ } else {
+ error = Some("Extension Sinew non configurée ou non installée".to_string());
+ }
+ }
+
+ #[cfg(target_os = "linux")]
+ {
+ let home = std::env::var("HOME").unwrap_or_default();
+ let p_buf = PathBuf::from(home).join(".config/google-chrome/NativeMessagingHosts/com.sinew.chrome_bridge.json");
+ if p_buf.is_file() {
+ available = true;
+ path = Some(p_buf.display().to_string());
+ // Try package.json in same directory
+ let pkg_json_path = p_buf.parent().map(|parent| parent.join("package.json"));
+ if let Some(pkg_path) = pkg_json_path {
+ if pkg_path.is_file() {
+ if let Ok(content) = std::fs::read_to_string(pkg_path) {
+ if let Ok(pkg) = serde_json::from_str::(&content) {
+ if let Some(v) = pkg.get("version").and_then(|v| v.as_str()) {
+ version = Some(v.to_string());
+ }
+ }
+ }
+ }
+ }
+ if version.is_none() {
+ version = Some("1.0.0".into());
+ }
+ } else {
+ error = Some("Extension Sinew non configurée ou non installée".to_string());
+ }
+ }
+
+ json!({
+ "available": available,
+ "path": path,
+ "version": version,
+ "error": error
+ })
+}
+
+fn check_binary(name: &str) -> Value {
+ let path = find_in_path(name);
+ let available = path.is_some();
+
+ let mut version = None;
+ let mut error = None;
+
+ if let Some(ref p) = path {
+ #[cfg(windows)]
+ let mut cmd = {
+ let is_batch = p.extension().is_some_and(|ext| ext == "cmd" || ext == "bat");
+ if is_batch {
+ let mut c = StdCommand::new("cmd");
+ c.arg("/C").arg(p);
+ c
+ } else {
+ StdCommand::new(p)
+ }
+ };
+
+ #[cfg(not(windows))]
+ let mut cmd = StdCommand::new(p);
+
+ cmd.arg("--version");
+ #[cfg(windows)]
+ {
+ use std::os::windows::process::CommandExt;
+ cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
+ }
+ match cmd.output() {
+ Ok(output) => {
+ if output.status.success() {
+ version = Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
+ } else {
+ let err_msg = String::from_utf8_lossy(&output.stderr).trim().to_string();
+ error = Some(format!(
+ "Exit status: {}. Error: {}",
+ output.status, err_msg
+ ));
+ }
+ }
+ Err(e) => {
+ error = Some(e.to_string());
+ }
+ }
+ } else {
+ error = Some("Executable not found in system PATH".to_string());
+ }
+
+ json!({
+ "available": available,
+ "path": path.map(|p| p.display().to_string()),
+ "version": version,
+ "error": error
+ })
+}
+
+fn find_in_path(name: &str) -> Option {
+ let paths = std::env::var_os("PATH")?;
+ let mut names = vec![];
+ #[cfg(windows)]
+ {
+ if !name.ends_with(".exe") && !name.ends_with(".cmd") && !name.ends_with(".bat") {
+ names.push(format!("{name}.exe"));
+ names.push(format!("{name}.cmd"));
+ names.push(format!("{name}.bat"));
+ }
+ names.push(name.to_string());
+ }
+ #[cfg(not(windows))]
+ {
+ names.push(name.to_string());
+ }
+ std::env::split_paths(&paths)
+ .flat_map(|path| names.iter().map(move |n| path.join(n)))
+ .find(|path| path.is_file())
+}
diff --git a/crates/sinew-app/src/codebase_search.rs b/crates/sinew-app/src/codebase_search.rs
new file mode 100644
index 00000000..72cf2cf5
--- /dev/null
+++ b/crates/sinew-app/src/codebase_search.rs
@@ -0,0 +1,145 @@
+use std::path::PathBuf;
+
+use anyhow::{bail, Result};
+use serde::Deserialize;
+use serde_json::{json, Value};
+use sinew_core::ToolDescriptor;
+use sinew_index::{index_and_search_workspace_isolated, process_isolation_enabled};
+
+use crate::{tool_names, tool_run::ToolRunResult};
+
+const DEFAULT_LIMIT: usize = 30;
+const MAX_LIMIT: usize = 50;
+
+#[derive(Debug, Clone)]
+pub struct CodebaseSearchTool {
+ workspace_root: PathBuf,
+}
+
+impl CodebaseSearchTool {
+ pub fn new(workspace_root: impl Into) -> Self {
+ Self {
+ workspace_root: workspace_root.into(),
+ }
+ }
+
+ pub fn descriptor(&self) -> ToolDescriptor {
+ ToolDescriptor {
+ name: tool_names::CODEBASE_SEARCH.into(),
+ description:
+ "Search the local workspace index for relevant code chunks by meaning or keywords."
+ .into(),
+ input_schema: json!({
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "Natural language or keyword query describing what to find in the codebase."
+ },
+ "path": {
+ "type": "string",
+ "description": "Optional path prefix within the workspace (e.g. src/auth)."
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": MAX_LIMIT,
+ "default": DEFAULT_LIMIT,
+ "description": "Maximum number of chunks to return."
+ }
+ },
+ "required": ["query"],
+ "additionalProperties": false
+ }),
+ }
+ }
+
+ pub async fn run(&self, input: Value) -> ToolRunResult {
+ match self.search(input).await {
+ Ok(output) => ToolRunResult::ok(output, Vec::new()),
+ Err(err) => ToolRunResult::err(err.to_string(), Vec::new()),
+ }
+ }
+
+ async fn search(&self, input: Value) -> Result {
+ let parsed: CodebaseSearchInput = serde_json::from_value(input)
+ .map_err(|err| anyhow::anyhow!("invalid codebase_search input: {err}"))?;
+ let query = parsed.query.trim();
+ if query.is_empty() {
+ bail!("query is required");
+ }
+ let limit = parsed.limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT);
+ let path_prefix = parsed
+ .path
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty());
+
+ let (stats, hits) =
+ index_and_search_workspace_isolated(&self.workspace_root, query, path_prefix, limit)?;
+
+ let mut output = format!(
+ "index: {} files, {} chunks (updated {} files this sync, process isolation {})\nresults: {}\n\n",
+ stats.files_indexed,
+ stats.chunks_indexed,
+ stats.files_updated,
+ if process_isolation_enabled() { "on" } else { "off" },
+ hits.len()
+ );
+ if hits.is_empty() {
+ output.push_str("No matches.");
+ return Ok(output);
+ }
+ for hit in hits {
+ output.push_str(&format!(
+ "{}:{}-{} (score {:.2})\n{}\n\n",
+ hit.path,
+ hit.start_line,
+ hit.end_line,
+ hit.score,
+ hit.snippet.trim()
+ ));
+ }
+ Ok(output.trim_end().to_string())
+ }
+}
+
+#[derive(Debug, Deserialize)]
+struct CodebaseSearchInput {
+ query: String,
+ #[serde(default)]
+ path: Option,
+ #[serde(default)]
+ limit: Option,
+}
+
+#[cfg(test)]
+mod tests {
+ use std::fs;
+
+ use super::*;
+
+ #[tokio::test]
+ async fn codebase_search_finds_indexed_symbols() {
+ let root = std::env::temp_dir().join(format!(
+ "sinew-codebase-search-{}",
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ ));
+ fs::create_dir_all(&root).unwrap();
+ let root = root.canonicalize().expect("canonical temp workspace");
+ fs::write(root.join("auth.rs"), "pub fn verify_session_token() {}\n").unwrap();
+
+ let tool = CodebaseSearchTool::new(&root);
+ let result = tool
+ .search(json!({ "query": "verify" }))
+ .await
+ .expect("search should succeed");
+
+ assert!(result.contains("auth.rs"));
+ assert!(result.contains("verify"));
+ let _ = fs::remove_dir_all(root);
+ }
+}
diff --git a/crates/sinew-app/src/computer_use.rs b/crates/sinew-app/src/computer_use.rs
new file mode 100644
index 00000000..10138e31
--- /dev/null
+++ b/crates/sinew-app/src/computer_use.rs
@@ -0,0 +1,457 @@
+use std::ptr::null_mut;
+use serde_json::{json, Value};
+use sinew_core::ToolDescriptor;
+use crate::{tool_names, tool_run::{ToolRunImage, ToolRunResult}};
+use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
+
+#[derive(Debug, Clone, Default)]
+pub struct ComputerUseTool;
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+struct BITMAPINFOHEADER {
+ bi_size: u32,
+ bi_width: i32,
+ bi_height: i32,
+ bi_planes: u16,
+ bi_bit_count: u16,
+ bi_compression: u32,
+ bi_size_image: u32,
+ bi_x_pels_per_meter: i32,
+ bi_y_pels_per_meter: i32,
+ bi_clr_used: u32,
+ bi_clr_important: u32,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+struct RGBQUAD {
+ rgb_blue: u8,
+ rgb_green: u8,
+ rgb_red: u8,
+ rgb_reserved: u8,
+}
+
+#[repr(C)]
+struct BITMAPINFO {
+ bmi_header: BITMAPINFOHEADER,
+ bmi_colors: [RGBQUAD; 1],
+}
+
+#[repr(C)]
+struct POINT {
+ x: i32,
+ y: i32,
+}
+
+#[cfg(target_os = "windows")]
+#[link(name = "user32")]
+extern "system" {
+ fn GetDC(hwnd: *mut std::ffi::c_void) -> *mut std::ffi::c_void;
+ fn ReleaseDC(hwnd: *mut std::ffi::c_void, hdc: *mut std::ffi::c_void) -> i32;
+ fn GetSystemMetrics(n_index: i32) -> i32;
+ fn SetCursorPos(x: i32, y: i32) -> i32;
+ fn mouse_event(dw_flags: u32, dx: i32, dy: i32, dw_data: u32, dw_extra_info: usize);
+ fn keybd_event(b_vk: u8, b_scan: u8, dw_flags: u32, dw_extra_info: usize);
+ fn GetCursorPos(lp_point: *mut POINT) -> i32;
+ fn VkKeyScanW(ch: u16) -> i16;
+}
+
+#[cfg(target_os = "windows")]
+#[link(name = "gdi32")]
+extern "system" {
+ fn CreateCompatibleDC(hdc: *mut std::ffi::c_void) -> *mut std::ffi::c_void;
+ fn CreateCompatibleBitmap(hdc: *mut std::ffi::c_void, cx: i32, cy: i32) -> *mut std::ffi::c_void;
+ fn SelectObject(hdc: *mut std::ffi::c_void, hgdiobj: *mut std::ffi::c_void) -> *mut std::ffi::c_void;
+ fn BitBlt(hdc_dest: *mut std::ffi::c_void, x_dest: i32, y_dest: i32, width: i32, height: i32, hdc_src: *mut std::ffi::c_void, x_src: i32, y_src: i32, dw_rop: u32) -> i32;
+ fn DeleteDC(hdc: *mut std::ffi::c_void) -> i32;
+ fn DeleteObject(ho: *mut std::ffi::c_void) -> i32;
+ fn GetDIBits(hdc: *mut std::ffi::c_void, hbm: *mut std::ffi::c_void, start: u32, lines: u32, lpv_bits: *mut u8, lpbmi: *mut BITMAPINFO, usage: u32) -> i32;
+}
+
+#[cfg(target_os = "windows")]
+const SRCCOPY: u32 = 0x00CC0020;
+#[cfg(target_os = "windows")]
+const DIB_RGB_COLORS: u32 = 0;
+#[cfg(target_os = "windows")]
+const BI_RGB: u32 = 0;
+
+impl ComputerUseTool {
+ pub fn new() -> Self {
+ Self
+ }
+
+ pub fn descriptor(&self) -> ToolDescriptor {
+ ToolDescriptor {
+ name: tool_names::COMPUTER_USE.into(),
+ description: "Control the Windows desktop. Take screenshots, move mouse, click, type text, or press keyboard shortcuts.".into(),
+ input_schema: json!({
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "enum": [
+ "screenshot",
+ "mouse_move",
+ "left_click",
+ "right_click",
+ "middle_click",
+ "double_click",
+ "left_click_drag",
+ "type",
+ "key",
+ "cursor_position"
+ ],
+ "description": "The computer use action to perform."
+ },
+ "coordinate": {
+ "type": "array",
+ "items": { "type": "integer" },
+ "minItems": 2,
+ "maxItems": 2,
+ "description": "Optional [x, y] screen coordinates for mouse action."
+ },
+ "text": {
+ "type": "string",
+ "description": "Text to type or key name to press."
+ }
+ },
+ "required": ["action"],
+ "additionalProperties": false
+ }),
+ }
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ pub async fn run(&self, _input: Value) -> ToolRunResult {
+ ToolRunResult::err("Computer Use is only supported on Windows.", Vec::new())
+ }
+
+ #[cfg(target_os = "windows")]
+ pub async fn run(&self, input: Value) -> ToolRunResult {
+ let action = input.get("action").and_then(Value::as_str).unwrap_or("");
+ let coordinate = input.get("coordinate").and_then(Value::as_array);
+ let text = input.get("text").and_then(Value::as_str).unwrap_or("");
+
+ // If coordinates are provided for a mouse action, move cursor first
+ if let Some(coords) = coordinate {
+ if coords.len() >= 2 {
+ let x = coords[0].as_i64().unwrap_or(0) as i32;
+ let y = coords[1].as_i64().unwrap_or(0) as i32;
+ unsafe {
+ SetCursorPos(x, y);
+ }
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ }
+ }
+
+ match action {
+ "screenshot" => {
+ match self.capture_screen_jpeg() {
+ Ok((width, height, jpeg_bytes)) => {
+ let base64_data = BASE64_STANDARD.encode(&jpeg_bytes);
+ let img = ToolRunImage {
+ media_type: "image/jpeg".to_string(),
+ data: base64_data,
+ path: None,
+ };
+ ToolRunResult::ok_with_images(
+ format!("Screenshot captured. Screen resolution: {}x{}", width, height),
+ vec![img],
+ Vec::new()
+ )
+ }
+ Err(err) => ToolRunResult::err(format!("Failed to capture screenshot: {}", err), Vec::new())
+ }
+ }
+ "mouse_move" => {
+ if let Some(coords) = coordinate {
+ let x = coords[0].as_i64().unwrap_or(0) as i32;
+ let y = coords[1].as_i64().unwrap_or(0) as i32;
+ ToolRunResult::ok(format!("Moved mouse to ({}, {})", x, y), Vec::new())
+ } else {
+ ToolRunResult::err("action mouse_move requires coordinate argument", Vec::new())
+ }
+ }
+ "left_click" => {
+ unsafe {
+ mouse_event(0x0002, 0, 0, 0, 0); // DOWN
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ mouse_event(0x0004, 0, 0, 0, 0); // UP
+ }
+ ToolRunResult::ok("Left click performed", Vec::new())
+ }
+ "right_click" => {
+ unsafe {
+ mouse_event(0x0008, 0, 0, 0, 0); // DOWN
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ mouse_event(0x0010, 0, 0, 0, 0); // UP
+ }
+ ToolRunResult::ok("Right click performed", Vec::new())
+ }
+ "middle_click" => {
+ unsafe {
+ mouse_event(0x0020, 0, 0, 0, 0); // DOWN
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ mouse_event(0x0040, 0, 0, 0, 0); // UP
+ }
+ ToolRunResult::ok("Middle click performed", Vec::new())
+ }
+ "double_click" => {
+ unsafe {
+ mouse_event(0x0002, 0, 0, 0, 0); // DOWN
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ mouse_event(0x0004, 0, 0, 0, 0); // UP
+ std::thread::sleep(std::time::Duration::from_millis(150));
+ mouse_event(0x0002, 0, 0, 0, 0); // DOWN
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ mouse_event(0x0004, 0, 0, 0, 0); // UP
+ }
+ ToolRunResult::ok("Double click performed", Vec::new())
+ }
+ "left_click_drag" => {
+ if let Some(coords) = coordinate {
+ let x = coords[0].as_i64().unwrap_or(0) as i32;
+ let y = coords[1].as_i64().unwrap_or(0) as i32;
+ unsafe {
+ mouse_event(0x0002, 0, 0, 0, 0); // DOWN
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ SetCursorPos(x, y);
+ std::thread::sleep(std::time::Duration::from_millis(50));
+ mouse_event(0x0004, 0, 0, 0, 0); // UP
+ }
+ ToolRunResult::ok(format!("Left click drag performed to ({}, {})", x, y), Vec::new())
+ } else {
+ ToolRunResult::err("action left_click_drag requires coordinate argument", Vec::new())
+ }
+ }
+ "type" => {
+ if text.is_empty() {
+ return ToolRunResult::err("action type requires text argument", Vec::new());
+ }
+ match self.type_text(text) {
+ Ok(_) => ToolRunResult::ok(format!("Typed text: \"{}\"", text), Vec::new()),
+ Err(err) => ToolRunResult::err(format!("Failed to type text: {}", err), Vec::new())
+ }
+ }
+ "key" => {
+ if text.is_empty() {
+ return ToolRunResult::err("action key requires key name in text argument", Vec::new());
+ }
+ match self.press_key(text) {
+ Ok(_) => ToolRunResult::ok(format!("Pressed key: {}", text), Vec::new()),
+ Err(err) => ToolRunResult::err(format!("Failed to press key: {}", err), Vec::new())
+ }
+ }
+ "cursor_position" => {
+ let mut pt = POINT { x: 0, y: 0 };
+ let success = unsafe { GetCursorPos(&mut pt) };
+ if success != 0 {
+ ToolRunResult::ok(format!("Cursor position: [{}, {}]", pt.x, pt.y), Vec::new())
+ } else {
+ ToolRunResult::err("Failed to get cursor position", Vec::new())
+ }
+ }
+ _ => ToolRunResult::err(format!("Unknown computer use action: {}", action), Vec::new())
+ }
+ }
+
+ #[cfg(target_os = "windows")]
+ fn capture_screen_jpeg(&self) -> anyhow::Result<(i32, i32, Vec)> {
+ unsafe {
+ let width = GetSystemMetrics(0);
+ let height = GetSystemMetrics(1);
+ if width <= 0 || height <= 0 {
+ anyhow::bail!("invalid screen dimensions: {}x{}", width, height);
+ }
+
+ let hdc_screen = GetDC(null_mut());
+ if hdc_screen.is_null() {
+ anyhow::bail!("failed to get screen DC");
+ }
+
+ let hdc_mem = CreateCompatibleDC(hdc_screen);
+ if hdc_mem.is_null() {
+ ReleaseDC(null_mut(), hdc_screen);
+ anyhow::bail!("failed to create compatible DC");
+ }
+
+ let h_bitmap = CreateCompatibleBitmap(hdc_screen, width, height);
+ if h_bitmap.is_null() {
+ DeleteDC(hdc_mem);
+ ReleaseDC(null_mut(), hdc_screen);
+ anyhow::bail!("failed to create compatible bitmap");
+ }
+
+ let h_old = SelectObject(hdc_mem, h_bitmap);
+ let success = BitBlt(hdc_mem, 0, 0, width, height, hdc_screen, 0, 0, SRCCOPY);
+ if success == 0 {
+ SelectObject(hdc_mem, h_old);
+ DeleteObject(h_bitmap);
+ DeleteDC(hdc_mem);
+ ReleaseDC(null_mut(), hdc_screen);
+ anyhow::bail!("failed BitBlt");
+ }
+
+ let mut bmi = BITMAPINFO {
+ bmi_header: BITMAPINFOHEADER {
+ bi_size: std::mem::size_of::() as u32,
+ bi_width: width,
+ bi_height: -height,
+ bi_planes: 1,
+ bi_bit_count: 32,
+ bi_compression: BI_RGB,
+ bi_size_image: 0,
+ bi_x_pels_per_meter: 0,
+ bi_y_pels_per_meter: 0,
+ bi_clr_used: 0,
+ bi_clr_important: 0,
+ },
+ bmi_colors: [RGBQUAD { rgb_blue: 0, rgb_green: 0, rgb_red: 0, rgb_reserved: 0 }],
+ };
+
+ let mut buffer = vec![0u8; (width * height * 4) as usize];
+ let lines_copied = GetDIBits(
+ hdc_mem,
+ h_bitmap,
+ 0,
+ height as u32,
+ buffer.as_mut_ptr(),
+ &mut bmi,
+ DIB_RGB_COLORS,
+ );
+
+ SelectObject(hdc_mem, h_old);
+ DeleteObject(h_bitmap);
+ DeleteDC(hdc_mem);
+ ReleaseDC(null_mut(), hdc_screen);
+
+ if lines_copied == 0 {
+ anyhow::bail!("failed GetDIBits");
+ }
+
+ // Convert BGRA to RGBA
+ for chunk in buffer.chunks_exact_mut(4) {
+ let b = chunk[0];
+ let r = chunk[2];
+ chunk[0] = r;
+ chunk[2] = b;
+ }
+
+ // Compress to JPEG using image crate
+ use image::ImageEncoder;
+ let mut jpeg_bytes = Vec::new();
+ let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut jpeg_bytes, 85);
+ encoder.write_image(
+ &buffer,
+ width as u32,
+ height as u32,
+ image::ExtendedColorType::Rgba8,
+ )?;
+
+ Ok((width, height, jpeg_bytes))
+ }
+ }
+
+ #[cfg(target_os = "windows")]
+ fn type_text(&self, text: &str) -> anyhow::Result<()> {
+ for c in text.encode_utf16() {
+ unsafe {
+ let res = VkKeyScanW(c);
+ if res == -1 {
+ continue;
+ }
+ let vk = (res & 0xFF) as u8;
+ let shift = (res >> 8) & 1 != 0;
+ let ctrl = (res >> 8) & 2 != 0;
+ let alt = (res >> 8) & 4 != 0;
+
+ if shift { keybd_event(0x10, 0, 0, 0); }
+ if ctrl { keybd_event(0x11, 0, 0, 0); }
+ if alt { keybd_event(0x12, 0, 0, 0); }
+
+ keybd_event(vk, 0, 0, 0);
+ keybd_event(vk, 0, 0x0002, 0);
+
+ if alt { keybd_event(0x12, 0, 0x0002, 0); }
+ if ctrl { keybd_event(0x11, 0, 0x0002, 0); }
+ if shift { keybd_event(0x10, 0, 0x0002, 0); }
+
+ std::thread::sleep(std::time::Duration::from_millis(15));
+ }
+ }
+ Ok(())
+ }
+
+ #[cfg(target_os = "windows")]
+ fn press_key(&self, key: &str) -> anyhow::Result<()> {
+ let parts: Vec<&str> = key.split('+').collect();
+ let mut vks = Vec::new();
+ for part in parts {
+ if let Some(vk) = self.map_key_to_vk(part) {
+ vks.push(vk);
+ } else {
+ anyhow::bail!("unknown key name: {}", part);
+ }
+ }
+
+ unsafe {
+ for &vk in &vks {
+ keybd_event(vk, 0, 0, 0);
+ }
+ for &vk in vks.iter().rev() {
+ keybd_event(vk, 0, 0x0002, 0);
+ }
+ }
+ Ok(())
+ }
+
+ #[cfg(target_os = "windows")]
+ fn map_key_to_vk(&self, key: &str) -> Option {
+ match key.to_lowercase().as_str() {
+ "enter" | "return" => Some(0x0D),
+ "esc" | "escape" => Some(0x1B),
+ "tab" => Some(0x09),
+ "backspace" => Some(0x08),
+ "space" => Some(0x20),
+ "up" => Some(0x26),
+ "down" => Some(0x28),
+ "left" => Some(0x25),
+ "right" => Some(0x27),
+ "pgup" | "pageup" => Some(0x21),
+ "pgdn" | "pagedown" => Some(0x22),
+ "home" => Some(0x24),
+ "end" => Some(0x23),
+ "insert" => Some(0x2D),
+ "delete" => Some(0x2E),
+ "ctrl" | "control" => Some(0x11),
+ "alt" => Some(0x12),
+ "shift" => Some(0x10),
+ "win" | "super" | "command" => Some(0x5B),
+ "f1" => Some(0x70),
+ "f2" => Some(0x71),
+ "f3" => Some(0x72),
+ "f4" => Some(0x73),
+ "f5" => Some(0x74),
+ "f6" => Some(0x75),
+ "f7" => Some(0x76),
+ "f8" => Some(0x77),
+ "f9" => Some(0x78),
+ "f10" => Some(0x79),
+ "f11" => Some(0x7A),
+ "f12" => Some(0x7B),
+ _ => {
+ if key.len() == 1 {
+ let c = key.chars().next().unwrap();
+ if c.is_ascii_alphabetic() {
+ return Some(c.to_ascii_uppercase() as u8);
+ }
+ if c.is_ascii_digit() {
+ return Some(c as u8);
+ }
+ }
+ None
+ }
+ }
+ }
+}
diff --git a/crates/sinew-app/src/delete_file.rs b/crates/sinew-app/src/delete_file.rs
new file mode 100644
index 00000000..ec6a3cbb
--- /dev/null
+++ b/crates/sinew-app/src/delete_file.rs
@@ -0,0 +1,103 @@
+use std::path::PathBuf;
+
+use anyhow::{bail, Result};
+use serde::Deserialize;
+use serde_json::{json, Value};
+use sinew_core::ToolDescriptor;
+
+use crate::{
+ tool_names,
+ tool_run::ToolRunResult,
+ workspace::{normalize_workspace_relative_path, resolve_workspace_path},
+};
+
+#[derive(Debug, Clone)]
+pub struct DeleteFileTool {
+ workspace_root: PathBuf,
+}
+
+impl DeleteFileTool {
+ pub fn new(workspace_root: impl Into) -> Self {
+ Self {
+ workspace_root: workspace_root.into(),
+ }
+ }
+
+ pub fn descriptor(&self) -> ToolDescriptor {
+ ToolDescriptor {
+ name: tool_names::DELETE_FILE.into(),
+ description: "Delete a file or empty directory inside the workspace.".into(),
+ input_schema: json!({
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Relative path to the file or empty directory to delete."
+ }
+ },
+ "required": ["path"],
+ "additionalProperties": false
+ }),
+ }
+ }
+
+ pub async fn run(&self, input: Value) -> ToolRunResult {
+ match self.delete(input) {
+ Ok(output) => ToolRunResult::ok(output, Vec::new()),
+ Err(err) => ToolRunResult::err(err.to_string(), Vec::new()),
+ }
+ }
+
+ fn delete(&self, input: Value) -> Result {
+ let parsed: DeleteFileInput = serde_json::from_value(input)
+ .map_err(|err| anyhow::anyhow!("invalid delete_file input: {err}"))?;
+ let relative = parsed.path.trim();
+ if relative.is_empty() {
+ bail!("path is required");
+ }
+ let relative = normalize_workspace_relative_path(relative)?;
+ let target = resolve_workspace_path(&self.workspace_root, &relative)?;
+ if !target.exists() {
+ bail!("path not found: {relative}");
+ }
+ if target.is_dir() {
+ std::fs::remove_dir(&target)
+ .map_err(|err| anyhow::anyhow!("unable to delete directory `{relative}`: {err}"))?;
+ } else {
+ std::fs::remove_file(&target)
+ .map_err(|err| anyhow::anyhow!("unable to delete file `{relative}`: {err}"))?;
+ }
+ Ok(format!("deleted {relative}"))
+ }
+}
+
+#[derive(Debug, Deserialize)]
+struct DeleteFileInput {
+ path: String,
+}
+
+#[cfg(test)]
+mod tests {
+ use std::fs;
+
+ use super::*;
+
+ #[tokio::test]
+ async fn deletes_workspace_file() {
+ let root = std::env::temp_dir().join(format!(
+ "sinew-delete-file-{}",
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ ));
+ fs::create_dir_all(&root).unwrap();
+ fs::write(root.join("tmp.txt"), "x").unwrap();
+ let root = root.canonicalize().expect("canonical temp workspace");
+ let tool = DeleteFileTool::new(&root);
+ let result = tool.run(json!({ "path": "tmp.txt" })).await;
+ assert!(!result.is_error);
+ assert!(!root.join("tmp.txt").exists());
+ let _ = fs::remove_dir_all(root);
+ }
+}
diff --git a/crates/sinew-app/src/edit.rs b/crates/sinew-app/src/edit.rs
index 164be8b3..df728a71 100644
--- a/crates/sinew-app/src/edit.rs
+++ b/crates/sinew-app/src/edit.rs
@@ -1,4 +1,4 @@
-use std::{
+use std::{
collections::{BTreeMap, HashMap},
fs,
path::{Component, Path, PathBuf},
@@ -133,7 +133,10 @@ impl EditFileTool {
let mut writes = Vec::new();
for group in grouped.values_mut() {
- let expected = read_fingerprints.get(&group.relative_path).ok_or_else(|| {
+ let expected = read_fingerprints
+ .get(&group.relative_path)
+ .or_else(|| read_fingerprints.get(&group.relative_path.to_lowercase()))
+ .ok_or_else(|| {
anyhow::anyhow!(
"edit_file requires a successful read of {} before editing it",
group.relative_path
@@ -674,7 +677,7 @@ fn line_spans(text: &str) -> Vec {
spans
}
-fn line_text<'a>(text: &'a str, span: LineSpan) -> &'a str {
+fn line_text(text: &str, span: LineSpan) -> &str {
&text[span.start..span.end]
}
@@ -735,8 +738,8 @@ fn block_anchor_matches(content: &str, find: &str) -> Vec {
if line_text(content, spans[start]).trim() != first {
continue;
}
- for end in start + 2..spans.len() {
- if line_text(content, spans[end]).trim() == last {
+ for (end, span) in spans.iter().enumerate().skip(start + 2) {
+ if line_text(content, *span).trim() == last {
candidates.push((start, end));
break;
}
@@ -1124,7 +1127,7 @@ fn next_char_boundary(text: &str, offset: usize) -> usize {
fn apply_replacements(original: &str, replacements: &[PlannedReplacement]) -> String {
let mut updated = original.to_string();
let mut sorted = replacements.iter().collect::>();
- sorted.sort_by(|left, right| right.start.cmp(&left.start));
+ sorted.sort_by_key(|right| std::cmp::Reverse(right.start));
for replacement in sorted {
updated.replace_range(
@@ -1368,6 +1371,8 @@ fn resolve_existing_workspace_file(root: &Path, raw: &str) -> Result<(String, Pa
})
.collect::>()
.join("/");
+ #[cfg(target_os = "windows")]
+ let relative = relative.to_lowercase();
if relative.is_empty() {
bail!("path cannot be the workspace root");
}
diff --git a/crates/sinew-app/src/editor_diagnostics.rs b/crates/sinew-app/src/editor_diagnostics.rs
new file mode 100644
index 00000000..d9167c55
--- /dev/null
+++ b/crates/sinew-app/src/editor_diagnostics.rs
@@ -0,0 +1,65 @@
+use std::sync::{Arc, RwLock};
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EditorDiagnostic {
+ pub path: String,
+ pub line: u32,
+ pub column: u32,
+ pub end_line: u32,
+ pub end_column: u32,
+ pub severity: String,
+ pub message: String,
+ #[serde(default)]
+ pub source: String,
+}
+
+#[derive(Debug, Default)]
+pub struct EditorDiagnosticsStore {
+ pub updated_at_ms: i64,
+ pub diagnostics: Vec,
+}
+
+impl EditorDiagnosticsStore {
+ pub fn replace(&mut self, diagnostics: Vec) {
+ self.updated_at_ms = current_time_ms();
+ self.diagnostics = diagnostics;
+ }
+
+ pub fn matching<'a>(
+ &'a self,
+ paths: Option<&[String]>,
+ ) -> impl Iterator {
+ let paths = paths.map(normalize_path_set);
+ self.diagnostics.iter().filter(move |diag| {
+ paths
+ .as_ref()
+ .map(|set| set.contains(&normalize_path(&diag.path)))
+ .unwrap_or(true)
+ })
+ }
+}
+
+pub type SharedEditorDiagnosticsStore = Arc>;
+
+pub fn new_editor_diagnostics_store() -> SharedEditorDiagnosticsStore {
+ Arc::new(RwLock::new(EditorDiagnosticsStore::default()))
+}
+
+fn normalize_path_set(paths: &[String]) -> std::collections::HashSet {
+ paths.iter().map(|path| normalize_path(path)).collect()
+}
+
+fn normalize_path(path: &str) -> String {
+ path.replace('\\', "/").trim_start_matches("./").to_string()
+}
+
+fn current_time_ms() -> i64 {
+ use std::time::{SystemTime, UNIX_EPOCH};
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| duration.as_millis() as i64)
+ .unwrap_or(0)
+}
diff --git a/crates/sinew-app/src/image.rs b/crates/sinew-app/src/image.rs
index 55c6b8db..06ff319d 100644
--- a/crates/sinew-app/src/image.rs
+++ b/crates/sinew-app/src/image.rs
@@ -25,10 +25,6 @@ use crate::tool_run::{FileChange, FileChangeKind, ToolRunImage, ToolRunResult};
const OPENAI_IMAGES_URL: &str = "https://api.openai.com/v1/images/generations";
const OPENAI_CODEX_RESPONSES_URL: &str = "https://chatgpt.com/backend-api/codex/responses";
-const GPT_IMAGE_MODEL: &str = "gpt-image-2";
-const NANO_BANANA_MODEL: &str = "gemini-3.1-flash-image-preview";
-const NANO_BANANA_URL: &str =
- "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-image-preview:generateContent";
const OPENAI_SUBSCRIPTION_IMAGE_INSTRUCTIONS: &str = "You are Sinew, a concise coding assistant. When the user asks for an image, immediately call the image_generation tool with their prompt. Do not reply with text.";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(300);
const USER_AGENT: &str = "sinew/0.1";
@@ -39,11 +35,22 @@ pub struct CreateImageTool {
workspace_root: PathBuf,
image_provider: ImageProvider,
openai_image_use_subscription: bool,
+ gemini_image_use_subscription: bool,
+ openai_image_model: String,
+ gemini_image_model: String,
openai_api_key: Option,
nano_banana_api_key: Option,
write_lock: Option>,
}
+pub fn composer_mcp_descriptor(base: &ToolDescriptor) -> ToolDescriptor {
+ ToolDescriptor {
+ name: "mcp__sinew__create_image".into(),
+ description: base.description.clone(),
+ input_schema: base.input_schema.clone(),
+ }
+}
+
impl CreateImageTool {
pub fn new(workspace_root: impl Into) -> Self {
Self::with_api_key(workspace_root, None)
@@ -54,6 +61,9 @@ impl CreateImageTool {
workspace_root,
ImageProvider::GptImage2,
false,
+ false,
+ None,
+ None,
api_key,
None,
)
@@ -63,9 +73,18 @@ impl CreateImageTool {
workspace_root: impl Into,
image_provider: ImageProvider,
openai_image_use_subscription: bool,
+ gemini_image_use_subscription: bool,
+ openai_image_model: Option,
+ gemini_image_model: Option,
openai_api_key: Option,
nano_banana_api_key: Option,
) -> Self {
+ let openai_model = openai_image_model
+ .filter(|m| !m.trim().is_empty())
+ .unwrap_or_else(|| "gpt-image-2".to_string());
+ let gemini_model = gemini_image_model
+ .filter(|m| !m.trim().is_empty())
+ .unwrap_or_else(|| "gemini-3.1-flash-image-preview".to_string());
Self {
http: reqwest::Client::builder()
.timeout(REQUEST_TIMEOUT)
@@ -75,6 +94,9 @@ impl CreateImageTool {
workspace_root: workspace_root.into(),
image_provider,
openai_image_use_subscription,
+ gemini_image_use_subscription,
+ openai_image_model: openai_model,
+ gemini_image_model: gemini_model,
openai_api_key: normalize_configured_key(openai_api_key),
nano_banana_api_key: normalize_configured_key(nano_banana_api_key),
write_lock: None,
@@ -227,7 +249,7 @@ impl CreateImageTool {
}
let mut body = Map::new();
- body.insert("model".into(), json!(GPT_IMAGE_MODEL));
+ body.insert("model".into(), json!(self.openai_image_model));
body.insert("prompt".into(), json!(prompt));
body.insert("size".into(), json!(size));
body.insert("quality".into(), json!(quality));
@@ -308,7 +330,8 @@ impl CreateImageTool {
}
let mut output = format!(
- "model: {GPT_IMAGE_MODEL}\nimages: {}\nsize: {size}\nquality: {quality}\nformat: {output_format}",
+ "model: {}\nimages: {}\nsize: {size}\nquality: {quality}\nformat: {output_format}",
+ self.openai_image_model,
images.len()
);
if let Some(request_id) = request_id {
@@ -416,7 +439,8 @@ impl CreateImageTool {
}
let mut output = format!(
- "model: {GPT_IMAGE_MODEL}\nsource: OpenAI subscription\nimages: {}\nsize: {size}\nquality: {quality}\nformat: {output_format}",
+ "model: {}\nsource: OpenAI subscription\nimages: {}\nsize: {size}\nquality: {quality}\nformat: {output_format}",
+ self.openai_image_model,
images.len()
);
let request_ids = generated
@@ -496,6 +520,7 @@ impl CreateImageTool {
.http
.post(OPENAI_CODEX_RESPONSES_URL)
.header(AUTHORIZATION, format!("Bearer {access_token}"))
+ .header("user-agent", "codex-cli")
.header("openai-beta", "responses=experimental")
.header(CONTENT_TYPE, "application/json")
.header(ACCEPT, "text/event-stream");
@@ -538,10 +563,19 @@ impl CreateImageTool {
bail!("prompt is required");
}
- let api_key = load_nano_banana_api_key(self.nano_banana_api_key.as_deref())?;
let aspect_ratio = normalize_aspect_ratio(parsed.aspect_ratio.as_deref())?;
let image_size = normalize_image_size(parsed.image_size.as_deref())?;
+ let model = if self.gemini_image_model.is_empty() {
+ "gemini-3.1-flash-image-preview"
+ } else {
+ &self.gemini_image_model
+ };
+ let url = format!(
+ "https://generativelanguage.googleapis.com/v1beta/models/{}:generateContent",
+ model
+ );
+
let body = json!({
"contents": [{
"parts": [{ "text": prompt }]
@@ -558,16 +592,34 @@ impl CreateImageTool {
}
}
});
- let response = self
- .http
- .post(NANO_BANANA_URL)
- .header("x-goog-api-key", api_key)
- .header(CONTENT_TYPE, "application/json")
- .header(ACCEPT, "application/json")
- .json(&body)
- .send()
- .await
- .context("Nano Banana 2 image request failed")?;
+ let response = if self.gemini_image_use_subscription {
+ let credential = sinew_google::Credential::load_default()?.ok_or_else(|| {
+ anyhow::anyhow!(
+ "Gemini subscription image generation requires Google to be connected in Settings > Providers."
+ )
+ })?;
+ let token = credential.bearer(&self.http).await?;
+ self.http
+ .post(&url)
+ .bearer_auth(token)
+ .header(CONTENT_TYPE, "application/json")
+ .header(ACCEPT, "application/json")
+ .json(&body)
+ .send()
+ .await
+ .context("Nano Banana 2 subscription image request failed")?
+ } else {
+ let api_key = load_nano_banana_api_key(self.nano_banana_api_key.as_deref())?;
+ self.http
+ .post(&url)
+ .header("x-goog-api-key", api_key)
+ .header(CONTENT_TYPE, "application/json")
+ .header(ACCEPT, "application/json")
+ .json(&body)
+ .send()
+ .await
+ .context("Nano Banana 2 image request failed")?
+ };
let status = response.status();
let request_id = response
@@ -644,7 +696,8 @@ impl CreateImageTool {
}
let mut output = format!(
- "model: {NANO_BANANA_MODEL}\nimages: {}\naspect_ratio: {aspect_ratio}\nimage_size: {image_size}\nthinking_level: high",
+ "model: {}\nimages: {}\naspect_ratio: {aspect_ratio}\nimage_size: {image_size}\nthinking_level: high",
+ self.gemini_image_model,
images.len()
);
if let Some(request_id) = request_id {
diff --git a/crates/sinew-app/src/lib.rs b/crates/sinew-app/src/lib.rs
index 7b0ebe80..fba820f1 100644
--- a/crates/sinew-app/src/lib.rs
+++ b/crates/sinew-app/src/lib.rs
@@ -1,13 +1,20 @@
pub mod agent;
pub mod bash;
+pub mod check_sota;
+pub mod codebase_search;
+pub mod computer_use;
pub mod compact;
+pub mod delete_file;
+pub mod editor_diagnostics;
pub mod edit;
pub mod glob;
pub mod grep;
pub mod image;
+pub mod list_dir;
pub mod mcp;
pub mod question;
pub mod read;
+pub mod read_lints;
mod ripgrep;
pub mod skill;
pub mod store;
@@ -24,24 +31,34 @@ pub mod write;
pub use agent::{
clean_context_descriptor, run_turn, system_prompt_for_mode,
system_prompt_for_mode_with_plan_prompt, AgentEvent, AgentEventScope, AgentMode,
- ConversationEvent, EngineCommand, QuestionReply, TurnCancel, TurnContext,
+ ConversationEvent, EngineCommand, QuestionReply, TurnCancel, TurnContext, TurnOutput,
};
pub use bash::{active_shell_display_name, shell_system_prompt, BashTool};
+pub use codebase_search::CodebaseSearchTool;
+pub use check_sota::CheckSotaTool;
+pub use computer_use::ComputerUseTool;
pub use compact::{compact_conversation_history, CompactConversationOutput};
+pub use delete_file::DeleteFileTool;
+pub use editor_diagnostics::{
+ new_editor_diagnostics_store, EditorDiagnostic, EditorDiagnosticsStore,
+ SharedEditorDiagnosticsStore,
+};
pub use edit::EditFileTool;
pub use glob::GlobTool;
pub use grep::GrepTool;
-pub use image::CreateImageTool;
+pub use image::{composer_mcp_descriptor, CreateImageTool};
+pub use list_dir::ListDirTool;
pub use mcp::{probe_mcp_servers, McpServerProbe, McpSettings, McpToolRegistry};
pub use question::QuestionTool;
pub use read::{ReadFingerprint, ReadTool};
+pub use read_lints::ReadLintsTool;
pub use skill::{
create_installed_skill, list_installed_skills, InstalledSkill, SkillConfig, SkillSettings,
SkillSource, SkillTool,
};
pub use store::{
tool_settings_view, AppStore, ConversationSummary, GoalWorkflowState, ModeModelSettings,
- OpenRouterModelRecord, PlanArtifactState, PlanWorkflowState, SavedConversation, ToolConfig,
+ OpenRouterModelRecord, OtherWorkspaceSummary, PlanArtifactState, PlanWorkflowState, SavedConversation, ToolConfig,
ToolConfigView, ToolSettings, ToolSettingsView, TurnCheckpointRecord, WebSearchProvider,
WorkspaceBootstrap, DEFAULT_PLAN_MODE_PROMPT,
};
@@ -61,12 +78,12 @@ pub use tool_run::{
pub use web::{WebFetchTool, WebSearchTool};
pub use workspace::{
copy_workspace_entries, create_workspace_directory, create_workspace_file,
- delete_workspace_entry, import_workspace_paths, list_workspace_entries, list_workspace_files,
- normalize_workspace_root, read_external_file, read_workspace_file, rename_workspace_entry,
- resolve_terminal_path, restore_workspace_deleted_entries, search_workspace_files,
- trash_workspace_entry, write_workspace_file, FileDocument, ImportedEntry,
- TerminalPathResolution, WorkspaceCopyOperation, WorkspaceDeletedEntry, WorkspaceEntry,
- WorkspaceEntryKind, WorkspaceFileChangeEvent, WorkspaceInfo, WorkspaceSearchFile,
- WorkspaceSearchMatch, WorkspaceSearchResult,
+ codebase_index_status, delete_workspace_entry, import_workspace_paths, list_workspace_entries,
+ list_workspace_files, normalize_workspace_root, read_external_file, read_workspace_file,
+ rename_workspace_entry, resolve_terminal_path, restore_workspace_deleted_entries,
+ search_workspace_files, trash_workspace_entry, write_workspace_file, CodebaseIndexStatus,
+ FileDocument, ImportedEntry, TerminalPathResolution, WorkspaceCopyOperation,
+ WorkspaceDeletedEntry, WorkspaceEntry, WorkspaceEntryKind, WorkspaceFileChangeEvent,
+ WorkspaceInfo, WorkspaceSearchFile, WorkspaceSearchMatch, WorkspaceSearchResult,
};
pub use write::WriteFileTool;
diff --git a/crates/sinew-app/src/list_dir.rs b/crates/sinew-app/src/list_dir.rs
new file mode 100644
index 00000000..19a73842
--- /dev/null
+++ b/crates/sinew-app/src/list_dir.rs
@@ -0,0 +1,144 @@
+use std::path::PathBuf;
+
+use anyhow::{bail, Context, Result};
+use serde::Deserialize;
+use serde_json::{json, Value};
+use sinew_core::ToolDescriptor;
+
+use crate::{
+ tool_names,
+ tool_run::ToolRunResult,
+ workspace::{normalize_workspace_relative_path, resolve_workspace_path},
+};
+
+const MAX_ENTRIES: usize = 500;
+
+#[derive(Debug, Clone)]
+pub struct ListDirTool {
+ workspace_root: PathBuf,
+}
+
+impl ListDirTool {
+ pub fn new(workspace_root: impl Into) -> Self {
+ Self {
+ workspace_root: workspace_root.into(),
+ }
+ }
+
+ pub fn descriptor(&self) -> ToolDescriptor {
+ ToolDescriptor {
+ name: tool_names::LIST_DIR.into(),
+ description: "List files and directories in a single workspace directory (non-recursive).".into(),
+ input_schema: json!({
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Directory to list. Relative to the workspace root. Defaults to the workspace root."
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": MAX_ENTRIES,
+ "description": "Maximum entries to return."
+ }
+ },
+ "additionalProperties": false
+ }),
+ }
+ }
+
+ pub async fn run(&self, input: Value) -> ToolRunResult {
+ match self.list(input) {
+ Ok(output) => ToolRunResult::ok(output, Vec::new()),
+ Err(err) => ToolRunResult::err(err.to_string(), Vec::new()),
+ }
+ }
+
+ fn list(&self, input: Value) -> Result {
+ let parsed: ListDirInput = serde_json::from_value(input)
+ .map_err(|err| anyhow::anyhow!("invalid list_dir input: {err}"))?;
+ let limit = parsed.limit.unwrap_or(MAX_ENTRIES).clamp(1, MAX_ENTRIES);
+ let relative = parsed
+ .path
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .unwrap_or(".");
+ let relative = normalize_workspace_relative_path(relative)?;
+ let target = if relative.is_empty() {
+ self.workspace_root
+ .canonicalize()
+ .with_context(|| format!("unable to resolve workspace root {}", self.workspace_root.display()))?
+ } else {
+ resolve_workspace_path(&self.workspace_root, &relative)?
+ };
+ if !target.is_dir() {
+ bail!("path is not a directory: {relative}");
+ }
+
+ let mut dirs = Vec::new();
+ let mut files = Vec::new();
+ for entry in std::fs::read_dir(&target)? {
+ let entry = entry?;
+ let file_name = entry.file_name().to_string_lossy().into_owned();
+ if file_name.starts_with('.') && file_name != ".git" {
+ continue;
+ }
+ let file_type = entry.file_type()?;
+ if file_type.is_dir() {
+ dirs.push(format!("{file_name}/"));
+ } else if file_type.is_file() || file_type.is_symlink() {
+ files.push(file_name);
+ }
+ }
+ dirs.sort();
+ files.sort();
+
+ let mut lines = Vec::new();
+ for name in dirs.into_iter().chain(files).take(limit) {
+ lines.push(name);
+ }
+ if lines.is_empty() {
+ return Ok(format!("{relative} is empty"));
+ }
+ Ok(lines.join("\n"))
+ }
+}
+
+#[derive(Debug, Deserialize)]
+struct ListDirInput {
+ #[serde(default)]
+ path: Option,
+ #[serde(default)]
+ limit: Option,
+}
+
+#[cfg(test)]
+mod tests {
+ use std::fs;
+
+ use super::*;
+
+ #[tokio::test]
+ async fn lists_single_directory_level() {
+ let root = std::env::temp_dir().join(format!(
+ "sinew-list-dir-{}",
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ ));
+ fs::create_dir_all(root.join("src")).unwrap();
+ fs::write(root.join("Cargo.toml"), "").unwrap();
+ let root = root.canonicalize().expect("canonical temp workspace");
+ let tool = ListDirTool::new(&root);
+ let result = tool
+ .run(json!({ "path": "." }))
+ .await;
+ assert!(!result.is_error);
+ assert!(result.content.contains("src/"));
+ assert!(result.content.contains("Cargo.toml"));
+ let _ = fs::remove_dir_all(root);
+ }
+}
diff --git a/crates/sinew-app/src/mcp.rs b/crates/sinew-app/src/mcp.rs
index 64c97a02..ccf30b52 100644
--- a/crates/sinew-app/src/mcp.rs
+++ b/crates/sinew-app/src/mcp.rs
@@ -7,7 +7,7 @@ use std::{
path::{Path, PathBuf},
process::Stdio,
sync::OnceLock,
- time::Duration,
+ time::{Duration, Instant},
};
use anyhow::{anyhow, bail, Context, Result};
@@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use sinew_core::{ChatMessage, Part, ToolDescriptor};
use tokio::{
- io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
+ io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
process::{Child, ChildStdin, ChildStdout, Command},
sync::RwLock,
time::timeout,
@@ -41,7 +41,7 @@ pub struct McpSettings {
pub servers: Vec,
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpServerConfig {
pub id: String,
@@ -55,9 +55,11 @@ pub struct McpServerConfig {
pub cwd: Option,
#[serde(default = "default_enabled")]
pub enabled: bool,
+ #[serde(default)]
+ pub auto_load: bool,
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct McpEnvVar {
pub key: String,
@@ -105,7 +107,54 @@ pub struct McpToolLabel {
pub tool_name: String,
}
-#[derive(Debug)]
+use std::sync::Mutex as StdMutex;
+
+#[derive(Debug, Clone)]
+struct McpToolsCacheEntry {
+ result: Result, String>,
+ fetched_at: Instant,
+ config_hash: u64,
+}
+
+static MCP_TOOLS_CACHE: OnceLock>> = OnceLock::new();
+
+fn get_mcp_tools_cache() -> &'static StdMutex> {
+ MCP_TOOLS_CACHE.get_or_init(|| StdMutex::new(HashMap::new()))
+}
+
+fn calculate_config_hash(config: &McpServerConfig) -> u64 {
+ let mut hasher = DefaultHasher::new();
+ config.hash(&mut hasher);
+ hasher.finish()
+}
+
+async fn get_cached_mcp_tools(config: &McpServerConfig) -> Option, String>> {
+ let cache = get_mcp_tools_cache();
+ let lock = cache.lock().ok()?;
+ let entry = lock.get(&config.id)?;
+ let hash = calculate_config_hash(config);
+ if entry.config_hash == hash && entry.fetched_at.elapsed() < Duration::from_secs(30) {
+ Some(entry.result.clone())
+ } else {
+ None
+ }
+}
+
+async fn set_cached_mcp_tools(config: &McpServerConfig, result: Result, String>) {
+ let cache = get_mcp_tools_cache();
+ if let Ok(mut lock) = cache.lock() {
+ let hash = calculate_config_hash(config);
+ lock.insert(
+ config.id.clone(),
+ McpToolsCacheEntry {
+ result,
+ fetched_at: Instant::now(),
+ config_hash: hash,
+ },
+ );
+ }
+}
+
pub struct McpToolRegistry {
settings: McpSettings,
bindings: RwLock>,
@@ -125,19 +174,32 @@ impl McpToolRegistry {
let mut next_bindings = HashMap::new();
for server in enabled_servers(&self.settings) {
- let mut client = match McpStdioClient::connect(server).await {
- Ok(client) => client,
- Err(err) => {
- warn!("unable to connect MCP server {}: {err}", server.name);
- continue;
- }
- };
+ let tools = match get_cached_mcp_tools(server).await {
+ Some(Ok(tools)) => tools,
+ Some(Err(_)) => continue,
+ None => {
+ let mut client = match McpStdioClient::connect(server).await {
+ Ok(client) => client,
+ Err(err) => {
+ let err_msg = err.to_string();
+ warn!("unable to connect MCP server {}: {err_msg}", server.name);
+ set_cached_mcp_tools(server, Err(err_msg)).await;
+ continue;
+ }
+ };
- let tools = match client.list_tools().await {
- Ok(tools) => tools,
- Err(err) => {
- warn!("unable to list MCP tools for {}: {err}", server.name);
- continue;
+ match client.list_tools().await {
+ Ok(tools) => {
+ set_cached_mcp_tools(server, Ok(tools.clone())).await;
+ tools
+ }
+ Err(err) => {
+ let err_msg = err.to_string();
+ warn!("unable to list MCP tools for {}: {err_msg}", server.name);
+ set_cached_mcp_tools(server, Err(err_msg)).await;
+ continue;
+ }
+ }
}
};
@@ -167,6 +229,13 @@ impl McpToolRegistry {
}
loaded.retain(|name| next_bindings.contains_key(name));
+ // Auto-load tools for configured servers
+ for (name, binding) in &next_bindings {
+ if binding.server.auto_load {
+ loaded.insert(name.clone());
+ }
+ }
+
*self.bindings.write().await = next_bindings;
*self.loaded.write().await = loaded;
self.descriptors().await
@@ -440,13 +509,15 @@ pub async fn probe_mcp_servers(settings: &McpSettings) -> Vec {
let mut client = match McpStdioClient::connect(server).await {
Ok(client) => client,
Err(err) => {
+ let err_msg = err.to_string();
+ set_cached_mcp_tools(server, Err(err_msg.clone())).await;
probes.push(McpServerProbe {
server_id: server.id.clone(),
server_name: server.name.clone(),
enabled: true,
ok: false,
tools: Vec::new(),
- error: Some(err.to_string()),
+ error: Some(err_msg),
});
continue;
}
@@ -454,6 +525,7 @@ pub async fn probe_mcp_servers(settings: &McpSettings) -> Vec {
match client.list_tools().await {
Ok(tools) => {
+ set_cached_mcp_tools(server, Ok(tools.clone())).await;
let mut infos = Vec::with_capacity(tools.len());
for tool in tools {
let tool_name = unique_tool_name(server, &tool.name, &known_names);
@@ -486,14 +558,18 @@ pub async fn probe_mcp_servers(settings: &McpSettings) -> Vec {
error: None,
});
}
- Err(err) => probes.push(McpServerProbe {
- server_id: server.id.clone(),
- server_name: server.name.clone(),
- enabled: true,
- ok: false,
- tools: Vec::new(),
- error: Some(err.to_string()),
- }),
+ Err(err) => {
+ let err_msg = err.to_string();
+ set_cached_mcp_tools(server, Err(err_msg.clone())).await;
+ probes.push(McpServerProbe {
+ server_id: server.id.clone(),
+ server_name: server.name.clone(),
+ enabled: true,
+ ok: false,
+ tools: Vec::new(),
+ error: Some(err_msg),
+ })
+ }
}
}
@@ -831,10 +907,13 @@ impl McpStdioClient {
.take()
.ok_or_else(|| anyhow!("MCP server stdout unavailable"))?;
- if let Some(mut stderr) = child.stderr.take() {
+ if let Some(stderr) = child.stderr.take() {
+ let server_name = config.name.clone();
tokio::spawn(async move {
- let mut sink = Vec::new();
- let _ = stderr.read_to_end(&mut sink).await;
+ let mut reader = BufReader::new(stderr).lines();
+ while let Ok(Some(line)) = reader.next_line().await {
+ tracing::info!(target: "mcp_server", "[{}] {}", server_name, line);
+ }
});
}
@@ -936,8 +1015,12 @@ impl McpStdioClient {
bail!("MCP server closed stdout ({status:?})");
}
- let value: Value = serde_json::from_str(line.trim())
- .with_context(|| "MCP server emitted invalid JSON")?;
+ let trimmed = line.trim();
+ if trimmed.is_empty() || !trimmed.starts_with('{') {
+ continue;
+ }
+ let value: Value = serde_json::from_str(trimmed)
+ .with_context(|| format!("MCP server emitted invalid JSON: {:?}", trimmed))?;
if value.get("id") == Some(&json!(id)) {
if let Some(error) = value.get("error") {
bail!("{}", format_json_rpc_error(error));
@@ -1223,7 +1306,7 @@ struct McpListToolsResult {
next_cursor: Option,
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Clone, Deserialize)]
struct McpServerTool {
name: String,
#[serde(default)]
diff --git a/crates/sinew-app/src/read.rs b/crates/sinew-app/src/read.rs
index bcc7dfa2..67b69cc1 100644
--- a/crates/sinew-app/src/read.rs
+++ b/crates/sinew-app/src/read.rs
@@ -1,6 +1,6 @@
use std::{
fs,
- path::{Component, Path, PathBuf},
+ path::{Path, PathBuf},
time::UNIX_EPOCH,
};
@@ -65,10 +65,13 @@ impl ReadTool {
}
pub async fn run(&self, input: Value) -> ToolRunResult {
- match self.read(input) {
+ let tool = self.clone();
+ tokio::task::spawn_blocking(move || match tool.read(input) {
Ok(output) => output,
Err(err) => ToolRunResult::err(err.to_string(), Vec::new()),
- }
+ })
+ .await
+ .unwrap_or_else(|err| ToolRunResult::err(format!("blocking thread panicked: {err}"), Vec::new()))
}
pub fn normalize_path(&self, raw: &str) -> Result {
@@ -255,18 +258,50 @@ fn fingerprint_for_bytes(
}
}
+fn clean_windows_path(path: &Path) -> String {
+ let s = path.to_string_lossy().replace('\\', "/");
+ let clean = if let Some(stripped) = s.strip_prefix("//?/") {
+ stripped.to_string()
+ } else {
+ s
+ };
+ clean.to_lowercase()
+}
+
fn relative_from_root(root: &Path, path: &Path) -> Result {
- let relative = path
- .strip_prefix(root)
- .with_context(|| format!("{} is outside the workspace", path.display()))?;
- Ok(relative
- .components()
- .filter_map(|component| match component {
- Component::Normal(value) => Some(value.to_string_lossy().into_owned()),
- _ => None,
- })
- .collect::>()
- .join("/"))
+ let root_canonical = root
+ .canonicalize()
+ .unwrap_or_else(|_| root.to_path_buf());
+ let path_canonical = path
+ .canonicalize()
+ .unwrap_or_else(|_| path.to_path_buf());
+
+ #[cfg(target_os = "windows")]
+ {
+ let root_str = clean_windows_path(&root_canonical);
+ let path_str = clean_windows_path(&path_canonical);
+
+ if path_str == root_str || path_str.starts_with(&format!("{}/", root_str)) {
+ let relative = &path_str[root_str.len()..];
+ Ok(relative.trim_start_matches('/').to_string())
+ } else {
+ bail!("{} is outside the workspace", path.display());
+ }
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ let relative = path_canonical
+ .strip_prefix(&root_canonical)
+ .with_context(|| format!("{} is outside the workspace", path.display()))?;
+ Ok(relative
+ .components()
+ .filter_map(|component| match component {
+ Component::Normal(value) => Some(value.to_string_lossy().into_owned()),
+ _ => None,
+ })
+ .collect::>()
+ .join("/"))
+ }
}
#[cfg(test)]
diff --git a/crates/sinew-app/src/read_lints.rs b/crates/sinew-app/src/read_lints.rs
new file mode 100644
index 00000000..269e56e1
--- /dev/null
+++ b/crates/sinew-app/src/read_lints.rs
@@ -0,0 +1,563 @@
+use std::{
+ collections::HashSet,
+ path::{Path, PathBuf},
+ time::Duration,
+};
+
+use anyhow::{Context, Result};
+use serde::Deserialize;
+use serde_json::{json, Value};
+use sinew_core::ToolDescriptor;
+use tokio::{process::Command, time::timeout};
+
+use crate::{
+ editor_diagnostics::{EditorDiagnostic, SharedEditorDiagnosticsStore},
+ tool_names,
+ tool_run::ToolRunResult,
+ workspace::{normalize_workspace_relative_path, resolve_workspace_path},
+};
+
+const MAX_DIAGNOSTICS: usize = 200;
+const PROJECT_LINT_TIMEOUT: Duration = Duration::from_secs(45);
+#[cfg(windows)]
+const CREATE_NO_WINDOW: u32 = 0x08000000;
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+struct DiagnosticKey {
+ path: String,
+ line: u32,
+ column: u32,
+ severity: String,
+ message: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct ReadLintsTool {
+ workspace_root: PathBuf,
+ editor_store: SharedEditorDiagnosticsStore,
+}
+
+impl ReadLintsTool {
+ pub fn new(
+ workspace_root: impl Into,
+ editor_store: SharedEditorDiagnosticsStore,
+ ) -> Self {
+ Self {
+ workspace_root: workspace_root.into(),
+ editor_store,
+ }
+ }
+
+ pub fn descriptor(&self) -> ToolDescriptor {
+ ToolDescriptor {
+ name: tool_names::READ_LINTS.into(),
+ description: "Read linter/compiler diagnostics for workspace files. Uses live editor diagnostics when available, plus project linters (cargo, eslint, ruff) for supported languages.".into(),
+ input_schema: json!({
+ "type": "object",
+ "properties": {
+ "paths": {
+ "oneOf": [
+ { "type": "string" },
+ { "type": "array", "items": { "type": "string" } }
+ ],
+ "description": "Optional file or directory paths (relative to the workspace root). When omitted, returns diagnostics for all known open editor files plus workspace-wide checks where available."
+ }
+ },
+ "additionalProperties": false
+ }),
+ }
+ }
+
+ pub async fn run(&self, input: Value) -> ToolRunResult {
+ match self.read(input).await {
+ Ok(output) => ToolRunResult::ok(output, Vec::new()),
+ Err(err) => ToolRunResult::err(err.to_string(), Vec::new()),
+ }
+ }
+
+ async fn read(&self, input: Value) -> Result {
+ let parsed: ReadLintsInput = serde_json::from_value(input)
+ .map_err(|err| anyhow::anyhow!("invalid read_lints input: {err}"))?;
+ let requested_paths = normalize_requested_paths(parsed.paths)?;
+ let path_filter = if requested_paths.is_empty() {
+ None
+ } else {
+ Some(requested_paths.as_slice())
+ };
+ let diagnostics = self.collect_editor_diagnostics(path_filter);
+ let project_paths = if requested_paths.is_empty() {
+ self.default_project_paths(&diagnostics)
+ } else {
+ requested_paths
+ };
+ let mut diagnostics = diagnostics;
+ if let Ok(project_diags) = self.collect_project_diagnostics(&project_paths).await {
+ diagnostics.extend(project_diags);
+ }
+ let mut diagnostics = dedupe_diagnostics(diagnostics);
+ diagnostics.sort_by(|left, right| {
+ left.path
+ .cmp(&right.path)
+ .then(left.line.cmp(&right.line))
+ .then(left.column.cmp(&right.column))
+ });
+ diagnostics.truncate(MAX_DIAGNOSTICS);
+ Ok(format_diagnostics(&diagnostics))
+ }
+
+ fn collect_editor_diagnostics(&self, paths: Option<&[String]>) -> Vec {
+ let store = self
+ .editor_store
+ .read()
+ .unwrap_or_else(|err| err.into_inner());
+ store.matching(paths).cloned().collect()
+ }
+
+ fn default_project_paths(&self, editor: &[EditorDiagnostic]) -> Vec {
+ let mut paths = HashSet::new();
+ for diag in editor {
+ paths.insert(diag.path.clone());
+ }
+ if paths.is_empty()
+ && self.workspace_root.join("Cargo.toml").is_file() {
+ paths.insert(".".to_string());
+ }
+ paths.into_iter().collect()
+ }
+
+ async fn collect_project_diagnostics(
+ &self,
+ paths: &[String],
+ ) -> Result> {
+ let mut out = Vec::new();
+ let mut rust_paths = Vec::new();
+ let mut js_paths = Vec::new();
+ let mut py_paths = Vec::new();
+
+ for path in paths {
+ let normalized = normalize_workspace_relative_path(path)?;
+ if normalized.is_empty() {
+ if self.workspace_root.join("Cargo.toml").is_file() {
+ rust_paths.push(".".to_string());
+ }
+ continue;
+ }
+ let absolute = resolve_workspace_path(&self.workspace_root, &normalized)?;
+ if absolute.is_dir() {
+ if normalized == "." || self.workspace_root.join("Cargo.toml").is_file() {
+ rust_paths.push(normalized);
+ }
+ continue;
+ }
+ if is_rust_path(&absolute) {
+ rust_paths.push(normalized);
+ } else if is_js_path(&absolute) {
+ js_paths.push(normalized);
+ } else if is_py_path(&absolute) {
+ py_paths.push(normalized);
+ }
+ }
+
+ if !rust_paths.is_empty() && self.workspace_root.join("Cargo.toml").is_file() {
+ out.extend(run_cargo_check(&self.workspace_root, &rust_paths).await?);
+ }
+ if !js_paths.is_empty() && eslint_config_exists(&self.workspace_root) {
+ out.extend(run_eslint(&self.workspace_root, &js_paths).await?);
+ }
+ if !py_paths.is_empty() {
+ out.extend(run_ruff(&self.workspace_root, &py_paths).await?);
+ }
+ Ok(out)
+ }
+}
+
+#[derive(Debug, Deserialize)]
+struct ReadLintsInput {
+ #[serde(default)]
+ paths: Option,
+}
+
+fn normalize_requested_paths(paths: Option) -> Result> {
+ let Some(paths) = paths else {
+ return Ok(Vec::new());
+ };
+ let values = match paths {
+ Value::String(path) => vec![path],
+ Value::Array(items) => items
+ .into_iter()
+ .filter_map(|value| value.as_str().map(str::to_string))
+ .collect(),
+ _ => {
+ return Err(anyhow::anyhow!(
+ "paths must be a string or an array of strings"
+ ))
+ }
+ };
+ let mut out = Vec::new();
+ for path in values {
+ let trimmed = path.trim();
+ if trimmed.is_empty() {
+ continue;
+ }
+ out.push(normalize_workspace_relative_path(trimmed)?.to_string());
+ }
+ Ok(out)
+}
+
+fn format_diagnostics(diagnostics: &[EditorDiagnostic]) -> String {
+ if diagnostics.is_empty() {
+ return "No linter errors found.".to_string();
+ }
+ diagnostics
+ .iter()
+ .map(|diag| {
+ let source = if diag.source.trim().is_empty() {
+ String::new()
+ } else {
+ format!(" ({})", diag.source.trim())
+ };
+ format!(
+ "{}:{}:{}-{}: {}{}",
+ diag.path,
+ diag.line,
+ diag.column,
+ diag.severity,
+ diag.message.trim(),
+ source
+ )
+ })
+ .collect::>()
+ .join("\n")
+}
+
+fn is_rust_path(path: &Path) -> bool {
+ path.extension()
+ .and_then(|ext| ext.to_str())
+ .is_some_and(|ext| ext.eq_ignore_ascii_case("rs"))
+}
+
+fn is_js_path(path: &Path) -> bool {
+ matches!(
+ path.extension()
+ .and_then(|ext| ext.to_str())
+ .map(|ext| ext.to_ascii_lowercase()),
+ Some(ext) if matches!(
+ ext.as_str(),
+ "ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs"
+ )
+ )
+}
+
+fn is_py_path(path: &Path) -> bool {
+ path.extension()
+ .and_then(|ext| ext.to_str())
+ .is_some_and(|ext| ext.eq_ignore_ascii_case("py"))
+}
+
+fn dedupe_diagnostics(diagnostics: Vec) -> Vec {
+ let mut seen = HashSet::new();
+ diagnostics
+ .into_iter()
+ .filter(|diag| seen.insert(diag_key(diag)))
+ .collect()
+}
+
+fn eslint_config_exists(workspace_root: &Path) -> bool {
+ [
+ "eslint.config.js",
+ "eslint.config.mjs",
+ "eslint.config.cjs",
+ "eslint.config.ts",
+ ".eslintrc",
+ ".eslintrc.js",
+ ".eslintrc.cjs",
+ ".eslintrc.json",
+ ".eslintrc.yaml",
+ ".eslintrc.yml",
+ ]
+ .iter()
+ .any(|name| workspace_root.join(name).is_file())
+}
+
+async fn run_cargo_check(workspace_root: &Path, paths: &[String]) -> Result> {
+ let mut command = Command::new("cargo");
+ command
+ .arg("check")
+ .arg("--message-format=json")
+ .current_dir(workspace_root)
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped());
+ #[cfg(windows)]
+ command.creation_flags(CREATE_NO_WINDOW);
+
+ let output = timeout(PROJECT_LINT_TIMEOUT, command.output())
+ .await
+ .context("cargo check timed out")?
+ .context("unable to run cargo check")?;
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let allowed = path_prefixes(workspace_root, paths)?;
+ parse_cargo_messages(&stdout, &allowed)
+}
+
+fn path_prefixes(workspace_root: &Path, paths: &[String]) -> Result> {
+ let mut allowed = HashSet::new();
+ for path in paths {
+ if path == "." {
+ return Ok(HashSet::new());
+ }
+ let absolute = resolve_workspace_path(workspace_root, path)?;
+ allowed.insert(normalize_path_string(&absolute));
+ }
+ Ok(allowed)
+}
+
+fn normalize_path_string(path: &Path) -> String {
+ path.to_string_lossy().replace('\\', "/")
+}
+
+fn parse_cargo_messages(stdout: &str, allowed: &HashSet) -> Result> {
+ let filter_all = allowed.is_empty();
+ let mut diagnostics = Vec::new();
+ let mut seen = HashSet::new();
+
+ for line in stdout.lines() {
+ let value: Value = match serde_json::from_str(line) {
+ Ok(value) => value,
+ Err(_) => continue,
+ };
+ if value.get("reason").and_then(Value::as_str) != Some("compiler-message") {
+ continue;
+ }
+ let message = value
+ .pointer("/message")
+ .ok_or_else(|| anyhow::anyhow!("missing cargo message payload"))?;
+ let level = message
+ .get("level")
+ .and_then(Value::as_str)
+ .unwrap_or("warning");
+ if !matches!(level, "error" | "warning") {
+ continue;
+ }
+ let spans = message
+ .get("spans")
+ .and_then(Value::as_array)
+ .filter(|spans| !spans.is_empty())
+ .ok_or_else(|| anyhow::anyhow!("missing cargo span"))?;
+ let primary = spans
+ .iter()
+ .find(|span| span.get("is_primary").and_then(Value::as_bool).unwrap_or(false))
+ .or_else(|| spans.first())
+ .context("missing primary cargo span")?;
+ let file_name = primary
+ .get("file_name")
+ .and_then(Value::as_str)
+ .unwrap_or_default();
+ if file_name.is_empty() {
+ continue;
+ }
+ let absolute = normalize_path_string(Path::new(file_name));
+ if !filter_all && !allowed.iter().any(|prefix| absolute.starts_with(prefix)) {
+ continue;
+ }
+ let relative = workspace_relative_from_absolute(file_name);
+ let rendered = message
+ .get("rendered")
+ .and_then(Value::as_str)
+ .or_else(|| message.get("message").and_then(Value::as_str))
+ .unwrap_or("compiler diagnostic");
+ let line = primary.get("line_start").and_then(Value::as_u64).unwrap_or(1) as u32;
+ let column = primary.get("column_start").and_then(Value::as_u64).unwrap_or(1) as u32;
+ let end_line = primary.get("line_end").and_then(Value::as_u64).unwrap_or(line as u64) as u32;
+ let end_column =
+ primary.get("column_end").and_then(Value::as_u64).unwrap_or(column as u64) as u32;
+ let diag = EditorDiagnostic {
+ path: relative,
+ line,
+ column,
+ end_line,
+ end_column,
+ severity: level.to_string(),
+ message: rendered.trim().to_string(),
+ source: "rustc".to_string(),
+ };
+ if seen.insert(diag_key(&diag)) {
+ diagnostics.push(diag);
+ }
+ }
+ Ok(diagnostics)
+}
+
+fn workspace_relative_from_absolute(path: &str) -> String {
+ path.replace('\\', "/")
+ .trim_start_matches("./")
+ .to_string()
+}
+
+async fn run_eslint(workspace_root: &Path, paths: &[String]) -> Result> {
+ let mut command = Command::new("npx");
+ command
+ .arg("--yes")
+ .arg("eslint")
+ .arg("--format")
+ .arg("json")
+ .current_dir(workspace_root);
+ for path in paths {
+ command.arg(path);
+ }
+ command
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped());
+ #[cfg(windows)]
+ command.creation_flags(CREATE_NO_WINDOW);
+
+ let output = timeout(PROJECT_LINT_TIMEOUT, command.output())
+ .await
+ .context("eslint timed out")?
+ .context("unable to run eslint")?;
+ if output.stdout.is_empty() {
+ return Ok(Vec::new());
+ }
+ parse_eslint_json(&String::from_utf8_lossy(&output.stdout))
+}
+
+fn parse_eslint_json(stdout: &str) -> Result> {
+ let files: Value = serde_json::from_str(stdout).context("invalid eslint json output")?;
+ let Some(files) = files.as_array() else {
+ return Ok(Vec::new());
+ };
+ let mut diagnostics = Vec::new();
+ for file in files {
+ let Some(path) = file.get("filePath").and_then(Value::as_str) else {
+ continue;
+ };
+ let relative = workspace_relative_from_absolute(path);
+ let Some(messages) = file.get("messages").and_then(Value::as_array) else {
+ continue;
+ };
+ for message in messages {
+ let severity = match message.get("severity").and_then(Value::as_u64) {
+ Some(2) => "error",
+ Some(1) => "warning",
+ _ => "info",
+ };
+ diagnostics.push(EditorDiagnostic {
+ path: relative.clone(),
+ line: message.get("line").and_then(Value::as_u64).unwrap_or(1) as u32,
+ column: message.get("column").and_then(Value::as_u64).unwrap_or(1) as u32,
+ end_line: message.get("endLine").and_then(Value::as_u64).unwrap_or(1) as u32,
+ end_column: message.get("endColumn").and_then(Value::as_u64).unwrap_or(1) as u32,
+ severity: severity.to_string(),
+ message: message
+ .get("message")
+ .and_then(Value::as_str)
+ .unwrap_or("eslint diagnostic")
+ .to_string(),
+ source: message
+ .get("ruleId")
+ .and_then(Value::as_str)
+ .unwrap_or("eslint")
+ .to_string(),
+ });
+ }
+ }
+ Ok(diagnostics)
+}
+
+async fn run_ruff(workspace_root: &Path, paths: &[String]) -> Result> {
+ let mut command = Command::new("ruff");
+ command
+ .arg("check")
+ .arg("--output-format")
+ .arg("json")
+ .current_dir(workspace_root);
+ for path in paths {
+ command.arg(path);
+ }
+ command
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped());
+ #[cfg(windows)]
+ command.creation_flags(CREATE_NO_WINDOW);
+
+ let output = timeout(PROJECT_LINT_TIMEOUT, command.output())
+ .await
+ .context("ruff timed out")?
+ .context("unable to run ruff")?;
+ if output.stdout.is_empty() {
+ return Ok(Vec::new());
+ }
+ parse_ruff_json(&String::from_utf8_lossy(&output.stdout))
+}
+
+fn parse_ruff_json(stdout: &str) -> Result> {
+ let items: Value = serde_json::from_str(stdout).context("invalid ruff json output")?;
+ let Some(items) = items.as_array() else {
+ return Ok(Vec::new());
+ };
+ let mut diagnostics = Vec::new();
+ for item in items {
+ let filename = item
+ .get("filename")
+ .and_then(Value::as_str)
+ .unwrap_or_default();
+ diagnostics.push(EditorDiagnostic {
+ path: workspace_relative_from_absolute(filename),
+ line: item.get("location").and_then(|v| v.get("row")).and_then(Value::as_u64).unwrap_or(1) as u32,
+ column: item.get("location").and_then(|v| v.get("column")).and_then(Value::as_u64).unwrap_or(1) as u32,
+ end_line: item.get("end_location").and_then(|v| v.get("row")).and_then(Value::as_u64).unwrap_or(1) as u32,
+ end_column: item.get("end_location").and_then(|v| v.get("column")).and_then(Value::as_u64).unwrap_or(1) as u32,
+ severity: "warning".to_string(),
+ message: item
+ .get("message")
+ .and_then(Value::as_str)
+ .unwrap_or("ruff diagnostic")
+ .to_string(),
+ source: item
+ .get("code")
+ .and_then(Value::as_str)
+ .unwrap_or("ruff")
+ .to_string(),
+ });
+ }
+ Ok(diagnostics)
+}
+
+fn diag_key(diag: &EditorDiagnostic) -> DiagnosticKey {
+ DiagnosticKey {
+ path: diag.path.clone(),
+ line: diag.line,
+ column: diag.column,
+ severity: diag.severity.clone(),
+ message: diag.message.clone(),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn formats_diagnostics_for_model() {
+ let text = format_diagnostics(&[EditorDiagnostic {
+ path: "src/main.rs".into(),
+ line: 4,
+ column: 12,
+ end_line: 4,
+ end_column: 18,
+ severity: "error".into(),
+ message: "expected `;`".into(),
+ source: "rustc".into(),
+ }]);
+ assert!(text.contains("src/main.rs:4:12-error: expected `;` (rustc)"));
+ }
+
+ #[test]
+ fn parses_cargo_json_diagnostics() {
+ let stdout = r#"{"reason":"compiler-message","message":{"level":"error","message":"cannot find value `x`","rendered":"error: cannot find value `x`","spans":[{"file_name":"src/lib.rs","line_start":2,"column_start":5,"line_end":2,"column_end":6,"is_primary":true}]}}"#;
+ let allowed = HashSet::from([normalize_path_string(Path::new("src/lib.rs"))]);
+ let diagnostics = parse_cargo_messages(stdout, &allowed).expect("parse cargo");
+ assert_eq!(diagnostics.len(), 1);
+ assert_eq!(diagnostics[0].path, "src/lib.rs");
+ assert_eq!(diagnostics[0].source, "rustc");
+ }
+}
diff --git a/crates/sinew-app/src/store.rs b/crates/sinew-app/src/store.rs
index 11ce7178..1ef8152d 100644
--- a/crates/sinew-app/src/store.rs
+++ b/crates/sinew-app/src/store.rs
@@ -1,7 +1,8 @@
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
- time::{SystemTime, UNIX_EPOCH},
+ thread,
+ time::{Instant, SystemTime, UNIX_EPOCH},
};
use anyhow::{Context, Result};
@@ -29,6 +30,7 @@ const SUB_AGENT_SETTINGS_KEY: &str = "sub_agent_settings";
const TOOL_SETTINGS_KEY: &str = "tool_settings";
const SKILL_SETTINGS_KEY: &str = "skill_settings";
const OPENROUTER_MODELS_KEY: &str = "openrouter_models";
+const OLLAMA_MODELS_KEY: &str = "ollama_models";
const HIDDEN_TOOL_SETTING_NAMES: &[&str] = &["skill"];
pub const DEFAULT_PLAN_MODE_PROMPT: &str = r#"You are in Plan mode.
@@ -61,6 +63,13 @@ pub struct ConversationSummary {
pub updated_at_ms: i64,
}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct OtherWorkspaceSummary {
+ pub workspace_id: String,
+ pub count: i64,
+}
+
#[derive(Debug, Clone)]
pub struct TurnCheckpointRecord {
pub history_index: usize,
@@ -72,6 +81,7 @@ pub struct TurnCheckpointRecord {
pub struct SavedConversation {
pub id: String,
pub workspace_id: String,
+ pub git_remote_url: Option,
pub title: String,
pub model: ModelRef,
pub mode_model_settings: ModeModelSettings,
@@ -207,6 +217,12 @@ pub struct ToolSettings {
#[serde(default)]
pub openai_image_use_subscription: bool,
#[serde(default)]
+ pub gemini_image_use_subscription: bool,
+ #[serde(default)]
+ pub openai_image_model: String,
+ #[serde(default)]
+ pub gemini_image_model: String,
+ #[serde(default)]
pub openai_image_api_key: String,
#[serde(default)]
pub nano_banana_api_key: String,
@@ -256,6 +272,9 @@ pub struct ToolSettingsView {
pub default_plan_mode_prompt: String,
pub image_provider: ImageProvider,
pub openai_image_use_subscription: bool,
+ pub gemini_image_use_subscription: bool,
+ pub openai_image_model: String,
+ pub gemini_image_model: String,
pub openai_image_api_key: String,
pub nano_banana_api_key: String,
pub web_search_provider: WebSearchProvider,
@@ -315,6 +334,14 @@ impl ToolSettings {
self.openai_image_api_key = self.openai_image_api_key.trim().to_string();
self.nano_banana_api_key = self.nano_banana_api_key.trim().to_string();
self.linkup_api_key = self.linkup_api_key.trim().to_string();
+ self.openai_image_model = self.openai_image_model.trim().to_string();
+ if self.openai_image_model.is_empty() {
+ self.openai_image_model = "gpt-image-2".to_string();
+ }
+ self.gemini_image_model = self.gemini_image_model.trim().to_string();
+ if self.gemini_image_model.is_empty() {
+ self.gemini_image_model = "gemini-3.1-flash-image-preview".to_string();
+ }
self.tools = self
.tools
.into_iter()
@@ -450,6 +477,9 @@ pub fn tool_settings_view(settings: &ToolSettings, catalog: &[ToolDescriptor]) -
default_plan_mode_prompt: DEFAULT_PLAN_MODE_PROMPT.to_string(),
image_provider: settings.image_provider,
openai_image_use_subscription: settings.openai_image_use_subscription,
+ gemini_image_use_subscription: settings.gemini_image_use_subscription,
+ openai_image_model: settings.openai_image_model.clone(),
+ gemini_image_model: settings.gemini_image_model.clone(),
openai_image_api_key: settings.openai_image_api_key.clone(),
nano_banana_api_key: settings.nano_banana_api_key.clone(),
web_search_provider: settings.web_search_provider,
@@ -571,21 +601,30 @@ impl AppStore {
pub fn bootstrap_workspace(
&self,
workspace_root: &Path,
+ project_id: &str,
+ git_remote_url: Option<&str>,
default_model: &ModelRef,
default_system: &str,
) -> Result {
- let workspace_id = workspace_root.display().to_string();
let mode_model_settings = self.load_mode_model_settings(default_model)?;
- let mut conversations = self.list_conversations(&workspace_id)?;
- let active_conversation = if let Some(first) = conversations.first() {
- self.load_conversation(&workspace_id, &first.id)?
+ let mut conversations = self.list_conversations(project_id, git_remote_url)?;
+ let mut active_conversation = if let Some(first) = conversations.first() {
+ self.load_conversation(project_id, &first.id)?
.context("conversation listed in index but missing from store")?
} else {
- let created = self.create_conversation(&workspace_id, default_model, default_system)?;
- conversations = self.list_conversations(&workspace_id)?;
+ let created = self.create_conversation(
+ project_id,
+ git_remote_url,
+ default_model,
+ default_system,
+ )?;
+ conversations = self.list_conversations(project_id, git_remote_url)?;
created
};
+ // Keep absolute path for frontend to be happy
+ active_conversation.workspace_id = workspace_root.display().to_string();
+
Ok(WorkspaceBootstrap {
workspace: workspace_info(workspace_root),
conversations,
@@ -597,12 +636,21 @@ impl AppStore {
pub fn create_conversation(
&self,
workspace_id: &str,
+ git_remote_url: Option<&str>,
default_model: &ModelRef,
default_system: &str,
) -> Result {
let id = Uuid::new_v4().to_string();
let now = now_ms();
- let title = DEFAULT_CONVERSATION_TITLE.to_string();
+ let host = std::env::var("COMPUTERNAME")
+ .or_else(|_| std::env::var("HOSTNAME"))
+ .unwrap_or_default();
+ let tag = if !host.trim().is_empty() {
+ format!("[{}] ", host.trim())
+ } else {
+ "".to_string()
+ };
+ let title = format!("{}{}", tag, DEFAULT_CONVERSATION_TITLE);
let todo_list = TodoListState::default();
let todo_list_json = serde_json::to_string(&todo_list)?;
let plan_workflow = PlanWorkflowState::default();
@@ -614,11 +662,12 @@ impl AppStore {
let mode_model_settings_json = serde_json::to_string(&mode_model_settings)?;
let conn = self.connection()?;
conn.execute(
- "insert into conversations (id, workspace_id, title, title_initialized, model_json, mode_model_settings_json, system_prompt, todo_list_json, plan_workflow_json, goal_workflow_json, created_at_ms, updated_at_ms)
- values (?1, ?2, ?3, 0, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
+ "insert into conversations (id, workspace_id, git_remote_url, title, title_initialized, model_json, mode_model_settings_json, system_prompt, todo_list_json, plan_workflow_json, goal_workflow_json, created_at_ms, updated_at_ms)
+ values (?1, ?2, ?3, ?4, 0, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
params![
&id,
workspace_id,
+ git_remote_url,
&title,
serde_json::to_string(&conversation_model)?,
mode_model_settings_json,
@@ -635,6 +684,7 @@ impl AppStore {
Ok(SavedConversation {
id,
workspace_id: workspace_id.to_string(),
+ git_remote_url: git_remote_url.map(|s| s.to_string()),
title,
model: conversation_model,
mode_model_settings,
@@ -646,8 +696,21 @@ impl AppStore {
})
}
- pub fn list_conversations(&self, workspace_id: &str) -> Result> {
+ pub fn list_conversations(
+ &self,
+ workspace_id: &str,
+ git_remote_url: Option<&str>,
+ ) -> Result> {
let conn = self.connection()?;
+
+ // Auto-migrate any conversations that share the same git_remote_url to the current workspace_id!
+ if let Some(remote_url) = git_remote_url {
+ let _ = conn.execute(
+ "update conversations set workspace_id = ?1 where git_remote_url = ?2 and workspace_id != ?1",
+ params![workspace_id, remote_url],
+ );
+ }
+
let mut statement = conn
.prepare(
"select id, title, updated_at_ms from conversations
@@ -673,16 +736,91 @@ impl AppStore {
Ok(conversations)
}
+ pub fn list_other_workspaces(
+ &self,
+ current_workspace_id: &str,
+ current_project_id: Option<&str>,
+ ) -> Result> {
+ let conn = self.connection()?;
+
+ // Clean both paths by converting them to lowercase and replacing forward/backward slashes
+ // to minimize slight path formatting differences
+ let normalized_current = current_workspace_id.replace('\\', "/").to_lowercase();
+ let normalized_project = current_project_id.map(|p| p.to_lowercase());
+
+ let mut statement = conn
+ .prepare(
+ "select workspace_id, count(*) from conversations
+ group by workspace_id
+ order by count(*) desc",
+ )
+ .context("unable to prepare other workspaces list query")?;
+
+ let rows = statement
+ .query_map([], |row| {
+ let workspace_id: String = row.get(0)?;
+ let count: i64 = row.get(1)?;
+ Ok((workspace_id, count))
+ })
+ .context("unable to read other workspaces list")?;
+
+ let mut list = Vec::new();
+ for row in rows {
+ let (workspace_id, count) = row.context("bad workspace summary row")?;
+ let normalized_db = workspace_id.replace('\\', "/").to_lowercase();
+ // Exclude current workspace and current project UUID from other workspaces list
+ if normalized_db != normalized_current
+ && Some(&normalized_db) != normalized_project.as_ref()
+ {
+ list.push(OtherWorkspaceSummary {
+ workspace_id,
+ count,
+ });
+ }
+ }
+ Ok(list)
+ }
+
+ pub fn migrate_conversations(
+ &self,
+ src_workspace_id: &str,
+ dest_workspace_id: &str,
+ ) -> Result<()> {
+ let conn = self.connection()?;
+ // Normalize slashes and use case-insensitive matching for paths
+ let src_norm = src_workspace_id.replace('\\', "/").to_lowercase();
+
+ // Find all distinct workspace_ids in the database that normalize to the same path
+ let mut stmt = conn.prepare("select distinct workspace_id from conversations")?;
+ let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
+ let mut ids_to_migrate = Vec::new();
+ for id in rows.flatten() {
+ if id.replace('\\', "/").to_lowercase() == src_norm {
+ ids_to_migrate.push(id);
+ }
+ }
+
+ for id in ids_to_migrate {
+ conn.execute(
+ "update conversations set workspace_id = ?2 where workspace_id = ?1",
+ params![id, dest_workspace_id],
+ )?;
+ }
+ Ok(())
+ }
+
pub fn load_conversation(
&self,
workspace_id: &str,
id: &str,
) -> Result