merge(workstate): intègre les résumés de conversation (Lot C) dans develop

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 19:29:10 +02:00
18 changed files with 1295 additions and 77 deletions

View File

@ -1,13 +1,9 @@
---
upTo: b916af4a-6f94-4777-92b1-c5f89f898cde
upTo: 182c982c-8a54-42f8-a9ea-c430001fbfa8
objective: Tâche : ajouter UN test fonctionnel anti-régression de la communication inter-agent (round-trip question/réponse) au plus haut niveau de fidélité possible SANS lancer de vrai CLI IA (zéro token). Cont
---
**Objectif :** Tâche : ajouter UN test fonctionnel anti-régression de la communication inter-agent (round-trip question/réponse) au plus haut niveau de fidélité possible SANS lancer de vrai CLI IA (zéro token). Cont
- **Response:** ✅ LS5 — Tests parser regex niveau 2 (`RateLimitParser`) + module `timeparse` : TOUT VERT, 0 warning, zéro régression. Commande : `cargo test -p infrastructure` Résultat lib : **221 passed; 0 failed; 0 ignored** (195 antérieurs + **26 nouveaux LS5**). Tous les fichiers d'intégration + doctests verts. **Build de test : 0 warning** (vérifié `--no-run | grep -c warning` = 0). Confirmation explicite NON-RÉGRESSION : - **LS2** (`session::tests::parse_reset_ms*` / `parse_event*` / `send_emits_rate*`) : tous verts APRÈS le refactor qui a déplacé les helpers vers `timeparse` ✓ - **LS3** (`scheduler::tests::*`) : 7/7 verts ✓ (+ les 2 warnings `irrefutable if let` corrigés — voir plus bas) === `ratelimit/mod.rs` — 15 tests (in-file `#[cfg(test)] mod tests`) === new + detect : - `new_returns_none_on_invalid_regex` : regex invalide `"rate limit ("` ⇒ None (jamais de panique). - `detect_returns_none_when_pattern_does_not_match` : pas de match ⇒ None. - `detect_match_without_reset_capture_has_no_time` : match sans reset_capture ⇒ `SessionLimit{resets_at_ms:None, source:Pattern, detected_at_ms==now}`. - formats ABSOLUS : `detect_epoch_seconds_format` (×1000), `detect_epoch_millis_format` (tel quel), `detect_iso8601_format` (`2023-11-14T22:13:20Z`→1_700_000_000_000). - format RELATIF : `detect_relative_seconds_format_uses_now` (capture « 600 », now=T ⇒ T+600_000). - format MURAL (passage de minuit, math calculée à la main sur DAY_START=1_699_920_000_000 = 2023-11-14T00:00Z) : `detect_wall_clock_same_day_when_future` (now=10h, « 3pm » ⇒ 15h même jour) ; `detect_wall_clock_next_day_when_past` (now=16h ⇒ 15h DEMAIN, +24h). - capture inexploitable ⇒ détection sans heure : `detect_match_with_missing_capture_group_has_no_time`, `detect_match_with_unparsable_value_has_no_time` (⇒ `resets_at_ms:None`). - `detect_can_be_called_multiple_times` : regex compilé une seule fois, detect appelé 3× sans souci. - `source == RateLimitSource::Pattern` asserté dans tous les cas détectés. applies(profile) : - `applies_false_for_structured_profile_even_with_pattern` (structuré ⇒ false, niveau 1 prend la main) ; - `applies_true_for_pty_profile_with_pattern` (PTY + pattern ⇒ true) ; - `applies_false_for_pty_profile_without_pattern` (PTY sans pattern ⇒ false). === `timeparse.rs` — 11 tests === - `days_from_civil_epoch_is_zero` (1970-01-01→0) ; `days_from_civil_leap_day_2024` (2024-02-29→19782, + cohérence avec le 1ᵉʳ mars). - `parse_wall_clock_variants` (« 3pm »→(15,0,0), « 15:00:30 »→(15,0,30), « 3:30 pm »→(15,30,0), 12am→0h, 12pm→12h) ; `parse_wall_clock_rejects_invalid` (texte, 25:00, 10:75, trop de composantes ⇒ None). - `wall_clock_to_ms_*` : même jour / lendemain / cas-limite « cible == now » (⇒ lendemain, jamais le présent). - `parse_absolute_ms_*` : epoch s/ms, ISO8601, garbage→None (recoupe LS2). - `parse_rfc3339_offsets_convert_to_utc` : +01:00 et 01:00 ⇒ même instant UTC. === Nettoyage LS3 demandé === Les 2 warnings `irrefutable if let` (scheduler/mod.rs ~253/285, `ScheduledTask` mono-variante) sont corrigés : remplacés par une **déstructuration directe** `let ScheduledTask::ResumeAgent { conversation_id, .. } = task;` (code de TEST uniquement). Suite désormais 0 warning. Aucun bug détecté : le code DevBackend est conforme au cadrage §21 niveau 2 sur tous les axes (robustesse regex, stratégies de format absolu/relatif/mural, passage de minuit UTC, règle anti-double-détection `applies`, factorisation `timeparse` sans casser LS2). Je n'ai modifié AUCUN code de prod (uniquement ajouté des tests + corrigé mes propres tests LS3). Feature LS5 validée côté tests infra. Prêt pour la suite du cycle.
- **Prompt:** Tâche QA — tests unitaires de la slice LS7-front (UI limites de session des agents), branche `feature/agent-session-limits`. DevFrontend vient d'implémenter (typecheck `tsc --noEmit` propre, 39 tests agents existants toujours verts, non commité). Périmètre à couvrir (frontend/) : 1. `src/features/agents/useAgents.ts` — nouvel état `limitByAgent: Record<string, AgentLimitState>` (`{ limitedUntil?, resumeFireAt?, suspected? }`) peuplé depuis 5 DOMAIN_EVENT dans l'abonnement `onDomainEvent`. À tester (via le mock gateway qui émet des events) : - `agentRateLimited` {agentId, resetsAtMs} → entrée `{ limitedUntil: resetsAtMs, suspected: false }`. - `agentResumeScheduled` {agentId, fireAtMs} → arme `resumeFireAt`. - `agentResumeCancelled` {agentId} → retire `resumeFireAt`, reste limité. - `agentResumed` {agentId} → supprime l'entrée (état effacé). - `agentRateLimitSuspected` {agentId, resetsAtMs?} → `{ ..., suspected: true }`, y compris le cas SANS `resetsAtMs` (heure inconnue). - séquence réaliste : rateLimited → resumeScheduled → cancelResume (action) → vérifier retrait optimiste + appel `input.cancelResume` (mock `cancelledResumes` / `cancelResumeResult`). - Action `cancelResume(agentId)` exposée par le hook : retrait optimiste + verdict backend renvoyé (teste les deux verdicts via `cancelResumeResult`). 2. `src/features/agents/AgentLimitBadge.tsx` — helpers purs exportés `formatResetTime(epochMs)` (→ HH:MM) et `formatCountdown(remainingMs)` (→ « Xm Ys »/« Ys », clampé à 0 pour négatif). Teste ces helpers purs directement (cas limites : 0, négatif, < 60 s, > 60 s, minuit). Teste le rendu du badge (limité jusqu'à HH:MM vs « limité » sans heure vs « heure inconnue » pour suspected sans resetsAtMs) et que le bouton « Annuler la reprise » appelle l'action quand `resumeFireAt` est armé. 3. Adapter mock `src/adapters/mock/index.ts``MockInputGateway.cancelResume` (`cancelledResumes`, `cancelResumeResult`) : couvre via les tests du hook. Suis le style des tests existants (cf. `singletonAgent.test.tsx`, `mock.test.ts`, les tests agents). Exécute la suite (`npm test` ou l'outil du repo) et RENDS-MOI via idea_reply : la commande exacte lancée, le résultat réel (nb tests passés/échoués), et le rapport d'erreurs complet si rouge. Si un test révèle un bug d'implémentation, décris-le précisément (fichier:ligne, attendu vs obtenu) — je relaierai à DevFrontend pour correction. Ne commit rien.
- **Response:** QA LS7-front — VERT. Aucun bug d'implémentation trouvé. Rien commité. ## Fichiers de test ajoutés (2) - `frontend/src/features/agents/useAgentsLimits.test.tsx` (13 tests) — état `limitByAgent` du hook via le `MockSystemGateway` qui émet les events + `MockInputGateway`. - `frontend/src/features/agents/AgentLimitBadge.test.tsx` (11 tests) — helpers purs + rendu du badge. ## Commandes exactes lancées (cwd = frontend/) 1. `npx vitest run src/features/agents/useAgentsLimits.test.tsx src/features/agents/AgentLimitBadge.test.tsx`**Test Files 2 passed (2) · Tests 24 passed (24)** 2. `npx vitest run src/features/agents/` (suite agents complète, régression) → **Test Files 4 passed (4) · Tests 63 passed (63)** (39 existants + 24 nouveaux, 0 régression) ## Couverture livrée useAgents (§21), tous via events réels : - `agentRateLimited{resetsAtMs}``{limitedUntil, resumeFireAt:undefined, suspected:false}` ✓ - `agentResumeScheduled{fireAtMs}` → arme `resumeFireAt` par-dessus l'état limité ✓ - `agentResumeCancelled` → retire `resumeFireAt`, reste limité ✓ ; + no-op sur agent inconnu (aucune entrée créée) ✓ - `agentResumed` → entrée supprimée (`agentId in map === false`) ✓ - `agentRateLimitSuspected` AVEC et SANS `resetsAtMs``suspected:true`, `limitedUntil` undefined dans le cas sans heure ✓ - séquence réaliste rateLimited→scheduled→`cancelResume(action)` : retrait optimiste du countdown + agent toujours limité + `input.cancelledResumes === [agentId]` + verdict ✓ - action `cancelResume` : verdict `true` ET `false` (via `cancelResumeResult`), + appel du port même sans reprise armée ✓ - deux agents foldés indépendamment ✓ AgentLimitBadge : - `formatCountdown` : 0→"0s", négatif→"0s" (clamp), <60s→"Xs", ceil partiel (4.2s→"5s"), 60s→"Xm Ys" (60000→"1m 0s", 125000→"2m 5s") - `formatResetTime` : HH:MM sans secondes (même minute +30s = même libellé), minuit stable - rendu : "limité jusqu'à HH:MM" (heure connue) / "limité" (sans heure) / note "heure inconnue reprise à préciser" (suspected sans heure) + absence de la note quand suspected A une heure - bouton "Annuler la reprise" : appelle `onCancelResume` au clic quand `resumeFireAt` armé, countdown rendu, bouton désactivé si `busy` MockInputGateway.cancelResume (`cancelledResumes`/`cancelResumeResult`) : couvert via les tests du hook. ## Note hors-périmètre (PRÉEXISTANTE, pas causée par LS7-front) `src/adapters/mock/mock.test.ts:16` échoue (1 test) : il assert « thirteen gateways » mais le `permission` gateway porte le compte à 14. Vérifié : la gateway `permission` existe déjà dans HEAD committé (eca2ba9) l'assertion de comptage est simplement périmée, sans rapport avec les limites de session. Mes deux fichiers ne touchent pas ce test. Aucune action de ma part (hors scope) ; à relayer pour une mise à jour du compteur si tu veux la suite `adapters/mock` 100% verte.
- **Prompt:** Tâche QA tests unitaires LS8-backend (filet humain niveau 3), branche `feature/agent-session-limits`. DevBackend a implémenté (build 0 erreur/0 warning, suites domain/application/app-tauri déjà vertes, non commité). À COUVRIR nouvelle méthode `SessionLimitService::confirm_human_resume(agent_id, node_id, conversation_id, resets_at_ms: i64)` dans `crates/application/src/agent/session_limit.rs`. Teste via les fakes existants (Clock/Scheduler/EventBus) du fichier `crates/application/tests/session_limit_service.rs` (suis ce style). Cas (cadrage Architect) : (a) heure FUTURE publie `AgentRateLimited{Some(resets_at_ms)}` puis `AgentResumeScheduled{fire_at_ms == resets_at_ms}` + un ScheduleId armé sur le scheduler. (b) heure PASSÉE (resets_at_ms < now) clamp : `fire_at_ms == now` (reprise quasi-immédiate). Vérifie l'événement `AgentResumeScheduled` avec fire_at_ms == now. (c) DÉDOUBLONNAGE croisé : `confirm_human_resume` APRÈS un `on_rate_limited` déjà armé pour le même agent le précédent réveil est désarmé, une seule entrée armée (et l'inverse : `on_rate_limited` après `confirm_human_resume`). Vérifie qu'il n'y a qu'un seul armement actif. (d) ANNULABILITÉ : `cancel_resume(agent_id)` après `confirm_human_resume` publie `AgentResumeCancelled` et renvoie true (l'armement humain s'annule par la même voie). (e) PARITÉ auto/humain : même séquence d'événements qu'`on_rate_limited` cas Scheduled (à reset identique). À COUVRIR commande `set_resume_at` (app-tauri) si testable au niveau intégration comme `session_limit_wiring.rs` : au minimum le cas `node_for_agent == None` erreur NOT_FOUND (pas d'armement orphelin). Si le câblage rend ce test trop lourd (besoin d'une cellule vivante), note-le et concentre-toi sur la couverture application (ae) qui est l'essentiel. Exécute (`cargo test -p application` + `-p app-tauri` si pertinent). RENDS-MOI via idea_reply : commande exacte, résultat réel (passés/échoués), rapport d'erreurs complet si rouge. Si un test révèle un bug (notamment sur le dédoublonnage croisé ou le clamp passé), décris-le précisément (attendu vs obtenu) je relaierai à DevBackend. Ne commit rien.
- **Response:** QA LS8-backend (filet humain niveau 3) — VERT. Aucun bug d'implémentation trouvé (ni sur le dédoublonnage croisé, ni sur le clamp passé). Rien commité. ## Fichiers de test modifiés (2 — ajouts only, fakes existants réutilisés) - `crates/application/tests/session_limit_service.rs` : +6 tests `confirm_human_resume` (style des fakes Clock/Scheduler/EventBus existants). - `crates/app-tauri/tests/session_limit_wiring.rs` : +2 tests (précondition NOT_FOUND + parité runtime humaine sur le vrai bus). ## Commandes exactes lancées + résultats réels 1. `cargo test -p application --test session_limit_service`**15 passed; 0 failed** (10 existants + 5 nouveaux). 2. `cargo test -p app-tauri --test session_limit_wiring`**4 passed; 0 failed** (2 existants + 2 nouveaux). 3. `cargo test -p application` (régression complète) → **toutes suites vertes, 0 failed**. 4. `cargo test -p app-tauri` (régression complète) → **toutes suites vertes, 0 failed**. ## Couverture application (ae) — l'essentiel - **(a) heure FUTURE** `confirm_human_resume_future_arms_and_emits_in_order` : exactement 1 `arm(fire_at_ms==resets_at_ms, ResumeAgent{agent,node,conv})`, 1 ScheduleId émis, events `AgentRateLimited{Some(reset)}` PUIS `AgentResumeScheduled{fire_at_ms==reset}` dans cet ordre. ✓ - **(b) heure PASSÉE** `confirm_human_resume_past_reset_clamps_fire_at_to_now` : `fire_at_ms == now` (clamp anti-passé) ; `AgentRateLimited` garde l'heure brute passée, `AgentResumeScheduled` porte le `now` clampé. ✓ - **(c) dédoublonnage CROISÉ** (les deux sens) : - `confirm_human_resume_after_auto_dedups_single_active_arm` : confirm APRÈS on_rate_limited → l'ancien ScheduleId auto est cancel-é (`cancels == [issued[0]]`), pas d'`AgentResumeCancelled` (silencieux). Unicité prouvée : 1er `cancel_resume`→true, 2e→false. ✓ - `auto_after_confirm_human_resume_dedups_single_active_arm` : on_rate_limited APRÈS confirm → symétrique, même invariant d'unicité. ✓ - **(d) ANNULABILITÉ** `cancel_resume_after_confirm_human_resume_returns_true_and_emits_cancelled` : renvoie `true` + publie `AgentResumeCancelled` (même voie que l'auto). ✓ - **(e) PARITÉ auto/humain** `confirm_human_resume_is_event_for_event_identical_to_auto_scheduled` : à reset futur identique, `bus.events()` ET `scheduler.armed()` strictement égaux entre `confirm_human_resume` et `on_rate_limited`. La source Human vs Structured n'a aucun effet observable. ✓ ## Couverture app-tauri `set_resume_at` - **NOT_FOUND** `set_resume_at_resolves_no_cell_for_an_agent_without_a_live_session` : ✓ couvert au niveau précondition. NOTE : la commande `#[tauri::command] set_resume_at` exige `State<AppState>`, non constructible hors runtime Tauri — le harnais wiring existant n'invoque jamais les commandes, seulement le service. J'ai donc testé la **précondition exacte** qui produit le NOT_FOUND : sur un `AppState::build` neuf, `structured_sessions.node_for_agent(unknown)` ET `terminal_sessions.node_for_agent(unknown)` renvoient `None` → la branche `ok_or_else(NotFound)` (commands.rs:1420-1428) est prise → aucun armement orphelin. C'est la couverture maximale réaliste sans faire tourner Tauri. - **Parité runtime** `confirm_human_resume_arms_a_cancellable_resume_over_the_real_bus` : sur le vrai `TokioBroadcastEventBus`, `confirm_human_resume` publie `AgentRateLimited` puis `AgentResumeScheduled` et l'armement est annulable (`cancel_resume`→true), exactement comme la branche auto déjà testée. ✓ ## Observation (non bloquante, pas un bug) `confirm_human_resume` est total et défensif : le cas `ResumePlan::HumanFallback` y est inatteignable (`resets_at_ms` toujours `Some`) → traité en no-op via `if let Scheduled`. Conforme au cadrage Architect ; pas de test dédié (branche morte par construction). Build app-tauri OK (0 erreur). Rien n'a été commité.
- **Prompt:** Tâche QA — validation LS8-front (formulaire de saisie d'heure, filet humain niveau 3) sur `feature/agent-session-limits`. DevFrontend a implémenté ET ajusté/ajouté des tests (typecheck propre, `npx vitest run src/features/agents/ src/adapters/mock/` = 109 tests verts). Ton rôle : valider la couverture, exécuter, et signaler tout trou ou bug. Points à vérifier/exécuter : 1. Helper pur `timeInputToEpochMs(value, now)` (AgentLimitBadge.tsx) : cas couverts ? « HH:MM » valide → aujourd'hui à cette heure ; instant passé dans la journée → epoch < now (le backend clampe, pas le front) ; malformé/vide/`25:00`/`12:60` `null`. Ajoute les cas manquants si besoin. 2. Soumission du formulaire (état suspected SANS heure) `onSetResumeAt` / action hook `setResumeAt` appelée avec l'epoch attendu ; bouton désactivé si saisie vide ou `busy` ; saisie malformée (`null`) ne déclenche PAS d'appel. 3. Action hook `useAgents.setResumeAt(agentId, resetsAtMs)` : délègue bien au port `input.setResumeAt`, aucune mutation optimiste (vérifie que l'état ne bascule QUE sur réception de `agentResumeScheduled`, pas avant). Mock instrumenté `resumeArmings`. 4. Bascule complète : agent suspected sans heure submit form émettre `agentResumeScheduled` (via mock) le badge passe à l'état nominal (countdown + bouton Annuler), le formulaire disparaît. 5. Non-régression : tout `src/features/agents/` + `src/adapters/mock/` vert. Confirme le compteur gateways corrigé (14) dans mock.test.ts. Exécute `npx vitest run src/features/agents/ src/adapters/mock/` (+ `tsc --noEmit` si tu veux). RENDS-MOI via idea_reply : commande exacte, résultat réel (passés/échoués), trous comblés (fichiers/tests ajoutés), et tout bug trouvé (attendu vs obtenu, fichier:ligne) je relaierai à DevFrontend. Ne commit rien.
- **Prompt:** QA rapide correctif IdeA Codex structured runtime: j'ai ajouté `PreparedContext.project_root`, relayé ce champ depuis `LaunchAgent`, puis `StructuredSessionFactory` passe `vec![ctx.project_root]` à `CodexExecSession`. `CodexExecSession` ajoute maintenant `--add-dir <project-root>` après `--sandbox workspace-write` et avant le prompt, sans `--ask-for-approval`. Tests ciblés verts: `cargo test -p infrastructure codex_new_conversation_command_carries_exact_args`, `cargo test -p infrastructure codex_resume_command_carries_exact_args`; `cargo check --workspace` vert avec warning préexistant `may_write_directly`. Peux-tu relire le risque principal et indiquer s'il manque un test critique avant rebuild AppImage ?
@ -24,3 +20,7 @@ objective: Tâche : ajouter UN test fonctionnel anti-régression de la communica
- **Response:** Verdict QA globale Lot A `feature/conversation-ux-live-state` : vert, avec réserve environnementale non bloquante sur les tests socket Unix réels. Périmètre relu : - Backend application : `GetProjectWorkState` agrège en read-only depuis `AgentContextStore::load_manifest`, `LiveSessions::live_agent_snapshots()` et `InputMediator::busy_state`. - Pas de nouvelle persistance observée dans le read-model ; pas de lecture log/handoff ; pas de modélisation FIFO complète. - Ordre manifeste conservé par itération sur `manifest.entries`. - Live agents hors manifeste ignorés par construction, car seuls les agents du manifeste sont émis. - Snapshot live typé ajouté sans changer `live_agents()` existant. - Tauri : commande `get_project_work_state(projectId)` câblée dans `commands.rs`, `state.rs`, `lib.rs`, DTO camelCase avec `live.kind` et `busy.state/sinceMs`. - Frontend : onglet `Work`, port/gateway/adapters/mock/hook/panel ajoutés ; panneau read-only, appelle uniquement `getProjectWorkState`, affiche live/offline + busy/idle, refresh manuel et refresh sur événements existants. Commandes exécutées et résultats : ```text cargo fmt -p application -p app-tauri -- --check ``` Résultat : OK, aucune sortie. ```text cargo test -p application workstate ``` Résultat : ```text running 5 tests test workstate_attaches_live_pty_session_to_manifest_agent ... ok test workstate_includes_busy_state_from_input_mediator ... ok test workstate_attaches_live_structured_session_to_manifest_agent ... ok test workstate_ignores_live_agents_absent_from_manifest ... ok test workstate_lists_manifest_agents_idle_without_live_sessions ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` Warning existant observé : ```text warning: method `writes` is never used --> crates/application/tests/orchestrator_service.rs:1642:8 ``` ```text cargo test -p application ``` Résultat : ```text running 43 tests ... test result: ok. 43 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ... Running tests/workstate.rs running 5 tests ... test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests application running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` Même warning existant `CapturingFs::writes` unused. ```text cargo check -p app-tauri ``` Résultat : ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.70s ``` ```text cargo test -p app-tauri --test dto_agents ``` Résultat : ```text running 19 tests ... test project_work_state_dto_serialises_live_and_busy_camelcase ... ok ... test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ```text cargo test -p app-tauri --test list_live_agents_r0b ``` Résultat : ```text running 5 tests test no_sessions_yields_empty_list ... ok test both_kinds_live_listed_without_duplicates ... ok test pty_live_agent_is_listed ... ok test same_agent_in_both_registries_is_deduplicated ... ok test structured_live_agent_is_listed ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` ```text cargo test -p app-tauri --lib -- --skip state::mcp_e2e_loopback_tests --skip state::bind_endpoint_d1_tests --skip mcp_bridge::tests::end_to_end_over_real_loopback ``` Résultat : ```text running 39 tests ... test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.01s ``` Les 8 tests filtrés restent les tests nécessitant un vrai socket Unix loopback ; le sandbox courant refuse `UnixListener::bind` avec `PermissionDenied`, diagnostic déjà établi. À rejouer hors sandbox permissif socket Unix, non bloquant pour ce Lot A. ```text cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx ``` Résultat : ```text Test Files 2 passed (2) Tests 13 passed (13) ``` Warnings Vite existants : options `esbuild` dépréciées / `oxc` prioritaire. ```text cd frontend && npx tsc --noEmit ``` Résultat : succès, aucune sortie. Risques restants : - Couverture volontairement limitée au read-model minimal live/busy ; pas de FIFO complète, pas dhistorique conversationnel, pas de lecture log/handoff. - Tests socket Unix réels filtrés pour contrainte sandbox connue ; ils restent à exécuter dans un environnement qui autorise le bind de sockets filesystem. - Warning existant `CapturingFs::writes` unused dans `orchestrator_service.rs`, sans lien avec ce lot. Aucune correction requise. Je nai modifié aucun fichier.
- **Prompt:** Valide le Lot B complet `workstate delegation/queue snapshot` sur la branche active `feature/workstate-delegation-queue`. Périmètre à vérifier: - Backend/Tauri: port read-only `AgentQueueSnapshot`, snapshots FIFO d'`InMemoryMailbox`, `GetProjectWorkState.agents[].tickets`, DTO camelCase, wiring `AppState`. - Frontend: types `tickets`, mock normalisé, panneau Work affiche tickets FIFO inProgress/queued, refresh sur `delegationReady`. - Les tickets human et agent sont inclus; l'UI ne doit pas les présenter tous comme délégations agent. Commandes minimales demandées (adapte si le repo impose un cwd): - `cargo fmt --all -- --check` - `cargo test -p infrastructure mailbox --lib` - `cargo test -p application --test workstate` - `cargo test -p app-tauri --test dto_agents` - `cargo check -p app-tauri` - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - `cd frontend && npx tsc --noEmit` Tu peux ajouter des commandes pertinentes, mais ne committe pas et ne modifie pas les fichiers applicatifs sauf tests QA indispensables. Ignore le dirty runtime `.ideai/*` s'il est seulement live-session. Retour attendu: verdict clair, commandes exécutées avec résultats réels, anomalies éventuelles avec fichiers/lignes, et si rouge, donne la sortie d'erreur exploitable.
- **Response:** Verdict: VERT pour le Lot B `workstate delegation/queue snapshot` sur `feature/workstate-delegation-queue`. Je nai modifié aucun fichier. Dirty ignoré côté runtime `.ideai/*`; les fichiers applicatifs/test du lot étaient déjà modifiés dans le worktree. Commandes exécutées et résultats réels: 1. `cargo fmt --all -- --check` - Exit 0, aucune sortie. 2. `cargo test -p infrastructure mailbox --lib` - Exit 0. - `running 13 tests` - `test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 215 filtered out; finished in 0.00s` - Couvre notamment `snapshot_preserves_fifo_order_and_positions`, `snapshot_carries_ticket_metadata`, `snapshot_is_read_only`, updates après cancel/resolve. 3. `cargo test -p application --test workstate` - Exit 0. - `running 12 tests` - `test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` - Couvre `workstate_lists_two_tickets_in_fifo_order`, `workstate_marks_busy_head_in_progress_and_rest_queued`, `workstate_marks_all_queued_when_agent_idle`, `workstate_maps_human_and_agent_ticket_sources`, queue absente/agent hors manifest. 4. `cargo test -p app-tauri --test dto_agents` - Exit 0. - `running 20 tests` - `test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` - Couvre `project_work_state_dto_serialises_tickets_camelcase`: `tickets`, `inProgress`, source `agent.agentId`, source `human`, absence de snake_case. 5. `cargo check -p app-tauri` - Exit 0. - `Finished dev profile [unoptimized + debuginfo] target(s) in 0.59s` 6. `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - Exit 0. - Warnings Vite non bloquants: options `esbuild` dépréciées/ignorées au profit de `oxc`. - `Test Files 2 passed (2)` - `Tests 17 passed (17)` - `Duration 2.02s` 7. `cd frontend && npx tsc --noEmit` - Exit 0, aucune sortie. 8. Commande QA ajoutée, car `crates/domain/src/mailbox.rs` est touché par le lot: `cargo test -p domain mailbox --lib` - Exit 0. - `running 6 tests` - `test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 170 filtered out; finished in 0.00s` Vérifications de périmètre: - Backend/Tauri: - Port read-only présent: `domain::mailbox::AgentQueueSnapshot` dans `crates/domain/src/mailbox.rs:69`. - `InMemoryMailbox` implémente la snapshot read-only dans `crates/infrastructure/src/mailbox/mod.rs:166`; tests FIFO/métadonnées/read-only verts. - `GetProjectWorkState` consomme `Arc<dyn AgentQueueSnapshot>` dans `crates/application/src/workstate/mod.rs:130` et mappe `agents[].tickets`; tests application verts. - DTO `tickets` camelCase dans `crates/app-tauri/src/dto.rs:1602` et test camelCase dédié vert. - Wiring `AppState`: même `InMemoryMailbox` exposé comme `AgentMailbox` et `AgentQueueSnapshot` dans `crates/app-tauri/src/state.rs:994-999`, puis injecté dans `GetProjectWorkState::new` vers `state.rs:1034-1038`. - Frontend: - Types `tickets` présents dans `frontend/src/domain/index.ts:153-179`. - Mock normalisé avec fallback `tickets: []` dans `frontend/src/adapters/mock/index.ts:1670-1684`. - Panneau Work trie les tickets par `position` avant affichage dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx:65`. - UI distingue source humaine et agent: `Human` pour `source.kind === "human"`, sinon `label (agent-id-prefix)` dans `ProjectWorkStatePanel.tsx:21-27`; test `renders an in-progress human ticket` et test FIFO agent verts. - Refresh sur `delegationReady` couvert par `frontend/src/features/workstate/workstate.test.tsx:223` et vert. Anomalies: aucune anomalie bloquante détectée. Les seuls messages observés sont les warnings Vite sur la config `esbuild` dépréciée, sans échec de test ni impact Lot B constaté.
- **Prompt:** Valide le Lot C complet `workstate conversation summaries` sur la branche active `feature/workstate-conversation-summaries`. Périmètre: - Backend/Tauri: `ProjectWorkState.conversations` top-level, previews best-effort depuis `HandoffStore` puis `ConversationLog::last(3)`, dédup des conversation ids issus des tickets, DTO camelCase. - Frontend: types TS, mock normalise `conversations: []`, `ProjectWorkStatePanel` joint `tickets[].conversationId` vers `conversations[]` et affiche badge + preview compacte. - Aucune nouvelle persistance, aucun log brut exposé, aucune action UX. Commandes minimales demandées: - `cargo fmt --all -- --check` - `cargo test -p application --test workstate` - `cargo test -p app-tauri --test dto_agents` - `cargo check -p app-tauri` - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - `cd frontend && npx tsc --noEmit` Ajoute une commande infra/domain seulement si tu juges nécessaire après inspection. Ne committe pas. Ignore dirty runtime `.ideai/*` s'il est seulement live-session. Retour attendu: verdict clair, commandes exécutées avec résultats réels, anomalies éventuelles avec fichiers/lignes, et si rouge, sortie d'erreur exploitable.
- **Response:** Verdict: VERT pour le Lot C `workstate conversation summaries` sur `feature/workstate-conversation-summaries`. Je nai modifié aucun fichier et je nai pas committé. Dirty runtime `.ideai/*` ignoré; le dirty restant correspond aux fichiers du lot déjà présents dans le worktree. Commandes exécutées et résultats réels: 1. `cargo fmt --all -- --check` - Exit 0, aucune sortie. 2. `cargo test -p application --test workstate` - Exit 0. - Warning non bloquant: - `warning: variant Absent is never constructed` - `crates/application/tests/workstate.rs:173:5` - `application` test `workstate` generated 1 warning. - Résultat: - `running 21 tests` - `test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` - Tests pertinents verts: `workstate_conversation_ready_from_handoff`, `workstate_conversation_missing_with_bounded_recent_turns`, `workstate_conversation_partial_when_handoff_errors_but_log_ok`, `workstate_conversation_unavailable_when_handoff_and_log_fail`, `workstate_dedups_conversation_ids_from_tickets`, `workstate_preview_failure_preserves_agents_live_busy_tickets`, `workstate_conversation_previews_truncated_and_normalised`. 3. `cargo test -p app-tauri --test dto_agents` - Exit 0. - `running 21 tests` - `test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` - Test pertinent vert: `project_work_state_dto_serialises_conversations_camelcase`. 4. `cargo check -p app-tauri` - Exit 0. - `Finished dev profile [unoptimized + debuginfo] target(s) in 0.58s` 5. `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - Exit 0. - Warnings Vite non bloquants: - `esbuild option was specified by vite:react-babel plugin. This option is deprecated, please use oxc instead.` - `optimizeDeps.esbuildOptions option was specified by vite:react-babel plugin. This option is deprecated, please use optimizeDeps.rolldownOptions instead.` - `Both esbuild and oxc options were set. oxc options will be used and esbuild options will be ignored.` - Résultat: - `Test Files 2 passed (2)` - `Tests 19 passed (19)` - `Duration 2.10s` 6. `cd frontend && npx tsc --noEmit` - Exit 0, aucune sortie. Je nai pas ajouté de commande infra/domain: après inspection, le lot touche le read-model application, le wiring Tauri, les DTO et le frontend; les tests ciblés couvrent directement ces surfaces. Vérifications de périmètre: - Backend/Tauri: - `ProjectWorkState.conversations` top-level présent dans `crates/application/src/workstate/mod.rs:50-57`. - `ConversationWorkSummary` best-effort, read-only, sans projection durable, défini dans `crates/application/src/workstate/mod.rs:60-81`. - Provider `ConversationLogProvider` per-project-root dans `crates/application/src/workstate/mod.rs:112-121`. - Wiring optionnel `with_conversation_sources` dans `crates/application/src/workstate/mod.rs:242-253`. - Dédup des conversation ids issus des tickets en ordre first-seen/FIFO dans `crates/application/src/workstate/mod.rs:345-357`. - Algorithme handoff primaire puis fallback `ConversationLog::last(_, 3)` dans `crates/application/src/workstate/mod.rs:360-454`; `RECENT_TURNS_MAX = 3` à `workstate/mod.rs:35-36`. - Câblage Tauri des providers dans `crates/app-tauri/src/state.rs:121-128` et injection via `.with_conversation_sources(...)` à `state.rs:1063-1066`. - DTO camelCase `ConversationWorkSummaryDto` / `ProjectWorkStateDto.conversations` dans `crates/app-tauri/src/dto.rs:1610-1738`, validé par le test DTO. - Frontend: - Types TS `ConversationPreviewStatus`, `ConversationTurnWorkPreview`, `ConversationWorkSummary`, et `ProjectWorkState.conversations` présents dans `frontend/src/domain/index.ts:173-212`. - Mock normalise `conversations: []` dans `frontend/src/adapters/mock/index.ts:1666-1686`. - `ProjectWorkStatePanel` construit une map `conversations` par `conversationId` dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx:196-204`. - Les tickets sont joints via `conversations.get(ticket.conversationId)` dans `ProjectWorkStatePanel.tsx:183-188`. - Affichage compact badge + preview dans `ProjectWorkStatePanel.tsx:35-75`; pas de nouvelle action UX constatée. - Tests frontend verts pour objectif/résumé joint par conversation id, partial/unavailable compact, tickets conservés. Anomalies: - Non bloquant: warning Rust dans le test `crates/application/tests/workstate.rs:173:5`, variante `HandoffOutcome::Absent` jamais construite. Aucun échec associé. - Non bloquant: warnings Vite sur options `esbuild` dépréciées/ignorées. Aucun impact Lot C constaté. Aucune anomalie bloquante détectée.
- **Prompt:** Revalide rapidement le Lot C après nettoyage du warning `HandoffOutcome::Absent` dans `crates/application/tests/workstate.rs`. Merci de relancer au minimum: - `cargo fmt --all -- --check` - `cargo test -p application --test workstate` - `cargo test -p app-tauri --test dto_agents` - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - `cd frontend && npx tsc --noEmit` Vérifie surtout qu'il n'y a plus le warning Rust signalé. Ne committe pas. Retourne verdict et résultats réels.
- **Response:** Verdict: VERT après revalidation rapide du Lot C sur `feature/workstate-conversation-summaries`. Aucun fichier modifié, aucun commit. Le warning Rust précédemment signalé sur `HandoffOutcome::Absent` a bien disparu: `cargo test -p application --test workstate` ne produit plus de warning. Commandes exécutées et résultats réels: 1. `cargo fmt --all -- --check` - Exit 0, aucune sortie. 2. `cargo test -p application --test workstate` - Exit 0. - Aucune sortie `warning:`. - `running 21 tests` - `test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` 3. `cargo test -p app-tauri --test dto_agents` - Exit 0. - `running 21 tests` - `test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s` 4. `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - Exit 0. - Warnings Vite non bloquants toujours présents sur les options `esbuild` dépréciées/ignorées au profit de `oxc`. - `Test Files 2 passed (2)` - `Tests 19 passed (19)` - `Duration 2.08s` 5. `cd frontend && npx tsc --noEmit` - Exit 0, aucune sortie. Anomalies: aucune anomalie bloquante. Le warning Rust demandé est résolu.

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
---
upTo: 7d70c87a-e7b5-468a-90c7-41decf1185bc
upTo: 11b7df5e-f50e-4f4e-b680-37d5fb10af72
objective: [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) "DevFrontend OK — pont inter-agent fonctionnel", (2) le résultat de 144 ÷ 12, e
---
**Objectif :** [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu reçois ce message, réponds via idea_reply avec : (1) "DevFrontend OK — pont inter-agent fonctionnel", (2) le résultat de 144 ÷ 12, e
@ -20,3 +20,5 @@ objective: [Ping inter-agent depuis Main] Test du pont MCP inter-agents. Si tu r
- **Response:** Lot A frontend implémenté, sans commit. Fichiers modifiés/ajoutés frontend: - `frontend/src/domain/index.ts`: types `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`. - `frontend/src/ports/index.ts`: port `WorkStateGateway` + ajout dans `Gateways`. - `frontend/src/adapters/workState.ts`: adapter Tauri `get_project_work_state`. - `frontend/src/adapters/index.ts`: câblage `TauriWorkStateGateway`. - `frontend/src/adapters/mock/index.ts`: `MockWorkStateGateway` + helper `_setProjectWorkState` + câblage mock. - `frontend/src/features/workstate/useProjectWorkState.ts`: hook avec refresh initial et refresh sur `agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed`. - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`: UI read-only, empty/error/refresh, live/offline, idle/busy, ticket court. - `frontend/src/features/workstate/index.ts`: export feature. - `frontend/src/features/workstate/workstate.test.tsx`: tests panel empty, idle/offline, live+busy ticket court, refresh event. - `frontend/src/features/projects/ProjectsView.tsx`: onglet sidebar `Work`. - `frontend/src/features/projects/projects.test.tsx`: injection mock workState + test onglet Work rendu. Commandes exécutées: - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - Résultat: OK, 2 fichiers passés, 13 tests passés. - Vite affiche seulement des warnings existants sur options `esbuild`/`oxc`. - `cd frontend && npx tsc --noEmit` - Résultat: OK, aucune erreur. Risques / limites: - Le refresh event est volontairement large car les events cités ne portent pas tous `projectId`; le hook re-fetch le projet actif sur event pertinent. - UI strictement read-only comme demandé: pas dattach/stop/voir conversation. - Je nai pas lancé tout le `vitest` global, seulement le ciblé demandé. - Le worktree contenait déjà des changements backend et `.ideai/*`; je ne les ai pas touchés.
- **Prompt:** Implémente le Lot B frontend `workstate delegation/queue snapshot` sur la branche active `feature/workstate-delegation-queue`, en t'appuyant sur le backend/Tauri déjà modifié par DevBackend. Contexte backend disponible: - `ProjectWorkState.agents[].tickets` est maintenant exposé côté DTO camelCase. - Ticket fields TS attendus: `ticketId`, `conversationId`, `position`, `status` (`"inProgress" | "queued"`), `source` (`{ kind: "human" } | { kind: "agent"; agentId: string }`), `requesterLabel`, `taskPreview`, `taskLen`. - Backend inclut les tickets `human` et `agent`; l'UI doit donc libeller clairement "tickets"/"inputs" ou gérer Human/Agent sans prétendre que tout est délégation agent. À faire: 1. Étendre les types frontend dans `frontend/src/domain/index.ts` (`AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource`, `AgentWorkState.tickets`). 2. Adapter le mock gateway pour accepter/normaliser `tickets` si nécessaire. 3. Dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`, afficher sous chaque agent une mini-liste compacte des tickets FIFO: - `#1 In progress` ou `#2 Queued` (position + 1), badge visuel cohérent avec l'existant, - requester/source (`Human` pour source human, label requester et/ou agent id court pour agent), - `taskPreview`, indication légère si `taskLen > taskPreview.length`, ticket id court monospace, - agent sans ticket: ne pas ajouter de bruit visuel excessif. 4. Dans le hook `useProjectWorkState`, ajouter un refresh sur l'event `delegationReady` si l'infrastructure d'events frontend existe déjà pour les autres events; sinon explique précisément pourquoi. 5. Mettre à jour/ajouter les tests Vitest pertinents: agent avec ticket inProgress, deux tickets en ordre, agent sans ticket, event `delegationReady` déclenche refresh, mock renvoie tickets. Contraintes: - Respecte le design existant, pas d'actions d'annulation/résolution, pas de nouvelle commande Tauri, pas de lecture logs/handoff. - Ne committe pas. - Ignore les fichiers runtime `.ideai/*` dirty. Vérifications demandées: `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`, puis `cd frontend && npx tsc --noEmit`. Retourne fichiers modifiés, choix UI, résultats réels des commandes, et tout blocage.
- **Response:** Lot B frontend `workstate delegation/queue snapshot` implémenté sur la branche active `feature/workstate-delegation-queue`. Aucun commit effectué. Fichiers modifiés: - `frontend/src/domain/index.ts` - `frontend/src/adapters/mock/index.ts` - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx` - `frontend/src/features/workstate/useProjectWorkState.ts` - `frontend/src/features/workstate/workstate.test.tsx` Choix UI: - Sous-liste compacte uniquement pour les agents qui ont des tickets, sans bruit visuel pour les agents sans ticket. - Tickets triés par `position`, affichés en `#N In progress` / `#N Queued`. - Source humaine libellée `Human`; source agent affichée avec `requesterLabel` + id agent court quand disponible. - `taskPreview` affiché inline, avec suffixe léger `+N` si `taskLen > taskPreview.length`. - `ticketId` court en monospace avec `title` complet. Détails techniques: - Ajout des types `AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource` et `AgentWorkState.tickets`. - `MockWorkStateGateway` accepte et normalise les états legacy sans `tickets` en `tickets: []`. - `useProjectWorkState` rafraîchit maintenant aussi sur lévénement domaine `delegationReady`, via linfrastructure `SystemGateway.onDomainEvent` déjà existante. Vérifications réelles: - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` : OK, `2 passed`, `17 passed`. - `cd frontend && npx tsc --noEmit` : OK, aucune sortie derreur. Blocage: aucun. Les fichiers runtime `.ideai/*` dirty et les changements backend existants ont été ignorés comme demandé.
- **Prompt:** Implémente le Lot C frontend `workstate conversation summaries` sur la branche active `feature/workstate-conversation-summaries`, en t'appuyant sur le backend/Tauri déjà modifié par DevBackend. Contexte backend disponible: - `ProjectWorkState` contient maintenant `conversations: ConversationWorkSummary[]` top-level. - `AgentTicketState.conversationId` existe déjà; le frontend doit joindre via `conversationId`. - DTO camelCase attendu: - `ConversationWorkSummary`: `conversationId`, `status` (`ready|missing|partial|unavailable`), `objectivePreview: string | null`, `summaryPreview: string | null`, `summaryLen`, `upTo: string | null`, `recentTurns: ConversationTurnWorkPreview[]`. - `ConversationTurnWorkPreview`: `role` (`prompt|response|toolActivity`), `source` (`{kind:'human'} | {kind:'agent'; agentId:string}`), `atMs`, `textPreview`, `textLen`. À faire: 1. Étendre les types frontend dans `frontend/src/domain/index.ts`: `ProjectWorkState.conversations`, `ConversationWorkSummary`, `ConversationPreviewStatus`, `ConversationTurnWorkPreview`. 2. Adapter le mock gateway pour accepter/normaliser les états legacy sans `conversations` en `conversations: []`. 3. Dans `ProjectWorkStatePanel.tsx`, construire une map `conversationId -> summary` et sous chaque ticket afficher une ligne compacte thread si summary disponible: - priorité: `Goal: <objectivePreview>` si présent; sinon `summaryPreview`; sinon dernier `recentTurn.textPreview` avec préfixe `Last:`; sinon rien de verbeux. - badge discret: `Summary` pour ready, `No summary` pour missing, `Partial`, `Unavailable`. - Ne pas afficher JSON brut, chemins, markdown volumineux, ni créer de modal/action. - Ne pas casser l'affichage ticket si aucune summary n'existe pour ce `conversationId`. 4. Mettre à jour/ajouter les tests Vitest: mock sans `conversations` normalisé; ticket avec `conversationId` affiche objectif/résumé; `partial`/`unavailable` ne masque pas le ticket; refresh existants restent verts. Contraintes: - Pas de nouvelle commande Tauri, pas d'action UX, pas de lecture logs/handoff côté frontend. - Respecte le style dense existant du panneau Work. - Ne committe pas. - Ignore dirty runtime `.ideai/*`. Vérifications demandées: - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` - `cd frontend && npx tsc --noEmit` Retourne fichiers modifiés, choix UI, résultats réels des commandes, et tout blocage.
- **Response:** Lot C frontend `workstate conversation summaries` implémenté sur la branche active `feature/workstate-conversation-summaries`. Aucun commit effectué. Fichiers modifiés: - `frontend/src/domain/index.ts` - `frontend/src/adapters/mock/index.ts` - `frontend/src/features/workstate/ProjectWorkStatePanel.tsx` - `frontend/src/features/workstate/workstate.test.tsx` Choix UI: - `ProjectWorkStatePanel` construit une map `conversationId -> ConversationWorkSummary` au niveau du panel et joint les summaries sous chaque ticket. - La ligne thread reste compacte: badge discret (`Summary`, `No summary`, `Partial`, `Unavailable`) + texte court. - Priorité daffichage respectée: `Goal: <objectivePreview>`, sinon `summaryPreview`, sinon `Last: <recentTurn.textPreview>`, sinon aucune ligne summary. - Aucun JSON brut, chemin, markdown long, modal ou action UX ajouté. - Si un ticket na pas de summary correspondant, laffichage ticket reste inchangé. Détails techniques: - Ajout des types TS `ConversationWorkSummary`, `ConversationPreviewStatus`, `ConversationTurnWorkPreview` et `ProjectWorkState.conversations`. - `MockWorkStateGateway` normalise les états legacy sans `conversations` en `conversations: []`; létat vide par défaut renvoie aussi `{ agents: [], conversations: [] }`. - Tests ajoutés pour normalisation mock, objectif/résumé joint via `conversationId`, et badges `partial`/`unavailable` sans masquer les tickets. Vérifications réelles: - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` : OK, `2 passed`, `19 passed`. - `cd frontend && npx tsc --noEmit` : OK, aucune sortie derreur. Blocage: aucun. Les fichiers runtime `.ideai/*` dirty et les changements backend existants ont été ignorés comme demandé.

View File

@ -14,3 +14,5 @@
{"id":"a0b9d44f-2761-4652-b2f3-28222dbc4c62","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781971459114,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Lot A frontend implémenté, sans commit.\n\nFichiers modifiés/ajoutés frontend:\n- `frontend/src/domain/index.ts`: types `ProjectWorkState`, `AgentWorkState`, `LiveWorkSession`, `WorkBusyState`.\n- `frontend/src/ports/index.ts`: port `WorkStateGateway` + ajout dans `Gateways`.\n- `frontend/src/adapters/workState.ts`: adapter Tauri `get_project_work_state`.\n- `frontend/src/adapters/index.ts`: câblage `TauriWorkStateGateway`.\n- `frontend/src/adapters/mock/index.ts`: `MockWorkStateGateway` + helper `_setProjectWorkState` + câblage mock.\n- `frontend/src/features/workstate/useProjectWorkState.ts`: hook avec refresh initial et refresh sur `agentLaunched`, `agentExited`, `agentBusyChanged`, `orchestratorRequestProcessed`.\n- `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`: UI read-only, empty/error/refresh, live/offline, idle/busy, ticket court.\n- `frontend/src/features/workstate/index.ts`: export feature.\n- `frontend/src/features/workstate/workstate.test.tsx`: tests panel empty, idle/offline, live+busy ticket court, refresh event.\n- `frontend/src/features/projects/ProjectsView.tsx`: onglet sidebar `Work`.\n- `frontend/src/features/projects/projects.test.tsx`: injection mock workState + test onglet Work rendu.\n\nCommandes exécutées:\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n - Résultat: OK, 2 fichiers passés, 13 tests passés.\n - Vite affiche seulement des warnings existants sur options `esbuild`/`oxc`.\n- `cd frontend && npx tsc --noEmit`\n - Résultat: OK, aucune erreur.\n\nRisques / limites:\n- Le refresh event est volontairement large car les events cités ne portent pas tous `projectId`; le hook re-fetch le projet actif sur event pertinent.\n- UI strictement read-only comme demandé: pas dattach/stop/voir conversation.\n- Je nai pas lancé tout le `vitest` global, seulement le ciblé demandé.\n- Le worktree contenait déjà des changements backend et `.ideai/*`; je ne les ai pas touchés."}
{"id":"01871912-13d6-42dd-b4df-497f0a1a9703","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781973915788,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot B frontend `workstate delegation/queue snapshot` sur la branche active `feature/workstate-delegation-queue`, en t'appuyant sur le backend/Tauri déjà modifié par DevBackend.\n\nContexte backend disponible:\n- `ProjectWorkState.agents[].tickets` est maintenant exposé côté DTO camelCase.\n- Ticket fields TS attendus: `ticketId`, `conversationId`, `position`, `status` (`\"inProgress\" | \"queued\"`), `source` (`{ kind: \"human\" } | { kind: \"agent\"; agentId: string }`), `requesterLabel`, `taskPreview`, `taskLen`.\n- Backend inclut les tickets `human` et `agent`; l'UI doit donc libeller clairement \"tickets\"/\"inputs\" ou gérer Human/Agent sans prétendre que tout est délégation agent.\n\nÀ faire:\n1. Étendre les types frontend dans `frontend/src/domain/index.ts` (`AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource`, `AgentWorkState.tickets`).\n2. Adapter le mock gateway pour accepter/normaliser `tickets` si nécessaire.\n3. Dans `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`, afficher sous chaque agent une mini-liste compacte des tickets FIFO:\n - `#1 In progress` ou `#2 Queued` (position + 1), badge visuel cohérent avec l'existant,\n - requester/source (`Human` pour source human, label requester et/ou agent id court pour agent),\n - `taskPreview`, indication légère si `taskLen > taskPreview.length`, ticket id court monospace,\n - agent sans ticket: ne pas ajouter de bruit visuel excessif.\n4. Dans le hook `useProjectWorkState`, ajouter un refresh sur l'event `delegationReady` si l'infrastructure d'events frontend existe déjà pour les autres events; sinon explique précisément pourquoi.\n5. Mettre à jour/ajouter les tests Vitest pertinents: agent avec ticket inProgress, deux tickets en ordre, agent sans ticket, event `delegationReady` déclenche refresh, mock renvoie tickets.\n\nContraintes:\n- Respecte le design existant, pas d'actions d'annulation/résolution, pas de nouvelle commande Tauri, pas de lecture logs/handoff.\n- Ne committe pas.\n- Ignore les fichiers runtime `.ideai/*` dirty.\n\nVérifications demandées: `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`, puis `cd frontend && npx tsc --noEmit`. Retourne fichiers modifiés, choix UI, résultats réels des commandes, et tout blocage."}
{"id":"7d70c87a-e7b5-468a-90c7-41decf1185bc","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781974090422,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Lot B frontend `workstate delegation/queue snapshot` implémenté sur la branche active `feature/workstate-delegation-queue`. Aucun commit effectué.\n\nFichiers modifiés:\n- `frontend/src/domain/index.ts`\n- `frontend/src/adapters/mock/index.ts`\n- `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`\n- `frontend/src/features/workstate/useProjectWorkState.ts`\n- `frontend/src/features/workstate/workstate.test.tsx`\n\nChoix UI:\n- Sous-liste compacte uniquement pour les agents qui ont des tickets, sans bruit visuel pour les agents sans ticket.\n- Tickets triés par `position`, affichés en `#N In progress` / `#N Queued`.\n- Source humaine libellée `Human`; source agent affichée avec `requesterLabel` + id agent court quand disponible.\n- `taskPreview` affiché inline, avec suffixe léger `+N` si `taskLen > taskPreview.length`.\n- `ticketId` court en monospace avec `title` complet.\n\nDétails techniques:\n- Ajout des types `AgentTicketState`, `TicketWorkStatus`, `TicketWorkSource` et `AgentWorkState.tickets`.\n- `MockWorkStateGateway` accepte et normalise les états legacy sans `tickets` en `tickets: []`.\n- `useProjectWorkState` rafraîchit maintenant aussi sur lévénement domaine `delegationReady`, via linfrastructure `SystemGateway.onDomainEvent` déjà existante.\n\nVérifications réelles:\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` : OK, `2 passed`, `17 passed`.\n- `cd frontend && npx tsc --noEmit` : OK, aucune sortie derreur.\n\nBlocage: aucun. Les fichiers runtime `.ideai/*` dirty et les changements backend existants ont été ignorés comme demandé."}
{"id":"5a2a42de-9ce4-461b-8d08-5b282a7b4db2","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781976069973,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot C frontend `workstate conversation summaries` sur la branche active `feature/workstate-conversation-summaries`, en t'appuyant sur le backend/Tauri déjà modifié par DevBackend.\n\nContexte backend disponible:\n- `ProjectWorkState` contient maintenant `conversations: ConversationWorkSummary[]` top-level.\n- `AgentTicketState.conversationId` existe déjà; le frontend doit joindre via `conversationId`.\n- DTO camelCase attendu:\n - `ConversationWorkSummary`: `conversationId`, `status` (`ready|missing|partial|unavailable`), `objectivePreview: string | null`, `summaryPreview: string | null`, `summaryLen`, `upTo: string | null`, `recentTurns: ConversationTurnWorkPreview[]`.\n - `ConversationTurnWorkPreview`: `role` (`prompt|response|toolActivity`), `source` (`{kind:'human'} | {kind:'agent'; agentId:string}`), `atMs`, `textPreview`, `textLen`.\n\nÀ faire:\n1. Étendre les types frontend dans `frontend/src/domain/index.ts`: `ProjectWorkState.conversations`, `ConversationWorkSummary`, `ConversationPreviewStatus`, `ConversationTurnWorkPreview`.\n2. Adapter le mock gateway pour accepter/normaliser les états legacy sans `conversations` en `conversations: []`.\n3. Dans `ProjectWorkStatePanel.tsx`, construire une map `conversationId -> summary` et sous chaque ticket afficher une ligne compacte thread si summary disponible:\n - priorité: `Goal: <objectivePreview>` si présent; sinon `summaryPreview`; sinon dernier `recentTurn.textPreview` avec préfixe `Last:`; sinon rien de verbeux.\n - badge discret: `Summary` pour ready, `No summary` pour missing, `Partial`, `Unavailable`.\n - Ne pas afficher JSON brut, chemins, markdown volumineux, ni créer de modal/action.\n - Ne pas casser l'affichage ticket si aucune summary n'existe pour ce `conversationId`.\n4. Mettre à jour/ajouter les tests Vitest: mock sans `conversations` normalisé; ticket avec `conversationId` affiche objectif/résumé; `partial`/`unavailable` ne masque pas le ticket; refresh existants restent verts.\n\nContraintes:\n- Pas de nouvelle commande Tauri, pas d'action UX, pas de lecture logs/handoff côté frontend.\n- Respecte le style dense existant du panneau Work.\n- Ne committe pas.\n- Ignore dirty runtime `.ideai/*`.\n\nVérifications demandées:\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx`\n- `cd frontend && npx tsc --noEmit`\n\nRetourne fichiers modifiés, choix UI, résultats réels des commandes, et tout blocage."}
{"id":"11b7df5e-f50e-4f4e-b680-37d5fb10af72","conversation":"09b15ec3-ce2f-0af2-2903-ef699df9de41","atMs":1781976255451,"source":{"kind":"agent","agentId":"af7f86da-76bc-48e1-9900-71f45a624800"},"role":"response","text":"Lot C frontend `workstate conversation summaries` implémenté sur la branche active `feature/workstate-conversation-summaries`. Aucun commit effectué.\n\nFichiers modifiés:\n- `frontend/src/domain/index.ts`\n- `frontend/src/adapters/mock/index.ts`\n- `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`\n- `frontend/src/features/workstate/workstate.test.tsx`\n\nChoix UI:\n- `ProjectWorkStatePanel` construit une map `conversationId -> ConversationWorkSummary` au niveau du panel et joint les summaries sous chaque ticket.\n- La ligne thread reste compacte: badge discret (`Summary`, `No summary`, `Partial`, `Unavailable`) + texte court.\n- Priorité daffichage respectée: `Goal: <objectivePreview>`, sinon `summaryPreview`, sinon `Last: <recentTurn.textPreview>`, sinon aucune ligne summary.\n- Aucun JSON brut, chemin, markdown long, modal ou action UX ajouté.\n- Si un ticket na pas de summary correspondant, laffichage ticket reste inchangé.\n\nDétails techniques:\n- Ajout des types TS `ConversationWorkSummary`, `ConversationPreviewStatus`, `ConversationTurnWorkPreview` et `ProjectWorkState.conversations`.\n- `MockWorkStateGateway` normalise les états legacy sans `conversations` en `conversations: []`; létat vide par défaut renvoie aussi `{ agents: [], conversations: [] }`.\n- Tests ajoutés pour normalisation mock, objectif/résumé joint via `conversationId`, et badges `partial`/`unavailable` sans masquer les tickets.\n\nVérifications réelles:\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` : OK, `2 passed`, `19 passed`.\n- `cd frontend && npx tsc --noEmit` : OK, aucune sortie derreur.\n\nBlocage: aucun. Les fichiers runtime `.ideai/*` dirty et les changements backend existants ont été ignorés comme demandé."}

View File

@ -1,11 +1,9 @@
---
upTo: 1cbd7c12-6608-45b4-aa9c-41dff0fedd51
upTo: 4a9ff50a-6ad3-410c-89a8-36cfee9adc94
objective: Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est
---
**Objectif :** Nouvelle feature cadrée par Architect, prête à démarrer le développement : « Gestion des limites de session des agents » (détection hiérarchique des limites de session + reprise auto annulable). C'est
- **Response:** Décision Git appliquée (local uniquement, aucun push). **Branche active : `feature/agent-skill-awareness-v2`** (créée depuis `develop`, working tree propre). **Actions effectuées :** 1. Les 7 fichiers dirty étaient tous de l'état runtime `.ideai/` (conversations handoff+log, layouts.json, MEMORY.md, nouveau checkpoint-blocked-until-appimage-030-restart.md). Conformément à mon rôle (séparer code de feature et état runtime), je les ai committés sur `develop` : - `c... ` chore(wip): état runtime .ideai (conversations, layouts, mémoire, checkpoint blocage AppImage 0.3.0) 2. Création de la branche neuve `feature/agent-skill-awareness-v2` depuis `develop` (tip = ce commit). J'ai suivi la reco Architect : la vieille `feature/agent-skill-awareness` (5be8987) n'est PAS mergée et reste en place comme archive — repart d'une base develop propre, pas de dette d'historique divergent (develop avait 7 commits d'avance / la feature 5 d'écart). **`git status --short --branch` :** ``` ## feature/agent-skill-awareness-v2 ``` (working tree propre) **Contexte branches :** - `develop` : +9 puis +1 (le commit runtime) vs origin → désormais ahead 10, non poussé. - `feature/agent-skill-awareness` : intacte (5be8987), abandonnée, conservée comme archive. - `feature/agent-skill-awareness-v2` : NOUVELLE base de travail. **Consignes pour DevBackend / DevFrontend / QA :** - Travaillez tous sur **`feature/agent-skill-awareness-v2`**. Ne touchez pas à `develop` ni à la vieille `feature/agent-skill-awareness`. - Ne committez pas vous-mêmes : signalez-moi (via Main) quand l'implémentation est prête, je fais les commits atomiques (code feature séparé de l'état runtime `.ideai/`). - Rappel checkpoint mémoire : un blocage de validation e2e attendait un restart sur AppImage 0.3.0 — vérifier que l'AppImage qui tourne est bien rebuildée avant de conclure une validation (le binaire qui tourne = AppImage, pas les sources). - Quand QA est vert avec sortie réelle, revenez vers moi : je rebase si besoin puis merge `--no-ff` v2 → develop.
- **Prompt:** Reprise chantier `feature/agent-skill-awareness-v2`. État observé: branche actuelle `feature/agent-skill-awareness-v2`, dirty avec changements code du hotfix livraison délégation/logs submit et fichiers runtime `.ideai/conversations/*`, `.ideai/layouts.json`, `.ideai/memory/MEMORY.md`, plus nouvelle mémoire `checkpoint-delivery-submit-logging-fix.md`. Merci de décider la stratégie locale: rester sur cette branche ou ajuster, quoi committer plus tard, quoi laisser hors commit/ignorer, et conditions avant commit/merge. Ne fais pas de commit pour l'instant.
- **Response:** Décision Git (aucun commit effectué). ## État constaté - Branche courante : `feature/agent-skill-awareness-v2`, pointée exactement sur `e832af5` = tip de `develop`. `develop` est ancêtre de HEAD → **base saine, aucune divergence** (rebase inutile pour l'instant). - Working tree dirty, deux natures bien distinctes : - **Code (hotfix livraison délégation + logs submit)** — 8 fichiers, +352/-23 : - `crates/app-tauri/src/commands.rs`, `crates/application/src/orchestrator/service.rs`, `crates/infrastructure/src/input/mod.rs` - `frontend/src/adapters/input.ts`, `frontend/src/adapters/terminal.ts`, `frontend/src/domain/index.ts` - `frontend/src/features/terminals/useWritePortal.ts` + `useWritePortal.test.tsx` - **État runtime `.ideai/`** — conversations (handoff/log.jsonl ×3), `layouts.json`, `memory/MEMORY.md`, + nouveau `memory/checkpoint-delivery-submit-logging-fix.md` (non suivi). ## Stratégie de branche **On reste sur `feature/agent-skill-awareness-v2`.** Elle part proprement de `develop`, pas de divergence, c'est le bon support pour reprendre le chantier. Pas de nouvelle branche. (Réserve mineure : le nom de branche parle de « skill-awareness » alors que le diff actuel est un hotfix livraison/logs ; je l'assume car Main reprend ce chantier sous ce nom. Si tu préfères isoler le hotfix sous `fix/delivery-submit-logging`, dis-le, je crée et bascule — sinon on garde.) ## Plan de commit (PLUS TARD, sur ton GO) Deux commits atomiques séparés, par intention : 1. `fix(input): livraison délégation + journalisation submit` → les 8 fichiers code (crates + frontend + test). 2. `chore(wip): état runtime .ideai` → conversations, `layouts.json`, `MEMORY.md` + le nouveau checkpoint mémoire (cohérent avec l'historique `chore(wip)` existant). ## Hors commit / à laisser Rien à exclure ni à ignorer en plus pour l'instant : l'historique du dépôt versionne déjà l'état runtime `.ideai/` via des `chore(wip)`, donc je le committe à part plutôt que de le mélanger au code. (Remarque de fond : ces fichiers runtime sont du bruit de diff ; si tu veux un jour les `.gitignore`, c'est une décision produit séparée à trancher hors de ce ticket.) ## Conditions avant commit / merge - **Avant le commit code** : QA verte — tests Rust (`cargo test` sur les crates touchés) **et** front (`useWritePortal.test.tsx` notamment). Tant que c'est rouge, je ne committe pas le hotfix. - **Avant merge `feature/agent-skill-awareness-v2` → `develop`** : tests verts + rebase sur `develop` uniquement si elle a avancé entre-temps (actuellement non), puis merge `--no-ff`. - **Aucune action sortante** (push/PR) sans ton GO explicite. J'attends ton signal pour committer.
- **Prompt:** QA final vert accepté avec réserve environnementale pour `feature/agent-skill-awareness-v2`. Merci de committer localement selon ta stratégie. État attendu: changements code hotfix délégation/logs submit + nouveau changement `crates/application/src/agent/lifecycle.rs` pour awareness skills; fichiers runtime `.ideai/*` modifiés et nouveau checkpoint mémoire. Conditions QA: application/front/infrastructure/app-tauri check verts; tests loopback socket Unix réels non exécutables dans sandbox (`UnixListener::bind` PermissionDenied), alternatives avec skips vertes. Merci de faire des commits atomiques adaptés (tu avais proposé code puis chore runtime), décider si le changement `lifecycle.rs` doit être dans un commit séparé du hotfix ou groupé selon intention, puis retourner hashes, messages, fichiers inclus/exclus, et état final. Pas de push.
- **Response:** Commits effectués localement sur `feature/agent-skill-awareness-v2` (aucun push). Décision : `lifecycle.rs` = intention distincte (la feature réelle de la branche) → commit séparé du hotfix. ## 3 commits atomiques (par intention) **1. `befff76` — feat(skills): injecte un paragraphe d'awareness skills dans le fichier de convention** - Inclus : `crates/application/src/agent/lifecycle.rs` - La feature de la branche : `compose_convention_file` émet l'awareness skills (assigné = contexte opérationnel, pas commande magique/sous-tâche ; capitalisation via `idea_create_skill` en MCP / `skill.create` en protocole fichier), sans injecter les corps des skills non assignés. **2. `018eb1a` — fix(input): fiabilise la livraison de délégation et journalise le submit** - Inclus : `crates/app-tauri/src/commands.rs`, `crates/application/src/orchestrator/service.rs`, `crates/infrastructure/src/input/mod.rs`, `frontend/src/adapters/input.ts`, `frontend/src/adapters/terminal.ts`, `frontend/src/domain/index.ts`, `frontend/src/features/terminals/useWritePortal.ts`, `useWritePortal.test.tsx` - Le hotfix livraison délégation + logs submit (bout-en-bout commande Tauri → orchestrateur → file infra → portail front). **3. `cc575ef` — chore(wip): état runtime .ideai (conversations, layouts, mémoire, checkpoint skill-awareness)** - Inclus : `.ideai/conversations/*` (handoff + log.jsonl ×5), `.ideai/layouts.json`, `.ideai/memory/MEMORY.md`, nouveau `.ideai/memory/checkpoint-delivery-submit-logging-fix.md` - État runtime non-code, isolé du code comme convenu. ## Exclus / hors commit Rien laissé de côté : working tree **propre** (`git status` vide). La réserve QA (tests loopback socket Unix réels non exécutables en sandbox — `UnixListener::bind` PermissionDenied, alternatives skips vertes) est consignée dans le message du commit `018eb1a`. ## État final - Branche : `feature/agent-skill-awareness-v2`, tip = `cc575ef`. - Base : toujours `develop` (`e832af5`) comme ancêtre direct, **pas de divergence** → merge `--no-ff` vers `develop` possible sans rebase dès que tu donnes le GO. - Aucune action sortante (pas de push). Dis-moi si tu veux que je merge vers `develop` maintenant ou si on garde sur la feature.
@ -24,3 +22,5 @@ objective: Nouvelle feature cadrée par Architect, prête à démarrer le dével
- **Prompt:** Lot B `workstate delegation/queue snapshot` est validé QA vert sur `feature/workstate-delegation-queue`. Merci de gérer les commits atomiques et la décision de merge local, sans push. Contexte: - Branche active attendue: `feature/workstate-delegation-queue`. - QA vert: - `cargo fmt --all -- --check` OK - `cargo test -p infrastructure mailbox --lib` OK (13 passed) - `cargo test -p application --test workstate` OK (12 passed) - `cargo test -p app-tauri --test dto_agents` OK (20 passed) - `cargo check -p app-tauri` OK - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK (2 files / 17 tests) - `cd frontend && npx tsc --noEmit` OK - `cargo test -p domain mailbox --lib` OK (6 passed) - Backend files likely touched: `crates/domain/src/{lib.rs,mailbox.rs}`, `crates/infrastructure/src/mailbox/mod.rs`, `crates/application/src/{lib.rs,workstate/mod.rs}`, `crates/application/tests/workstate.rs`, `crates/app-tauri/src/{dto.rs,state.rs}`, `crates/app-tauri/tests/dto_agents.rs`. - Frontend files likely touched: `frontend/src/domain/index.ts`, `frontend/src/adapters/mock/index.ts`, `frontend/src/features/workstate/{ProjectWorkStatePanel.tsx,useProjectWorkState.ts,workstate.test.tsx}`. - Dirty runtime `.ideai/*` may be present from live conversation/layout. Isolate it as you judge appropriate (historically chore runtime commit acceptable), do not mix runtime with feature code. Décide: 1. commits atomiques (backend/Tauri, frontend, runtime si nécessaire), 2. merge local éventuel vers `develop`, 3. état final. Ne push pas, ne supprime pas de branche. Retourne commits créés, merge ou non, branche finale, status final.
- **Response:** Lot B `workstate delegation/queue snapshot` intégré. QA re-vérifié vert de mon côté avant tout commit (fmt OK ; domain 6, infra mailbox 13, application workstate 12, app-tauri dto_agents 20 ; vitest 2 fichiers/17 ; tsc clean). **3 commits atomiques** sur `feature/workstate-delegation-queue` (séparation code feature / runtime) : - `cc7d99a` feat(workstate): snapshot des délégations en file par agent (Lot B backend) — domain `QueuedTicketSnapshot` + trait ségrégué `AgentQueueSnapshot` (ISP), infra `InMemoryMailbox` vue lecture, application read-model, DTO camelCase app-tauri + état. - `c600604` feat(workstate): UI des délégations en file par agent (Lot B frontend) — domaine/mock alignés, `ProjectWorkStatePanel` enrichi (requester, aperçu, position FIFO), hook. - `5cb99fd` chore(wip): état runtime .ideai (flux conversation live, layouts) — runtime isolé, jamais mélangé au code feature. **Merge local** : feature TERMINÉE et VERTE → `git merge --no-ff feature/workstate-delegation-queue` dans **develop** → commit de merge `64c2c14`. `develop` n'avait pas divergé (il pointait sur la base de la feature) ; `--no-ff` choisi pour garder la trace d'intégration du lot, cohérent avec l'intégration du Lot A (`3bfb932`). **Pas de push, aucune branche supprimée.** `feature/workstate-delegation-queue` est conservée (sur `cc7d99a→5cb99fd`). **État final** : - Branche courante : `develop` (HEAD = `64c2c14`). - Working tree : propre (`git status` vide). - `feature/workstate-delegation-queue` préservée et entièrement contenue dans `develop`. - Rien en attente côté git. Prêt pour une future release `develop → main` sur ta validation explicite.
- **Prompt:** Prépare le dépôt pour le Lot C `workstate conversation summaries` après Lot B mergé dans `develop` @ `64c2c14`. État attendu côté Main: `develop` ahead origin, avec dirty runtime/memory lié au checkpoint récent (`.ideai/conversations/...`, `.ideai/memory/MEMORY.md`, `.ideai/memory/checkpoint-workstate-delegation-queue-lot-b.md`). Merci de: - gérer ce dirty sans perte et sans le mélanger au futur code applicatif (commit runtime/memory si tu juges cohérent avec l'historique), - créer/basculer sur une branche feature propre depuis `develop` pour Lot C, - ne push pas, ne supprime pas de branche, - retourner branche courante, commits éventuels, et `git status` final. Nom de branche suggéré: `feature/workstate-conversation-summaries`.
- **Response:** Dépôt prêt pour le Lot C `workstate conversation summaries`. **Dirty traité** : tout le dirty était runtime/mémoire (aucun code applicatif), donc commité tel quel sur `develop`, sans risque de le mélanger au futur code Lot C : - `e5dd4f8` chore(wip): état runtime .ideai + mémoire (checkpoint Lot B délégations/file) — conversations live, `.ideai/memory/MEMORY.md` et nouvelle note `checkpoint-workstate-delegation-queue-lot-b.md` (fichier non suivi désormais versionné). Cohérent avec l'historique des commits runtime. **Branche feature** : créée et basculée depuis `develop` (@ `e5dd4f8`) : - `feature/workstate-conversation-summaries` **Pas de push, aucune branche supprimée.** **État final** : - Branche courante : `feature/workstate-conversation-summaries` (HEAD = `e5dd4f8`, partagé avec `develop`). - Working tree : propre (`git status` vide). - `develop` est ahead de origin (le checkpoint runtime/mémoire est local, non poussé). Le dev Lot C peut démarrer sur cette branche. Reviens vers moi pour committer/merger une fois QA vert.
- **Prompt:** Lot C `workstate conversation summaries` est validé QA vert sur `feature/workstate-conversation-summaries`. Merci de gérer les commits atomiques et la décision de merge local, sans push. Contexte QA final: - `cargo fmt --all -- --check` OK - `cargo test -p application --test workstate` OK, 21 passed, aucun warning Rust - `cargo test -p app-tauri --test dto_agents` OK, 21 passed - `cargo check -p app-tauri` OK (validé avant nettoyage warning; le nettoyage ne touche qu'un test) - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK, 2 files / 19 tests (warnings Vite non bloquants) - `cd frontend && npx tsc --noEmit` OK Fichiers attendus: - Backend/Tauri: `crates/application/src/workstate/mod.rs`, `crates/application/src/lib.rs`, `crates/app-tauri/src/dto.rs`, `crates/app-tauri/src/state.rs`, `crates/application/tests/workstate.rs`, `crates/app-tauri/tests/dto_agents.rs`. - Frontend: `frontend/src/domain/index.ts`, `frontend/src/adapters/mock/index.ts`, `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`, `frontend/src/features/workstate/workstate.test.tsx`. - Dirty runtime `.ideai/*` possible; isole-le si présent, ne mélange pas avec feature code. Décide: 1. commits atomiques, 2. merge local éventuel vers `develop`, 3. état final. Ne push pas, ne supprime pas de branche. Retourne commits créés, merge ou non, branche finale, status final.

View File

@ -51,3 +51,5 @@
{"id":"53773ad0-5bb6-4e24-8456-d7ab17be3db0","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781974288762,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Lot B `workstate delegation/queue snapshot` est validé QA vert sur `feature/workstate-delegation-queue`. Merci de gérer les commits atomiques et la décision de merge local, sans push.\n\nContexte:\n- Branche active attendue: `feature/workstate-delegation-queue`.\n- QA vert:\n - `cargo fmt --all -- --check` OK\n - `cargo test -p infrastructure mailbox --lib` OK (13 passed)\n - `cargo test -p application --test workstate` OK (12 passed)\n - `cargo test -p app-tauri --test dto_agents` OK (20 passed)\n - `cargo check -p app-tauri` OK\n - `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK (2 files / 17 tests)\n - `cd frontend && npx tsc --noEmit` OK\n - `cargo test -p domain mailbox --lib` OK (6 passed)\n- Backend files likely touched: `crates/domain/src/{lib.rs,mailbox.rs}`, `crates/infrastructure/src/mailbox/mod.rs`, `crates/application/src/{lib.rs,workstate/mod.rs}`, `crates/application/tests/workstate.rs`, `crates/app-tauri/src/{dto.rs,state.rs}`, `crates/app-tauri/tests/dto_agents.rs`.\n- Frontend files likely touched: `frontend/src/domain/index.ts`, `frontend/src/adapters/mock/index.ts`, `frontend/src/features/workstate/{ProjectWorkStatePanel.tsx,useProjectWorkState.ts,workstate.test.tsx}`.\n- Dirty runtime `.ideai/*` may be present from live conversation/layout. Isolate it as you judge appropriate (historically chore runtime commit acceptable), do not mix runtime with feature code.\n\nDécide:\n1. commits atomiques (backend/Tauri, frontend, runtime si nécessaire),\n2. merge local éventuel vers `develop`,\n3. état final.\n\nNe push pas, ne supprime pas de branche. Retourne commits créés, merge ou non, branche finale, status final."}
{"id":"58b15907-ee31-488c-afaa-40246fce5228","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781974372502,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Lot B `workstate delegation/queue snapshot` intégré. QA re-vérifié vert de mon côté avant tout commit (fmt OK ; domain 6, infra mailbox 13, application workstate 12, app-tauri dto_agents 20 ; vitest 2 fichiers/17 ; tsc clean).\n\n**3 commits atomiques** sur `feature/workstate-delegation-queue` (séparation code feature / runtime) :\n- `cc7d99a` feat(workstate): snapshot des délégations en file par agent (Lot B backend) — domain `QueuedTicketSnapshot` + trait ségrégué `AgentQueueSnapshot` (ISP), infra `InMemoryMailbox` vue lecture, application read-model, DTO camelCase app-tauri + état.\n- `c600604` feat(workstate): UI des délégations en file par agent (Lot B frontend) — domaine/mock alignés, `ProjectWorkStatePanel` enrichi (requester, aperçu, position FIFO), hook.\n- `5cb99fd` chore(wip): état runtime .ideai (flux conversation live, layouts) — runtime isolé, jamais mélangé au code feature.\n\n**Merge local** : feature TERMINÉE et VERTE → `git merge --no-ff feature/workstate-delegation-queue` dans **develop** → commit de merge `64c2c14`. `develop` n'avait pas divergé (il pointait sur la base de la feature) ; `--no-ff` choisi pour garder la trace d'intégration du lot, cohérent avec l'intégration du Lot A (`3bfb932`).\n\n**Pas de push, aucune branche supprimée.** `feature/workstate-delegation-queue` est conservée (sur `cc7d99a→5cb99fd`).\n\n**État final** :\n- Branche courante : `develop` (HEAD = `64c2c14`).\n- Working tree : propre (`git status` vide).\n- `feature/workstate-delegation-queue` préservée et entièrement contenue dans `develop`.\n- Rien en attente côté git. Prêt pour une future release `develop → main` sur ta validation explicite."}
{"id":"1cbd7c12-6608-45b4-aa9c-41dff0fedd51","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781975270116,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Prépare le dépôt pour le Lot C `workstate conversation summaries` après Lot B mergé dans `develop` @ `64c2c14`.\n\nÉtat attendu côté Main: `develop` ahead origin, avec dirty runtime/memory lié au checkpoint récent (`.ideai/conversations/...`, `.ideai/memory/MEMORY.md`, `.ideai/memory/checkpoint-workstate-delegation-queue-lot-b.md`).\n\nMerci de:\n- gérer ce dirty sans perte et sans le mélanger au futur code applicatif (commit runtime/memory si tu juges cohérent avec l'historique),\n- créer/basculer sur une branche feature propre depuis `develop` pour Lot C,\n- ne push pas, ne supprime pas de branche,\n- retourner branche courante, commits éventuels, et `git status` final.\n\nNom de branche suggéré: `feature/workstate-conversation-summaries`."}
{"id":"2896e211-78cf-448f-8e78-b559a326ad0a","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781975298096,"source":{"kind":"agent","agentId":"cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5"},"role":"response","text":"Dépôt prêt pour le Lot C `workstate conversation summaries`.\n\n**Dirty traité** : tout le dirty était runtime/mémoire (aucun code applicatif), donc commité tel quel sur `develop`, sans risque de le mélanger au futur code Lot C :\n- `e5dd4f8` chore(wip): état runtime .ideai + mémoire (checkpoint Lot B délégations/file) — conversations live, `.ideai/memory/MEMORY.md` et nouvelle note `checkpoint-workstate-delegation-queue-lot-b.md` (fichier non suivi désormais versionné). Cohérent avec l'historique des commits runtime.\n\n**Branche feature** : créée et basculée depuis `develop` (@ `e5dd4f8`) :\n- `feature/workstate-conversation-summaries`\n\n**Pas de push, aucune branche supprimée.**\n\n**État final** :\n- Branche courante : `feature/workstate-conversation-summaries` (HEAD = `e5dd4f8`, partagé avec `develop`).\n- Working tree : propre (`git status` vide).\n- `develop` est ahead de origin (le checkpoint runtime/mémoire est local, non poussé).\n\nLe dev Lot C peut démarrer sur cette branche. Reviens vers moi pour committer/merger une fois QA vert."}
{"id":"4a9ff50a-6ad3-410c-89a8-36cfee9adc94","conversation":"6bc594e8-a37c-0dbd-1de6-6e3b73002cb4","atMs":1781976486735,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Lot C `workstate conversation summaries` est validé QA vert sur `feature/workstate-conversation-summaries`. Merci de gérer les commits atomiques et la décision de merge local, sans push.\n\nContexte QA final:\n- `cargo fmt --all -- --check` OK\n- `cargo test -p application --test workstate` OK, 21 passed, aucun warning Rust\n- `cargo test -p app-tauri --test dto_agents` OK, 21 passed\n- `cargo check -p app-tauri` OK (validé avant nettoyage warning; le nettoyage ne touche qu'un test)\n- `cd frontend && npx vitest run src/features/workstate/workstate.test.tsx src/features/projects/projects.test.tsx` OK, 2 files / 19 tests (warnings Vite non bloquants)\n- `cd frontend && npx tsc --noEmit` OK\n\nFichiers attendus:\n- Backend/Tauri: `crates/application/src/workstate/mod.rs`, `crates/application/src/lib.rs`, `crates/app-tauri/src/dto.rs`, `crates/app-tauri/src/state.rs`, `crates/application/tests/workstate.rs`, `crates/app-tauri/tests/dto_agents.rs`.\n- Frontend: `frontend/src/domain/index.ts`, `frontend/src/adapters/mock/index.ts`, `frontend/src/features/workstate/ProjectWorkStatePanel.tsx`, `frontend/src/features/workstate/workstate.test.tsx`.\n- Dirty runtime `.ideai/*` possible; isole-le si présent, ne mélange pas avec feature code.\n\nDécide:\n1. commits atomiques,\n2. merge local éventuel vers `develop`,\n3. état final.\n\nNe push pas, ne supprime pas de branche. Retourne commits créés, merge ou non, branche finale, status final."}

File diff suppressed because one or more lines are too long

View File

@ -72,3 +72,7 @@
{"id":"7b0a1b7a-bfad-4d97-89f8-f4bb9b32740d","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781971869030,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot B backend/Tauri `workstate delegation/queue snapshot` sur la branche `feature/workstate-delegation-queue`. Respecte le cadrage Architect.\n\nObjectif:\n- Ajouter un port read-only `AgentQueueSnapshot` dans `domain/src/mailbox.rs` avec snapshots clonés des tickets, sans exposer ni cloner les senders.\n- Implémenter ce port pour `InMemoryMailbox` en conservant l'ordre FIFO et positions recalculées.\n- Étendre `GetProjectWorkState` pour inclure `agents[].tickets`, en croisant snapshot queue + `InputMediator::busy_state` pour dériver `inProgress` vs `queued`.\n- Conserver manifest boundary: tickets d'agents absents du manifeste ignorés.\n- Générer `taskPreview` côté application: trim, espaces normalisés, coupure ~160 caractères; conserver `taskLen` original utile.\n- Exposer DTO Tauri camelCase dans la commande existante `get_project_work_state`.\n\nDécision de périmètre: afficher les tickets `source == Agent` en priorité pour délégations; si tu inclus aussi human, explique et teste le libellé/source. Ne change pas la sémantique enqueue/resolve/cancel. Pas de nouvelle persistance, pas lecture log/handoff, pas événement queue changé.\n\nTests attendus:\n- infrastructure mailbox snapshot: ordre/metadata/read-only/update after resolve/cancel.\n- application workstate: queue vide, deux tickets ordonnés, busy first inProgress, idle queued, absent manifest ignored, preview truncation, source mapping.\n- app-tauri DTO serialization tickets.\n\nCommandes: `cargo fmt`, `cargo test -p infrastructure mailbox --lib`, `cargo test -p application --test workstate`, `cargo test -p app-tauri --test dto_agents`, `cargo check -p app-tauri`, et autres pertinentes. Ne committe pas. Ignore les fichiers runtime `.ideai/*` dirty, ne les modifie pas volontairement."}
{"id":"2c25d734-281e-4f2a-850e-684fec6e0ba7","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781973784611,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Reprise orchestration Main. Je retrouve la branche `feature/workstate-delegation-queue` avec des modifications backend/Tauri non committées pour le Lot B `workstate delegation/queue snapshot` (ticket mentionné par l'utilisateur: dbca904a-2843-4f41-9926-d13b7804c70f). Peux-tu confirmer ton état exact: périmètre implémenté, choix notables (notamment tickets human inclus ou exclus), commandes exécutées et résultats réels, et ce qu'il reste selon toi ? Ne modifie rien, ne committe pas."}
{"id":"b0d07b39-565c-4d30-a9ae-0c3721338aeb","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781973900817,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"response","text":"État DevBackend — branche `feature/workstate-delegation-queue`, Lot B « workstate delegation/queue snapshot » (ticket dbca904a). **Rien committé, rien modifié pendant ce diagnostic.**\n\n## Périmètre implémenté\nExtension du read-model live-state (Lot A) pour exposer, **par agent**, ses tickets de délégation en file FIFO. 4 couches touchées (sens hexagonal respecté) :\n\n- **domain/mailbox.rs** : nouveau value object `QueuedTicketSnapshot` (données du ticket + `position`, 0 = tête ; **jamais** le `oneshot::Sender`) + nouveau **port lecture seule** `AgentQueueSnapshot` (`queue_for(agent) -> Vec<QueuedTicketSnapshot>`), **ségrégé (ISP)** du port mutant `AgentMailbox`. Re-export depuis `domain/lib.rs`.\n- **infrastructure/mailbox/mod.rs** : `impl AgentQueueSnapshot for InMemoryMailbox` — clone les données sous le lock, recalcule la position depuis l'ordre courant, lecture pure (n'altère pas la file).\n- **application/workstate/mod.rs** : nouveaux `AgentTicketState`, `TicketWorkStatus` (InProgress/Queued), `TicketWorkSource` (Human/Agent), `task_preview` (160 car., espaces normalisés) + `task_len`. `GetProjectWorkState` gagne une dépendance `queue: Arc<dyn AgentQueueSnapshot>` ; champ `AgentWorkState.tickets` peuplé.\n- **app-tauri/dto.rs** : `AgentTicketStateDto`, `TicketWorkStatusDto`, `TicketWorkSourceDto` (camelCase, enum source taggée `kind`), champ `AgentWorkStateDto.tickets`.\n- **app-tauri/state.rs** : composition root — même `InMemoryMailbox` concret partagé en **deux vues Arc** (`AgentMailbox` mutant + `AgentQueueSnapshot` lecture), injecté dans `GetProjectWorkState`.\n\n## Choix notables\n- **Tickets human INCLUS** (pas de filtrage par source). `TicketWorkSource::Human`/`Agent{agent_id}` mappé dans les deux sens, couvert par le test `workstate_maps_human_and_agent_ticket_sources`. La file liste tous les tickets quelle que soit l'origine.\n- **Statut dérivé, jamais stocké** : InProgress = ticket dont l'id == ticket busy courant de l'agent ; les autres Queued ; agent idle → tout Queued.\n- **ISP** : port lecture distinct plutôt que d'élargir `AgentMailbox` ; observer la file ne la mute jamais (test dédié).\n- **task_preview borné** à 160 car., texte intégral jamais envoyé à l'UI, longueur réelle via `task_len`.\n- **Frontière manifest préservée** : seuls les agents du manifest reçoivent des tickets ; file d'un agent absent du manifest ignorée (test dédié).\n\n## Commandes exécutées (à l'instant) + résultats réels\n`cargo test -p domain -p application -p infrastructure -p app-tauri` → **tout vert, 0 échec**.\n- domain : 176 unit (+1 nouveau `queued_ticket_snapshot_…`)\n- infrastructure : 228 unit (+6 nouveaux `snapshot_*` : empty/fifo-order/metadata/read-only/après resolve/après cancel)\n- application : 43 unit + `workstate.rs` **12** (+7 nouveaux : sans-queue, fifo, busy→in-progress, idle→tout-queued, agent-hors-manifest, troncature preview, sources human+agent)\n- app-tauri : lib 47 + `dto_agents.rs` (+1 `project_work_state_dto_serialises_tickets_camelcase`)\nAucun warning bloquant.\n\n## Ce qu'il reste (selon moi)\n1. **Frontend (périmètre DevFrontend)** : aucune consommation TS/React des nouveaux champs DTO `tickets` — rendu du panneau de délégations non fait.\n2. **Commit/merge (périmètre Git)** : 14 fichiers non commités ; le code backend est prêt à committer. À noter : les fichiers `.ideai/` modifiés (conversations/handoff/log/layouts) sont du **bruit runtime**, pas la feature.\n3. **Validation e2e/AppImage** : non faite (binaire qui tourne = AppImage, pas les sources).\n4. **Cadrage Architect** : si tu veux une validation formelle du nouveau port `AgentQueueSnapshot` dans `architect.md`, je l'ai conçu en cohérence ISP mais ce n'est pas tranché dans la cartographie.\n\nEn résumé : backend Lot B **complet et vert** au niveau tests unitaires/intégration ; restent le front, le commit Git et la validation e2e."}
{"id":"c12097a1-1a74-4326-be18-c28424c43947","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781975317853,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"Implémente le Lot C backend/Tauri `workstate conversation summaries` sur la branche active `feature/workstate-conversation-summaries`. Respecte strictement le cadrage Architect ci-dessous.\n\nObjectif produit:\n- Enrichir `ProjectWorkState` avec `conversations: Vec<ConversationWorkSummary>` top-level, jointes côté frontend via `tickets[].conversationId`.\n- Résumer uniquement les conversations visibles via les tickets du work-state (conversation ids présents dans `AgentTicketState`).\n- Read-only, best-effort: aucune nouvelle persistance, aucune mutation/réparation de log/handoff, aucun blocage du live/busy/tickets si preview KO.\n\nSources:\n- Primaire: `HandoffStore` (`.ideai/conversations/<id>/handoff.md`).\n- Fallback: `ConversationLog::last(conversation, 3)` uniquement, jamais lecture complète.\n- `ConversationRegistry` n'est pas source de vérité pour ce lot.\n\nModèle application recommandé:\n- `ProjectWorkState { agents: Vec<AgentWorkState>, conversations: Vec<ConversationWorkSummary> }`.\n- `ConversationWorkSummary { conversation_id, status, objective_preview, summary_preview, summary_len, up_to, recent_turns }`.\n- `ConversationPreviewStatus`: `Ready | Missing | Partial | Unavailable`.\n- `ConversationTurnWorkPreview { role, source, at_ms, text_preview, text_len }`.\n\nBornes:\n- `HANDOFF_PREVIEW_MAX_CHARS = 480`\n- `OBJECTIVE_PREVIEW_MAX_CHARS = 160`\n- `RECENT_TURNS_MAX = 3`\n- `TURN_PREVIEW_MAX_CHARS = 220`\n- Normalisation: trim + collapse whitespace + truncation char-safe.\n\nAlgorithme:\n1. Dédupliquer les `conversation_id` issus des tickets projetés.\n2. Tenter `handoffs.load(conversation)`.\n3. Si handoff présent et utilisable: `status = Ready`, `summaryPreview`, `summaryLen`, `objectivePreview`, `upTo`.\n4. Si absent: `status = Missing`, tenter `log.last(conversation, 3)` et remplir `recentTurns` si possible.\n5. Si handoff illisible/erreur mais log lisible: `status = Partial`, `summaryPreview = None`, `recentTurns`.\n6. Si handoff et log échouent: `status = Unavailable`, champs preview vides.\n7. Ne jamais transformer une erreur preview en `AppError` global.\n\nDTO Tauri:\n- Garder `get_project_work_state`.\n- Ajouter `ProjectWorkStateDto.conversations` camelCase.\n- Statuts en `ready/missing/partial/unavailable`.\n- Types TS seront faits par DevFrontend ensuite, mais les DTO Rust doivent être clairs.\n\nTests attendus:\n- `crates/application/tests/workstate.rs`: agents/live/busy/tickets conservés si preview échoue; conversation ids dédupliqués; handoff présent ready; handoff absent + log présent missing avec recentTurns borné; handoff erreur + log lisible partial; handoff/log KO unavailable; previews tronquées et whitespace normalisé.\n- `crates/app-tauri/tests/dto_agents.rs`: sérialisation camelCase `conversations`, statuts, `recentTurns`.\n- Infrastructure seulement si nécessaire; ne crée pas d'adapter inutile.\n\nCommandes minimales:\n- `cargo fmt --all -- --check`\n- `cargo test -p application --test workstate`\n- `cargo test -p app-tauri --test dto_agents`\n- `cargo check -p app-tauri`\n- autres pertinentes si tu touches infra/domain.\n\nContraintes:\n- Ne committe pas.\n- Ignore dirty runtime éventuel `.ideai/*`.\n- Retourne fichiers modifiés, signatures/types ajoutés, choix exacts si divergence, commandes exécutées et résultats réels."}
{"id":"b9900b46-41ff-4276-a225-991520cf0923","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781976051405,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"response","text":"Lot C backend/Tauri `workstate conversation summaries` **implémenté et vert** sur `feature/workstate-conversation-summaries`. **Rien commité.**\n\n## Fichiers modifiés (6 ; aucun infra/domain — ports HandoffStore/ConversationLog réutilisés)\n- `crates/application/src/workstate/mod.rs` — modèle + algo best-effort\n- `crates/application/src/lib.rs` — re-exports\n- `crates/app-tauri/src/dto.rs` — DTO camelCase\n- `crates/app-tauri/src/state.rs` — wiring composition root\n- `crates/application/tests/workstate.rs` — fakes + 9 tests\n- `crates/app-tauri/tests/dto_agents.rs` — +1 test + 2 littéraux existants mis à jour\n\n## Signatures/types ajoutés\n**Application :**\n- `ProjectWorkState.conversations: Vec<ConversationWorkSummary>` (top-level)\n- `ConversationWorkSummary { conversation_id, status, objective_preview: Option<String>, summary_preview: Option<String>, summary_len: usize, up_to: Option<TurnId>, recent_turns: Vec<ConversationTurnWorkPreview> }`\n- `enum ConversationPreviewStatus { Ready, Missing, Partial, Unavailable }`\n- `ConversationTurnWorkPreview { role: TurnRole, source: TicketWorkSource, at_ms: u64, text_preview: String, text_len: usize }`\n- `trait ConversationLogProvider { fn conversation_log_for(&self, root: &ProjectPath) -> Option<Arc<dyn ConversationLog>> }` (nouveau, jumeau de `HandoffProvider`)\n- `GetProjectWorkState::with_conversation_sources(handoffs: Arc<dyn HandoffProvider>, logs: Arc<dyn ConversationLogProvider>)` (builder)\n- Constantes : `HANDOFF_PREVIEW_MAX_CHARS=480`, `OBJECTIVE_PREVIEW_MAX_CHARS=160`, `RECENT_TURNS_MAX=3`, `TURN_PREVIEW_MAX_CHARS=220`\n\n**DTO Tauri :** `ProjectWorkStateDto.conversations`, `ConversationWorkSummaryDto`, `ConversationPreviewStatusDto` (`ready/missing/partial/unavailable`), `ConversationTurnWorkPreviewDto`. **state.rs :** `AppConversationLogProvider` (matérialise `FsConversationLog::new(root)`), câblé via `.with_conversation_sources(AppHandoffProvider, AppConversationLogProvider)`.\n\n## Choix exacts / divergences\n1. **Réutilisé `HandoffProvider` existant (P7)** comme source primaire + ajouté le jumeau `ConversationLogProvider` pour le repli. Résolus **par project root** (`input.project.root`) car `GetProjectWorkState` est singleton AppState alors que les `Fs*` figent leur racine à la construction — strictement parallèle au pattern P7/P8b.\n2. **Builder `with_conversation_sources`** plutôt que d'étendre `new(...)` : sources OFF par défaut, zéro régression sur call sites/tests existants (best-effort absent = aucune summary).\n3. **« provider non câblé » = handoff Absent (chemin Missing), jamais une erreur.** `Unavailable` strictement = handoff **erreur** ET log erreur/absent. `Missing` reste Missing même si le log échoue (alors recentTurns vide). Conforme aux étapes 4/5/6 du cadrage.\n4. **Ready** ne remplit pas `recent_turns` (le handoff EST le résumé) ; recentTurns peuplé seulement en Missing/Partial. Re-cap défensif à `RECENT_TURNS_MAX` en plus de `log.last(_, 3)`.\n5. **Dédup** des `conversation_id` sur tous les tickets projetés (humains inclus), **ordre first-seen** pour refléter l'ordre du panneau.\n6. **`TurnRole` domaine réutilisé tel quel dans le DTO** (sérialise déjà camelCase `prompt/response/toolActivity`), cohérent avec le réemploi de `AgentBusyState` ; `source` du tour réutilise `TicketWorkSourceDto` (`kind: human/agent`).\n7. **Champs preview optionnels sérialisés en `null`** quand absents (pas omis) — prévisible pour le front, aligné sur le pattern `live` (divergence assumée vs `ConversationDetailsDto` qui omet).\n8. **Normalisation factorisée** : helper `preview(text, max)` partagé (trim + collapse whitespace + troncature char-safe), `task_preview` (Lot B) refactoré dessus.\n\n## Commandes exécutées + résultats réels\n- `cargo fmt --all -- --check` → **clean** (après `cargo fmt --all`).\n- `cargo test -p application --test workstate` → **21 passed; 0 failed** (12 existants + 9 nouveaux : has_no_conversations, dedups, ready, missing+bounded(3), partial, unavailable, preview_failure_preserves_agents/live/busy/tickets, previews_truncated+normalised, turn_text_truncated).\n- `cargo test -p app-tauri --test dto_agents` → **21 passed; 0 failed** (+1 `project_work_state_dto_serialises_conversations_camelcase`).\n- `cargo check -p app-tauri` → **clean** (domain/application/infrastructure/app-tauri compilent).\n\nPas d'adapter infra créé (réemploi de `FsHandoffStore`/`FsConversationLog` existants). Dirty runtime `.ideai/*` ignoré. Reste : types TS (DevFrontend) + commit/merge (Git) + validation e2e."}
{"id":"0834676a-dc01-4cdf-97a4-37cff81bf3d2","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781976363926,"source":{"kind":"agent","agentId":"a6ced819-b893-4213-b003-9e9dc79b9641"},"role":"prompt","text":"QA Lot C est verte mais signale un warning Rust non bloquant: `variant Absent is never constructed` dans `crates/application/tests/workstate.rs:173:5`, probablement un enum/fake de test `HandoffOutcome::Absent` inutilisé.\n\nMerci de nettoyer ce warning de façon minimale sans changer le comportement applicatif, puis relance au moins:\n- `cargo fmt --all -- --check`\n- `cargo test -p application --test workstate`\n\nNe committe pas. Retourne le diff conceptuel, les fichiers modifiés, et les résultats réels."}
{"id":"8e30c171-2d32-48ab-9971-c6d23a010052","conversation":"d5068bc8-786e-0428-1d14-83b93961a15e","atMs":1781976412762,"source":{"kind":"agent","agentId":"73c853d1-c0fd-463b-ad17-1d24fefa371f"},"role":"response","text":"Warning nettoyé. **Rien commité.**\n\n## Diff conceptuel\nLa variante `HandoffOutcome::Absent` du fake de test n'était jamais construite : l'absence de handoff était déjà modélisée en **ne configurant pas** la conversation (le `load` du fake retombait sur l'arm `_ => Ok(None)`). Correctif minimal, zéro impact applicatif (uniquement le fixture de test) :\n- Suppression de la variante `Absent` (+ doc-comment expliquant que l'absence = conversation non configurée).\n- Le match de `FakeHandoffStore::load` passe de `_ => Ok(None)` à `None => Ok(None)` (désormais exhaustif sur `Present`/`Error`/`None`, plus de wildcard).\nAucun test ne référençait `Absent` (le test `missing` repose sur une conversation non configurée), donc aucune logique de test ni d'assertion modifiée.\n\n## Fichier modifié\n- `crates/application/tests/workstate.rs` (uniquement)\n\n## Résultats réels\n- `cargo fmt --all -- --check` → **clean** (exit 0).\n- `cargo test -p application --test workstate` → **21 passed; 0 failed**, **aucun warning** (le `variant Absent is never constructed` a disparu)."}

View File

@ -9,11 +9,12 @@
use serde::{Deserialize, Serialize};
use application::{
AgentTicketState, AppError, CreateProjectInput, CreateProjectOutput, GitGraphOutput,
HealthInput, HealthReport, LayoutKind, ListProjectsOutput, LiveSessionKind, OpenProjectOutput,
AgentTicketState, AppError, ConversationPreviewStatus, ConversationTurnWorkPreview,
ConversationWorkSummary, CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput,
HealthReport, LayoutKind, ListProjectsOutput, LiveSessionKind, OpenProjectOutput,
ProjectWorkState, TicketWorkSource, TicketWorkStatus,
};
use domain::{AgentBusyState, Project, ProjectId};
use domain::{AgentBusyState, Project, ProjectId, TurnRole};
/// Request DTO for the `health` command.
#[derive(Debug, Clone, Default, Deserialize)]
@ -1603,12 +1604,106 @@ pub struct AgentWorkStateDto {
pub tickets: Vec<AgentTicketStateDto>,
}
/// How much of a [`ConversationWorkSummaryDto`] could be derived, best-effort.
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum ConversationPreviewStatusDto {
/// A handoff was present and usable.
Ready,
/// No handoff yet; `recentTurns` may carry the log fallback.
Missing,
/// The handoff was unreadable but the log fallback was readable.
Partial,
/// Neither the handoff nor the log could be read.
Unavailable,
}
impl From<ConversationPreviewStatus> for ConversationPreviewStatusDto {
fn from(status: ConversationPreviewStatus) -> Self {
match status {
ConversationPreviewStatus::Ready => Self::Ready,
ConversationPreviewStatus::Missing => Self::Missing,
ConversationPreviewStatus::Partial => Self::Partial,
ConversationPreviewStatus::Unavailable => Self::Unavailable,
}
}
}
/// One recent turn surfaced in a conversation summary's log fallback.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConversationTurnWorkPreviewDto {
/// Nature of the turn (`prompt`/`response`/`toolActivity`).
pub role: TurnRole,
/// Origin of the turn (human operator or a delegating agent).
pub source: TicketWorkSourceDto,
/// Timestamp (epoch milliseconds) of the turn.
pub at_ms: u64,
/// Bounded excerpt of the turn text.
pub text_preview: String,
/// Character length of the original (un-truncated) turn text.
pub text_len: usize,
}
impl From<ConversationTurnWorkPreview> for ConversationTurnWorkPreviewDto {
fn from(turn: ConversationTurnWorkPreview) -> Self {
Self {
role: turn.role,
source: turn.source.into(),
at_ms: turn.at_ms,
text_preview: turn.text_preview,
text_len: turn.text_len,
}
}
}
/// Best-effort, read-only summary of one conversation visible through the tickets.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConversationWorkSummaryDto {
/// The conversation (pair) this summary describes.
pub conversation_id: String,
/// How much could be derived.
pub status: ConversationPreviewStatusDto,
/// Bounded excerpt of the handoff objective, when present.
pub objective_preview: Option<String>,
/// Bounded excerpt of the handoff summary, when present.
pub summary_preview: Option<String>,
/// Character length of the original (un-truncated) handoff summary (`0` when none).
pub summary_len: usize,
/// Cursor (last turn id) covered by the handoff summary, when present.
pub up_to: Option<String>,
/// Bounded, recent turns from the log fallback.
pub recent_turns: Vec<ConversationTurnWorkPreviewDto>,
}
impl From<ConversationWorkSummary> for ConversationWorkSummaryDto {
fn from(summary: ConversationWorkSummary) -> Self {
Self {
conversation_id: summary.conversation_id.to_string(),
status: summary.status.into(),
objective_preview: summary.objective_preview,
summary_preview: summary.summary_preview,
summary_len: summary.summary_len,
up_to: summary.up_to.map(|cursor| cursor.to_string()),
recent_turns: summary
.recent_turns
.into_iter()
.map(ConversationTurnWorkPreviewDto::from)
.collect(),
}
}
}
/// Project-level read model for conversation/delegation UX.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectWorkStateDto {
/// Manifest agents in manifest order, enriched with live/busy state.
pub agents: Vec<AgentWorkStateDto>,
/// Best-effort summaries of the conversations referenced by the tickets,
/// joined frontend-side via `tickets[].conversationId`.
pub conversations: Vec<ConversationWorkSummaryDto>,
}
impl From<ProjectWorkState> for ProjectWorkStateDto {
@ -1634,6 +1729,11 @@ impl From<ProjectWorkState> for ProjectWorkStateDto {
.collect(),
})
.collect(),
conversations: state
.conversations
.into_iter()
.map(ConversationWorkSummaryDto::from)
.collect(),
}
}
}

View File

@ -110,6 +110,25 @@ impl application::HandoffProvider for AppHandoffProvider {
}
}
/// Implémente [`ConversationLogProvider`](application::ConversationLogProvider) (Lot C)
/// en matérialisant un [`FsConversationLog`] ciblant le **project root** courant.
///
/// Jumeau stateless de [`AppHandoffProvider`] : le read-model work-state est unique
/// pour tous les projets, alors que le log est **par project root**
/// (`<root>/.ideai/conversations/`). On construit donc un log frais par appel, ciblant
/// le bon dossier. Sert de **repli** lecture seule (`last(_, 3)`) aux résumés de
/// conversation. Sans état (zéro champ), partagé via un simple `Arc`.
struct AppConversationLogProvider;
impl application::ConversationLogProvider for AppConversationLogProvider {
fn conversation_log_for(
&self,
root: &domain::project::ProjectPath,
) -> Option<Arc<dyn domain::ConversationLog>> {
Some(Arc::new(FsConversationLog::new(root)) as Arc<dyn domain::ConversationLog>)
}
}
/// Implémente [`ProviderSessionProvider`](application::ProviderSessionProvider) (lot
/// P8b) en matérialisant un [`FsProviderSessionStore`] ciblant le **project root** du
/// lancement en cours.
@ -1031,12 +1050,22 @@ impl AppState {
Arc::clone(&terminal_sessions),
Arc::clone(&structured_sessions),
));
let get_project_work_state = Arc::new(GetProjectWorkState::new(
Arc::clone(&contexts_port),
Arc::clone(&live_sessions),
Arc::clone(&input_mediator),
queue_snapshot,
));
// Lot C — résumés de conversation best-effort : on câble les sources par
// project root (handoff = primaire, log = repli `last(_, 3)`). Lecture seule,
// aucune persistance ; un échec de preview ne bloque ni live/busy ni tickets.
let get_project_work_state = Arc::new(
GetProjectWorkState::new(
Arc::clone(&contexts_port),
Arc::clone(&live_sessions),
Arc::clone(&input_mediator),
queue_snapshot,
)
.with_conversation_sources(
Arc::new(AppHandoffProvider) as Arc<dyn application::HandoffProvider>,
Arc::new(AppConversationLogProvider)
as Arc<dyn application::ConversationLogProvider>,
),
);
// --- Limites de session des agents (ARCHITECTURE §21, LS7) ---
// Service pur-ports « détecter → planifier → reprendre » câblé sur l'existant :

View File

@ -222,6 +222,7 @@ fn project_work_state_dto_serialises_live_and_busy_camelcase() {
},
tickets: vec![],
}],
conversations: vec![],
});
let v = serde_json::to_value(&dto).unwrap();
@ -279,6 +280,7 @@ fn project_work_state_dto_serialises_tickets_camelcase() {
},
],
}],
conversations: vec![],
});
let v = serde_json::to_value(&dto).unwrap();
@ -304,6 +306,80 @@ fn project_work_state_dto_serialises_tickets_camelcase() {
assert_eq!(next["requesterLabel"], "User");
}
#[test]
fn project_work_state_dto_serialises_conversations_camelcase() {
use application::{
ConversationPreviewStatus, ConversationTurnWorkPreview, ConversationWorkSummary,
};
use domain::{ConversationId, TurnId, TurnRole};
let requester = AgentId::from_uuid(Uuid::from_u128(20));
let ready_conv = ConversationId::from_uuid(Uuid::from_u128(40));
let missing_conv = ConversationId::from_uuid(Uuid::from_u128(41));
let up_to = TurnId::from_uuid(Uuid::from_u128(50));
let dto = app_tauri_lib::dto::ProjectWorkStateDto::from(ProjectWorkState {
agents: vec![],
conversations: vec![
ConversationWorkSummary {
conversation_id: ready_conv,
status: ConversationPreviewStatus::Ready,
objective_preview: Some("Livrer le lot C".to_owned()),
summary_preview: Some("Résumé du fil".to_owned()),
summary_len: 1024,
up_to: Some(up_to),
recent_turns: vec![],
},
ConversationWorkSummary {
conversation_id: missing_conv,
status: ConversationPreviewStatus::Missing,
objective_preview: None,
summary_preview: None,
summary_len: 0,
up_to: None,
recent_turns: vec![ConversationTurnWorkPreview {
role: TurnRole::Prompt,
source: TicketWorkSource::Agent {
agent_id: requester,
},
at_ms: 1_700,
text_preview: "Analyse le module".to_owned(),
text_len: 17,
}],
},
],
});
let v = serde_json::to_value(&dto).unwrap();
let ready = &v["conversations"][0];
assert_eq!(ready["conversationId"], ready_conv.to_string());
assert_eq!(ready["status"], "ready");
assert_eq!(ready["objectivePreview"], "Livrer le lot C");
assert_eq!(ready["summaryPreview"], "Résumé du fil");
assert_eq!(ready["summaryLen"], 1024);
assert_eq!(ready["upTo"], up_to.to_string());
assert_eq!(ready["recentTurns"], json!([]));
// No snake_case leak.
assert!(ready.get("conversation_id").is_none());
assert!(ready.get("summary_preview").is_none());
assert!(ready.get("up_to").is_none());
let missing = &v["conversations"][1];
assert_eq!(missing["status"], "missing");
// Absent previews surface as null (predictable, not omitted).
assert!(missing["summaryPreview"].is_null());
assert!(missing["upTo"].is_null());
let turn = &missing["recentTurns"][0];
assert_eq!(turn["role"], "prompt");
assert_eq!(turn["source"]["kind"], "agent");
assert_eq!(turn["source"]["agentId"], requester.to_string());
assert_eq!(turn["atMs"], 1_700);
assert_eq!(turn["textPreview"], "Analyse le module");
assert_eq!(turn["textLen"], 17);
assert!(turn.get("at_ms").is_none());
assert!(turn.get("text_preview").is_none());
}
#[test]
fn launch_agent_request_carries_conversation_id_for_resume() {
let raw = json!({

View File

@ -119,6 +119,8 @@ pub use terminal::{
};
pub use window::{MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput};
pub use workstate::{
AgentTicketState, AgentWorkState, GetProjectWorkState, GetProjectWorkStateInput,
LiveWorkSession, ProjectWorkState, TicketWorkSource, TicketWorkStatus,
AgentTicketState, AgentWorkState, ConversationLogProvider, ConversationPreviewStatus,
ConversationTurnWorkPreview, ConversationWorkSummary, GetProjectWorkState,
GetProjectWorkStateInput, LiveWorkSession, ProjectWorkState, TicketWorkSource,
TicketWorkStatus,
};

View File

@ -5,18 +5,20 @@
//! beyond loading the manifest through the existing context store and creates no
//! durable projection.
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use domain::input::{AgentBusyState, InputSource};
use domain::ports::AgentContextStore;
use domain::{
AgentId, AgentQueueSnapshot, ConversationId, InputMediator, NodeId, ProfileId, Project,
QueuedTicketSnapshot, SessionId, TicketId,
AgentId, AgentQueueSnapshot, ConversationId, ConversationLog, ConversationTurn, Handoff,
HandoffStore, InputMediator, NodeId, ProfileId, Project, ProjectPath, QueuedTicketSnapshot,
SessionId, TicketId, TurnId, TurnRole,
};
use crate::error::AppError;
use crate::terminal::{LiveSessionKind, LiveSessionSnapshot, LiveSessions};
use crate::HandoffProvider;
/// Maximum length (in characters) of a derived [`AgentTicketState::task_preview`].
///
@ -24,6 +26,18 @@ use crate::terminal::{LiveSessionKind, LiveSessionSnapshot, LiveSessions};
/// excerpt, with [`AgentTicketState::task_len`] signalling there is more.
const TASK_PREVIEW_MAX_CHARS: usize = 160;
/// Maximum length (in characters) of a conversation [`ConversationWorkSummary::summary_preview`].
const HANDOFF_PREVIEW_MAX_CHARS: usize = 480;
/// Maximum length (in characters) of a conversation [`ConversationWorkSummary::objective_preview`].
const OBJECTIVE_PREVIEW_MAX_CHARS: usize = 160;
/// Maximum number of recent turns surfaced in a [`ConversationWorkSummary::recent_turns`] fallback.
const RECENT_TURNS_MAX: usize = 3;
/// Maximum length (in characters) of a [`ConversationTurnWorkPreview::text_preview`].
const TURN_PREVIEW_MAX_CHARS: usize = 220;
/// Input for [`GetProjectWorkState::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetProjectWorkStateInput {
@ -36,6 +50,75 @@ pub struct GetProjectWorkStateInput {
pub struct ProjectWorkState {
/// Agents in manifest order.
pub agents: Vec<AgentWorkState>,
/// Best-effort summaries of the conversations visible through the agents'
/// tickets, joined frontend-side via `tickets[].conversation_id`. Built from
/// the [`HandoffStore`] (primary) with a bounded [`ConversationLog`] fallback;
/// a preview failure never blocks `agents`.
pub conversations: Vec<ConversationWorkSummary>,
}
/// Best-effort, read-only summary of one conversation visible through the tickets.
///
/// Derived live from the [`HandoffStore`] (primary source) with a bounded
/// [`ConversationLog`] fallback — never a durable projection, never a repair of the
/// underlying log/handoff. Joined frontend-side to tickets via
/// [`ConversationWorkSummary::conversation_id`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConversationWorkSummary {
/// The conversation (pair) this summary describes.
pub conversation_id: ConversationId,
/// How much could be derived (see [`ConversationPreviewStatus`]).
pub status: ConversationPreviewStatus,
/// Bounded excerpt of the handoff objective, when present.
pub objective_preview: Option<String>,
/// Bounded excerpt of the handoff summary, when a handoff is readable.
pub summary_preview: Option<String>,
/// Character length of the **original** (un-truncated) handoff summary (`0` when none).
pub summary_len: usize,
/// Cursor (last [`TurnId`]) covered by the handoff summary, when present.
pub up_to: Option<TurnId>,
/// Bounded, recent turns from the log fallback (empty when a handoff is `Ready`).
pub recent_turns: Vec<ConversationTurnWorkPreview>,
}
/// How much of a [`ConversationWorkSummary`] could be derived, best-effort.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConversationPreviewStatus {
/// A handoff was present and usable: summary/objective/cursor are filled.
Ready,
/// No handoff yet; `recent_turns` carries the log fallback if any.
Missing,
/// The handoff was unreadable but the log fallback was readable.
Partial,
/// Neither the handoff nor the log could be read.
Unavailable,
}
/// One recent turn, projected for the conversation-summary fallback.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConversationTurnWorkPreview {
/// Nature of the turn (prompt, response, tool activity).
pub role: TurnRole,
/// Origin of the turn (human operator or a delegating agent).
pub source: TicketWorkSource,
/// Timestamp (epoch milliseconds) of the turn.
pub at_ms: u64,
/// Bounded, whitespace-normalised excerpt of the turn text.
pub text_preview: String,
/// Character length of the **original** (un-truncated) turn text.
pub text_len: usize,
}
/// Builds the [`ConversationLog`] bound to a given project `root`, best-effort.
///
/// Twin of [`HandoffProvider`]: the work-state read model is a singleton (the project
/// root arrives per call via [`GetProjectWorkStateInput`]) while the `Fs*` adapters fix
/// their root at construction. This port materialises the log targeting the **right**
/// folder on each call; `None` ⇒ no log fallback (zero regression for call
/// sites/tests that do not wire it). Implemented in `app-tauri` (sole owner of `Fs*`).
pub trait ConversationLogProvider: Send + Sync {
/// Builds the [`ConversationLog`] whose persistence targets `root`, or `None`.
fn conversation_log_for(&self, root: &ProjectPath) -> Option<Arc<dyn ConversationLog>>;
}
/// Read model for one manifest agent.
@ -128,10 +211,17 @@ pub struct GetProjectWorkState {
live: Arc<LiveSessions>,
input: Arc<dyn InputMediator>,
queue: Arc<dyn AgentQueueSnapshot>,
/// Per-root handoff source (primary), wired best-effort; `None` ⇒ no summaries.
handoffs: Option<Arc<dyn HandoffProvider>>,
/// Per-root log source (fallback), wired best-effort; `None` ⇒ no fallback turns.
logs: Option<Arc<dyn ConversationLogProvider>>,
}
impl GetProjectWorkState {
/// Builds the read-model use case from existing stores/registries.
///
/// Conversation summaries are off by default; wire them with
/// [`Self::with_conversation_sources`].
#[must_use]
pub fn new(
contexts: Arc<dyn AgentContextStore>,
@ -144,9 +234,25 @@ impl GetProjectWorkState {
live,
input,
queue,
handoffs: None,
logs: None,
}
}
/// Wires the best-effort conversation-summary sources (Lot C): the handoff
/// store (primary) and the conversation log (bounded fallback), both resolved
/// per project root. Builder so existing call sites/tests stay unchanged.
#[must_use]
pub fn with_conversation_sources(
mut self,
handoffs: Arc<dyn HandoffProvider>,
logs: Arc<dyn ConversationLogProvider>,
) -> Self {
self.handoffs = Some(handoffs);
self.logs = Some(logs);
self
}
/// Executes the read-only aggregation.
///
/// # Errors
@ -192,7 +298,169 @@ impl GetProjectWorkState {
})
})
.collect::<Result<Vec<_>, AppError>>()?;
Ok(ProjectWorkState { agents })
// Lot C — best-effort conversation summaries. Only the conversations
// visible through the projected tickets are summarised, deduplicated in
// first-seen order. A preview failure degrades the per-conversation status
// (never an `AppError`): `agents`/live/busy/tickets stay intact above.
let conversations = self
.summarise_conversations(&input.project.root, &agents)
.await;
Ok(ProjectWorkState {
agents,
conversations,
})
}
/// Derives the best-effort summaries of the conversations referenced by the
/// projected tickets. Resolves the per-root handoff/log stores once, then folds
/// each distinct conversation id through [`conversation_summary`].
async fn summarise_conversations(
&self,
root: &ProjectPath,
agents: &[AgentWorkState],
) -> Vec<ConversationWorkSummary> {
let conversation_ids = distinct_conversation_ids(agents);
if conversation_ids.is_empty() {
return Vec::new();
}
let handoff_store = self
.handoffs
.as_ref()
.and_then(|provider| provider.handoff_store_for(root));
let log_store = self
.logs
.as_ref()
.and_then(|provider| provider.conversation_log_for(root));
let mut summaries = Vec::with_capacity(conversation_ids.len());
for conversation in conversation_ids {
summaries.push(conversation_summary(conversation, &handoff_store, &log_store).await);
}
summaries
}
}
/// Collects the conversation ids referenced by the agents' tickets, deduplicated in
/// first-seen (FIFO) order so the summary list mirrors the panel's ordering.
fn distinct_conversation_ids(agents: &[AgentWorkState]) -> Vec<ConversationId> {
let mut seen = HashSet::new();
let mut ids = Vec::new();
for agent in agents {
for ticket in &agent.tickets {
if seen.insert(ticket.conversation_id) {
ids.push(ticket.conversation_id);
}
}
}
ids
}
/// Derives one conversation summary, best-effort, following the Lot C algorithm:
/// handoff present ⇒ `Ready`; absent ⇒ `Missing` (+ log fallback); unreadable but
/// log readable ⇒ `Partial`; both KO ⇒ `Unavailable`. Never returns an error.
async fn conversation_summary(
conversation: ConversationId,
handoff_store: &Option<Arc<dyn HandoffStore>>,
log_store: &Option<Arc<dyn ConversationLog>>,
) -> ConversationWorkSummary {
// A handoff store absent from the wiring is treated as "no handoff yet"
// (`Missing` path), never as an error: the read model stays best-effort.
let handoff = match handoff_store {
Some(store) => Some(store.load(conversation).await),
None => None,
};
match handoff {
Some(Ok(Some(handoff))) => ready_summary(conversation, &handoff),
Some(Ok(None)) | None => {
// Handoff legitimately absent: status stays Missing even if the log
// fallback is itself unreadable (then simply no recent turns).
let recent_turns = recent_turns(log_store, conversation)
.await
.unwrap_or_default();
ConversationWorkSummary {
conversation_id: conversation,
status: ConversationPreviewStatus::Missing,
objective_preview: None,
summary_preview: None,
summary_len: 0,
up_to: None,
recent_turns,
}
}
Some(Err(_)) => match recent_turns(log_store, conversation).await {
// Handoff unreadable but the log answered: partial view from the turns.
Some(recent_turns) => ConversationWorkSummary {
conversation_id: conversation,
status: ConversationPreviewStatus::Partial,
objective_preview: None,
summary_preview: None,
summary_len: 0,
up_to: None,
recent_turns,
},
// Both sources failed: nothing usable to show.
None => ConversationWorkSummary {
conversation_id: conversation,
status: ConversationPreviewStatus::Unavailable,
objective_preview: None,
summary_preview: None,
summary_len: 0,
up_to: None,
recent_turns: Vec::new(),
},
},
}
}
/// Projects a readable [`Handoff`] into a `Ready` summary with bounded previews.
fn ready_summary(conversation: ConversationId, handoff: &Handoff) -> ConversationWorkSummary {
ConversationWorkSummary {
conversation_id: conversation,
status: ConversationPreviewStatus::Ready,
objective_preview: handoff
.objective
.as_deref()
.map(|objective| preview(objective, OBJECTIVE_PREVIEW_MAX_CHARS)),
summary_preview: Some(preview(&handoff.summary_md, HANDOFF_PREVIEW_MAX_CHARS)),
summary_len: handoff.summary_md.chars().count(),
up_to: Some(handoff.up_to),
recent_turns: Vec::new(),
}
}
/// Reads the bounded recent-turns fallback for `conversation`.
///
/// Returns `Some(turns)` (possibly empty) when a log store is wired and answers,
/// `None` when there is no log store or the read failed — the caller uses that
/// distinction to separate `Partial` from `Unavailable`. Never reads the full thread
/// (only [`ConversationLog::last`]) and re-caps defensively to [`RECENT_TURNS_MAX`].
async fn recent_turns(
log_store: &Option<Arc<dyn ConversationLog>>,
conversation: ConversationId,
) -> Option<Vec<ConversationTurnWorkPreview>> {
let store = log_store.as_ref()?;
match store.last(conversation, RECENT_TURNS_MAX).await {
Ok(turns) => Some(
turns
.into_iter()
.take(RECENT_TURNS_MAX)
.map(turn_preview)
.collect(),
),
Err(_) => None,
}
}
/// Projects one [`ConversationTurn`] into its bounded read-model preview.
fn turn_preview(turn: ConversationTurn) -> ConversationTurnWorkPreview {
ConversationTurnWorkPreview {
role: turn.role,
source: turn.source.into(),
at_ms: turn.at_ms,
text_preview: preview(&turn.text, TURN_PREVIEW_MAX_CHARS),
text_len: turn.text.chars().count(),
}
}
@ -216,17 +484,24 @@ fn ticket_state(snapshot: QueuedTicketSnapshot, busy_ticket: Option<TicketId>) -
}
}
/// Builds a bounded, whitespace-normalised excerpt of a task for the UI panel.
/// Builds the bounded task excerpt for the UI panel (see [`preview`]).
///
/// The original length is reported separately as [`AgentTicketState::task_len`].
fn task_preview(task: &str) -> String {
preview(task, TASK_PREVIEW_MAX_CHARS)
}
/// Builds a bounded, whitespace-normalised excerpt of `text` for the UI.
///
/// Trims, collapses any run of whitespace to a single space, then truncates to
/// [`TASK_PREVIEW_MAX_CHARS`] characters (never mid-codepoint). The original length
/// is reported separately as [`AgentTicketState::task_len`].
fn task_preview(task: &str) -> String {
let normalised = task.split_whitespace().collect::<Vec<_>>().join(" ");
if normalised.chars().count() <= TASK_PREVIEW_MAX_CHARS {
/// `max_chars` characters (never mid-codepoint). The original length is reported
/// separately by each caller's `*_len` field.
fn preview(text: &str, max_chars: usize) -> String {
let normalised = text.split_whitespace().collect::<Vec<_>>().join(" ");
if normalised.chars().count() <= max_chars {
normalised
} else {
normalised.chars().take(TASK_PREVIEW_MAX_CHARS).collect()
normalised.chars().take(max_chars).collect()
}
}

View File

@ -8,8 +8,9 @@ use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use application::{
GetProjectWorkState, GetProjectWorkStateInput, LiveSessionKind, LiveSessions,
StructuredSessions, TerminalSessions, TicketWorkSource, TicketWorkStatus,
ConversationLogProvider, ConversationPreviewStatus, GetProjectWorkState,
GetProjectWorkStateInput, HandoffProvider, LiveSessionKind, LiveSessions, StructuredSessions,
TerminalSessions, TicketWorkSource, TicketWorkStatus,
};
use domain::mailbox::{
AgentQueueSnapshot, MailboxError, PendingReply, QueuedTicketSnapshot, Ticket,
@ -18,9 +19,10 @@ use domain::ports::{
AgentContextStore, AgentSession, AgentSessionError, PtyHandle, ReplyStream, StoreError,
};
use domain::{
Agent, AgentBusyState, AgentId, AgentManifest, AgentOrigin, ConversationId, InputMediator,
InputSource, ManifestEntry, MarkdownDoc, NodeId, ProfileId, Project, ProjectId, ProjectPath,
PtySize, RemoteRef, SessionId, SessionKind, TerminalSession, TicketId,
Agent, AgentBusyState, AgentId, AgentManifest, AgentOrigin, ConversationId, ConversationLog,
ConversationTurn, Handoff, HandoffStore, InputMediator, InputSource, ManifestEntry,
MarkdownDoc, NodeId, ProfileId, Project, ProjectId, ProjectPath, PtySize, RemoteRef, SessionId,
SessionKind, TerminalSession, TicketId, TurnId, TurnRole,
};
use uuid::Uuid;
@ -162,6 +164,131 @@ impl AgentQueueSnapshot for FakeQueue {
}
}
/// Configurable per-conversation outcome for the fake handoff store.
///
/// Absence is modelled by simply not configuring a conversation (see the `_` arm
/// of [`FakeHandoffStore::load`]), so it needs no dedicated variant.
#[derive(Clone)]
enum HandoffOutcome {
/// A readable handoff (status Ready).
Present(Handoff),
/// An unreadable handoff (status Partial/Unavailable depending on the log).
Error,
}
#[derive(Default)]
struct FakeHandoffStore {
outcomes: Mutex<HashMap<ConversationId, HandoffOutcome>>,
}
impl FakeHandoffStore {
fn set(&self, conversation: ConversationId, outcome: HandoffOutcome) {
self.outcomes.lock().unwrap().insert(conversation, outcome);
}
}
#[async_trait]
impl HandoffStore for FakeHandoffStore {
async fn load(&self, conversation: ConversationId) -> Result<Option<Handoff>, StoreError> {
match self.outcomes.lock().unwrap().get(&conversation) {
Some(HandoffOutcome::Present(handoff)) => Ok(Some(handoff.clone())),
Some(HandoffOutcome::Error) => Err(StoreError::Io("handoff unreadable".to_owned())),
// Unconfigured conversation ⇒ no handoff yet (never an error).
None => Ok(None),
}
}
async fn save(
&self,
_conversation: ConversationId,
_handoff: Handoff,
) -> Result<(), StoreError> {
Ok(())
}
}
/// Configurable per-conversation outcome for the fake conversation log.
#[derive(Clone)]
enum LogOutcome {
/// A readable thread (possibly empty).
Turns(Vec<ConversationTurn>),
/// An unreadable thread.
Error,
}
#[derive(Default)]
struct FakeLog {
outcomes: Mutex<HashMap<ConversationId, LogOutcome>>,
}
impl FakeLog {
fn set(&self, conversation: ConversationId, outcome: LogOutcome) {
self.outcomes.lock().unwrap().insert(conversation, outcome);
}
}
#[async_trait]
impl ConversationLog for FakeLog {
async fn append(
&self,
_conversation: ConversationId,
_turn: ConversationTurn,
) -> Result<(), StoreError> {
Ok(())
}
async fn read(
&self,
_conversation: ConversationId,
_since: Option<TurnId>,
) -> Result<Vec<ConversationTurn>, StoreError> {
Ok(Vec::new())
}
async fn last(
&self,
conversation: ConversationId,
n: usize,
) -> Result<Vec<ConversationTurn>, StoreError> {
match self.outcomes.lock().unwrap().get(&conversation) {
Some(LogOutcome::Turns(turns)) => {
let start = turns.len().saturating_sub(n);
Ok(turns[start..].to_vec())
}
Some(LogOutcome::Error) => Err(StoreError::Io("log unreadable".to_owned())),
None => Ok(Vec::new()),
}
}
}
/// Stateless providers handing the same fake stores back regardless of root.
struct FakeHandoffProvider(Arc<FakeHandoffStore>);
impl HandoffProvider for FakeHandoffProvider {
fn handoff_store_for(&self, _root: &ProjectPath) -> Option<Arc<dyn HandoffStore>> {
Some(Arc::clone(&self.0) as Arc<dyn HandoffStore>)
}
}
struct FakeLogProvider(Arc<FakeLog>);
impl ConversationLogProvider for FakeLogProvider {
fn conversation_log_for(&self, _root: &ProjectPath) -> Option<Arc<dyn ConversationLog>> {
Some(Arc::clone(&self.0) as Arc<dyn ConversationLog>)
}
}
fn turn(id: u128, conversation: ConversationId, role: TurnRole, text: &str) -> ConversationTurn {
ConversationTurn::new(
TurnId::from_uuid(Uuid::from_u128(id)),
conversation,
1_700,
InputSource::Human,
role,
text,
)
}
fn snapshot(
id: u128,
position: u32,
@ -259,6 +386,52 @@ fn fixture(agents: &[Agent]) -> Fixture {
}
}
/// Conversation id minted by [`snapshot`] for a ticket of the given `id`.
fn conv_of(id: u128) -> ConversationId {
ConversationId::from_uuid(Uuid::from_u128(id + 1000))
}
struct ConvFixture {
usecase: GetProjectWorkState,
queue: Arc<FakeQueue>,
input: Arc<FakeInput>,
handoffs: Arc<FakeHandoffStore>,
logs: Arc<FakeLog>,
project: Project,
}
/// Fixture wiring the best-effort conversation sources (handoff + log) onto the
/// read model, exposing the fake stores so each test configures their outcomes.
fn conv_fixture(agents: &[Agent]) -> ConvFixture {
let pty = Arc::new(TerminalSessions::new());
let structured = Arc::new(StructuredSessions::new());
let live = Arc::new(LiveSessions::new(pty, structured));
let input = Arc::new(FakeInput::default());
let queue = Arc::new(FakeQueue::default());
let handoffs = Arc::new(FakeHandoffStore::default());
let logs = Arc::new(FakeLog::default());
let usecase = GetProjectWorkState::new(
Arc::new(FakeContexts {
manifest: manifest(agents),
}),
live,
Arc::clone(&input) as Arc<dyn InputMediator>,
Arc::clone(&queue) as Arc<dyn AgentQueueSnapshot>,
)
.with_conversation_sources(
Arc::new(FakeHandoffProvider(Arc::clone(&handoffs))) as Arc<dyn HandoffProvider>,
Arc::new(FakeLogProvider(Arc::clone(&logs))) as Arc<dyn ConversationLogProvider>,
);
ConvFixture {
usecase,
queue,
input,
handoffs,
logs,
project: project(),
}
}
#[tokio::test]
async fn workstate_lists_manifest_agents_idle_without_live_sessions() {
let a = agent(10, "alpha");
@ -517,3 +690,292 @@ async fn workstate_maps_human_and_agent_ticket_sources() {
);
assert_eq!(tickets[1].requester_label, "Main");
}
// ---------------------------------------------------------------------------
// Lot C — conversation summaries (best-effort, read-only)
// ---------------------------------------------------------------------------
#[tokio::test]
async fn workstate_has_no_conversations_without_tickets() {
let a = agent(10, "alpha");
let f = conv_fixture(std::slice::from_ref(&a));
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
assert!(out.conversations.is_empty());
}
#[tokio::test]
async fn workstate_dedups_conversation_ids_from_tickets() {
let a = agent(10, "alpha");
let f = conv_fixture(std::slice::from_ref(&a));
let from = aid(20);
let shared = ConversationId::from_uuid(Uuid::from_u128(7777));
// Two tickets pointing at the *same* conversation must yield one summary.
let mut t1 = snapshot(1, 0, InputSource::agent(from), "Main", "first");
t1.conversation = shared;
let mut t2 = snapshot(2, 1, InputSource::agent(from), "Main", "second");
t2.conversation = shared;
f.queue.set(a.id, vec![t1, t2]);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
assert_eq!(out.conversations.len(), 1);
assert_eq!(out.conversations[0].conversation_id, shared);
}
#[tokio::test]
async fn workstate_conversation_ready_from_handoff() {
let a = agent(10, "alpha");
let f = conv_fixture(std::slice::from_ref(&a));
let from = aid(20);
f.queue.set(
a.id,
vec![snapshot(1, 0, InputSource::agent(from), "Main", "task")],
);
let conv = conv_of(1);
let up_to = TurnId::from_uuid(Uuid::from_u128(500));
f.handoffs.set(
conv,
HandoffOutcome::Present(Handoff::new(
"Résumé du fil",
up_to,
Some("Livrer le lot C".to_owned()),
)),
);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
let summary = &out.conversations[0];
assert_eq!(summary.conversation_id, conv);
assert_eq!(summary.status, ConversationPreviewStatus::Ready);
assert_eq!(summary.summary_preview.as_deref(), Some("Résumé du fil"));
assert_eq!(summary.summary_len, "Résumé du fil".chars().count());
assert_eq!(
summary.objective_preview.as_deref(),
Some("Livrer le lot C")
);
assert_eq!(summary.up_to, Some(up_to));
assert!(
summary.recent_turns.is_empty(),
"Ready uses the handoff, not turns"
);
}
#[tokio::test]
async fn workstate_conversation_missing_with_bounded_recent_turns() {
let a = agent(10, "alpha");
let f = conv_fixture(std::slice::from_ref(&a));
let from = aid(20);
f.queue.set(
a.id,
vec![snapshot(1, 0, InputSource::agent(from), "Main", "task")],
);
let conv = conv_of(1);
// No handoff (Absent) but a readable log of 5 turns ⇒ Missing + last 3.
f.logs.set(
conv,
LogOutcome::Turns(vec![
turn(1, conv, TurnRole::Prompt, "t1"),
turn(2, conv, TurnRole::Response, "t2"),
turn(3, conv, TurnRole::Prompt, "t3"),
turn(4, conv, TurnRole::Response, "t4"),
turn(5, conv, TurnRole::Prompt, "t5"),
]),
);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
let summary = &out.conversations[0];
assert_eq!(summary.status, ConversationPreviewStatus::Missing);
assert!(summary.summary_preview.is_none());
assert_eq!(summary.summary_len, 0);
assert_eq!(summary.recent_turns.len(), 3, "bounded to RECENT_TURNS_MAX");
// The *last* three turns, in order.
assert_eq!(summary.recent_turns[0].text_preview, "t3");
assert_eq!(summary.recent_turns[2].text_preview, "t5");
}
#[tokio::test]
async fn workstate_conversation_partial_when_handoff_errors_but_log_ok() {
let a = agent(10, "alpha");
let f = conv_fixture(std::slice::from_ref(&a));
let from = aid(20);
f.queue.set(
a.id,
vec![snapshot(1, 0, InputSource::agent(from), "Main", "task")],
);
let conv = conv_of(1);
f.handoffs.set(conv, HandoffOutcome::Error);
f.logs.set(
conv,
LogOutcome::Turns(vec![turn(1, conv, TurnRole::Response, "only turn")]),
);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
let summary = &out.conversations[0];
assert_eq!(summary.status, ConversationPreviewStatus::Partial);
assert!(summary.summary_preview.is_none(), "no summary on partial");
assert_eq!(summary.recent_turns.len(), 1);
assert_eq!(summary.recent_turns[0].text_preview, "only turn");
}
#[tokio::test]
async fn workstate_conversation_unavailable_when_handoff_and_log_fail() {
let a = agent(10, "alpha");
let f = conv_fixture(std::slice::from_ref(&a));
let from = aid(20);
f.queue.set(
a.id,
vec![snapshot(1, 0, InputSource::agent(from), "Main", "task")],
);
let conv = conv_of(1);
f.handoffs.set(conv, HandoffOutcome::Error);
f.logs.set(conv, LogOutcome::Error);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
let summary = &out.conversations[0];
assert_eq!(summary.status, ConversationPreviewStatus::Unavailable);
assert!(summary.summary_preview.is_none());
assert!(summary.recent_turns.is_empty());
}
#[tokio::test]
async fn workstate_preview_failure_preserves_agents_live_busy_tickets() {
let a = agent(10, "alpha");
let f = conv_fixture(std::slice::from_ref(&a));
let from = aid(20);
f.queue.set(
a.id,
vec![snapshot(1, 0, InputSource::agent(from), "Main", "task")],
);
let conv = conv_of(1);
// Both sources KO for this conversation ⇒ Unavailable, but the rest stands.
f.handoffs.set(conv, HandoffOutcome::Error);
f.logs.set(conv, LogOutcome::Error);
let busy = AgentBusyState::Busy {
ticket: ticket_id(1),
since_ms: 9,
};
f.input.set_busy(a.id, busy);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
// Agents/busy/tickets fully intact despite the failed preview.
assert_eq!(out.agents.len(), 1);
assert_eq!(out.agents[0].busy, busy);
assert_eq!(out.agents[0].tickets.len(), 1);
assert_eq!(
out.agents[0].tickets[0].status,
TicketWorkStatus::InProgress
);
assert_eq!(
out.conversations[0].status,
ConversationPreviewStatus::Unavailable
);
}
#[tokio::test]
async fn workstate_conversation_previews_truncated_and_normalised() {
let a = agent(10, "alpha");
let f = conv_fixture(std::slice::from_ref(&a));
let from = aid(20);
f.queue.set(
a.id,
vec![snapshot(1, 0, InputSource::agent(from), "Main", "task")],
);
let conv = conv_of(1);
// Whitespace runs to normalise; lengths well over each cap (480 / 160).
// Each " s " collapses to one " s" (2 chars) ⇒ pick counts past the caps.
let long_summary = format!("start{}end", " s ".repeat(300));
let long_objective = format!("goal{}done", " o ".repeat(120));
f.handoffs.set(
conv,
HandoffOutcome::Present(Handoff::new(
long_summary.clone(),
TurnId::from_uuid(Uuid::from_u128(1)),
Some(long_objective),
)),
);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
let summary = &out.conversations[0];
let preview = summary.summary_preview.as_deref().unwrap();
assert_eq!(preview.chars().count(), 480, "summary capped");
assert!(!preview.contains(" "), "summary whitespace normalised");
assert_eq!(
summary.summary_len,
long_summary.chars().count(),
"original length preserved"
);
let objective = summary.objective_preview.as_deref().unwrap();
assert_eq!(objective.chars().count(), 160, "objective capped");
assert!(!objective.contains(" "), "objective whitespace normalised");
}
#[tokio::test]
async fn workstate_conversation_turn_text_preview_truncated() {
let a = agent(10, "alpha");
let f = conv_fixture(std::slice::from_ref(&a));
let from = aid(20);
f.queue.set(
a.id,
vec![snapshot(1, 0, InputSource::agent(from), "Main", "task")],
);
let conv = conv_of(1);
let long_text = format!("begin{}fin", " w ".repeat(120));
f.logs.set(
conv,
LogOutcome::Turns(vec![turn(1, conv, TurnRole::Prompt, &long_text)]),
);
let out = f
.usecase
.execute(GetProjectWorkStateInput { project: f.project })
.await
.unwrap();
let turn_preview = &out.conversations[0].recent_turns[0];
assert_eq!(
turn_preview.text_preview.chars().count(),
220,
"turn capped"
);
assert!(!turn_preview.text_preview.contains(" "), "normalised");
assert_eq!(turn_preview.text_len, long_text.chars().count());
}

View File

@ -1662,7 +1662,9 @@ export class MockWorkStateGateway implements WorkStateGateway {
}
async getProjectWorkState(projectId: string): Promise<ProjectWorkState> {
return structuredClone(this.states.get(projectId) ?? { agents: [] });
return structuredClone(
this.states.get(projectId) ?? { agents: [], conversations: [] },
);
}
}
@ -1670,6 +1672,7 @@ type LegacyProjectWorkState = {
agents: Array<Omit<ProjectWorkState["agents"][number], "tickets"> & {
tickets?: ProjectWorkState["agents"][number]["tickets"];
}>;
conversations?: ProjectWorkState["conversations"];
};
function normalizeProjectWorkState(
@ -1680,6 +1683,7 @@ function normalizeProjectWorkState(
...structuredClone(agent),
tickets: structuredClone(agent.tickets ?? []),
})),
conversations: structuredClone(state.conversations ?? []),
};
}

View File

@ -169,6 +169,33 @@ export interface AgentTicketState {
taskLen: number;
}
/** Availability of a compact conversation summary in the work-state read-model. */
export type ConversationPreviewStatus =
| "ready"
| "missing"
| "partial"
| "unavailable";
/** Recent turn preview attached to a conversation summary. */
export interface ConversationTurnWorkPreview {
role: "prompt" | "response" | "toolActivity";
source: TicketWorkSource;
atMs: number;
textPreview: string;
textLen: number;
}
/** Compact thread context joined to tickets by conversation id. */
export interface ConversationWorkSummary {
conversationId: string;
status: ConversationPreviewStatus;
objectivePreview: string | null;
summaryPreview: string | null;
summaryLen: number;
upTo: string | null;
recentTurns: ConversationTurnWorkPreview[];
}
/** One agent row in the project work-state read-model. */
export interface AgentWorkState {
agentId: string;
@ -182,6 +209,7 @@ export interface AgentWorkState {
/** Minimal read-only live-state surface for a project. */
export interface ProjectWorkState {
agents: AgentWorkState[];
conversations: ConversationWorkSummary[];
}
// ---------------------------------------------------------------------------

View File

@ -3,7 +3,12 @@
* and idle/busy state from the backend read-model, plus the current input queue.
*/
import type { AgentTicketState, AgentWorkState } from "@/domain";
import type {
AgentTicketState,
AgentWorkState,
ConversationPreviewStatus,
ConversationWorkSummary,
} from "@/domain";
import { Button, Panel, Spinner, cn } from "@/shared";
import { useProjectWorkState } from "./useProjectWorkState";
@ -27,46 +32,105 @@ function requesterLabel(ticket: AgentTicketState): string {
: `Agent ${id}`;
}
function TicketRow({ ticket }: { ticket: AgentTicketState }) {
const truncated = ticket.taskLen > ticket.taskPreview.length;
function summaryLabel(status: ConversationPreviewStatus): string {
switch (status) {
case "ready":
return "Summary";
case "missing":
return "No summary";
case "partial":
return "Partial";
case "unavailable":
return "Unavailable";
}
}
function summaryText(summary: ConversationWorkSummary): string | null {
if (summary.objectivePreview) return `Goal: ${summary.objectivePreview}`;
if (summary.summaryPreview) return summary.summaryPreview;
const lastTurn = summary.recentTurns.at(-1);
return lastTurn ? `Last: ${lastTurn.textPreview}` : null;
}
function ConversationSummaryLine({
summary,
}: {
summary: ConversationWorkSummary;
}) {
const text = summaryText(summary);
if (!text) return null;
return (
<li className="flex min-w-0 items-start gap-2 text-xs text-muted">
<div className="mt-1 flex min-w-0 items-start gap-2 pl-1 text-xs text-muted">
<span
className={cn(
"mt-0.5 shrink-0 rounded-full px-1.5 py-0.5 font-medium",
ticket.status === "inProgress"
? "bg-warning/15 text-warning"
summary.status === "ready"
? "bg-success/10 text-success"
: "bg-raised text-muted",
)}
>
#{ticket.position + 1} {formatStatus(ticket.status)}
{summaryLabel(summary.status)}
</span>
<span className="min-w-0 flex-1">
<span className="font-medium text-content">{requesterLabel(ticket)}</span>
<span className="text-muted"> · </span>
<span className="break-words">{ticket.taskPreview}</span>
{truncated && (
<span
aria-label={`${ticket.taskLen - ticket.taskPreview.length} more characters`}
className="text-muted"
>
{" "}
+{ticket.taskLen - ticket.taskPreview.length}
</span>
)}
</span>
<code
aria-label={`ticket ${shortTicket(ticket.ticketId)}`}
title={ticket.ticketId}
className="mt-0.5 shrink-0 rounded bg-raised px-1.5 py-0.5 text-[11px] text-muted"
>
{shortTicket(ticket.ticketId)}
</code>
<span className="min-w-0 break-words">{text}</span>
</div>
);
}
function TicketRow({
ticket,
summary,
}: {
ticket: AgentTicketState;
summary?: ConversationWorkSummary;
}) {
const truncated = ticket.taskLen > ticket.taskPreview.length;
return (
<li className="min-w-0 text-xs text-muted">
<div className="flex min-w-0 items-start gap-2">
<span
className={cn(
"mt-0.5 shrink-0 rounded-full px-1.5 py-0.5 font-medium",
ticket.status === "inProgress"
? "bg-warning/15 text-warning"
: "bg-raised text-muted",
)}
>
#{ticket.position + 1} {formatStatus(ticket.status)}
</span>
<span className="min-w-0 flex-1">
<span className="font-medium text-content">{requesterLabel(ticket)}</span>
<span className="text-muted"> · </span>
<span className="break-words">{ticket.taskPreview}</span>
{truncated && (
<span
aria-label={`${ticket.taskLen - ticket.taskPreview.length} more characters`}
className="text-muted"
>
{" "}
+{ticket.taskLen - ticket.taskPreview.length}
</span>
)}
</span>
<code
aria-label={`ticket ${shortTicket(ticket.ticketId)}`}
title={ticket.ticketId}
className="mt-0.5 shrink-0 rounded bg-raised px-1.5 py-0.5 text-[11px] text-muted"
>
{shortTicket(ticket.ticketId)}
</code>
</div>
{summary && <ConversationSummaryLine summary={summary} />}
</li>
);
}
function AgentRow({ agent }: { agent: AgentWorkState }) {
function AgentRow({
agent,
conversations,
}: {
agent: AgentWorkState;
conversations: Map<string, ConversationWorkSummary>;
}) {
const live = agent.live !== undefined;
const busy = agent.busy.state === "busy";
const tickets = [...agent.tickets].sort((a, b) => a.position - b.position);
@ -117,7 +181,11 @@ function AgentRow({ agent }: { agent: AgentWorkState }) {
className="mt-2 flex flex-col gap-1"
>
{tickets.map((ticket) => (
<TicketRow key={ticket.ticketId} ticket={ticket} />
<TicketRow
key={ticket.ticketId}
ticket={ticket}
summary={conversations.get(ticket.conversationId)}
/>
))}
</ul>
)}
@ -128,6 +196,12 @@ function AgentRow({ agent }: { agent: AgentWorkState }) {
export function ProjectWorkStatePanel({ projectId }: ProjectWorkStatePanelProps) {
const vm = useProjectWorkState(projectId);
const agents = vm.state?.agents ?? [];
const conversations = new Map(
(vm.state?.conversations ?? []).map((summary) => [
summary.conversationId,
summary,
]),
);
return (
<Panel
@ -161,7 +235,11 @@ export function ProjectWorkStatePanel({ projectId }: ProjectWorkStatePanelProps)
) : (
<ul className="flex flex-col divide-y divide-border">
{agents.map((agent) => (
<AgentRow key={agent.agentId} agent={agent} />
<AgentRow
key={agent.agentId}
agent={agent}
conversations={conversations}
/>
))}
</ul>
)}

View File

@ -189,9 +189,159 @@ describe("ProjectWorkStatePanel", () => {
tickets: [],
},
],
conversations: [],
});
});
it("renders conversation objective and summary joined by conversation id", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-8",
name: "Summarizer",
profileId: "codex",
busy: { state: "busy", ticket: "ticket-goal-abcdef", sinceMs: 123 },
tickets: [
{
ticketId: "ticket-goal-abcdef",
conversationId: "conversation-goal",
position: 0,
status: "inProgress",
source: { kind: "human" },
requesterLabel: "Anthony",
taskPreview: "Continue implementation",
taskLen: 23,
},
{
ticketId: "ticket-summary-abcdef",
conversationId: "conversation-summary",
position: 1,
status: "queued",
source: { kind: "agent", agentId: "agent-main-123456" },
requesterLabel: "Main",
taskPreview: "Follow up",
taskLen: 9,
},
],
},
],
conversations: [
{
conversationId: "conversation-summary",
status: "ready",
objectivePreview: null,
summaryPreview: "Previous turn established the adapter boundary.",
summaryLen: 47,
upTo: "turn-2",
recentTurns: [],
},
{
conversationId: "conversation-goal",
status: "ready",
objectivePreview: "Ship the Work panel summaries",
summaryPreview: "This should be lower priority than the goal.",
summaryLen: 48,
upTo: "turn-1",
recentTurns: [],
},
],
});
renderPanel(workState);
const list = await screen.findByLabelText("Summarizer tickets");
expect(within(list).getAllByText("Summary")).toHaveLength(2);
expect(
within(list).getByText("Goal: Ship the Work panel summaries"),
).toBeTruthy();
expect(
within(list).getByText(
"Previous turn established the adapter boundary.",
),
).toBeTruthy();
});
it("keeps partial and unavailable summaries compact without hiding tickets", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState(PROJECT_ID, {
agents: [
{
agentId: "agent-9",
name: "Contextualizer",
profileId: "claude",
busy: { state: "idle" },
tickets: [
{
ticketId: "ticket-partial-abcdef",
conversationId: "conversation-partial",
position: 0,
status: "inProgress",
source: { kind: "agent", agentId: "agent-qa-123456" },
requesterLabel: "QA",
taskPreview: "Validate partial context",
taskLen: 24,
},
{
ticketId: "ticket-unavailable-abcdef",
conversationId: "conversation-unavailable",
position: 1,
status: "queued",
source: { kind: "human" },
requesterLabel: "Anthony",
taskPreview: "Validate unavailable context",
taskLen: 28,
},
],
},
],
conversations: [
{
conversationId: "conversation-partial",
status: "partial",
objectivePreview: null,
summaryPreview: null,
summaryLen: 0,
upTo: null,
recentTurns: [
{
role: "prompt",
source: { kind: "agent", agentId: "agent-qa-123456" },
atMs: 100,
textPreview: "Only the latest prompt was available.",
textLen: 37,
},
],
},
{
conversationId: "conversation-unavailable",
status: "unavailable",
objectivePreview: null,
summaryPreview: "Summary service unavailable right now.",
summaryLen: 38,
upTo: null,
recentTurns: [],
},
],
});
renderPanel(workState);
const list = await screen.findByLabelText("Contextualizer tickets");
expect(within(list).getByText("#1 In progress")).toBeTruthy();
expect(within(list).getByText("Validate partial context")).toBeTruthy();
expect(within(list).getByText("Partial")).toBeTruthy();
expect(
within(list).getByText("Last: Only the latest prompt was available."),
).toBeTruthy();
expect(within(list).getByText("#2 Queued")).toBeTruthy();
expect(within(list).getByText("Validate unavailable context")).toBeTruthy();
expect(within(list).getByText("Unavailable")).toBeTruthy();
expect(
within(list).getByText("Summary service unavailable right now."),
).toBeTruthy();
});
it("refreshes when a relevant domain event fires", async () => {
const workState = new MockWorkStateGateway();
const system = new MockSystemGateway();