-
v0.1.0
Stablereleased this
2026-09-05 16:23:20 +02:00 | 42 commits to main since this releaseAdded
-
scaffold : monorepo, docs (SPEC, STATUS, DECISIONS, CHANGELOG, ECARTS, API, DATA, WORKFLOW), agents Claude Code, CI Forgejo, scripts (T-001)
-
server :
/health,/v1/dictate(pipeline STT → profil → rewrite → fallback degraded),/v1/agentstub, vérif JWT HS256, 9 tests (T-001) -
desktop : squelette Tauri v2 (overlay + settings), machine à états, hotkey, modules stubs, contrat
src/lib/tauri.ts(T-001) -
supabase : migration
aria_init(5 tablesaria_*, RLS, trigger profil), seed 20 apps (T-001) -
supabase : migration
aria_init+ seed catalogue appliqués pour de vrai sur l'instance self-hosted partagée ;packages/contract/src/supabase.types.tsgénéré (T-005) -
scripts :
scripts/filter_supabase_types.py+gen-contract.shfiltrent désormais les types Supabase générés aux objetsaria_*(instance partagée, cf. E-008) (T-005) -
server : fallback JWKS pour la vérification JWT Supabase (clés asymétriques RS256/ES256,
jwt.PyJWKClientavec cache), utilisé quandSUPABASE_JWT_SECRETest vide ; 20 tests avec tokens forgés (T-010) -
server : couverture de tests du client Supabase clé service (
SupabaseDB,aria_server/db/supabase.py) — lecturearia_app_catalog/aria_app_profiles/aria_profilesvia PostgREST, 14 tests avechttpx.MockTransport(T-011) -
server : couverture de tests de la résolution app → profil (
aria_server/apps/resolver.py,docs/DATA.md§Résolution) — 10 tests avec catalogue/profils mockés en mémoire (tests/test_resolver.py) : app connue avec/sans profil dédié,custom_app_id, profil par défaut, prompt global implicite,rewrite_enabled, overridellm_model, minuscules Windows, isolation par utilisateur (T-012) -
server : couverture de tests du provider STT OpenRouter (
OpenRouterSTT), 17 tests avechttpx.MockTransport, dont un test d'intégration optionnel sauté par défaut (opt-in humain requis, pas d'appel réseau réel) (T-013) -
server : couverture de tests du provider LLM OpenRouter (
OpenRouterLLM, chat completions), 14 tests avechttpx.MockTransport(requête sortante, extraction/strip du contenu, mapping de toutes les erreurs versLLMError) ; couverture de tests du prompt système de base et de l'assemblage (aria_server/pipeline/prompts.py,build_system_prompt), 13 tests (tests/test_prompts.py) : style/hésitations/ponctuation/langue conservée, ordre base → profil → titre de fenêtre, fallback app_name/app_id (T-014) -
server :
light_cleanup()(aria_server/pipeline/dictate.py) — nettoyage léger sans LLM appliqué quandrewrite_enabled=False(docs/SPEC.md§5.3 point 5, portée tranchée en ADR-008 : normalisation des espaces uniquement) ; test end-to-end de l'échec STT via/v1/dictate(502stt_failed, distinct du fallbackdegradedpour un échec LLM) ; 9 tests ajoutés (tests/test_dictate.py,tests/test_pipeline_dictate.py) (T-015) -
server : couverture de tests des tâches de fond
SupabaseDB.upsert_seen_app/insert_transcription(aria_seen_apps/aria_transcriptionsviaBackgroundTasks), 10 tests (tests/test_supabase_db.py,tests/test_dictate.py) : payload PostgREST, headerPrefer: resolution=merge-duplicatessur l'upsert, no-op quand non configuré,upsert_seen_appsystématique vsinsert_transcriptionconditionné àaria_profiles.history_enabled, cohérence de l'historique avec le fallbackdegraded(T-016) -
server : échappement des valeurs interpolées dans les filtres PostgREST de
SupabaseDB(db/supabase.py) —_pg_array_element(littéral tableaucs.{…}) et_pg_filter_value(eq.…/or=(...)) quotent conditionnellement toute valeur contenant un caractère réservé (,{}()"\, espace), appliqué àfind_canonical_app/find_app_profile/find_default_profile/get_profile_settings; 17 tests ajoutés (tests/test_supabase_db.py) ; résorption de E-010 (T-092) -
server : validation stricte du format WAV (
aria_server/api/wav.py,validate_wav) — PCM 16 bits / 16 kHz / mono conforme au contrat (docs/API.md), rejette422 audio_not_wav/audio_invalid_format: …(mauvaise fréquence, canaux, profondeur, format non-PCM) plutôt que de laisser passer un WAV mal formé au provider STT ; 13 tests (tests/test_wav.py) (T-017) -
server :
STTTimeoutError(sous-classe deSTTError) distingue un timeout provider STT d'un échec générique ;/v1/dictateet/v1/agentrépondent504 stt_timeoutau lieu de502 stt_faileddans ce cas ; 2 tests provider + 1 test route (T-017) -
server : middleware
limit_upload_size(main.py) — rejette413 audio_too_largeau vu duContent-Lengthavant même le parsing multipart quand l'en-tête est présent, en complément du contrôle post-lecture existant (_read_audio) ; 5 tests (tests/test_main_limits.py) (T-017) -
server : couverture de tests dédiée de
POST /v1/agent(tests/test_agent.py, 10 tests) — parité des garanties avec/v1/dictate(401/413/422/502/504) et absence d'effets de bord en v1 (pas d'appel LLM, pas de tâche de fondupsert_seen_app/insert_transcription, la route ne prenant nibackgroundnidb) ; ancien test minimaltest_agent_stubdetests/test_dictate.pyretiré (doublon) (T-018) -
desktop : couverture de tests de la logique de garde/transition de la machine à états (
apps/desktop/src-tauri/src/session.rs) —can_start,is_recording,other_mode,is_too_short,should_reset_after_timeout,classify_resultextraits en fonctions pures sans dépendanceAppHandleet testés (8 tests) ;SessionStatedérive désormaisPartialEq; tests d'intégration bout-en-bout (effets de bord Tauri réels viatauri::test) hors périmètre de cette tâche, cf. E-011/T-093 (T-030) -
desktop :
hotkey::parse_or_default(apps/desktop/src-tauri/src/hotkey.rs) retombe surDEFAULT_HOTKEY(Control+Space, désormaispub constdansconfig.rsplutôt que dupliquée) si la hotkey stockée en config est invalide, au lieu de faire échouerregister_main(et donc le démarrage de l'app) ; 10 tests unitaires purs ajoutés couvrant le parsing du défaut, le rejet des chaînes invalides, un combo custom valide, et le fallback (T-031) -
desktop : capture micro réelle (
apps/desktop/src-tauri/src/audio.rs, résorption partielle d'E-002) —cpalsur thread dédié (leStreamn'est jamais envoyé hors du thread), device sélectionné depuisconfig.rs(fallback + warning si absent/introuvable), tous les formats d'échantillon courants supportés, WAV PCM 16 bits/16 kHz mono conforme au contrat serveur ; RMS calculé toutes les 50 ms (RmsWindower) et poussé viasession.tick, déjà câblé sursession://state.rmsdepuis T-030 ; logique de signal (downmix multi-canal, conversion de format, rééchantillonnage linéaire, calcul RMS, fenêtrage 50 ms) extraite en fonctions pures et testée, 16 tests ajoutés (T-032) -
infra :
docker-compose.yml(racine) +apps/server/.dockerignore—docker compose up --buildconstruit et démarreapps/serverseul (Dockerfile déjà scaffoldé en T-001), port 8787,env_file: .env, healthcheckGET /health; testé en local (build + run +/healthOK avec des valeurs d'env factices). Déploiement effectif sur le serveur de Malo (reverse proxy HTTPS, DNS) hors périmètre agent, laissé en vérification manuelle (T-020) -
server :
apps/server/tests/e2e/curl_dictate.shparamétré (APP_ID/APP_NAME/WINDOW_TITLE/PLATFORM/LOCALEen env, défauts Outlook rétrocompatibles) pour cibler n'importe quel profil stocké en base sans éditer le script (T-021) -
desktop : couverture de tests de
config.rs(AriaConfig/ConfigPatch, déjà scaffoldé et fonctionnellement utilisé depuis T-036/T-037/T-094 mais sans aucun test) — logique de fusion d'un patch partiel extraite en fonction puremerge_patch(testable sansAppHandle/tauri-plugin-store, même approche quesession.rs/hotkey.rs/audio.rs/api_client.rs) ; 10 tests couvrant les défauts, un patch vide, chaque champ isolément, la distinctionSome(None)/Nonepourmicrophone(remise au périphérique par défaut vs champ non fourni), plusieurs champs à la fois, et le round-tripserde_json(contratsrc/lib/tauri.ts) (T-038) -
desktop : refresh de session Supabase côté Rust (
auth.rs) —AuthStore::refreshappelle réellementPOST {SUPABASE_URL}/auth/v1/token?grant_type=refresh_tokenquand le token expire dans moins de 60 s, sérialisé par untokio::sync::Mutex(évite deux refresh concurrents sur un refresh token à usage unique) ; un rejet explicite (4xx) invalide la session et émetauth://expired, une panne réseau transitoire la conserve (AriaError::Network, pas de déconnexion forcée) ; 10 tests unitaires purs (détection d'expiration, parsing de la réponse GoTrue) ; résorption d'E-001 (T-036) -
desktop :
src-tauri/build.rspropage les clésVITE_*du.envracine du monorepo vers l'environnement de compilation Rust (cargo:rustc-env, parseur minimal fait main) — nécessaire pour queSUPABASE_URL/VITE_SUPABASE_ANON_KEY(auth.rs) etVITE_ARIA_SERVER_URL(config.rs, jamais réellement fonctionnel avant cette PR malgré sa présence depuis le scaffold) aient une valeur au runtime (T-036) -
desktop : page Settings › Compte (
src/settings/pages/Account.tsx, stub jusqu'ici) — login par mot de passe ou par code à 6 chiffres envoyé par email (signInWithOtp/verifyOtp, plutôt qu'un lien magique cliquable qui nécessiterait un deep link OS non posé par cette tâche), affichage de l'utilisateur connecté, déconnexion (signOut, propagée au core Rust viabindSessionToCore()déjà scaffoldée) (T-037) -
desktop :
windows.rs::bottom_center_positionextraite en fonction pure et testée (5 tests) ; overlay confirmé conforme àdocs/SPEC.md§4.1 (transparente, always-on-top, non focusable, skip taskbar, waveform 6 barres, libellés, couleur par mode, fondu 150 ms — déjà scaffoldé, aucune modification nécessaire côtéOverlay.tsx/tokens.css) (T-033, T-034) -
desktop :
tray.rs— icône tray à 3 variantes (idle / active-recording-processing-injecting / erreur, monochrome template macOS,icons/tray-idle*.png/tray-recording*.png/tray-error*.pnggénérées depuis 3 nouveaux SVG), mise à jour à chaque changement desession://state(TrayIcon::set_icon_with_as_template, API Tauri de haut niveau marshalée en interne vers le thread principal — pas d'appelobjc2/AppKit direct) ; menu tray "Ouvrir Aria" / "Activer-Désactiver la dictée" / séparateur / "Quitter" — le nouvel item bascule un drapeau global (hotkey::ENABLED) qui empêche la hotkey principale de démarrer un nouvel enregistrement tant qu'il est désactivé (sans jamais avaler la frappe dans ce cas, pour queCtrl+Spacereste utilisable normalement dans l'app active), un enregistrement déjà en cours va toujours à son terme ; fenêtre settings cachée au démarrage déjà confirmée conforme depuis T-003 (aucune modification nécessaire) ; 4 tests ajoutés (tray.rs: sélection d'icône par état, libellé du menu, validité/distinction des 3 PNG ;hotkey.rs: toggle) (T-039) -
desktop :
api_client.rs— 1 retry (délai 500 ms) sur erreur réseau uniquement (connexion/DNS/timeout, pas un 401/erreur serveur) pourPOST /v1/dictate//v1/agent; corps multipart reconstruit à chaque tentative (consommé par l'envoi) ; timeout de requête (45 s) inchangé, extrait en constante ; décision de retry extraite en fonction pureshould_retrytestée sans réseau (6 tests) (T-035) -
desktop : permissions macOS (
permissions.rs, nouveau module) —check_permissions(commands.rs) n'est plus un stub codé en dur : détection réelle Microphone (AVCaptureDevice.authorizationStatusForMediaType) et Accessibilité (AXIsProcessTrusted, sans invite système) sur macOS, toujours accordé sur Windows/Linux (docs/SPEC.md§4.5) ; nouvelle commandeopen_permission_settingsouvre directement le sous-panneau Réglages Système concerné (x-apple.systempreferences:…?Privacy_Microphone/Privacy_Accessibility, viatauri_plugin_opener::open_urlcôté Rust) ; nouvel onglet Settings « Permissions » (src/settings/pages/Permissions.tsx) affichant l'état de chaque permission avec bouton d'ouverture des réglages et « Revérifier », sélectionné automatiquement au montage si une permission manque ; la fenêtre Settings s'ouvre désormais automatiquement au démarrage sur macOS si une permission manque. Threading vérifié avant tout code (niauthorizationStatusForMediaTypeniAXIsProcessTrustedne sont main-thread-only, à la différence des pièges TSM/TIS de T-094/T-096 ou deNSWindow::setLevelen T-098) — revérifié par un smoke-test 4-threads réel en plus de la lecture des sourcesobjc2-av-foundation/tauri. Confirmé physiquement par l'humain (2026-09-05) : détection, ouverture du bon sous-panneau Réglages Système, revérification et cas nominal tous conformes (T-040) -
process : T-054 vérifiée sans modification de code — le contexte envoyé au serveur (
api_client.rs::Context:app_id/app_name/window_title/platform/locale) était déjà entièrement sérialisé et transmis depuis le scaffold, conforme àdocs/API.md; seul son contenu était inutilisable côté macOS tant qu'active_app.rsétait un stub (T-050 le rend désormais utile) -
desktop :
injector.rs::pastedistingue désormais texte / image / vide au moment de restaurer le clipboard après collage (snapshot/restore,arboard::get_image/set_image/clear) — un clipboard qui contenait une image ou qui était réellement vide n'est plus laissé avec le texte injecté en permanence après une dictée (T-052, ADR-004). T-053 (refocus) : vérifié sans modification de code que l'overlay ne prend jamais le focus (focus:false, aucunset_focus()sur ce chemin) — l'app cible garde le focus du système pendant tout le cycle -
process : critère M2 validé (T-041) — maintenir la touche → overlay → relâcher → texte reçu visible dans les logs et l'overlay, formalisé à partir des confirmations physiques déjà obtenues sur les cycles T-094 (press/release fiable)/T-033-T-034/T-098 (overlay visible, positionné, au-dessus de toute fenêtre)/T-095-T-096-T-097 (texte reçu → injecté sans crash ni bip) ; « visible dans l'overlay » interprété comme "l'overlay reflète l'état de réception/injection" (conforme à
docs/SPEC.md§4.1, l'overlay n'affiche jamais le texte lui-même). M2 clos — T-093/E-011 (généricitéR: tauri::Runtime) et T-100/E-018 (icône tray idle invisible) restent ouvertes pour un futur cycle, ni l'une ni l'autre n'étant un prérequis du critère -
desktop :
session.rs::stop_and_processjournalise désormais le texte reçu en toutes lettres (log::info!, niveau par défaut) en plus de sa longueur, pour que « texte reçu visible dans les logs » (critère M2, T-041) soit littéralement vrai sans configuration de log supplémentaire (T-041) -
desktop :
active_app.rsmacOS (résorption partielle d'E-003) —NSWorkspace::sharedWorkspace().frontmostApplication()(objc2-app-kit) pour le bundle id/nom de l'app au premier plan,AXUIElement(objc2-application-services, Accessibilité T-040 requise) pour le titre de la fenêtre focus ; capturé depuis le threadCGEventTapde la hotkey (session.rs::SessionManager::start()), pas le thread principal — vérifié documentairement (aucune des API utilisées n'estMainThreadOnly) et empiriquement (test dédié exécuté réellement sur macOS, résultat correct). E-007 résorbé au passage (objc2/objc2-app-kit/objc2-foundationalignés sur ce quetauri/wrytirent déjà en transitif, 0.5/0.2 → 0.6/0.3) ; résiduel non bloquant documenté (objc20.5.2 viaenigo→icrate, hors périmètre). Log ajouté (session.rs::start) après un premier test humain sans log visible (capture faite mais jamais journalisée). Confirmé physiquement par l'humain (com.apple.TextEdit/TextEdit/nom du document réels) (T-050) -
process : T-060 vérifiée sans modification de code — le trigger Supabase de création automatique de
aria_profilesà l'inscription (aria_handle_new_user/aria_on_auth_user_created) existait déjà dans la migration initiale (20260902120000_aria_init.sql, appliquée pour de vrai dès T-005) ; aucune migration0002nécessaire, comportement déjà éprouvé par les logins réels T-036/T-037/T-095/T-096/T-097 (T-060) -
supabase :
seed/aria_app_catalog.sqlétendu de 20 à 30 apps — Discord, Signal, Telegram, Zoom, Linear, Figma, Sublime Text, PowerPoint, Aperçu, Calendrier, chacune avecsuggested_prompt; Gmail/Notion/Linear-web/Jira-web restent hors scope (indistinguables d'un navigateur,docs/SPEC.md§12). Validé par dry-run réel contre l'instance partagée (30 lignes, rollback). Application réelle en attente (cf.docs/STATUS.md→ Vérifications manuelles) (T-061) -
desktop : Settings › Applications (
src/settings/pages/Applications.tsx, T-062+T-064 dans la même PR) — CRUD des profils de réécriture par app (aria_app_profiles) viasrc/lib/profiles.ts(hooks TanStack Query + supabase-js typés) etsrc/components/profile-form.tsx(formulaire réutilisable) : profil par défaut éditable, apps détectées (aria_seen_apps) avec correspondance catalogue côté client pour l'affichage, catalogue des apps non encore détectées, prompt suggéré préempli à la création, togglerewrite_enabled, modèle optionnel. Résumé statique des règles deBASE_STYLEaffiché en lecture (pas une lecture live du serveur, cf. E-020/T-101) (T-062, T-064) -
desktop :
injector.rs::paste_modifierextrait en fonction pure + 3 tests (T-068) — couverture automatisée absente jusqu'ici (arboard::Clipboard::new()a besoin d'un vrai clipboard système, indisponible en CI), dont une régression explicite d'E-014 (Key::Unicode('v')sur macOS) ; checklist manuelle M4 (apps/desktop/tests/manual/M4.md, T-068) -
desktop : Settings › Avancé (
src/settings/pages/Advanced.tsx, T-066) — version (getVersion), vérification manuelle de mise à jour (check(), sans installation — flux complet en T-081/M6), timings/statut de la dernière dictée (nouveaudiagnostics.rs, alimenté parapi_client.rs), logs récents (nouveaulogbuffer.rs, enveloppeenv_logger::Loggerexistant dans un tampon circulaire de 200 lignes sans changer la sortie stdout) (T-066) -
desktop : Settings › Historique (
src/settings/pages/History.tsx,src/lib/history.ts, T-065) — liste des dictées (aria_transcriptions, limite 200), recherche client, suppression individuelle/totale, togglearia_profiles.history_enabled(T-065) -
desktop : Settings › Général (
src/settings/pages/General.tsx, T-063) — capture de hotkey (combinaison modificateur(s)+touche compatiblehotkey.rs::parse), choix du microphone (listMicrophones) avec option explicite "périphérique par défaut", langue, URL serveur ; s'appuie surgetConfig/setConfig/onConfigChangeddéjà posés (T-038) (T-063) -
desktop :
src/components/ui/{button,input,textarea,select,switch,label,card}.tsx— premières primitives réutilisables du design system (T-067), style shadcn (class-variance-authority/cn) posé sur les tokens--aria-*existants ; 2 tokens ajoutés (--aria-surface,--aria-success, variantes clair/sombre) ; thème clair/sombre « suit l'OS » déjà conforme depuis le scaffold, aucune modification nécessaire sur ce point (T-067) -
process : T-070/T-071 vérifiées sans modification de code (M5, mode agent) —
Tabpendant Recording bascule déjàMode::Dictation ↔ Mode::Agent(session.rs::toggle_mode,hotkey.rs::register_recording_keys) et l'overlay reflète déjà le mode (libellé"Agent", point de couleur--aria-agentviolet vs--aria-accentbleu) depuis T-030/T-033/T-034 ; le client envoie déjàPOST /v1/agenten mode agent et affiche sonstatusdans l'overlay sans jamais injecter (api_client.rs::send,session.rs::classify_result→ProcessOutcome::ShowMessage) depuis T-030/T-035/T-066 (T-070, T-071) -
server :
AgentResultétendu — nouveau modèleAgentAction(type/label/payloadlibres, forme non figée tant que Hermes/T-073 n'est pas branché) et champsactions: list[AgentAction]/needs_confirmation: bool(défauts vides/faux), purement additif, aucun changement de comportement de/v1/agenten v1 ;docs/API.mddocumente la forme exacte ; 5 tests ajoutés (tests/test_schemas.py) + 2 assertions danstest_agent.py::test_agent_stub(T-072) -
server :
/v1/agentbranché sur Hermes (T-073) — nouveauHermesAgentProvider(aria_server/providers/agent/hermes.py,POST {HERMES_URL}/v1/responses, choix d'endpoint documenté en ADR-010) etpipeline/agent.py::run_agent(STT → Hermes siHERMES_URL/HERMES_API_KEYconfigurés, sinon reste"not_implemented"comme avant T-073) ; mappingoutput[]→AgentResult(message/output_text→messageassemblé,function_call→ unAgentActionpar item,argumentsJSON-parsé) ;status="needs_confirmation"/needs_confirmation=truedès qu'au moins une action est proposée (aucune n'est exécutée par Aria en v1, cf. ADR-010), sinon"done"; nouvellesAgentError/AgentTimeoutError→502 agent_failed/504 agent_timeoutsur échec Hermes (réseau, timeout, HTTP non-2xx, JSON invalide) — volontairement une erreur explicite plutôt qu'un fallback dégradé silencieux (à la différence de la réécriture LLM en dictée, ADR-008), un échec Hermes ne laissant rien d'exploitable côté client ; nouveau prompt système dédiébuild_agent_system_prompt(pipeline/prompts.py, distinct du prompt de dictée) ;docs/API.md§/v1/agentréécrit. 43 tests ajoutés (tests/test_hermes_agent.py: mapping/erreurs du provider avechttpx.MockTransport;tests/test_agent.py: comportement de la route Hermes configurée/non configurée avecFakeAgent). Testé en conditions réelles contre le vrai Hermes (provider isolé + pipeline serveur complet viaTestClient, STT mocké) — voirdocs/STATUS.mdT-073 pour le détail.tests/e2e/curl_agent.shajouté (miroir decurl_dictate.sh) (T-073) -
supabase : migration
aria_agent_sessions(T-103) — une ligne par utilisateur (hermes_response_id,updated_at), RLS deny-all pouranon/authenticated(seul le serveur, clé service, y accède) ; mémorise la conversation Hermes active pour la continuité de conversation du mode agent. Approuvée après dry-run réel contre l'instance partagée, mergée (T-103) -
server : continuité de conversation du mode agent (T-103, ADR-012) —
pipeline/agent.py::run_agent/stream_agent_eventslisentaria_agent_sessionsavant d'appeler Hermes ; si la dernière interaction date de moins de 2h,previous_response_idest transmis pour poursuivre la même conversation, sinon nouvelle conversation vierge ; la conversation est mise à jour après chaque réponse Hermes réussie (tâche de fond sur/v1/agent,awaitdirect en fin de flux sur/v1/agent/stream).HermesAgentProvider.run/run_streamenvoient désormaisstore: true(au lieu defalse) et acceptentprevious_response_id;AgentProviderResult.response_id(nouveau). 24 tests ajoutés (tests/test_supabase_db.py,tests/test_pipeline_agent.pynouveau,tests/test_hermes_agent.py,tests/test_agent.py) (T-103) -
server :
POST /v1/agent/stream(T-102, ADR-012) — miroir streaming de/v1/agent(text/event-stream), additif (/v1/agentJSON inchangé).HermesAgentProvider.run_streamappelle Hermes avecstream: true/store: true, parseur SSE minimal (_parse_sse_lines, fonction pure) ;pipeline/agent.py::stream_agent_eventsnormalise le flux brut d'Hermes en événements Aria ({"type": "token"/"tool"/"done"/"error", ...}) via_map_sse_event(fonction pure, réutilise le mapping message/actions déjà testé en T-073) —function_call_outputvolontairement non transmis au client. Une fois les en-têtes200 text/event-streamenvoyés, une erreur STT/Hermes devient un événementerrordans le flux plutôt qu'un statut HTTP. 32 tests ajoutés (tests/test_hermes_agent.py,tests/test_pipeline_agent_stream.pynouveau,tests/test_agent_stream.pynouveau). Testé en conditions réelles contre le vrai Hermes (streaming de tokens + visibilité outil + persistance de la continuité confirmés) (T-102) -
desktop : mode agent en streaming (T-102) —
api_client.rs::send_agent_streamconsommePOST /v1/agent/stream(reqwest::Response::chunk(), pas de nouvelle dépendance) ;drain_sse_events/parse_sse_frame(fonctions pures) parsent le flux SSE enStreamEvent(Token/Tool/Done/Error, miroir du contrat serveur) ;render_stream_progress/render_stream_done(fonctions pures) construisent le texte affiché (une ligne🔧 <outil>par outil démarré + texte accumulé, repli surformat_agent_displaysi aucuntokenreçu).session.rs::stop_and_process(mode Agent) route désormais vers le flux au lieu de l'ancien appel JSON unique à/v1/agent, met à jourSessionState::AgentMessageà chaque événement (aucun nouveau canal IPC,session://statedéjà émis à chaque mise à jour) ; comportement inchangé sur l'événement final (pas d'injection, reset après 6 s).ProcessOutcome::ShowMessageretiré (classify_resultne gère plus que la dictée, seul appelant restant). 22 tests ajoutés (T-102) -
desktop : updater automatique (T-081) —
updater.rs::spawn_periodic_check(tauri-plugin-updater, déjà enregistré depuis le scaffold) check au démarrage puis toutes les 6h, émetupdate://available(contrat déclaré côté WebView depuis T-001, jamais câblé côté Rust avant cette tâche) ;UpdateBanner.tsx(nouveau, persistant danssettings/App.tsx) propose « Installer et redémarrer » (downloadAndInstall()+relaunch()) ou « Ignorer ».pubkey(tauri.conf.json) reste un placeholder tant que T-008 n'est pas faite : le mécanisme échoue donc systématiquement aujourd'hui (signature non vérifiable), géré comme une erreur normale (loggué côté Rust, affiché sans crash côté UI), non testable en conditions réelles avant T-008 -
server/desktop :
GET /v1/prompts/base(T-101, résorption E-020) — exposeBASE_STYLE(pipeline/prompts.py) tel quel, authentifié comme les autres routes/v1.Settings › Applicationsl'affiche désormais en direct (api_client.rs::get_base_prompt→ commandeget_base_prompt→lib/tauri.ts::getBasePrompt()) à la place du résumé statique fait main (GLOBAL_PROMPT_SUMMARY, supprimé), qui pouvait dériver du texte réel. 3 tests serveur ajoutés (tests/test_prompts_base.py) -
scripts :
scripts/release.sh(T-080, ADR-005) — phase 1 (vX.Y.Z) bump les versions (package.json/tauri.conf.json/Cargo.toml+Cargo.lock/pyproject.toml/aria_server/__init__.py::__version__) et basculedocs/CHANGELOG.md, ouvre une PR (scripts/pr.sh, jamais de push direct surmain) ; phase 2 (--publish vX.Y.Z, après merge de cette PR) tague, build (pnpm tauri build), crée la release Forgejo, upload les installeurs trouvés, et désormais la signature updater :TAURI_SIGNING_PRIVATE_KEY/_PASSWORD(T-008, renseignées par l'humain ce cycle) sont consommées nativement partauri build(confirmé, pas deviné) ; les.sigproduits sont assemblés enlatest.json(format "Static" detauri-plugin-updater, confirmé en lisant la crate vendue) et publiés comme asset d'une release Forgejo séparée taguée littéralementlatest(créée si absente, fusionnée si unlatest.jsond'une plateforme précédente existe déjà) — hypothèse sur le comportement Forgejo documentée explicitement, non vérifiée par un vrai--publish. Toujours non exécuté pour de vrai (créerait un vrai tag/une vraie release) : seule la logique pure (bump, assemblagelatest.json, détection de cible updater) a été vérifiée sur des fichiers scratch — trois bugs shell/Python réels y ont été trouvés et corrigés à cette occasion (heredoc+pipe vers le même stdin, un vrai bug de parsing de bash 3.2 avec heredoc imbriqué dans une substitution de processus, f-string avec guillemet échappé incompatible avec Python < 3.12), cf.docs/STATUS.mdT-080 pour le détail. PR laissée enreview, approbation humaine requise (release, CLAUDE.md règle 3b)
Changed
- desktop : clé de signature updater (minisign) générée par l'humain, clé publique renseignée dans
apps/desktop/src-tauri/tauri.conf.json→plugins.updater.pubkey(remplace le placeholderREPLACE_WITH_MINISIGN_PUBLIC_KEY) ; clé privée conservée hors repo. Résorption de T-008. Reste à faire avant une release réellement signée :TAURI_SIGNING_PRIVATE_KEY/TAURI_SIGNING_PRIVATE_KEY_PASSWORDdans le.envlocal (secrets, non renseignés par un agent) et le câblage de l'étape de signature dansscripts/release.sh(T-080) - desktop :
SessionManager/ConfigStore/AuthStore/AppState(+ les fonctions libres qu'ils appellent avec unAppHandle:windows.rs,hotkey.rs,audio.rs::start,tray.rs::update_icon,api_client.rs::send/send_agent_stream/get_base_prompt) rendus génériques surR: tauri::Runtime(AppState<R = tauri::Wry>, défaut inchangé pour tout appelant existant) — permet des tests d'intégration de la machine à états viatauri::test::mock_builder()/MockRuntime; 3 tests ajoutés (session.rs::integration_tests, macOS uniquement) exerçantcancel()/toggle_mode()/stop_and_process()par la vraie API publique avec capture réelle des événementssession://state, sans toucher au micro/réseau réels. Aucun changement de comportement en production (résorption d'E-011) (T-093) - infra :
apps/serverdéployé surhttps://aria-api.temp.cloud-rennais.fr/(reverse proxy HTTPS, action humaine) ;/healthconfirmé joignable depuis l'extérieur du LAN (T-020) - process : critère M1 validé en conditions réelles —
curl(wav réel viasay, mono/16kHz/16-bit) + JWT Supabase réel +POST /v1/dictatecontre le serveur déployé →final_textréécrit selon un profil réel en base (custom_app_id=com.apple.TextEdit, Outlook non disponible sur la machine de test),degraded:false, timings renseignés ; appel OpenRouter réel STT+LLM autorisé explicitement (T-021) - server :
run_dictation(pipeline/dictate.py) — quandrewrite_enabled=False,final_textpasse désormais parlight_cleanup()(normalisation des espaces) au lieu de renvoyerraw_texttel quel (T-015) - workflow : orchestrateur point d'entrée unique, review + merge automatiques, vérifications manuelles tracées dans STATUS sans bloquer (T-090)
- ci : workflows sur runner forge-runner label docker (T-006)
- process : validation end-to-end de
scripts/pr.sh(création + merge programmé) via l'API Forgejo réelle,FORGEJO_TOKENdésormais disponible (T-007)
Fixed
- desktop : les tableaux markdown (syntaxe GFM
\| … \|) dans les réponses de la fenêtre agent s'affichaient en texte brut au lieu d'un vrai tableau —react-markdown(T-105) ne supporte que CommonMark par défaut, qui n'inclut pas les tableaux (extension GitHub-Flavored Markdown). Nouvelle dépendanceremark-gfm, composantstable/th/tddédiés (bordures, wrapperoverflow-x-auto) (T-108) - desktop/server : le flux
/v1/agent/streamétait coupé en plein milieu d'une réponse Hermes en cours (ex. pendant un outil/recherche web) — un timeout de durée totale de requête (reqwest::ClientBuilder::timeout, 45 s côté client ; timeouthttpxunique côté serveur, 30 s, appliqué aussi àread) ne tolérait pas un silence SSE prolongé pourtant normal. Corrigé : côté client, plus de timeout global sur ce flux, un timeout de lecture par chunk (200 s, réappliqué à chaque itération) à la place ; côté serveur,HermesAgentProviderutilise désormais unhttpx.Timeoutdédié (hermes_timeout_s, 180 s deread,connect/writecourts inchangés), distinct deprovider_timeout_s(STT/LLM). La fenêtre agent ne se réaffichait pas non plus après une fermeture via la croix native (bouton rouge macOS), contrairement àEsc— la croix déclencheWindowEvent::CloseRequested, non intercepté, donc la webview était détruite (comportement par défaut de Tauri) au lieu d'être cachée ;windows::install_agent_close_guardintercepte désormais cet événement pour la fenêtreagentet la cache (prevent_close+hide), même sémantique queEscquel que soit le déclencheur (T-107) - desktop : chaque token/outil du mode agent s'affichait en double —
useEffects'abonnant à un événement Tauri (Promise<UnlistenFn>) sans garde contre un démontage précoce (React 18StrictMode, laisse un abonnement orphelin en dev) ; nouvelle fonctionsubscribe()(lib/tauri.ts), appliquée àAgentWindow.tsxetOverlay.tsx(même motif latent, invisible côté overlay). La fenêtre agent ne se réaffichait plus après une fermeture (Esc) puis une nouvelle interaction — retrait deraise_window_level(NSPopUpMenuWindowLevel, pensé pour un cycle de vie court, hypothèse pas une fenêtre cachée/réaffichée en boucle) pour cette fenêtre,alwaysOnTopsuffit (T-106) - desktop/server : dans la fenêtre agent (T-104), les outils utilisés par Hermes s'affichaient toujours avant tout le texte, même utilisés au milieu de la réponse —
StreamSegment/push_stream_token/push_stream_tool/render_stream_segments(api_client.rs) préservent désormais l'ordre chronologique exact ; nouveau événement serveur{"type": "transcript", "text": ...}(pipeline/agent.py) affiche un vrai tour "utilisateur" ;Ctrl+Spacepour continuer la conversation (T-103) effaçait la réponse précédente — l'historique des tours est désormais construit côté front (AgentWindow.tsx) via un nouveau canalagent://event, jamais effacé saufEsc; rendu markdown des réponses (react-markdown) au lieu de texte brut (T-105) - server :
POST /v1/agent/streamn'affichait aucun contenu progressif derrière le reverse proxy déployé (buffering réseau par défaut) — en-têtesX-Accel-Buffering: no/Cache-Control: no-cacheajoutés à laStreamingResponse(T-104) - desktop : la réponse du mode agent était illisible dans la pilule de l'overlay (une ligne tronquée) — nouvelle fenêtre dédiée
agent(centrée, redimensionnable, scrollable), ouverte dès le début du traitement, fermée uniquement surEsc; l'overlay ne réaffiche plus le contenu de la réponse (session.rs::should_show_overlay) (T-104) - desktop : le mode agent n'affichait que
Agent : done(le statut brut) au lieu du message/des actions Hermes réels, et en rouge (étatErrorréutilisé) même en cas de succès —api_client.rsdésérialise désormaismessage/actions[], les assemble (format_agent_display, une ligne🔧 libellépar action puis le message, 5 tests) ; nouvel étatSessionState::AgentMessagedistinct d'Errordans l'overlay (violet, 6 s d'affichage au lieu de 2 s). Découvert et corrigé après le premier test réel bout-en-bout de T-073 (T-071) - desktop :
Tab(bascule mode agent) etEsc(annulation) pendant l'enregistrement ne fonctionnaient jamais sur macOS —tauri-plugin-global-shortcut/CarbonRegisterEventHotKeyne préempte pas fiablement un raccourci sans modificateur, l'app au premier plan consommait la touche (un vrai caractère de tabulation s'insérait dans le document actif).Tab/Escsurveillés désormais dans leCGEventTapdéjà actif pour la hotkey principale (hotkey.rs::macos_main, nouvel étatRecordingKeysState), avalés pendant Recording uniquement. Windows/Linux inchangés (T-070, E-021, ADR-011) - desktop :
api_client.rs::send_oncene journalisait que le code HTTP d'une réponse d'erreur (HTTP 502), jamais le corps (detail, ex.agent_failed/stt_failed/agent_timeout) — impossible de diagnostiquer depuis les logs client lequel de STT ou de l'agent (Hermes) avait échoué. Le corps de réponse est désormais loggué (log::warn!) en plus du code - desktop :
config.rs::ConfigPatch.microphone(Option<Option<String>>) collapsait un patch JSON{"microphone": null}(envoyé par la WebView pour remettre le micro au périphérique par défaut) enNone/« champ non fourni » au lieu deSome(None)/« explicitement remis à zéro » — remettre le micro au défaut depuis les settings était un no-op silencieux depuis T-038, jamais exercé par un test passant par le vrai JSON (les tests existants construisaientConfigPatchdirectement en Rust). Corrigé par undeserialize_withfait main (some_option), 4 tests ajoutés couvrant le JSON réel. Découvert en écrivant Settings › Général (T-063) - desktop : CI
desktop-rustrouge surmaindepuis T-040 (cargo clippy -- -D warnings,dead_codesurpermissions.rs::settings_pane_url, non détecté localement faute d'avoir utilisé la commande exacte de la CI) —settings_pane_url(et son test) passés sous#[cfg(target_os = "macos")], seule cible où la fonction a un appelant hors test ;scripts/check.sh --rust(commande CI) revérifié vert (E-019, résorbé) - desktop : icône tray toujours invisible à idle après T-099 (confirmé par l'humain, correctif insuffisant) —
tray.rs::setup()pose désormais l'icône initiale viaupdate_icon()(même chemin de code que toute mise à jour ultérieure) au lieu du builderTrayIconBuilder::icon()/.icon_as_template()à la construction, qui semble ne pas appliquer correctement le flag "template" macOS. Hypothèse infirmée (toujours invisible, confirmé par l'humain) — code conservé (amélioration légitime, un seul chemin de code désormais), cause réelle reportée à plus tard sur décision explicite de l'humain (T-100, E-018 ouvert — E-017/T-099 insuffisant) - desktop : overlay macOS relevé à
NSPopUpMenuWindowLevel(windows.rs::raise_overlay_level,objc2-app-kit) au lieu duNSFloatingWindowLevelpar défaut dealwaysOnTop— il apparaissait derrière un document ouvert au premier plan. Appel dispatché viaWebviewWindow::run_on_main_threadaprès un premier crash confirmé (setLevelappelé depuis le threadCGEventTapde la hotkey, AppKit exige le thread principal). Confirmé physiquement : visible au-dessus, plus de crash.position_overlayutilise désormaismonitor.work_area()(exclut Dock/barre de menu,NSScreen.visibleFrame()) au lieu des bornes brutes de l'écran — l'overlay chevauchait visuellement le Dock, retour utilisateur immédiat. Confirmé physiquement (T-098, E-016) - desktop :
hotkey.rs(macos_main) avale désormais la touche principale du combo (CGEventTapOptions::Default+CallbackResult::Drop) au lieu de seulement l'observer (ListenOnly) — l'auto-repeat clavier laissait fuir unKeyDownrépété vers l'app au premier plan, provoquant un bip macOS à chaque répétition quand elle n'a pas de raccourci pour cette combinaison. Seule la touche principale est avalée (jamais les modificateurs, jamais hors combo). Confirmé physiquement par l'humain : plus de bip, dictée toujours fonctionnelle (T-097, E-015) - desktop :
injector.rs::send_pastesur macOS n'utilise plusenigo::Key::Unicode('v')(résolution de keycode dépendante du layout clavier, appelait l'API TIS/TSM macOS et crashait depuis le thread tokio desession.rs::stop_and_process— même contrainte que E-012, cf. hotkey.rs) maisKey::Other(KeyCode::ANSI_V), un keycode brut qui contourne entièrement ce chemin de code. Autres OS inchangés. Découvert au premier cycle de dictée complet réussi (T-095 + T-096). Vérification physique humaine (collage réel sans crash) en attente (T-096, E-014) - server :
decode_supabase_jwt(aria_server/auth.py) route désormais sur l'algorithme réel du JWT (RS256/ES256→ toujours JWKS) au lieu de choisir HS256 dès qu'un secret est configuré — un vrai token de session Supabase self-hosted (ES256) était rejeté en 401 malgré un secret HS256 legacy aussi présent ; découvert au premier login réel de bout en bout, 5 tests ajoutés. Nécessite un redéploiement du serveur pour prendre effet (T-095, E-013) - desktop : logs de diagnostic supplémentaires (
auth::set,auth::access_token,api_clientsur 401) pour distinguer une session locale absente d'un rejet serveur — ont permis de diagnostiquer T-095 (T-095) - desktop :
windows.rs::position_overlaypositionne désormais l'overlay sur l'écran qui contient le curseur (active_monitor, viacursor_position()/monitor_from_point()) au lieu du dernier écran connu de la fenêtre elle-même (current_monitor()seul), avec repli sur le moniteur courant puis primaire ; résorption d'E-005. Vérification physique multi-écrans en attente (T-033, E-005) - desktop : hotkey principale (
Control+Space) sur macOS détectée via unCGEventTapmaison (cratecore-graphics,#[cfg(target_os = "macos")],hotkey.rs) au lieu detauri-plugin-global-shortcut(qui n'émettait jamaisReleased) ourdev(qui plantait) — ne lit que le keycode brut et les flags de modificateurs, jamais de conversion en chaîne via une API TIS/TSM. Autres OS inchangés (tauri-plugin-global-shortcutconservé). 14 tests sur la logique pure de détection de front/combo. Confirmé physiquement par l'humain : relâchement détecté à chaque essai, aucun crash (E-012 résorbé, ADR-009) (T-094) - desktop : revert du correctif
rdevpour la hotkey principale (T-094) — causait un crash systématique (EXC_BREAKPOINT,dispatch_assert_queue_faildans une API macOS TIS/TSM appelée hors thread principal parrdev), constaté en conditions réelles. Retour àtauri-plugin-global-shortcutseul :Pressedfonctionne,Releasedreste non fiable en attendant un correctifCGEventTapmaison (E-012, ADR-009) - desktop : ajout de logs manquants sur le chemin d'arrêt de la dictée (
audio::stop— nombre d'échantillons ;session::stop_and_process— enregistrement trop court ignoré, dictée injectée, statut agent) — jusqu'ici seulaudio startétait loggué, rendant le relâchement de la hotkey silencieux en l'absence d'overlay (T-033/034 pas encore posés), constaté lors d'un test manuel utilisateur (T-032) - server :
SupabaseDB.upsert_seen_app/insert_transcriptionattrapent et logguent (WARNING,aria.db) désormais toute erreur HTTP/réseau au lieu de la laisser remonter — ces écritures tournent enBackgroundTasksaprès l'envoi de la réponse/v1/dictate, une exception non gérée y serait invisible pour le client et seulement tracée en erreur non gérée par FastAPI (T-016) - server :
OpenRouterSTT.transcribeconvertit désormais un corps de réponse non-JSON ou JSON non-objet enSTTError(→502 stt_failed) au lieu de laisser fuiter une exception non catchée (→ 500) (T-013) - server :
OpenRouterLLM.rewriteconvertit désormais un corps de réponse non-JSON enLLMError(ValueError/json.JSONDecodeErrorajoutés auexcept) au lieu de laisser fuiter une exception non catchée ;LLMErrorreste absorbée par le pipeline endegraded=True, jamais mappée à un statut HTTP (T-014) - ci :
ci.yml—serverutiliseuv python install 3.12au lieu deactions/setup-python@v5,desktop-tsne fixe plus de version pnpm en double avecpackage.json,desktop-rustretiresudo(absent du conteneur) ; cacheuvactivé (T-009) - server : validation du bootstrap (
uv sync,uv run pytest— 9/9 verts,ruff format/ruff checkclean,/healthrépond) ; aucun correctif de code nécessaire, le scaffold M0 tournait déjà (T-002) - desktop : bootstrap macOS — génération des icônes app (
pnpm tauri icon) et tray (tray.png/tray@2x.pngdepuistray.svg, absents du scaffold) qui faisaient paniquergenerate_context!();cargo check/clippy -D warnings/test/build,tsc --noEmit,eslintverts sans correctif de code ;pnpm tauri devdémarre (T-003) - server :
MultiPartParser.spool_max_size(Starlette, 1 Mo par défaut) relevé au démarrage au-delà demax_audio_bytes— au-delà de ce seuil, Starlette écrivait silencieusement tout fichier uploadé sur un fichier temporaire disque (SpooledTemporaryFile), en contradiction avec la règle "l'audio n'est jamais écrit sur disque" dès qu'une dictée dépassait ~31 s ; vérifié qu'aucun fichier temporaire n'apparaît plus pour un audio de ~1,8 Mo (T-017) - server :
docs/API.md§/v1/agentcomplété avec les codes d'erreur (401/413/422/502/504, identiques à/v1/dictate) et la précisionllm_mstoujoursnull/pas de tâche de fond en v1 — absents du document avant cette PR, clarification desdetailuniquement, pas de changement de forme d'openapi.json(T-018) - server :
tests/conftest.py::clientfixait unSettingsde test sanshermes_url/hermes_api_keyexplicites —scripts/check.sh/scripts/_env.shexportant tout le.envracine dans l'environnement du processpytest, ces champs héritaient silencieusement des vraies valeurs Hermes du.envlocal dès qu'elles y sont présentes (même avec_env_file=None, qui ne désactive que la lecture d'un fichier dotenv, pasos.environ) ;test_agent_stubéchouait viascripts/check.sh(jamais viauv run pytestseul) tant que ce n'était pas corrigé. Corrigé en fixant explicitementhermes_url=""/hermes_api_key=""; piège documenté dansapps/server/CLAUDE.md. Découvert en implémentant T-073 (T-073)
Contract
- v1 initial :
docs/API.md,docs/DATA.md(T-001) packages/contract/src/openapi.types.tsgénéré pour de vrai pour la première fois depuispackages/contract/openapi.json(scripts/gen-contract.sh) — remplace le placeholderRecord<string, never>commité en T-001 et jamais régénéré depuis ; aucun changement de forme d'API (openapi.jsonetpackages/contract/src/supabase.types.tsidentiques bit à bit à la version précédente, aucune évolution de contrat depuis T-018) ;docs/API.mdvérifié déjà aligné, aucune modification nécessaire (T-019)AgentResult(/v1/agent) : ajout deactions: AgentAction[]({ type, label?, payload }) etneeds_confirmation: boolean, tous deux avec défaut vide/false— préparation du branchement Hermes (T-073).packages/contract/openapi.json/openapi.types.tsrégénérés. Mergée après approbation humaine explicite (CLAUDE.md règle 1/3b), statutdocs/STATUS.mdcorrigé en conséquence (T-072)- nouveau
POST /v1/agent/stream(SSE,text/event-stream) — endpoint additif,/v1/agentinchangé.packages/contract/openapi.json/openapi.types.tsrégénérés (supabase.types.tsnon touché, aucun changement de schéma). Approbation humaine requise avant merge (CLAUDE.md règle 1/3b) (T-102) - nouveau
GET /v1/prompts/base(BasePromptResult{base_style}) — endpoint additif, aucune route existante modifiée.packages/contract/openapi.json/openapi.types.tsrégénérés (supabase.types.tsnon touché, aucun changement de schéma Supabase). Approbation humaine requise avant merge (CLAUDE.md règle 1/3b) (T-101)
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-