diff --git a/.gitignore b/.gitignore index ac1ff01..2ccb252 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ src-tauri/gen/ .playwright-mcp/ *.local.png screenshots/ +# Console/network dumps captured while verifying a behaviour in the browser +# (e.g. proving the artifact CSP blocks an external request). Throwaway by nature. +*.log # === IDE / OS === .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 261fe78..b65319a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ that section and uses it as the GitHub release description, which the app displa as-is. The install instructions block (after the `` marker) is added by `release.yml` and stays **only** on the GitHub page — it does not appear in the app. +## v1.2.0 + +- New: the active goal (`/goal`) is now displayed — on Flight Deck cards and at the top of the conversation — so you always see what an agent is working toward. +- New: artifacts published by Claude are collected per conversation — an "Artifacts" chip in the composer lists them, and each one shows up inline in the thread. +- New: usage limits that apply to a single model (such as Fable's weekly allowance) now appear in the usage popover, next to the 5-hour and weekly limits. + ## v1.1.2 - Fixed: Claude usage now shows both the 5-hour and weekly limits whenever they exist, instead of only one at a time. diff --git a/CLAUDE.md b/CLAUDE.md index 76fe05b..3ee98e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -128,6 +128,7 @@ Réimplémentation clean-room en Rust du client de l'extension VS Code officiell - **Remote control** : `control_request{subtype:"remote_control", enabled:bool, name?}` → `control_response{response:{session_url, connect_url}}` (doublement niché `response.response`). Bridge santé : `system/bridge_state{state:"disconnected"|"error"}` — **dégrade seulement**, "connected" ne vient QUE de la réponse au control_request. Spawn avec **`--replay-user-messages` inconditionnel** (sans lui le binaire n'émet aucune ligne `user` sur stdout) ; on estampille chaque message envoyé d'un uuid et on supprime l'écho de NOS propres tours (`assembler.sent_user_uuids`, one-shot — l'uuid est consommé quel que soit le contenu du tour, texte comme image-seule) ; tour distant (uuid inconnu) surfacé. **Distinguer live vs historique est OBLIGATOIRE** : `history.rs` n'émet pas de `turn_result` → splicer l'historique regrouperait tous les tours user en haut (régression). - **Prompts de sous-agents ré-émis en `user`** : un prompt que Claude envoie à un sous-agent (`Task`/`Agent`) arrive en live comme une ligne `user` avec un **uuid FRAIS** (donc pas dans `sent_user_uuids`) + **`parent_tool_use_id` = le tool_use qui l'a spawné** ; `isReplay`/`isSidechain`/`isMeta` sont ABSENTS sur le wire live (champs disque-only, comme `sourceToolUseID` — la leçon du skill-body). VÉRIFIÉ contre claude 2.1.203 (probe pilotant le binaire avec les flags de prod + spawn forcé d'un sous-agent). Sans garde, il FUIT en bulle user dans la conversation principale (« comme si je l'avais envoyé ») → `assembler.ingest_user` ne surface la bulle QUE si `parent_tool_use_id.is_none()`. Contrairement à `isMeta`/`sourceToolUseID`, `parent_tool_use_id` est un champ TOP-LEVEL qui survit au stream → discriminant fiable. Les blocs `tool_result` (résultats internes du sous-agent, même parent) restent traités. Miroir de `history.rs` (skip `isSidechain:true` sur disque) + du guard context-meter de `ingest_stream_event`. Test `subagent_prompt_with_parent_is_not_surfaced_as_user_message`. [fix mergé dev — commit `fc40a4f`] - **`reload_plugins`** (`control_request{subtype:"reload_plugins"}`) : hot-reload des plugins d'une session VIVANTE. **VÉRIFIÉ live (2.1.187)** : prend bien en compte un enable/disable écrit dans `~/.claude/settings.json` EN COURS de session (re-scanne AUSSI les SKILLS du plugin) — contredit l'ancien « prend effet au prochain (re)démarrage » : ce message rend le toggle live. Sa réponse de contrôle porte déjà la liste `commands` fraîche (même forme que `initialize` : `response.response.{commands,agents,plugins,mcpServers}`) + le CLI émet un `system/commands_changed` en push. ⚠️ Les skills fournis par un plugin y apparaissent en noms NUS (`brand-guidelines`), PAS `plugin:skill`. +- **`/goal` (feature native Claude Code, v2.1.139+)** : objectif de session — Claude enchaîne les tours jusqu'à ce qu'un petit modèle valide la condition, puis auto-clear. Son état vit dans le **transcript** en lignes `attachment.type:"goal_status"` (`sentinel`+`met` distinguent set / unmet-check / achieved / clear ; un `/goal clear` écrit aussi un `` `Goal cleared:` / `No goal set`). **DISK-ONLY — JAMAIS sur le stdout live** (seul l'echo `/goal` remonte live via `--replay-user-messages` ; PAS de voie control-channel, vérifié). Interception = scan FILTRÉ du transcript ENTIER, en avant (`history.rs::load_active_goal` → IPC `load_session_goal`) : pas un tail-scan (un goal posé tôt et jamais terminé reste actif en fin de fichier), mais un pré-filtre par sous-chaîne brute (`goal_status` / `local-command-stdout`) AVANT tout parse JSON — sinon une flotte de cartes Flight Deck paierait un parse complet de plusieurs Mo chacune. Rafraîchi au load/reload, par carte Flight Deck (`seedActiveGoalOnce`, survit au quit/relaunch) et aux fronts de tour (gaté sur `goalSeen` pour ne rien coûter aux convs sans goal). Le bruit `/goal` (echo commande + stdout `Goal set/cleared`/`No goal set`) est masqué du fil (`is_goal_command_noise`, live+reload) ; les `/goal` du composer partent en **envoi silencieux** (live == reload). DISPLAY-ONLY via `goalStore` (mémoire, keyé convId) : chip du composer + icône target Flight Deck, popover partagée `GoalPopover` (condition + dernière raison + clear ; garde de clear pilotée par le cycle de vie de la mutation). Cf. mémoire `goal-feature-wire`. ## Backend Codex (app-server) — 2ᵉ producteur @@ -301,6 +302,17 @@ SemVer `MAJEUR.MINEUR.CORRECTIF`. En `0.y.z` : MINEUR pour toute nouveauté, COR - `repository_id` : `8c509e62-30cb-4f58-9074-086bac72528d` - `project_id` (Tosse Code) : `ef02be22-fe30-4463-9450-ec3b20746a35` + +## Artefacts (gestion + aperçu) — LIVRÉ + +Gestion des artefacts produits par l'outil **`Artifact`** de Claude (pages HTML/MD hébergées `claude.ai/code/artifact/`, privées par défaut). Contrat wire : `tool_use{name:"Artifact"}` porte `file_path` (dir TEMP éphémère `/private/tmp/claude-/…/scratchpad/*.html`), `description`, `favicon` (emoji), `label` (= nom de version) ; le `tool_result` est du **texte** dont on parse l'URL canonique (`Published … at https://claude.ai/code/artifact/`) ; republier le même `file_path` dans la même conversation garde la même URL et ajoute une version. Disponibilité dépendante de la version du binaire (absent en 2.1.178, présent depuis ~2.1.201). + +**Dérivation FRONT pure (zéro Rust/IPC/SQLite)** : `src/features/conversation/artifacts.ts` — `selectArtifacts`/`useArtifacts` scanne les tours de `conversationStore` (blocs `tool_use` name==="Artifact" joints à `toolResults[id]`), **main-thread only** (`parentToolUseId===null` → parité live/reload, miroir du skip_sidechain de `history.rs`), **groupé par `file_path`** (jamais par label — ils se répètent entre fichiers — ni par URL seule, connue seulement au tool_result), header **last-known-good** (un republish qui omet un champ ne blanchit pas l'en-tête), **exclut** les publications entièrement en échec, **strictement read-only** vers claude.ai (jamais de publish/`list` déclenché par l'UI = effet de bord réel sur le compte). Survit au reload gratuitement (tool_use+tool_result rejoués par `history.rs`), contrairement aux tâches de fond live-only. + +**Surfaces** : carte inline `ArtifactCard` à chaque publication (états pending/failed/degraded — un `is_error` affiche la raison, zéro-erreur-silencieuse) ; chip composer « Artefacts (N) » + popover portal groupé par version (`ArtifactsChip`, pattern `BackgroundTaskBadge`) ; **carte-lien `ArtifactRefCard`** (une URL d'artefact écrite par Claude dans sa prose → pilule cliquable, câblée dans `MentionLink` APRÈS le `useMemo` pour garder l'ordre des hooks stable). Segment `artifact` dédié dans `toolGroup.ts` (garde `field(input,"file_path")` → un `action:"list"`/url-update retombe en run normal ; peel hors du pli clean-output, comme un `plan`). Icône `artifact` (WF_PATHS) distincte du `globe` de Remote Control. + +**Aperçu in-app `ArtifactViewer`** (panneau latéral droit) : lit le fichier local via `commands.readFile` et rend l'**HTML dans un `