{"locale":"fr","source":{"rawBase":"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/docs","githubBase":"https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs","editBase":"https://github.com/earendil-works/pi/edit/main/packages/coding-agent/docs"},"redirects":[{"from":"/docs/latest/session","to":"/docs/latest/session-format"},{"from":"/docs/latest/tree","to":"/docs/latest/sessions"}],"fileToSlug":{"compaction.md":"compaction","containerization.md":"containerization","custom-provider.md":"custom-provider","development.md":"development","environment-variables.md":"environment-variables","extensions.md":"extensions","index.md":"index","json.md":"json","keybindings.md":"keybindings","llama-cpp.md":"llama-cpp","models.md":"models","packages.md":"packages","prompt-templates.md":"prompt-templates","providers.md":"providers","quickstart.md":"quickstart","rpc.md":"rpc","sdk.md":"sdk","security.md":"security","session-format.md":"session-format","sessions.md":"sessions","settings.md":"settings","shell-aliases.md":"shell-aliases","skills.md":"skills","terminal-setup.md":"terminal-setup","termux.md":"termux","themes.md":"themes","tmux.md":"tmux","tui.md":"tui","usage.md":"usage","windows.md":"windows"},"pages":{"fr":{"compaction":{"title":"Compactage et résumé des branches","markdown":"Les LLM ont des fenêtres contextuelles limitées. Lorsque les conversations deviennent trop longues, Pi utilise le compactage pour résumer le contenu plus ancien tout en préservant le travail récent. Cette page couvre à la fois l'auto-compaction et branch summarization.\n\n**Fichiers sources** ([pi-mono](https://github.com/earendil-works/pi-mono)):\n- [`packages/coding-agent/src/core/compaction/compaction.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) - Logique d'auto-compaction\n- [`packages/coding-agent/src/core/compaction/branch-summarization.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) - Résumé des branches\n- [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts) - Utilitaires partagés (suivi des fichiers, sérialisation)\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) - Types d'entrées (`CompactionEntry`, `BranchSummaryEntry`)\n- [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) - Types d'événements d'extension\n\nPour les définitions TypeScript de votre projet, inspectez `node_modules/@earendil-works/pi-coding-agent/dist/`.\n\n## Aperçu\n\nPi a deux mécanismes de résumé:\n\n| Mécanisme | Déclenchement | But |\n|-----------|---------|---------|\n| Compactage | Le contexte dépasse le seuil, ou `/compact` | Résumer les anciens messages pour libérer du contexte |\n| Résumé de la branche | `/tree`navigation | Préserver le contexte lors du changement de branche |\n\nLes deux utilisent le même format de résumé structuré et suivent les opérations sur les fichiers de manière cumulative. Les demandes de compactage et de résumé de branche utilisent de nouveaux ID de session de routage et, lorsque cela est pris en charge par le fournisseur, désactivent les écritures dans le cache d'invite, car il est peu probable que ces invites ponctuelles soient réutilisées.\n\n## Compactage\n\n### Quand ça se déclenche\n\nLe compactage automatique se déclenche lorsque:\n\n```\ncontextTokens > contextWindow - reserveTokens\n```\n\nPar défaut, `reserveTokens` correspond à 16384 jetons (configurable en `~/.pi/agent/settings.json` ou `<project-dir>/.pi/settings.json`). Cela laisse place à la réponse du LLM.\n\nVous pouvez également déclencher manuellement avec `/compact [instructions]`, où des instructions facultatives concentrent le résumé.\n\n### Comment ça marche\n\n1. **Trouver le point de coupure**: reculez à partir du message le plus récent, en accumulant les estimations de jetons jusqu'à ce que `keepRecentTokens` (20 000 par défaut, configurable en `~/.pi/agent/settings.json` ou `<project-dir>/.pi/settings.json`) soit atteint\n2. **Extraire les messages**: collectez les messages de la limite conservée précédente (ou du début de session) jusqu'au point de coupure\n3. **Générer un résumé**: appelez LLM pour résumer avec un format structuré, en transmettant le résumé précédent comme contexte itératif lorsqu'il est présent\n4. **Ajouter une entrée**: Enregistrez `CompactionEntry` avec le résumé et `firstKeptEntryId`\n5. **Recharger**: rechargements de session, en utilisant le résumé + les messages à partir de `firstKeptEntryId`\n\n```\nBefore compaction:\n\n  entry:  0     1     2     3      4     5     6      7      8     9\n        ┌─────┬─────┬─────┬─────┬──────┬─────┬──── ─┬──────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘\n                └────────┬───────┘ └──────────────┬──────────────┘\n               messagesToSummarize            kept messages\n                                   ↑\n                          firstKeptEntryId (entry 4)\n\nAfter compaction (new entry appended):\n\n  entry:  0     1     2     3      4     5     6      7      8     9     10\n        ┌─────┬─────┬─────┬─────┬──────┬─────┬──── ─┬──────┬─────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘\n               └──────────┬──────┘ └──────────────────────┬───────────────────┘\n                 not sent to LLM                    sent to LLM\n                                                         ↑\n                                              starts from firstKeptEntryId\n\nWhat the LLM sees:\n\n  ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐\n  │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │\n  └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘\n       ↑         ↑      └─────────────────┬────────────────┘\n    prompt   from cmp          messages from firstKeptEntryId\n```\n\nLors de compactages répétés, la durée résumée commence à la limite conservée du compactage précédent (`firstKeptEntryId`), et non à l'entrée de compactage elle-même, retombant à l'entrée après le compactage précédent si cette entrée conservée est introuvable dans le chemin. Cela préserve les messages qui ont survécu au compactage précédent en les incluant également dans la prochaine passe de résumé. Pi recalcule également `tokensBefore` à partir du contexte de session reconstruit avant d'écrire le nouveau `CompactionEntry`, de sorte que le nombre de jetons reflète le contexte de pré-compactage réel remplacé.\n\n### Tours fractionnés\n\nUn « tour » commence par un message utilisateur et inclut toutes les réponses de l'assistant et les appels d'outils jusqu'au prochain message utilisateur. Normalement, le compactage coupe aux limites des virages.\n\nLorsqu'un seul tour dépasse `keepRecentTokens`, le point de coupure atterrit à mi-tour sur un message de l'assistant. Il s'agit d'un \"tour partagé\":\n\n```\nSplit turn (one huge turn exceeds budget):\n\n  entry:  0     1     2      3     4      5      6     7      8\n        ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐\n        │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │\n        └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘\n                ↑                                     ↑\n         turnStartIndex = 1                  firstKeptEntryId = 7\n                │                                     │\n                └──── turnPrefixMessages (1-6) ───────┘\n                                                      └── kept (7-8)\n\n  isSplitTurn = true\n  messagesToSummarize = []  (no complete turns before)\n  turnPrefixMessages = [usr, ass, tool, ass, tool, tool]\n```\n\nPour les tours fractionnés, Pi génère deux résumés et les fusionne:\n1. **Résumé de l'historique**: contexte précédent (le cas échéant)\n2. **Résumé du préfixe de tour**: début du tour divisé\n\n### Règles de point de coupure\n\nLes points de coupure valides sont:\n- Messages utilisateur\n- Messages de l'assistant\n- Messages d'exécution Bash\n- Messages personnalisés (custom_message, branch_summary)\n\nNe coupez jamais aux résultats de l'outil (ils doivent rester avec leur appel d'outil).\n\n### Structure d'entrée de compactage\n\nDéfini en [`session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface CompactionEntry<T = unknown> {\n  type: \"compaction\";\n  id: string;\n  parentId: string;\n  timestamp: number;\n  summary: string;\n  firstKeptEntryId: string;\n  tokensBefore: number;\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default compaction uses this for details (from compaction.ts):\ninterface CompactionDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nExtensions peut stocker toutes les données sérialisables JSON dans `details`. Le compactage par défaut suit les opérations sur les fichiers, mais les implémentations d'extensions personnalisées peuvent utiliser leur propre structure. Les résumés générés et fournis par l'extension stockent leur LLM `usage` lorsqu'il est disponible afin que les totaux des sessions incluent le travail de synthèse.\n\nVoir [`prepareCompaction()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) et [`compact()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) pour l'implémentation. Pour un résumé programmatique direct, `generateSummary()` renvoie le texte du résumé et `generateSummaryWithUsage()` renvoie `{ text, usage }`.\n\n## Résumé de branche\n\n### Quand ça se déclenche\n\nLorsque vous utilisez `/tree` pour accéder à une autre branche, Pi propose de résumer le travail que vous quittez. Cela injecte le contexte de la branche gauche dans la nouvelle branche.\n\n### Comment ça marche\n\n1. **Trouver l'ancêtre commun**: nœud le plus profond partagé par les anciennes et les nouvelles positions\n2. **Collecter les entrées**: Revenir de l'ancienne feuille à l'ancêtre commun\n3. **Préparer avec le budget**: Incluez des messages jusqu'au budget symbolique (le plus récent en premier)\n4. **Générer un résumé**: Appelez LLM avec un format structuré\n5. **Ajouter une entrée**: Enregistrez `BranchSummaryEntry` au point de navigation\n\n```\nTree before navigation:\n\n         ┌─ B ─ C ─ D (old leaf, being abandoned)\n    A ───┤\n         └─ E ─ F (target)\n\nCommon ancestor: A\nEntries to summarize: B, C, D\n\nAfter navigation with summary:\n\n         ┌─ B ─ C ─ D\n    A ───┤\n         └─ E ─ F ─ [summary of B,C,D] (new leaf)\n```\n\n### Suivi cumulatif des fichiers\n\nLe compactage et branch summarization suivent les fichiers de manière cumulative. Lors de la génération d'un résumé, pi extrait les opérations sur les fichiers de:\n- Appels d'outils dans les messages en cours de synthèse\n- Résumé du compactage ou de la branche précédente `details` (le cas échéant)\n\nCela signifie que le suivi des fichiers s'accumule sur plusieurs compactages ou résumés de branches imbriqués, préservant ainsi l'historique complet des fichiers lus et modifiés.\n\n### Structure d'entrée BranchSummary\n\nDéfini en [`session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts):\n\n```typescript\ninterface BranchSummaryEntry<T = unknown> {\n  type: \"branch_summary\";\n  id: string;\n  parentId: string;\n  timestamp: number;\n  summary: string;\n  fromId: string;      // Entry we navigated from\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default branch summarization uses this for details (from branch-summarization.ts):\ninterface BranchSummaryDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nTout comme pour le compactage, les extensions peuvent stocker des données personnalisées dans `details`.\n\nVoir [`collectEntriesForBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts), [`prepareBranchEntries()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) et [`generateBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) pour l'implémentation.\n\n## Format du résumé\n\nLe compactage et branch summarization utilisent le même format structuré:\n\n```markdown\n## Goal\n[What the user is trying to accomplish]\n\n## Constraints & Preferences\n- [Requirements mentioned by user]\n\n## Progress\n### Done\n- [x] [Completed tasks]\n\n### In Progress\n- [ ] [Current work]\n\n### Blocked\n- [Issues, if any]\n\n## Key Decisions\n- **[Decision]**: [Rationale]\n\n## Next Steps\n1. [What should happen next]\n\n## Critical Context\n- [Data needed to continue]\n\n<read-files>\npath/to/file1.ts\npath/to/file2.ts\n</read-files>\n\n<modified-files>\npath/to/changed.ts\n</modified-files>\n```\n\n### Sérialisation des messages\n\nAvant le résumé, les messages sont sérialisés en texte via [`serializeConversation()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts):\n\n```\n[User]: What they said\n[Assistant thinking]: Internal reasoning\n[Assistant]: Response text\n[Assistant tool calls]: read(path=\"foo.ts\"); edit(path=\"bar.ts\", ...)\n[Tool result]: Output from tool\n```\n\nCela empêche le modèle de la traiter comme une conversation à poursuivre.\n\nLes résultats de l'outil sont tronqués à 2 000 caractères lors de la sérialisation. Le contenu au-delà de cette limite est remplacé par un marqueur indiquant le nombre de caractères tronqués. Cela maintient les demandes de résumé dans des budgets symboliques raisonnables, puisque les résultats des outils (en particulier ceux de `read` et `bash`) sont généralement ceux qui contribuent le plus à la taille du contexte.\n\n## Résumé personnalisé via Extensions\n\nExtensions peut intercepter et personnaliser à la fois le compactage et branch summarization. Voir [`extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) pour les définitions des types d'événements.\n\n### session_avant_compact\n\nLancé avant le compactage automatique ou `/compact`. Peut annuler ou fournir un résumé personnalisé. Voir `SessionBeforeCompactEvent` et `CompactionPreparation` dans le fichier de types.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // preparation.messagesToSummarize - messages to summarize\n  // preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)\n  // preparation.previousSummary - previous compaction summary\n  // preparation.fileOps - extracted file operations\n  // preparation.tokensBefore - context tokens before compaction\n  // preparation.firstKeptEntryId - where kept messages start\n  // preparation.settings - compaction settings\n\n  // branchEntries - all entries on current branch (for custom state)\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n  // signal - AbortSignal (pass to LLM calls)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"Your summary...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: { /* custom data */ },\n    }\n  };\n});\n```\n\n#### Conversion de messages en texte\n\nPour générer un résumé avec votre propre modèle, convertissez les messages en texte en utilisant `serializeConversation`:\n\n```typescript\nimport { convertToLlm, serializeConversation } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation } = event;\n  \n  // Convert AgentMessage[] to Message[], then serialize to text\n  const conversationText = serializeConversation(\n    convertToLlm(preparation.messagesToSummarize)\n  );\n  // Returns:\n  // [User]: message text\n  // [Assistant thinking]: thinking content\n  // [Assistant]: response text\n  // [Assistant tool calls]: read(path=\"...\"); bash(command=\"...\")\n  // [Tool result]: output text\n\n  // Now send to your model for summarization\n  const { summary, usage } = await myModel.summarize(conversationText);\n  \n  return {\n    compaction: {\n      summary,\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      usage,\n    }\n  };\n});\n```\n\nVoir [custom-compaction.ts](../examples/extensions/custom-compaction.ts) pour un exemple complet utilisant un modèle différent.\n\n### session_avant_arbre\n\nDéclenché avant la navigation `/tree`. Se déclenche toujours, que l'utilisateur ait choisi ou non de résumer. Peut annuler la navigation ou fournir un résumé personnalisé.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n\n  // preparation.targetId - where we're navigating to\n  // preparation.oldLeafId - current position (being abandoned)\n  // preparation.commonAncestorId - shared ancestor\n  // preparation.entriesToSummarize - entries that would be summarized\n  // preparation.userWantsSummary - whether user chose to summarize\n\n  // Cancel navigation entirely:\n  return { cancel: true };\n\n  // Provide custom summary (only used if userWantsSummary is true):\n  if (preparation.userWantsSummary) {\n    return {\n      summary: {\n        summary: \"Your summary...\",\n        // usage: summaryResponse.usage, // Optional; included in session totals\n        details: { /* custom data */ },\n      }\n    };\n  }\n});\n```\n\nVoir `SessionBeforeTreeEvent` et `TreePreparation` dans le fichier de types.\n\n## Paramètres\n\nConfigurez le compactage en `~/.pi/agent/settings.json` ou `<project-dir>/.pi/settings.json`:\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n| Paramètre | Défaut | Description |\n|---------|---------|-------------|\n| `enabled` | `true` | Activer le compactage automatique |\n| `reserveTokens` | `16384` | Jetons à réserver pour la réponse LLM |\n| `keepRecentTokens` | `20000` | Jetons récents à conserver (non résumés) |\n\nDésactivez le compactage automatique avec `\"enabled\": false`. Vous pouvez toujours compacter manuellement avec `/compact`.","sourceFile":"compaction.md"},"containerization":{"title":"Conteneurisation","markdown":"Pi s'exécute avec toutes les autorisations par défaut, mais dans certains cas, vous souhaiterez avoir plus de contrôle sur les répertoires dans lesquels Pi peut écrire et sur les accès dont il dispose.\n\nIl existe deux options générales. Vous pouvez soit\n1. exécuter l'ensemble du processus `pi` dans un environnement isolé, ou\n2. exécutez `pi` sur l'hôte et exécutez l'outil de routage dans un environnement isolé.\n\n## Choisissez un motif\n\n| Modèle | Ce qui est isolé | Idéal pour | Remarques |\n| --- | --- | --- | --- |\n| Extension Gondolin | Outils intégrés et commandes `!` | Isolation des micro-VM locales tout en conservant l'authentification sur l'hôte | Voir [`examples/extensions/gondolin/`](../examples/extensions/gondolin/). |\n| Plaine Docker | Processus `pi` entier dans un conteneur local | Isolement local simple | Les fournisseurs API key entrent dans le conteneur. |\n| OpenShell | Processus `pi` entier dans un sandbox contrôlé par une politique | Géré localement ou à distance sandbox | Nécessite une passerelle OpenShell |\n\nExtensions s'exécute partout où le processus `pi` s'exécute. Si vous exécutez l'hôte `pi` avec une extension de routage d'outils, d'autres outils d'extension personnalisés s'exécutent toujours sur l'hôte à moins qu'ils ne délèguent également leurs opérations.\n\n## Gondolin\n\n[Gondolin](https://github.com/earendil-works/gondolin) est une micro-VM Linux locale.\nUtilisez le [example extension](../examples/extensions/gondolin) lorsque vous voulez `pi` sur l'hôte mais tous les outils intégrés sont acheminés vers la VM.\n\nInstallation:\n\n```bash\ncp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin\ncd ~/.pi/agent/extensions/gondolin\nnpm install --ignore-scripts\n```\n\nExécutez à partir du projet que vous souhaitez monter:\n\n```bash\ncd /path/to/project\npi -e ~/.pi/agent/extensions/gondolin\n```\n\nL'extension monte le cwd hôte à `/workspace` dans la VM et remplace `read`, `write`, `edit`, `bash`, `grep`, `find` et `ls`.\nLes commandes de l'utilisateur `!` sont également acheminées vers la VM.\nLes modifications de fichier sous `/workspace` sont écrites sur l'hôte.\n\nExigences: Node.js >= 23.6.0 pour `@earendil-works/gondolin`, plus QEMU (nécessite une installation via votre gestionnaire de paquets).\n\n## Plaine Docker\n\nExécutez l'ensemble du processus `pi` dans Docker lorsque vous souhaitez la limite de conteneur local la plus simple.\n\n`Dockerfile.pi`:\n\n```dockerfile\nFROM node:24-bookworm-slim\n\nRUN apt-get update \\\n  && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \\\n  && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\nWORKDIR /workspace\nENTRYPOINT [\"pi\"]\n```\n\nConstruisez et exécutez:\n\n```bash\ndocker build -t pi-sandbox -f Dockerfile.pi .\n\ndocker run --rm -it \\\n  -e ANTHROPIC_API_KEY \\\n  -v \"$PWD:/workspace\" \\\n  -v pi-agent-home:/root/.pi/agent \\\n  pi-sandbox\n```\n\nLe `-v \"$PWD:/workspace\"` monte votre répertoire actuel dans le conteneur /workspace de telle sorte que les lectures et écritures dans `/workspace` à l'intérieur de Docker affectent directement vos fichiers hôtes, comme dans l'exemple Gondolin.\n\nUtilisez un volume nommé pour `/root/.pi/agent` si vous souhaitez des paramètres et des sessions locaux au conteneur. Le montage de votre hôte `~/.pi/agent` expose les fichiers d'authentification et de session de l'hôte au conteneur.\n\n## OpenShell\n\nUtilisez [NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview) lorsque vous souhaitez un sandbox contrôlé par une politique avec des contrôles de système de fichiers, de processus, de réseau, d'informations d'identification et d'inférence.\nOpenShell peut exécuter sandbox via une passerelle locale soutenue par Docker, Podman ou un runtime de VM, ou via une passerelle Kubernetes distante.\n\nChaque sandbox nécessite une passerelle active.\nInscrivez-vous et sélectionnez-en un avant de créer un sandbox:\n\n```bash\nopenshell gateway add <gateway-url> --name <name>\nopenshell gateway select <name>\n```\n\nLancez `pi` à l'intérieur d'un OpenShell sandbox:\n\n```bash\nopenshell sandbox create --name pi-sandbox --from pi -- pi\n```\n\nDans ce modèle, l'ensemble du processus `pi` s'exécute à l'intérieur du sandbox.\nLes outils intégrés, les commandes `!` et les outils d'extension s'exécutent à l'intérieur de la limite OpenShell.\n\nSi la passerelle est distante, les fichiers de projet ne sont pas montés en liaison depuis l'hôte, ce qui signifie que les écritures dans le sandbox ne sont pas reflétées sur votre machine.\nClonez le référentiel à l'intérieur du sandbox ou utilisez les commandes de transfert de fichiers OpenShell:\n\n```bash\nopenshell sandbox upload pi-sandbox ./repo /workspace\nopenshell sandbox download pi-sandbox /workspace/repo ./repo-out\n```\n\nLes fournisseurs OpenShell peuvent conserver les modèles bruts API key en dehors des sandbox.\nLorsque le routage d'inférence est configuré, le code à l'intérieur du sandbox peut appeler `https://inference.local` et la passerelle injecte les informations d'identification du fournisseur configurées en amont.\nConfigurez Pi pour utiliser le point de terminaison compatible OpenAI ou Anthropic correspondant si vous souhaitez que le trafic du modèle utilise cette route.","sourceFile":"containerization.md"},"custom-provider":{"title":"Personnalisé Providers","markdown":"Extensions peut enregistrer des fournisseurs de modèles personnalisés via `pi.registerProvider()`. Cela permet:\n\n- **Proxies** - Acheminer les demandes via des proxys d'entreprise ou des passerelles API\n- **Points de terminaison personnalisés** – Utilisez des déploiements de modèles auto-hébergés ou privés\n- **OAuth/SSO** - Ajouter des flux d'authentification pour les fournisseurs d'entreprise\n- ** APIs personnalisés** – Implémenter le streaming pour les LLM APIs non standard\n\n## Exemple Extensions\n\nConsultez ces exemples complets de fournisseurs:\n\n- [`examples/extensions/custom-provider-anthropic/`](../examples/extensions/custom-provider-anthropic/)\n- [`examples/extensions/custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/)\n\n## Table des matières\n\n- [Example Extensions](#example-extensions)\n- [Quick Reference](#quick-reference)\n- [Override Existing Provider](#override-existing-provider)\n- [Register New Provider](#register-new-provider)\n- [Unregister Provider](#unregister-provider)\n- [OAuth Support](#oauth-support)\n- [Custom Streaming API](#custom-streaming-api)\n- [Context Overflow Errors](#context-overflow-errors)\n- [Testing Your Implementation](#testing-your-implementation)\n- [Config Reference](#config-reference)\n- [Model Definition Reference](#model-definition-reference)\n\n## Référence rapide\n\nExtensions peut enregistrer soit un pi-ai `Provider` complet, soit utiliser l'ancien formulaire de configuration du fournisseur. Préférez un fournisseur complet lorsqu’un comportement personnalisé d’authentification, de filtrage, d’actualisation ou de streaming est requis. Pi compose `models.json` remplace les fournisseurs natifs enregistrés.\n\n```typescript\nimport { createProvider, openAICompletionsApi } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(createProvider({\n    id: \"native-local\",\n    name: \"Native Local\",\n    baseUrl: \"http://localhost:8080/v1\",\n    auth: {\n      apiKey: {\n        name: \"Local server API key\",\n        async login(interaction) {\n          return {\n            type: \"api_key\",\n            key: await interaction.prompt({ type: \"secret\", message: \"API key\" })\n          };\n        },\n        async resolve({ credential }) {\n          return credential?.key\n            ? { auth: { apiKey: credential.key }, source: \"stored API key\" }\n            : undefined;\n        }\n      }\n    },\n    models: [],\n    api: openAICompletionsApi()\n  }));\n\n  // Legacy provider-config form:\n  // Override baseUrl for existing provider\n  pi.registerProvider(\"anthropic\", {\n    baseUrl: \"https://proxy.example.com\"\n  });\n\n  // Register new provider with models\n  pi.registerProvider(\"my-provider\", {\n    name: \"My Provider\",\n    baseUrl: \"https://api.example.com\",\n    apiKey: \"$MY_API_KEY\",\n    api: \"openai-completions\",\n    models: [\n      {\n        id: \"my-model\",\n        name: \"My Model\",\n        reasoning: false,\n        input: [\"text\", \"image\"],\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n        contextWindow: 128000,\n        maxTokens: 4096\n      }\n    ]\n  });\n}\n```\n\nLa fabrique d'extensions peut également être `async`. Pour la découverte dynamique de modèles, récupérez et enregistrez les modèles dans l'usine au lieu de `session_start`. pi attend l'usine avant que le démarrage ne continue, le fournisseur est donc disponible pendant le démarrage interactif et jusqu'au `pi --list-models`.\n\n## Remplacer le fournisseur existant\n\nLe cas d'utilisation le plus simple: rediriger un fournisseur existant via un proxy.\n\n```typescript\n// All Anthropic requests now go through your proxy\npi.registerProvider(\"anthropic\", {\n  baseUrl: \"https://proxy.example.com\"\n});\n\n// Add custom headers to OpenAI requests\npi.registerProvider(\"openai\", {\n  headers: {\n    \"X-Custom-Header\": \"value\"\n  }\n});\n\n// Both baseUrl and headers\npi.registerProvider(\"google\", {\n  baseUrl: \"https://ai-gateway.corp.com/google\",\n  headers: {\n    \"X-Corp-Auth\": \"$CORP_AUTH_TOKEN\"  // env var or literal\n  }\n});\n```\n\nLorsque seuls `baseUrl` et/ou `headers` sont fournis (pas de `models`), tous les modèles existants pour ce fournisseur sont conservés avec le nouveau point de terminaison.\n\n## Enregistrer un nouveau fournisseur\n\nPour ajouter un tout nouveau fournisseur, spécifiez `models` avec la configuration requise.\n\nSi la liste de modèles provient d'un point de terminaison distant, utilisez une fabrique d'extensions asynchrone:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default async function (pi: ExtensionAPI) {\n  const response = await fetch(\"http://localhost:1234/v1/models\");\n  const payload = (await response.json()) as {\n    data: Array<{\n      id: string;\n      name?: string;\n      context_window?: number;\n      max_tokens?: number;\n    }>;\n  };\n\n  pi.registerProvider(\"local-openai\", {\n    baseUrl: \"http://localhost:1234/v1\",\n    apiKey: \"$LOCAL_OPENAI_API_KEY\",\n    api: \"openai-completions\",\n    models: payload.data.map((model) => ({\n      id: model.id,\n      name: model.name ?? model.id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: model.context_window ?? 128000,\n      maxTokens: model.max_tokens ?? 4096,\n    })),\n  });\n}\n```\n\nCela enregistre les modèles récupérés avant la fin du démarrage.\n\n```typescript\npi.registerProvider(\"my-llm\", {\n  baseUrl: \"https://api.my-llm.com/v1\",\n  apiKey: \"$MY_LLM_API_KEY\",  // env var reference\n  api: \"openai-completions\",  // which streaming API to use\n  models: [\n    {\n      id: \"my-llm-large\",\n      name: \"My LLM Large\",\n      reasoning: true,        // supports extended thinking\n      input: [\"text\", \"image\"],\n      cost: {\n        input: 3.0,           // $/million tokens\n        output: 15.0,\n        cacheRead: 0.3,\n        cacheWrite: 3.75\n      },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n```\n\nLorsque `models` est fourni, il **remplace** tous les modèles existants pour ce fournisseur.\n\n`apiKey` et les valeurs d'en-tête personnalisées utilisent la même syntaxe de valeur de configuration que `models.json`: `!command` au début exécute une commande pour la valeur entière, `$ENV_VAR` et `${ENV_VAR}` interpolent les variables d'environnement, `$` émet un littéral ``apiKey` et les valeurs d'en-tête personnalisées utilisent la même syntaxe de valeur de configuration que `models.json`: `!command` au début exécute une commande pour la valeur entière, `$ENV_VAR` et `${ENV_VAR}` interpolent les variables d'environnement, `$` émet un littéral  et `$!` émet un littéral `!`.\n\n## Désinscrire le fournisseur\n\nUtilisez `pi.unregisterProvider(name)` pour supprimer un fournisseur précédemment enregistré via `pi.registerProvider(name,...)`:\n\n```typescript\n// Register\npi.registerProvider(\"my-llm\", {\n  baseUrl: \"https://api.my-llm.com/v1\",\n  apiKey: \"$MY_LLM_API_KEY\",\n  api: \"openai-completions\",\n  models: [\n    {\n      id: \"my-llm-large\",\n      name: \"My LLM Large\",\n      reasoning: true,\n      input: [\"text\", \"image\"],\n      cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n\n// Later, remove it\npi.unregisterProvider(\"my-llm\");\n```\n\nLa désinscription supprime les modèles dynamiques de ce fournisseur, le repli API key, l'enregistrement du fournisseur OAuth et les enregistrements de gestionnaires de flux personnalisés. Tous les modèles intégrés ou comportements de fournisseur qui ont été remplacés sont restaurés.\n\nLes appels passés après la phase initiale de chargement de l'extension sont appliqués immédiatement, donc aucun `/reload` n'est requis.\n\n### API Types\n\nLe champ `api` détermine quelle implémentation de streaming est utilisée:\n\n| API | Utiliser pour |\n|-----|---------|\n| `anthropic-messages` | Anthropique Claude API et compatibles |\n| `openai-completions` | Complétions de chat OpenAI API et compatibles |\n| `openai-responses` | Réponses OpenAI API |\n| `azure-openai-responses` | Réponses Azure OpenAI API |\n| `openai-codex-responses` | Réponses du Codex OpenAI API |\n| `mistral-conversations` | Achèvements du chat Native Mistral en streaming |\n| `google-generative-ai` | IA générative Google API |\n| `google-vertex` | Google Vertex AI API |\n| `bedrock-converse-stream` | Amazon Bedrock Converse API |\n\nLa plupart des fournisseurs compatibles OpenAI fonctionnent avec `openai-completions`. Utilisez le niveau de modèle `thinkingLevelMap` pour les niveaux de réflexion spécifiques au modèle et `compat` pour les bizarreries du fournisseur. Les niveaux `xhigh` et `max` sont facultatifs, nécessitent des entrées de carte non nulles et peuvent être séparés par des trous non pris en charge:\n\n```typescript\nmodels: [{\n  id: \"custom-model\",\n  // ...\n  reasoning: true,\n  thinkingLevelMap: {              // map pi levels to provider values; null hides unsupported levels\n    minimal: null,\n    low: null,\n    medium: null,\n    high: \"default\",\n    xhigh: null,\n    max: \"max\"\n  },\n  compat: {\n    supportsDeveloperRole: false,   // use \"system\" instead of \"developer\"\n    supportsReasoningEffort: true,\n    maxTokensField: \"max_tokens\",   // instead of \"max_completion_tokens\"\n    requiresToolResultName: true,   // tool results need name field\n    thinkingFormat: \"qwen\",        // top-level enable_thinking: true\n    cacheControlFormat: \"anthropic\" // Anthropic-style cache_control markers\n  }\n}]\n```\n\nUtilisez `openrouter` pour les contrôles `reasoning: { effort }` de style OpenRouter. Utilisez `together` pour les contrôles `reasoning: { enabled }` de style Together; avec `supportsReasoningEffort`, il envoie également `reasoning_effort`. Utilisez `qwen-chat-template` pour les serveurs locaux compatibles Qwen qui lisent `chat_template_kwargs.enable_thinking` et ont besoin de `preserve_thinking`.\nUtilisez `cacheControlFormat: \"anthropic\"` pour les fournisseurs compatibles OpenAI qui exposent la mise en cache des invites de style Anthropic via `cache_control` sur l'invite système, la dernière définition d'outil et le contenu textuel du dernier utilisateur, assistant ou résultat de l'outil.\n\nPour les fournisseurs compatibles Anthropic utilisant `api: \"anthropic-messages\"`, définissez `compat.forceAdaptiveThinking: true` sur les modèles ou les fournisseurs dont le modèle en amont nécessite une pensée adaptative (`thinking.type: \"adaptive\"` plus `output_config.effort`). Les modèles Claude adaptatifs intégrés règlent cela automatiquement. Définissez `compat.allowEmptySignature: true` uniquement pour les fournisseurs qui émettent des signatures de pensée vides et attendent `signature: \"\"` lors de la relecture.\n\n> Note de migration: Mistral est passé de `openai-completions` à `mistral-conversations`.\n> Utilisez `mistral-conversations` pour les modèles natifs Mistral.\n> Si vous acheminez intentionnellement des points de terminaison compatibles Mistral/personnalisés via `openai-completions`, définissez explicitement les indicateurs `compat` si nécessaire.\n\n### En-tête d'authentification\n\nSi votre fournisseur attend `Authorization: Bearer <key>` mais n'utilise pas de API standard, définissez `authHeader: true`:\n\n```typescript\npi.registerProvider(\"custom-api\", {\n  baseUrl: \"https://api.example.com\",\n  apiKey: \"$MY_API_KEY\",\n  authHeader: true,  // adds Authorization: Bearer header\n  api: \"openai-completions\",\n  models: [...]\n});\n```\n\nLa clé est résolue pour chaque demande. Un en-tête de requête explicite `Authorization` est prioritaire sur la valeur générée.\n\n## OAuth Assistance\n\nAjoutez l'authentification OAuth/SSO qui s'intègre à `/login`:\n\n```typescript\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"@earendil-works/pi-ai\";\n\npi.registerProvider(\"corporate-ai\", {\n  baseUrl: \"https://ai.corp.com/v1\",\n  api: \"openai-responses\",\n  models: [...],\n  oauth: {\n    name: \"Corporate AI (SSO)\",\n\n    async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n      const method = await callbacks.onSelect({\n        message: \"Select login method:\",\n        options: [\n          { id: \"browser\", label: \"Browser OAuth\" },\n          { id: \"device\", label: \"Device code\" }\n        ]\n      });\n      if (!method) throw new Error(\"Login cancelled\");\n\n      let code: string;\n      if (method === \"device\") {\n        callbacks.onDeviceCode({\n          userCode: \"ABCD-1234\",\n          verificationUri: \"https://sso.corp.com/device\",\n          intervalSeconds: 5,\n          expiresInSeconds: 900\n        });\n        code = await pollDeviceCodeUntilComplete();\n      } else {\n        callbacks.onAuth({ url: \"https://sso.corp.com/authorize?...\" });\n        code = await callbacks.onPrompt({ message: \"Enter SSO code:\" });\n      }\n\n      // Exchange for tokens (your implementation)\n      const tokens = await exchangeCodeForTokens(code);\n\n      return {\n        refresh: tokens.refreshToken,\n        access: tokens.accessToken,\n        expires: Date.now() + tokens.expiresIn * 1000\n      };\n    },\n\n    async refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials> {\n      const tokens = await refreshAccessToken(credentials.refresh, signal);\n      return {\n        refresh: tokens.refreshToken ?? credentials.refresh,\n        access: tokens.accessToken,\n        expires: Date.now() + tokens.expiresIn * 1000\n      };\n    },\n\n    getApiKey(credentials: OAuthCredentials): string {\n      return credentials.access;\n    }\n  }\n});\n```\n\nAprès l'inscription, les utilisateurs peuvent s'authentifier via `/login corporate-ai`.\n\n### OAuthConnexionRappels\n\nL'objet `callbacks` fournit des interactions neutres en termes d'interface utilisateur pour le flux appartenant au fournisseur:\n\n```typescript\ninterface OAuthLoginCallbacks {\n  // Open URL in browser (for OAuth redirects)\n  onAuth(params: { url: string }): void;\n\n  // Show device code (for device authorization flow)\n  onDeviceCode(params: {\n    userCode: string;\n    verificationUri: string;\n    intervalSeconds?: number;\n    expiresInSeconds?: number;\n  }): void;\n\n  // Show transient progress\n  onProgress?(message: string): void;\n\n  // Prompt user for input (for manual token entry)\n  onPrompt(params: { message: string }): Promise<string>;\n\n  // Show an interactive selector, e.g. to choose browser OAuth vs device code\n  onSelect(params: {\n    message: string;\n    options: { id: string; label: string }[];\n  }): Promise<string | undefined>;\n}\n```\n\n### OAuthIdentifiants\n\nLes informations d'identification sont conservées dans `~/.pi/agent/auth.json`:\n\n```typescript\ninterface OAuthCredentials {\n  refresh: string;   // Refresh token (for refreshToken())\n  access: string;    // Access token (returned by getApiKey())\n  expires: number;   // Expiration timestamp in milliseconds\n}\n```\n\n## Diffusion personnalisée API\n\nPour les fournisseurs avec des API non standard, implémentez `streamSimple`. Étudiez les implémentations de fournisseurs existantes avant d'écrire la vôtre:\n\n**Implémentations de référence:**\n- [anthropic.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts) - Messages anthropiques API\n- [mistral.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/mistral.ts) - Conversations Mistral API\n- [openai-completions.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-completions.ts) – Achèvements du chat OpenAI\n- [openai-responses.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-responses.ts) - Réponses OpenAI API\n- [google.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/google.ts) – IA générative de Google\n- [amazon-bedrock.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/amazon-bedrock.ts) – Socle rocheux AWS\n\n### Modèle de flux\n\nTous les fournisseurs suivent le même modèle:\n\n```typescript\nimport {\n  type AssistantMessage,\n  type AssistantMessageEventStream,\n  type Context,\n  type Model,\n  type SimpleStreamOptions,\n  calculateCost,\n  createAssistantMessageEventStream,\n} from \"@earendil-works/pi-ai\";\n\nfunction streamMyProvider(\n  model: Model<any>,\n  context: Context,\n  options?: SimpleStreamOptions\n): AssistantMessageEventStream {\n  const stream = createAssistantMessageEventStream();\n\n  (async () => {\n    // Initialize output message\n    const output: AssistantMessage = {\n      role: \"assistant\",\n      content: [],\n      api: model.api,\n      provider: model.provider,\n      model: model.id,\n      usage: {\n        input: 0,\n        output: 0,\n        cacheRead: 0,\n        cacheWrite: 0,\n        totalTokens: 0,\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n      },\n      stopReason: \"pending\",\n      timestamp: Date.now(),\n    };\n\n    try {\n      // Push start event\n      stream.push({ type: \"start\", partial: output });\n\n      // Make API request and process response...\n      // Push content events as they arrive and set stopReason from the terminal event.\n      if (output.stopReason === \"pending\") {\n        throw new Error(\"Provider stream ended without a stop reason\");\n      }\n      if (output.stopReason === \"error\" || output.stopReason === \"aborted\") {\n        throw new Error(output.errorMessage || \"An unknown error occurred\");\n      }\n\n      // Push done event\n      stream.push({\n        type: \"done\",\n        reason: output.stopReason,\n        message: output\n      });\n      stream.end();\n    } catch (error) {\n      output.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n      output.errorMessage = error instanceof Error ? error.message : String(error);\n      stream.push({ type: \"error\", reason: output.stopReason, error: output });\n      stream.end();\n    }\n  })();\n\n  return stream;\n}\n```\n\n### Types d'événements\n\nPoussez les événements via `stream.push()` dans cet ordre:\n\n1. `{ type: \"start\", partial: output }` – Diffusion démarrée\n\n2. Événements de contenu (répétables, piste `contentIndex` pour chaque bloc):\n   - `{ type: \"text_start\", contentIndex, partial }` - Bloc de texte démarré\n   - `{ type: \"text_delta\", contentIndex, delta, partial }` - Morceau de texte\n   - `{ type: \"text_end\", contentIndex, content, partial }` - Bloc de texte terminé\n   - `{ type: \"thinking_start\", contentIndex, partial }` - La réflexion a commencé\n   - `{ type: \"thinking_delta\", contentIndex, delta, partial }` – Morceau de réflexion\n   - `{ type: \"thinking_end\", contentIndex, content, partial }` - La réflexion est terminée\n   - `{ type: \"toolcall_start\", contentIndex, partial }` - L'appel de l'outil a démarré\n   - `{ type: \"toolcall_delta\", contentIndex, delta, partial }` - Appel d'outil JSON morceau\n   - `{ type: \"toolcall_end\", contentIndex, toolCall, partial }` - Appel d'outil terminé\n\n3. `{ type: \"done\", reason, message }` ou `{ type: \"error\", reason, error }` – Diffusion terminée\n\nLe champ `partial` de chaque événement contient l'état `AssistantMessage` actuel. Mettez à jour `output.content` au fur et à mesure que vous recevez des données, puis incluez `output` comme `partial`.\n\n### Blocs de contenu\n\nAjoutez des blocs de contenu à `output.content` dès leur arrivée:\n\n```typescript\n// Text block\noutput.content.push({ type: \"text\", text: \"\" });\nstream.push({ type: \"text_start\", contentIndex: output.content.length - 1, partial: output });\n\n// As text arrives\nconst block = output.content[contentIndex];\nif (block.type === \"text\") {\n  block.text += delta;\n  stream.push({ type: \"text_delta\", contentIndex, delta, partial: output });\n}\n\n// When block completes\nstream.push({ type: \"text_end\", contentIndex, content: block.text, partial: output });\n```\n\n### Appels d'outils\n\nLes appels d'outils nécessitent d'accumuler JSON et d'analyser:\n\n```typescript\n// Start tool call\noutput.content.push({\n  type: \"toolCall\",\n  id: toolCallId,\n  name: toolName,\n  arguments: {}\n});\nstream.push({ type: \"toolcall_start\", contentIndex: output.content.length - 1, partial: output });\n\n// Accumulate JSON\nlet partialJson = \"\";\npartialJson += jsonDelta;\ntry {\n  block.arguments = JSON.parse(partialJson);\n} catch {}\nstream.push({ type: \"toolcall_delta\", contentIndex, delta: jsonDelta, partial: output });\n\n// Complete\nstream.push({\n  type: \"toolcall_end\",\n  contentIndex,\n  toolCall: { type: \"toolCall\", id, name, arguments: block.arguments },\n  partial: output\n});\n```\n\n### Utilisation et coût\n\nMettez à jour l'utilisation à partir de la réponse API et calculez le coût:\n\n```typescript\noutput.usage.input = response.usage.input_tokens;\noutput.usage.output = response.usage.output_tokens;\noutput.usage.cacheRead = response.usage.cache_read_tokens ?? 0;\noutput.usage.cacheWrite = response.usage.cache_write_tokens ?? 0;\noutput.usage.totalTokens = output.usage.input + output.usage.output +\n                           output.usage.cacheRead + output.usage.cacheWrite;\ncalculateCost(model, output.usage);\n```\n\n### Erreurs de débordement de contexte\n\nLorsqu'une requête dépasse la fenêtre contextuelle du modèle, pi peut récupérer automatiquement en compactant la conversation et en réessayant. Cette récupération ne démarre que si pi reconnaît l'échec comme un débordement.\n\nLa détection s'exécute sur le message finalisé de l'assistant:\n\n- `stopReason === \"error\"`\n- `errorMessage` correspond à l'un des modèles de débordement connus de pi (voir [`packages/ai/src/utils/overflow.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/utils/overflow.ts))\n\nSi votre fournisseur renvoie des erreurs de débordement avec un message que pi ne reconnaît pas, normalisez l'erreur à partir de la même extension qui enregistre le fournisseur. Utilisez un gestionnaire `message_end` pour réécrire le message de l'assistant afin que son `errorMessage` commence par une phrase que pi reconnaît. La solution de secours générique `context_length_exceeded` est le choix le plus sûr.\n\n```typescript\nconst MY_PROVIDER_OVERFLOW_PATTERN = /your provider's overflow phrase/i;\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"my-provider\", { /* ... */ });\n\n  pi.on(\"message_end\", (event, ctx) => {\n    const message = event.message;\n    if (message.role !== \"assistant\") return;\n    if (message.stopReason !== \"error\") return;\n    if (\n      message.provider !== \"my-provider\" &&\n      ctx.model?.provider !== \"my-provider\"\n    )\n      return;\n\n    const errorMessage = message.errorMessage ?? \"\";\n    if (errorMessage.includes(\"context_length_exceeded\")) return;\n    if (!MY_PROVIDER_OVERFLOW_PATTERN.test(errorMessage)) return;\n\n    return {\n      message: {\n        ...message,\n        errorMessage: `context_length_exceeded: ${errorMessage}`,\n      },\n    };\n  });\n}\n```\n\n`message_end` s'exécute avant que pi ne suive le message de l'assistant pour le compactage automatique, donc le `errorMessage` réécrit est ce que pi vérifie. Une fois cela en place, pi:\n\n1. Détectez le débordement de `errorMessage`.\n2. Supprimez le message de l'assistant ayant échoué du contexte en direct.\n3. Exécutez le compactage.\n4. Réessayez la demande une fois.\n\nGardez soigneusement la réécriture:\n\n- Étendez-le à votre fournisseur (`message.provider` et `ctx.model?.provider`) afin que les erreurs non liées provenant d'autres fournisseurs ne soient pas touchées.\n- Faites correspondre un modèle spécifique au fournisseur, et non les modèles de débordement génériques de pi. Les erreurs de réécriture de limite de débit ou de limitation (`rate limit`, `too many requests`) déclencheraient faussement le compactage au lieu du chemin normal de nouvelle tentative avec interruption de pi.\n- Ignorer lorsque `errorMessage` inclut déjà `context_length_exceeded` afin que le gestionnaire soit idempotent.\n\n### Inscription\n\nEnregistrez votre fonction de flux:\n\n```typescript\npi.registerProvider(\"my-provider\", {\n  baseUrl: \"https://api.example.com\",\n  apiKey: \"$MY_API_KEY\",\n  api: \"my-custom-api\",\n  models: [...],\n  streamSimple: streamMyProvider\n});\n```\n\n## Tester votre implémentation\n\nTestez votre fournisseur avec les mêmes suites de tests utilisées par les fournisseurs intégrés. Copiez et adaptez ces fichiers de test à partir de [packages/ai/test/](https://github.com/earendil-works/pi-mono/tree/main/packages/ai/test):\n\n| Test | But |\n|------|---------|\n| `stream.test.ts` | Streaming de base, sortie de texte |\n| `tokens.test.ts` | Comptage et utilisation des jetons |\n| `abort.test.ts` | AbandonnerGestion du signal |\n| `empty.test.ts` | Réponses vides/minimales |\n| `context-overflow.test.ts` | Limites de la fenêtre contextuelle |\n| `image-limits.test.ts` | Gestion de la saisie des images |\n| `unicode-surrogate.test.ts` | Cas extrêmes Unicode |\n| `tool-call-without-result.test.ts` | Cas extrêmes d’appel d’outil |\n| `image-tool-result.test.ts` | Images dans les résultats de l'outil |\n| `total-tokens.test.ts` | Calcul total du jeton |\n| `cross-provider-handoff.test.ts` | Transfert de contexte entre fournisseurs |\n\nExécutez des tests avec vos paires fournisseur/modèle pour vérifier la compatibilité.\n\n## Référence de configuration\n\n```typescript\ninterface ProviderConfig {\n  /** Display name for the provider in UI such as /login. */\n  name?: string;\n\n  /** API endpoint URL. Required when defining models. */\n  baseUrl?: string;\n\n  /** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), or !command. Required when defining models (unless oauth). */\n  apiKey?: string;\n\n  /** API type for streaming. Required at provider or model level when defining models. */\n  api?: Api;\n\n  /** Custom streaming implementation for non-standard APIs. */\n  streamSimple?: (\n    model: Model<Api>,\n    context: Context,\n    options?: SimpleStreamOptions\n  ) => AssistantMessageEventStream;\n\n  /** Custom headers to include in requests. Values use the same resolution syntax as apiKey. */\n  headers?: Record<string, string>;\n\n  /** If true, adds Authorization: Bearer header with the resolved API key. */\n  authHeader?: boolean;\n\n  /** Models to register. If provided, replaces all existing models for this provider. */\n  models?: ProviderModelConfig[];\n\n  /** OAuth provider for /login support. */\n  oauth?: {\n    name: string;\n    login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;\n    refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials>;\n    getApiKey(credentials: OAuthCredentials): string;\n  };\n}\n```\n\n## Référence de définition du modèle\n\n```typescript\ninterface ProviderModelConfig {\n  /** Model ID (e.g., \"claude-sonnet-4-20250514\"). */\n  id: string;\n\n  /** Display name (e.g., \"Claude 4 Sonnet\"). */\n  name: string;\n\n  /** API type override for this specific model. */\n  api?: Api;\n\n  /** API endpoint URL override for this specific model. */\n  baseUrl?: string;\n\n  /** Whether the model supports extended thinking. */\n  reasoning: boolean;\n\n  /** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */\n  thinkingLevelMap?: Partial<Record<\"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\", string | null>>;\n\n  /** Supported input types. */\n  input: (\"text\" | \"image\")[];\n\n  /** Cost per million tokens (for usage tracking). */\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n  };\n\n  /** Maximum context window size in tokens. */\n  contextWindow: number;\n\n  /** Maximum output tokens. */\n  maxTokens: number;\n\n  /** Custom headers for this specific model. */\n  headers?: Record<string, string>;\n\n  /** Compatibility settings for the selected API. */\n  compat?: {\n    // openai-completions\n    supportsStore?: boolean;\n    supportsDeveloperRole?: boolean;\n    supportsReasoningEffort?: boolean;\n    supportsUsageInStreaming?: boolean;\n    supportsFinishReason?: boolean;\n    supportsStrictMode?: boolean;\n    supportsOpenAIGrammarTools?: boolean; // openai-completions/openai-responses; false falls back to normal function tools\n    maxTokensField?: \"max_completion_tokens\" | \"max_tokens\";\n    requiresToolResultName?: boolean;\n    requiresAssistantAfterToolResult?: boolean;\n    requiresThinkingAsText?: boolean;\n    requiresReasoningContentOnAssistantMessages?: boolean;\n    thinkingFormat?: \"openai\" | \"openrouter\" | \"deepseek\" | \"together\" | \"baseten\" | \"zai\" | \"qwen\" | \"chat-template\" | \"qwen-chat-template\" | \"string-thinking\" | \"ant-ling\";\n    chatTemplateKwargs?: Record<string, string | number | boolean | null | { \"$var\": \"thinking.enabled\" | \"thinking.effort\"; omitWhenOff?: boolean }>;\n    chatTemplateArgs?: Record<string, string | number | boolean | null | { \"$var\": \"thinking.enabled\" | \"thinking.effort\"; omitWhenOff?: boolean }>;\n    cacheControlFormat?: \"anthropic\";\n    sessionAffinityFormat?: \"openai\" | \"openai-nosession\" | \"openrouter\";\n    sendSessionAffinityHeaders?: boolean;\n\n    // anthropic-messages\n    supportsEagerToolInputStreaming?: boolean;\n    supportsLongCacheRetention?: boolean;\n    sendSessionAffinityHeaders?: boolean;\n    supportsCacheControlOnTools?: boolean;\n    forceAdaptiveThinking?: boolean;\n    allowEmptySignature?: boolean;\n    supportsStrictTools?: boolean;\n  };\n}\n```\n\n`openrouter` envoie `reasoning: { effort }`. `deepseek` envoie `thinking: { type: \"enabled\" | \"disabled\" }` et `reasoning_effort` lorsqu'il est activé. `together` envoie `reasoning: { enabled }` et aussi `reasoning_effort` lorsque `supportsReasoningEffort` est activé. `qwen` est pour le niveau supérieur de style DashScope `enable_thinking`. Utilisez `qwen-chat-template` pour les serveurs locaux compatibles Qwen qui lisent `chat_template_kwargs.enable_thinking` et ont besoin de `preserve_thinking`. Utilisez `chat-template` pour `chat_template_kwargs` configurable, par exemple DeepSeek V3.x derrière vLLM avec `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }`. Utilisez `thinkingFormat: \"baseten\"` avec `chatTemplateArgs` lorsque le fournisseur s'attend à basculer les valeurs sous `chat_template_args` et prend éventuellement en charge `reasoning_effort` de niveau supérieur.\n`cacheControlFormat: \"anthropic\"` applique des marqueurs `cache_control` de style anthropique à l'invite système, à la dernière définition d'outil et au contenu textuel du dernier utilisateur, assistant ou résultat de l'outil.","sourceFile":"custom-provider.md"},"development":{"title":"Développement","markdown":"Voir [AGENTS.md](https://github.com/earendil-works/pi-mono/blob/main/AGENTS.md) pour des directives supplémentaires.\n\n## Installation\n\n```bash\ngit clone https://github.com/earendil-works/pi-mono\ncd pi-mono\nnpm install\nnpm run build\n```\n\nExécuter à partir des sources:\n\n```bash\n/path/to/pi-mono/pi-test.sh\n```\n\nLe script peut être exécuté à partir de n'importe quel répertoire. Pi conserve le répertoire de travail actuel de l'appelant.\n\n## Forkage / Rebranding\n\nConfigurer via `package.json`:\n\n```json\n{\n  \"piConfig\": {\n    \"name\": \"pi\",\n    \"configDir\": \".pi\"\n  }\n}\n```\n\nModifiez les champs `name`, `configDir` et `bin` pour votre fork. Affecte la bannière CLI, les chemins de configuration et les noms de variables d'environnement.\n\n## Résolution du chemin\n\nTrois modes d'exécution: npm installation, binaire autonome, tsx depuis les sources.\n\n**Utilisez toujours `src/config.ts`** pour les actifs du package:\n\n```typescript\nimport { getPackageDir, getThemeDir } from \"./config.js\";\n```\n\nN'utilisez jamais `__dirname` directement pour les actifs du package.\n\n## Commande de débogage\n\n`/debug` (caché) écrit dans `~/.pi/agent/pi-debug.log`:\n- Lignes TUI rendues avec des codes ANSI\n- Derniers messages envoyés au LLM\n\n## Essai\n\n```bash\n./test.sh                         # Run non-LLM tests (no API keys needed)\nnpm test                          # Run all tests\nnpm test -- test/specific.test.ts # Run specific test\n```\n\n## Structure du projet\n\n```\npackages/\n  ai/           # LLM provider abstraction\n  agent/        # Agent loop and message types  \n  tui/          # Terminal UI components\n  coding-agent/ # CLI and interactive mode\n```","sourceFile":"development.md"},"environment-variables":{"title":"Variables d'environnement","markdown":"Pi utilise les variables d'environnement de trois manières:\n\n- Des variables telles que `PI_OFFLINE` configurent le processus Pi.\n- Pi définit `PI_CODING_AGENT` afin que les processus enfants puissent détecter qu'ils s'exécutent à l'intérieur de Pi.\n- Les commandes exécutées par l'outil bash appelable LLM reçoivent des variables `PI_*` décrivant la session en cours.\n\nLes variables clés du fournisseur API sont documentées séparément dans [Providers](providers.md#environment-variables-or-auth-file).\n\n## Marqueur de processus\n\nLes points d'entrée CLI et RPC définissent `PI_CODING_AGENT=true`. Les processus enfants en héritent et peuvent l'utiliser pour détecter qu'ils s'exécutent à l'intérieur de Pi. Il n'est pas spécifique à la session et n'est pas défini automatiquement lorsque Pi est intégré via le SDK.\n\n## Environnement de session de l'outil Bash\n\nLes commandes exécutées par l'outil bash reçoivent l'état de session actuel Pi:\n\n| Variable | Description |\n|----------|-------------|\n| `PI_SESSION_ID` | ID de session actuelle |\n| `PI_SESSION_FILE` | Chemin absolu vers le fichier JSONL de la session en cours; désarmé pour les sessions éphémères |\n| `PI_PROVIDER` | Fournisseur de modèles actuellement sélectionné |\n| `PI_MODEL` | ID du modèle actuellement sélectionné |\n| `PI_REASONING_LEVEL` | Niveau de raisonnement effectif actuel: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` ou `max` |\n\nLes valeurs sont résolues au démarrage de chaque commande. Changer de modèle ou changer de niveau de raisonnement affecte donc la commande bash suivante sans redémarrer Pi. `PI_PROVIDER` et `PI_MODEL` identifient le modèle Pi sélectionné, et non un modèle en amont différent qu'un routeur peut choisir en interne.\n\nLorsqu'on vous demande quel modèle ou fournisseur est en cours d'exécution, inspectez ces variables au lieu de déduire la réponse à partir de l'invite du système:\n\n```bash\nprintf '%s/%s\\n' \"$PI_PROVIDER\" \"$PI_MODEL\"\nprintf 'reasoning=%s session=%s\\n' \"$PI_REASONING_LEVEL\" \"$PI_SESSION_ID\"\n```\n\nLe fichier de session peut être inspecté directement lorsque la session est persistante:\n\n```bash\nif [ -n \"$PI_SESSION_FILE\" ]; then\n  tail -n 1 \"$PI_SESSION_FILE\"\nfi\n```\n\nCes variables sont injectées dans l'outil bash appelable LLM. Ils ne sont pas injectés dans les commandes `!` ou `!!` saisies par l'utilisateur.\n\n### Outils Bash personnalisés\n\nLes outils Bash créés avec `createBashTool()` exposent l'environnement de session par défaut lorsqu'ils sont enregistrés avec Pi. L'injection a lieu avant `spawnHook`, donc un hook reçoit les variables en `ctx.env`:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  spawnHook: (ctx) => ({\n    ...ctx,\n    env: { ...ctx.env, CI: \"1\" },\n  }),\n});\n```\n\nDésactivez les métadonnées de session indépendamment du hook de spawn:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n  spawnHook: (ctx) => ctx,\n});\n```\n\nLorsqu'il est désactivé, Pi supprime les valeurs héritées de ces variables afin que les processus Pi imbriqués n'exposent pas les métadonnées obsolètes de la session parent.\n\n## Pi Configuration du processus\n\nCes variables sont lues par Pi lui-même:\n\n| Variable | Description |\n|----------|-------------|\n| `PI_CODING_AGENT_DIR` | Remplacez le répertoire de configuration; la valeur par défaut est `~/.pi/agent` |\n| `PI_CODING_AGENT_SESSION_DIR` | Remplacer le stockage de session; remplacé par `--session-dir` |\n| `PI_PACKAGE_DIR` | Remplacer le répertoire du package, utile pour les chemins du magasin Nix/Guix |\n| `PI_OFFLINE` | Désactivez les opérations réseau de démarrage, y compris les vérifications de mise à jour, les mises à jour de packages et la télémétrie d'installation/mise à jour. |\n| `PI_SKIP_VERSION_CHECK` | Désactivez la demande de dernière version `pi.dev` |\n| `PI_TELEMETRY` | Remplacer la télémétrie d'installation/mise à jour et les en-têtes d'attribution du fournisseur: `1`/`true`/`yes` ou `0`/`false`/`no` |\n| `PI_CACHE_RETENTION` | Défini sur `long` pour la mise en cache étendue des invites du fournisseur lorsque cela est pris en charge |\n| `PI_SHARE_VIEWER_URL` | Remplacer l'URL de base utilisée par `/share` |\n| `PI_HARDWARE_CURSOR` | Réglez sur `1` pour afficher le curseur matériel; voir [Terminal setup](terminal-setup.md) |\n| `VISUAL`, `EDITOR` | Solution de secours de l'éditeur externe lorsque `externalEditor` n'est pas défini |\n| `HTTP_PROXY`, `HTTPS_PROXY` | Requêtes HTTP sortantes du proxy |\n\nLes informations d'identification du fournisseur telles que `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` et la configuration du fournisseur cloud sont répertoriées dans [Providers](providers.md#environment-variables-or-auth-file).","sourceFile":"environment-variables.md"},"extensions":{"title":"Extensions","markdown":"> pi peut créer des extensions. Demandez-lui d'en créer un pour votre cas d'utilisation.\n\n\nExtensions sont des modules TypeScript qui étendent le comportement de pi. Ils peuvent s'abonner aux événements du cycle de vie, enregistrer des outils personnalisés appelables par le LLM, ajouter des commandes, etc.\n\n> **Placement pour /reload:** Placez les extensions dans `~/.pi/agent/extensions/` (global) ou `.pi/extensions/` (projet-local) pour la découverte automatique. Utilisez `pi -e./path.ts` uniquement pour les tests rapides. Extensions dans les emplacements découverts automatiquement peut être rechargé à chaud avec `/reload`.\n\n**Capacités clés:**\n- **Outils personnalisés** - Enregistrez les outils que le LLM peut appeler via `pi.registerTool()`\n- **Interception d'événements** – Bloquer ou modifier les appels d'outils, injecter du contexte, personnaliser le compactage\n- **Interaction utilisateur** - Inviter les utilisateurs via `ctx.ui` (sélectionner, confirmer, saisir, notifier)\n- **Composants d'interface utilisateur personnalisés** - Composants complets TUI avec saisie au clavier via `ctx.ui.custom()` pour des interactions complexes\n- **Commandes personnalisées** - Enregistrez des commandes comme `/mycommand` via `pi.registerCommand()`\n- **Persistance de session** – État de stockage qui survit aux redémarrages via `pi.appendEntry()`\n- **Rendu personnalisé** - Contrôlez la façon dont les appels/résultats et les messages de l'outil apparaissent dans TUI\n\n**Exemples de cas d'utilisation:**\n- Portes d'autorisation (confirmer avant `rm -rf`, `sudo`, etc.)\n- Git checkpointing (cache à chaque tour, restauration sur branche)\n- Protection du chemin (le bloc écrit dans `.env`, `node_modules/`)\n- Compactage personnalisé (résumez la conversation à votre manière)\n- Résumés de conversation (voir exemple `summarize.ts`)\n- Outils interactifs (questions, assistants, boîtes de dialogue personnalisées)\n- Outils avec état (listes de tâches, pools de connexions)\n- Intégrations externes (observateurs de fichiers, webhooks, déclencheurs CI)\n- Jeux en attendant (voir exemple `snake.ts`)\n\nVoir [examples/extensions/](../examples/extensions/) pour les implémentations fonctionnelles.\n\n## Table des matières\n\n- [Quick Start](#quick-start)\n- [Extension Locations](#extension-locations)\n- [Available Imports](#available-imports)\n- [Writing an Extension](#writing-an-extension)\n  - [Extension Styles](#extension-styles)\n- [Events](#events)\n  - [Lifecycle Overview](#lifecycle-overview)\n  - [Resource Events](#resource-events)\n  - [Session Events](#session-events)\n  - [Agent Events](#agent-events)\n  - [Model Events](#model-events)\n  - [Tool Events](#tool-events)\n- [ExtensionContext](#extensioncontext)\n- [ExtensionCommandContext](#extensioncommandcontext)\n- [ExtensionAPI Methods](#extensionapi-methods)\n- [State Management](#state-management)\n- [Custom Tools](#custom-tools)\n  - [Dynamic Tool Loading](#dynamic-tool-loading)\n- [Custom UI](#custom-ui)\n- [Error Handling](#error-handling)\n- [Mode Behavior](#mode-behavior)\n- [Examples Reference](#examples-reference)\n\n## Démarrage rapide\n\nCréez `~/.pi/agent/extensions/my-extension.ts`:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nexport default function (pi: ExtensionAPI) {\n  // React to events\n  pi.on(\"session_start\", async (_event, ctx) => {\n    ctx.ui.notify(\"Extension loaded!\", \"info\");\n  });\n\n  pi.on(\"tool_call\", async (event, ctx) => {\n    if (event.toolName === \"bash\" && event.input.command?.includes(\"rm -rf\")) {\n      const ok = await ctx.ui.confirm(\"Dangerous!\", \"Allow rm -rf?\");\n      if (!ok) return { block: true, reason: \"Blocked by user\" };\n    }\n  });\n\n  // Register a custom tool\n  pi.registerTool({\n    name: \"greet\",\n    label: \"Greet\",\n    description: \"Greet someone by name\",\n    parameters: Type.Object({\n      name: Type.String({ description: \"Name to greet\" }),\n    }),\n    async execute(toolCallId, params, signal, onUpdate, ctx) {\n      return {\n        content: [{ type: \"text\", text: `Hello, ${params.name}!` }],\n        details: {},\n      };\n    },\n  });\n\n  // Register a command\n  pi.registerCommand(\"hello\", {\n    description: \"Say hello\",\n    handler: async (args, ctx) => {\n      ctx.ui.notify(`Hello ${args || \"world\"}!`, \"info\");\n    },\n  });\n}\n```\n\nTestez avec le drapeau `--extension` (ou `-e`):\n\n```bash\npi -e ./my-extension.ts\n```\n\n## Emplacements des extensions\n\n> **Sécurité:** Extensions s'exécute avec toutes les autorisations de votre système et peut exécuter du code arbitraire. Installez uniquement à partir de sources fiables.\n\nExtensions sont découverts automatiquement à partir d'emplacements fiables. Les entrées `.pi/extensions` locales du projet se chargent uniquement une fois que le projet est approuvé.\n\n| Emplacement | Portée |\n|----------|-------|\n| `~/.pi/agent/extensions/*.ts` | Global (tous les projets) |\n| `~/.pi/agent/extensions/*/index.ts` | Global (sous-répertoire) |\n| `.pi/extensions/*.ts` | Projet-local |\n| `.pi/extensions/*/index.ts` | Projet-local (sous-répertoire) |\n\nChemins supplémentaires via `settings.json`:\n\n```json\n{\n  \"packages\": [\n    \"npm:@foo/bar@1.0.0\",\n    \"git:github.com/user/repo@v1\"\n  ],\n  \"extensions\": [\n    \"/path/to/local/extension.ts\",\n    \"/path/to/local/extension/dir\"\n  ]\n}\n```\n\nPour partager des extensions via npm ou git en tant que packages pi, voir [packages.md](packages.md).\n\n## Importations disponibles\n\n| Emballer | But |\n|---------|---------|\n| `@earendil-works/pi-coding-agent` | Types d'extensions (`ExtensionAPI`, `ExtensionContext`, événements) |\n| `typebox` | Définitions de schéma pour les paramètres d'outil |\n| `@earendil-works/pi-ai` | Utilitaires d'IA (`StringEnum` pour les énumérations compatibles Google) |\n| `@earendil-works/pi-tui` | TUI composants pour un rendu personnalisé |\n\nnpm les dépendances fonctionnent aussi. Ajoutez un `package.json` à côté de votre extension (ou dans un répertoire parent), exécutez `npm install` et les importations depuis `node_modules/` sont résolues automatiquement.\n\nPour les packages pi distribués installés avec `pi install` (npm ou git), les dépôts d'exécution doivent être en `dependencies`. L'installation du package utilise les installations de production (`npm install --omit=dev`) par défaut, donc `devDependencies` ne sont pas disponibles au moment de l'exécution; lorsque `npmCommand` est configuré, les packages git utilisent plain `install` pour la compatibilité avec les wrappers.\n\nNode.js intégrés (`node:fs`, `node:path`, etc.) sont également disponibles.\n\n## Écrire une extension\n\nUne extension exporte une fonction d'usine par défaut qui reçoit `ExtensionAPI`. La fabrique peut être synchrone ou asynchrone:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  // Subscribe to events\n  pi.on(\"event_name\", async (event, ctx) => {\n    // ctx.ui for user interaction\n    const ok = await ctx.ui.confirm(\"Title\", \"Are you sure?\");\n    ctx.ui.notify(\"Done!\", \"info\");\n    ctx.ui.setStatus(\"my-ext\", \"Processing...\");  // Footer status\n    ctx.ui.setWidget(\"my-ext\", [\"Line 1\", \"Line 2\"]);  // Widget above editor (default)\n  });\n\n  // Register tools, commands, shortcuts, flags\n  pi.registerTool({ ... });\n  pi.registerCommand(\"name\", { ... });\n  pi.registerShortcut(\"ctrl+x\", { ... });\n  pi.registerFlag(\"my-flag\", { ... });\n}\n```\n\nExtensions sont chargés via [jiti](https://github.com/unjs/jiti), donc TypeScript fonctionne sans compilation.\n\nSi l'usine renvoie un `Promise`, pi l'attend avant de continuer le démarrage. Cela signifie que l'initialisation asynchrone se termine avant `session_start`, avant `resources_discover` et avant que les enregistrements de fournisseurs mis en file d'attente via `pi.registerProvider()` ne soient vidés.\n\n### Fonctions d'usine asynchrone\n\nUtilisez une usine asynchrone pour un travail de démarrage ponctuel, comme la récupération de la configuration à distance ou la découverte dynamique des modèles disponibles.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default async function (pi: ExtensionAPI) {\n  const response = await fetch(\"http://localhost:1234/v1/models\");\n  const payload = (await response.json()) as {\n    data: Array<{\n      id: string;\n      name?: string;\n      context_window?: number;\n      max_tokens?: number;\n    }>;\n  };\n\n  pi.registerProvider(\"local-openai\", {\n    baseUrl: \"http://localhost:1234/v1\",\n    apiKey: \"$LOCAL_OPENAI_API_KEY\",\n    api: \"openai-completions\",\n    models: payload.data.map((model) => ({\n      id: model.id,\n      name: model.name ?? model.id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: model.context_window ?? 128000,\n      maxTokens: model.max_tokens ?? 4096,\n    })),\n  });\n}\n```\n\nCe modèle rend les modèles récupérés disponibles lors du démarrage normal et jusqu'à `pi --list-models`.\n\n### Ressources de longue durée et arrêt\n\nLes fabriques d'extensions peuvent s'exécuter dans des appels qui ne démarrent jamais de session. Ne démarrez pas les ressources en arrière-plan telles que les processus, les sockets, les observateurs de fichiers ou les minuteries depuis l'usine.\n\nDifférez le démarrage de la ressource en arrière-plan jusqu'à `session_start` ou jusqu'à la commande/outil/événement qui a besoin de la ressource. Enregistrez un gestionnaire `session_shutdown` idempotent pour fermer toutes les ressources de session que vous démarrez.\n\n### Styles d'extensions\n\n**Fichier unique** - le plus simple, pour les petites extensions:\n\n```\n~/.pi/agent/extensions/\n└── my-extension.ts\n```\n\n**Répertoire avec index.ts** - pour les extensions multi-fichiers:\n\n```\n~/.pi/agent/extensions/\n└── my-extension/\n    ├── index.ts        # Entry point (exports default function)\n    ├── tools.ts        # Helper module\n    └── utils.ts        # Helper module\n```\n\n**Package avec dépendances** - pour les extensions qui nécessitent npm packages:\n\n```\n~/.pi/agent/extensions/\n└── my-extension/\n    ├── package.json    # Declares dependencies and entry points\n    ├── package-lock.json\n    ├── node_modules/   # After npm install\n    └── src/\n        └── index.ts\n```\n\n```json\n// package.json\n{\n  \"name\": \"my-extension\",\n  \"dependencies\": {\n    \"zod\": \"^3.0.0\",\n    \"chalk\": \"^5.0.0\"\n  },\n  \"pi\": {\n    \"extensions\": [\"./src/index.ts\"]\n  }\n}\n```\n\nExécutez `npm install` dans le répertoire d'extension, puis les importations depuis `node_modules/` fonctionnent automatiquement.\n\n## Événements\n\n### Aperçu du cycle de vie\n\n```\npi starts\n  │\n  ├─► project_trust (user/global and CLI extensions only, before project resources load)\n  ├─► session_start { reason: \"startup\" }\n  └─► resources_discover { reason: \"startup\" }\n      │\n      ▼\nuser sends prompt ─────────────────────────────────────────┐\n  │                                                        │\n  ├─► (extension commands checked first, bypass if found)  │\n  ├─► input (can intercept, transform, or handle)          │\n  ├─► (skill/template expansion if not handled)            │\n  ├─► before_agent_start (can inject message, modify system prompt)\n  ├─► agent_start                                          │\n  ├─► message_start / message_update / message_end         │\n  │                                                        │\n  │   ┌─── turn (repeats while LLM calls tools) ───┐       │\n  │   │                                            │       │\n  │   ├─► turn_start                               │       │\n  │   ├─► context (can modify messages)            │       │\n  │   ├─► before_provider_headers (can mutate headers)     |\n  │   ├─► before_provider_request (can inspect or replace payload)\n  │   ├─► after_provider_response (status + headers, before stream consume)\n  │   │                                            │       │\n  │   │   LLM responds, may call tools:            │       │\n  │   │     ├─► tool_execution_start               │       │\n  │   │     ├─► tool_call (can block)              │       │\n  │   │     ├─► tool_execution_update              │       │\n  │   │     ├─► tool_result (can modify)           │       │\n  │   │     └─► tool_execution_end                 │       │\n  │   │                                            │       │\n  │   └─► turn_end                                 │       │\n  │                                                        │\n  ├─► agent_end                                            │\n  └─► agent_settled (no retry/compaction/follow-up left)   │\n                                                           │\nuser sends another prompt ◄────────────────────────────────┘\n\n/new (new session) or /resume (switch session)\n  ├─► session_before_switch (can cancel)\n  ├─► session_shutdown\n  ├─► session_start { reason: \"new\" | \"resume\", previousSessionFile? }\n  └─► resources_discover { reason: \"startup\" }\n\n/fork or /clone\n  ├─► session_before_fork (can cancel)\n  ├─► session_shutdown\n  ├─► session_start { reason: \"fork\", previousSessionFile }\n  └─► resources_discover { reason: \"startup\" }\n\n/name or pi.setSessionName()\n  └─► session_info_changed\n\n/compact or auto-compaction\n  ├─► session_before_compact (can cancel or customize)\n  └─► session_compact\n\n/tree navigation\n  ├─► session_before_tree (can cancel or customize)\n  └─► session_tree\n\n/model or Ctrl+P (model selection/cycling)\n  ├─► thinking_level_select (if model change changes/clamps thinking level)\n  └─► model_select\n\nthinking level changes (settings, keybinding, pi.setThinkingLevel())\n  └─► thinking_level_select\n\nexit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)\n  └─► session_shutdown\n```\n\n### Événements de démarrage\n\n#### projet_trust\n\nLancé avant que pi ne décide s'il doit faire confiance à un projet avec des configurations dynamiques (`.pi` ou `.agents/skills`). Il s'exécute au démarrage et lors du remplacement de session (par exemple `/resume`) entre dans un cwd dont la confiance n'a pas été résolue dans le processus en cours. Seules les extensions utilisateur/globales et les extensions CLI `-e` participent; les extensions locales du projet ne sont chargées qu'une fois la confiance résolue.\n\n```typescript\npi.on(\"project_trust\", async (event, ctx) => {\n  // event.cwd - current working directory\n  // ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers\n  if (await ctx.ui.confirm(\"Trust project?\", event.cwd)) {\n    return { trusted: \"yes\", remember: true };\n  }\n  return { trusted: \"undecided\" };\n});\n```\n\nUn gestionnaire `project_trust` doit renvoyer `{ trusted: \"yes\" | \"no\" | \"undecided\" }`. Un utilisateur/global ou une extension CLI qui renvoie `\"yes\"` ou `\"no\"` est propriétaire de la décision; la première décision oui/non l'emporte et supprime l'invite de confiance intégrée. Utilisez `remember: true` pour conserver une décision oui/non; sinon, cela s'applique uniquement au processus en cours. Renvoyez `\"undecided\"` pour laisser les gestionnaires ultérieurs ou le flux de confiance intégré décider. Vérifiez `ctx.hasUI` avant de demander. Si aucun gestionnaire ne renvoie oui/non, la résolution de confiance normale continue: les décisions `trust.json` enregistrées s'appliquent en premier, puis `defaultProjectTrust` contrôle si pi demande, approuve ou refuse par défaut.\n\n### Événements de ressources\n\n#### ressources_découvrir\n\nDéclenché après `session_start` afin que les extensions puissent apporter des chemins de compétences, d'invites et de thèmes supplémentaires.\nLe chemin de démarrage utilise `reason: \"startup\"`. Le rechargement utilise `reason: \"reload\"`.\n\n```typescript\npi.on(\"resources_discover\", async (event, _ctx) => {\n  // event.cwd - current working directory\n  // event.reason - \"startup\" | \"reload\"\n  return {\n    skillPaths: [\"/path/to/skills\"],\n    promptPaths: [\"/path/to/prompts\"],\n    themePaths: [\"/path/to/themes\"],\n  };\n});\n```\n\n### Événements de session\n\nVoir [Session Format](session-format.md) pour les composants internes du stockage de session et le SessionManager API.\n\n#### session_start\n\nDéclenché lorsqu'une session est démarrée, chargée ou rechargée.\n\n```typescript\npi.on(\"session_start\", async (event, ctx) => {\n  // event.reason - \"startup\" | \"reload\" | \"new\" | \"resume\" | \"fork\"\n  // event.previousSessionFile - present for \"new\", \"resume\", and \"fork\"\n  ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? \"ephemeral\"}`, \"info\");\n});\n```\n\n#### session_info_changed\n\nDéclenché lorsque le nom d'affichage de la session actuelle est défini via `/name`, RPC ou `pi.setSessionName()`.\n\n```typescript\npi.on(\"session_info_changed\", async (event, ctx) => {\n  // event.name - current normalized name, or undefined if cleared\n  ctx.ui.notify(`Session renamed: ${event.name ?? \"(none)\"}`, \"info\");\n});\n```\n\n#### session_avant_switch\n\nDéclenché avant de démarrer une nouvelle session (`/new`) ou de changer de session (`/resume`).\n\n```typescript\npi.on(\"session_before_switch\", async (event, ctx) => {\n  // event.reason - \"new\" or \"resume\"\n  // event.targetSessionFile - session we're switching to (only for \"resume\")\n\n  if (event.reason === \"new\") {\n    const ok = await ctx.ui.confirm(\"Clear?\", \"Delete all messages?\");\n    if (!ok) return { cancel: true };\n  }\n});\n```\n\nAprès un changement réussi ou une action de nouvelle session, pi émet `session_shutdown` pour l'ancienne instance d'extension, recharge et relie les extensions pour la nouvelle session, puis émet `session_start` avec `reason: \"new\" | \"resume\"` et `previousSessionFile`.\nEffectuez un travail de nettoyage en `session_shutdown`, puis rétablissez tout état en mémoire en `session_start`.\n\n#### session_avant_fork\n\nLancé lors d'un fork via `/fork` ou d'un clonage via `/clone`.\n\n```typescript\npi.on(\"session_before_fork\", async (event, ctx) => {\n  // event.entryId - ID of the selected entry\n  // event.position - \"before\" for /fork, \"at\" for /clone\n  return { cancel: true }; // Cancel fork/clone\n  // OR\n  return { skipConversationRestore: true }; // Reserved for future conversation restore control\n});\n```\n\nAprès un fork ou un clone réussi, pi émet `session_shutdown` pour l'ancienne instance d'extension, recharge et relie les extensions pour la nouvelle session, puis émet `session_start` avec `reason: \"fork\"` et `previousSessionFile`.\nEffectuez un travail de nettoyage en `session_shutdown`, puis rétablissez tout état en mémoire en `session_start`.\n\n#### session_avant_compact / session_compact\n\nTiré par compactage. Voir [compaction.md](compaction.md) pour plus de détails.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n    }\n  };\n});\n\npi.on(\"session_compact\", async (event, ctx) => {\n  // event.compactionEntry - the saved compaction\n  // event.fromExtension - whether extension provided it\n  // event.reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n});\n```\n\n#### session_avant_arbre / session_arbre\n\nTiré sur la navigation `/tree`. Voir [Sessions](sessions.md) pour les concepts de navigation dans l'arborescence.\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n  return { cancel: true };\n  // OR provide custom summary:\n  return {\n    summary: {\n      summary: \"...\",\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: {},\n    },\n  };\n});\n\npi.on(\"session_tree\", async (event, ctx) => {\n  // event.newLeafId, oldLeafId, summaryEntry, fromExtension\n});\n```\n\n#### session_shutdown\n\nLancé avant qu'un runtime de session démarré ne soit détruit. Utilisez-le pour nettoyer les ressources ouvertes à partir de `session_start` ou d'autres hooks à l'échelle de la session.\n\n```typescript\npi.on(\"session_shutdown\", async (event, ctx) => {\n  // event.reason - \"quit\" | \"reload\" | \"new\" | \"resume\" | \"fork\"\n  // event.targetSessionFile - destination session for session replacement flows\n  // Cleanup, save state, etc.\n});\n```\n\n### Événements d'agent\n\n#### avant_agent_start\n\nLancé après que l'utilisateur soumet l'invite, avant la boucle de l'agent. Peut injecter un message et/ou modifier l'invite du système.\n\n```typescript\npi.on(\"before_agent_start\", async (event, ctx) => {\n  // event.prompt - user's prompt text\n  // event.images - attached images (if any)\n  // event.systemPrompt - current chained system prompt for this handler\n  //   (includes changes from earlier before_agent_start handlers)\n  // event.systemPromptOptions - structured options used to build the system prompt\n  //   .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)\n  //   .selectedTools - tools currently active in the prompt\n  //   .toolSnippets - one-line descriptions for each tool\n  //   .promptGuidelines - custom guideline bullets\n  //   .appendSystemPrompt - text from --append-system-prompt flags\n  //   .cwd - working directory\n  //   .contextFiles - AGENTS.md files and other loaded context files\n  //   .skills - loaded skills\n\n  return {\n    // Inject a persistent message (stored in session, sent to LLM)\n    message: {\n      customType: \"my-extension\",\n      content: \"Additional context for the LLM\",\n      display: true,\n    },\n    // Replace the system prompt for this turn (chained across extensions)\n    systemPrompt: event.systemPrompt + \"\\n\\nExtra instructions for this turn...\",\n  };\n});\n```\n\nLe champ `systemPromptOptions` donne aux extensions l'accès aux mêmes données structurées que Pi utilise pour créer l'invite système. Cela vous permet d'inspecter ce que Pi a chargé (invites personnalisées, directives, extraits d'outils, context files, compétences) — sans redécouvrir les ressources ni réanalyser les indicateurs. Utilisez-le lorsque votre extension doit apporter des modifications approfondies et éclairées à l'invite système tout en respectant la configuration fournie par l'utilisateur.\n\nÀ l'intérieur de `before_agent_start`, `event.systemPrompt` et `ctx.getSystemPrompt()` reflètent tous deux l'invite système chaînée du gestionnaire actuel. Les gestionnaires `before_agent_start` ultérieurs peuvent toujours le modifier à nouveau.\n\n#### agent_start / agent_end / agent_settled\n\n`agent_start` se déclenche lorsqu'une exécution d'agent de bas niveau commence. `agent_end` se déclenche à la fin de cette exécution, mais Pi peut toujours réessayer automatiquement, compacter et réessayer automatiquement, ou continuer avec les messages de suivi en file d'attente. Utilisez `agent_settled` pour les intégrations de statut qui doivent savoir que Pi ne continuera pas à s'exécuter automatiquement.\n\n```typescript\npi.on(\"agent_start\", async (_event, ctx) => {});\n\npi.on(\"agent_end\", async (event, ctx) => {\n  // event.messages - messages from this low-level run\n});\n\npi.on(\"agent_settled\", async (_event, ctx) => {\n  // ctx.isIdle() is true here unless another extension started a new run.\n});\n```\n\n#### tour_début / tour_end\n\nDéclenché à chaque tour (une réponse LLM + appels d'outils).\n\n```typescript\npi.on(\"turn_start\", async (event, ctx) => {\n  // event.turnIndex, event.timestamp\n});\n\npi.on(\"turn_end\", async (event, ctx) => {\n  // event.turnIndex, event.message, event.toolResults\n});\n```\n\n#### message_start / message_update / message_end\n\nDéclenché pour les mises à jour du cycle de vie des messages.\n\n- `message_start` et `message_end` se déclenchent pour les messages utilisateur, assistant et toolResult.\n- `message_update` se déclenche pour les mises à jour en continu de l'assistant.\n- Les gestionnaires `message_end` peuvent renvoyer `{ message }` pour remplacer le message finalisé. Le remplaçant doit garder le même `role`.\n\n```typescript\npi.on(\"message_start\", async (event, ctx) => {\n  // event.message\n});\n\npi.on(\"message_update\", async (event, ctx) => {\n  // event.message\n  // event.assistantMessageEvent (token-by-token stream event)\n});\n\npi.on(\"message_end\", async (event, ctx) => {\n  if (event.message.role !== \"assistant\") return;\n\n  return {\n    message: {\n      ...event.message,\n      usage: {\n        ...event.message.usage,\n        cost: {\n          ...event.message.usage.cost,\n          total: 0.123,\n        },\n      },\n    },\n  };\n});\n```\n\n#### tool_execution_start/tool_execution_update/tool_execution_end\n\nDéclenché pour les mises à jour du cycle de vie d’exécution des outils.\n\nEn mode outil parallèle:\n- `tool_execution_start` est émis dans l'ordre des sources assistantes pendant la phase de contrôle en amont\n- `tool_execution_update` les événements peuvent s'entrelacer entre les outils\n- `tool_execution_end` est émis dans l'ordre d'achèvement des outils après la finalisation de chaque outil\n- Les événements de message finaux `toolResult` sont toujours émis plus tard dans l'ordre des sources de l'assistant\n\n```typescript\npi.on(\"tool_execution_start\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.args\n});\n\npi.on(\"tool_execution_update\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.args, event.partialResult\n});\n\npi.on(\"tool_execution_end\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.result, event.isError\n});\n```\n\n#### contexte\n\nLancé avant chaque appel LLM. Modifier les messages de manière non destructive. Voir [Session Format](session-format.md) pour les types de messages.\n\n```typescript\npi.on(\"context\", async (event, ctx) => {\n  // event.messages - deep copy, safe to modify\n  const filtered = event.messages.filter(m => !shouldPrune(m));\n  return { messages: filtered };\n});\n```\n\n#### avant_provider_headers\n\nDéclenché après l'assemblage des en-têtes HTTP sortants. Utilisez-le pour ajouter, remplacer ou supprimer des en-têtes de requête.\n\nLes gestionnaires mutent `event.headers` sur place. Définissez une clé sur une chaîne pour l'ajouter ou la remplacer, ou sur `null` pour la supprimer.\n\n```typescript\npi.on(\"before_provider_headers\", (event, ctx) => {\n  // Add or override — e.g. a session id for gateway tracing/attribution\n  event.headers[\"x-session-id\"] = ctx.sessionManager.getSessionId();\n\n  // Drop a tracking header pi adds for this call\n  event.headers[\"X-OpenRouter-Title\"] = null;\n});\n```\n\nS'exécute une fois par demande du fournisseur; les nouvelles tentatives réutilisent les mêmes en-têtes plutôt que de relancer le hook.\n\n#### avant_provider_request\n\nLancé après la création de la charge utile spécifique au fournisseur, juste avant l'envoi de la demande. Les gestionnaires s’exécutent dans l’ordre de chargement des extensions. Le retour de `undefined` maintient la charge utile inchangée. Le renvoi de toute autre valeur remplace la charge utile pour les gestionnaires ultérieurs et pour la demande réelle.\n\nCe hook peut réécrire les instructions système au niveau du fournisseur ou les supprimer complètement. Ces modifications au niveau de la charge utile ne sont pas reflétées par `ctx.getSystemPrompt()`, qui signale la chaîne d'invite système de Pi plutôt que la charge utile sérialisée finale du fournisseur.\n\n```typescript\npi.on(\"before_provider_request\", (event, ctx) => {\n  console.log(JSON.stringify(event.payload, null, 2));\n\n  // Optional: replace payload\n  // return { ...event.payload, temperature: 0 };\n});\n```\n\nCeci est principalement utile pour déboguer la sérialisation du fournisseur et le comportement du cache.\n\n#### after_provider_response\n\nDéclenché après la réception d'une réponse HTTP et avant que le corps de son flux ne soit consommé. Les gestionnaires s’exécutent dans l’ordre de chargement des extensions.\n\n```typescript\npi.on(\"after_provider_response\", (event, ctx) => {\n  // event.status - HTTP status code\n  // event.headers - normalized response headers\n  if (event.status === 429) {\n    console.log(\"rate limited\", event.headers[\"retry-after\"]);\n  }\n});\n```\n\nLa disponibilité de l'en-tête dépend du fournisseur et du transport. Providers que les réponses HTTP abstraites ne peuvent pas exposer les en-têtes.\n\n### Événements modèles\n\n#### model_select\n\nDéclenché lorsque le modèle change via la commande `/model`, le cycle de modèle (`Ctrl+P`) ou la restauration de session.\n\n```typescript\npi.on(\"model_select\", async (event, ctx) => {\n  // event.model - newly selected model\n  // event.previousModel - previous model (undefined if first selection)\n  // event.source - \"set\" | \"cycle\" | \"restore\"\n\n  const prev = event.previousModel\n    ? `${event.previousModel.provider}/${event.previousModel.id}`\n    : \"none\";\n  const next = `${event.model.provider}/${event.model.id}`;\n\n  ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, \"info\");\n});\n```\n\nUtilisez-le pour mettre à jour les éléments de l'interface utilisateur (barres d'état, pieds de page) ou effectuer une initialisation spécifique au modèle lorsque le modèle actif change.\n\n#### réflexion_level_select\n\nLancé lorsque le niveau de réflexion change. Il s'agit uniquement d'une notification; les valeurs de retour du gestionnaire sont ignorées.\n\n```typescript\npi.on(\"thinking_level_select\", async (event, ctx) => {\n  // event.level - newly selected thinking level\n  // event.previousLevel - previous thinking level\n\n  ctx.ui.setStatus(\"thinking\", `thinking: ${event.level}`);\n});\n```\n\nUtilisez-le pour mettre à jour l'interface utilisateur de l'extension lorsque `pi.setThinkingLevel()`, le modèle change ou les contrôles de niveau de réflexion intégrés modifient le niveau de réflexion actif.\n\n### Événements d'outils\n\n#### appel_outil\n\nLancé après `tool_execution_start`, avant que l'outil ne s'exécute. **Peut bloquer.** Utilisez `isToolCallEventType` pour affiner et obtenir des entrées saisies.\n\nAvant l'exécution de `tool_call`, pi attend que les événements d'agent précédemment émis finissent de s'écouler via `AgentSession`. Cela signifie que `ctx.sessionManager` est à jour via le message d'appel d'outil de l'assistant actuel.\n\nDans le mode d'exécution d'outil parallèle par défaut, les appels d'outils frères à partir du même message d'assistant sont contrôlés en amont de manière séquentielle, puis exécutés simultanément. `tool_call` n'est pas garanti de voir les résultats de l'outil frère de ce même message d'assistant dans `ctx.sessionManager`.\n\n`event.input` est mutable. Mutez-le sur place pour corriger les arguments de l'outil avant l'exécution.\n\nGaranties de comportement:\n- Les mutations vers `event.input` affectent l'exécution réelle de l'outil\n- Les gestionnaires `tool_call` ultérieurs voient les mutations effectuées par les gestionnaires précédents\n- Aucune revalidation n'est effectuée après votre mutation\n- Renvoie les valeurs de `tool_call` contrôle le blocage via `{ block: true, reason?: string, terminate?: boolean }`\n- `terminate` ne s'applique qu'à un appel bloqué; l'agent s'arrête plus tôt que lorsque chaque résultat finalisé du lot se termine\n\n```typescript\nimport { isToolCallEventType } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"tool_call\", async (event, ctx) => {\n  // event.toolName - \"bash\", \"read\", \"write\", \"edit\", etc.\n  // event.toolCallId\n  // event.input - tool parameters (mutable)\n\n  // Built-in tools: no type params needed\n  if (isToolCallEventType(\"bash\", event)) {\n    // event.input is { command: string; timeout?: number }\n    event.input.command = `source ~/.profile\\n${event.input.command}`;\n\n    if (event.input.command.includes(\"rm -rf\")) {\n      return { block: true, reason: \"Dangerous command\", terminate: true };\n    }\n  }\n\n  if (isToolCallEventType(\"read\", event)) {\n    // event.input is { path: string; offset?: number; limit?: number }\n    console.log(`Reading: ${event.input.path}`);\n  }\n});\n```\n\n#### Saisie d'une entrée d'outil personnalisée\n\nLes outils personnalisés doivent exporter leur type d'entrée:\n\n```typescript\n// my-extension.ts\nexport type MyToolInput = Static<typeof myToolSchema>;\n```\n\nUtilisez `isToolCallEventType` avec des paramètres de type explicites:\n\n```typescript\nimport { isToolCallEventType } from \"@earendil-works/pi-coding-agent\";\nimport type { MyToolInput } from \"my-extension\";\n\npi.on(\"tool_call\", (event) => {\n  if (isToolCallEventType<\"my_tool\", MyToolInput>(\"my_tool\", event)) {\n    event.input.action;  // typed\n  }\n});\n```\n\n#### résultat_outil\n\nDéclenché après la fin de l'exécution de l'outil et avant l'émission de `tool_execution_end` ainsi que des événements de message de résultat final de l'outil. **Peut modifier le résultat.**\n\nEn mode outil parallèle, `tool_result` et `tool_execution_end` peuvent s'entrelacer dans l'ordre d'achèvement de l'outil, tandis que les événements de message finaux `toolResult` sont toujours émis plus tard dans l'ordre des sources de l'assistant.\n\n`tool_result` chaîne de gestionnaires comme un middleware:\n- Les gestionnaires s'exécutent dans l'ordre de chargement des extensions\n- Chaque gestionnaire voit le dernier résultat après les modifications précédentes du gestionnaire\n- Les gestionnaires peuvent renvoyer des correctifs partiels (`content`, `details`, `isError` ou `usage`); les champs omis conservent leurs valeurs actuelles\n\nUtilisez `ctx.signal` pour le travail asynchrone imbriqué à l'intérieur du gestionnaire. Cela permet à Esc d'annuler les appels de modèle, `fetch()` et d'autres opérations prenant en compte l'abandon lancées par l'extension.\n\n```typescript\nimport { isBashToolResult } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"tool_result\", async (event, ctx) => {\n  // event.toolName, event.toolCallId, event.input\n  // event.content, event.details, event.isError, event.usage\n\n  if (isBashToolResult(event)) {\n    // event.details is typed as BashToolDetails\n  }\n\n  const response = await fetch(\"https://example.com/summarize\", {\n    method: \"POST\",\n    body: JSON.stringify({ content: event.content }),\n    signal: ctx.signal,\n  });\n\n  // Modify result:\n  return { content: [...], details: {...}, isError: false, usage: nestedModelUsage };\n});\n```\n\n### Événements de bash utilisateur\n\n#### utilisateur_bash\n\nLancé lorsque l'utilisateur exécute les commandes `!` ou `!!`. **Peut intercepter.**\n\n```typescript\nimport { createLocalBashOperations } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"user_bash\", (event, ctx) => {\n  // event.command - the bash command\n  // event.excludeFromContext - true if !! prefix\n  // event.cwd - working directory\n\n  // Option 1: Provide custom operations (e.g., SSH)\n  return { operations: remoteBashOps };\n\n  // Option 2: Wrap pi's built-in local bash backend\n  const local = createLocalBashOperations();\n  return {\n    operations: {\n      exec(command, cwd, options) {\n        return local.exec(`source ~/.profile\\n${command}`, cwd, options);\n      }\n    }\n  };\n\n  // Option 3: Full replacement - return result directly\n  return { result: { output: \"...\", exitCode: 0, cancelled: false, truncated: false } };\n});\n```\n\n### Événements d'entrée\n\n#### saisir\n\nDéclenché lorsque l'entrée de l'utilisateur est reçue, après la vérification des commandes d'extension mais avant l'expansion des compétences et du modèle. L'événement voit le texte brut d'entrée, donc `/skill:foo` et `/template` ne sont pas encore développés.\n\n**Ordre de traitement:**\n1. Commandes d'extension (`/cmd`) vérifiées en premier - si elles sont trouvées, le gestionnaire s'exécute et l'événement d'entrée est ignoré\n2. `input` événements déclenchés - peut intercepter, transformer ou gérer\n3. Si non géré: commandes de compétence (`/skill:name`) étendues au contenu de la compétence\n4. S'il n'est pas géré: prompt templates (`/template`) étendu au contenu du modèle\n5. Le traitement de l'agent commence (`before_agent_start`, etc.)\n\n```typescript\npi.on(\"input\", async (event, ctx) => {\n  // event.text - raw input (before skill/template expansion)\n  // event.images - attached images, if any\n  // event.source - \"interactive\" (typed), \"rpc\" (API), or \"extension\" (via sendUserMessage)\n  // event.streamingBehavior - \"steer\" | \"followUp\" | undefined\n  //   undefined when idle, \"steer\" for mid-stream interrupts,\n  //   \"followUp\" for messages queued until the agent finishes\n\n  // Transform: rewrite input before expansion\n  if (event.text.startsWith(\"?quick \"))\n    return { action: \"transform\", text: `Respond briefly: ${event.text.slice(7)}` };\n\n  // Handle: respond without LLM (extension shows its own feedback)\n  if (event.text === \"ping\") {\n    ctx.ui.notify(\"pong\", \"info\");\n    return { action: \"handled\" };\n  }\n\n  // Route by source: skip processing for extension-injected messages\n  if (event.source === \"extension\") return { action: \"continue\" };\n\n  // Intercept skill commands before expansion\n  if (event.text.startsWith(\"/skill:\")) {\n    // Could transform, block, or let pass through\n  }\n\n  return { action: \"continue\" };  // Default: pass through to expansion\n});\n```\n\n**Résultats:**\n- `continue` - transmission inchangée (par défaut si le gestionnaire ne renvoie rien)\n- `transform` - modifier le texte/les images, puis continuer l'expansion\n- `handled` - ignorer complètement l'agent (le premier gestionnaire à renvoyer cela gagne)\n\nTransforme la chaîne entre les gestionnaires. Voir [input-transform.ts](../examples/extensions/input-transform.ts) et [input-transform-streaming.ts](../examples/extensions/input-transform-streaming.ts) pour le routage compatible `streamingBehavior`.\n\n## Contexte d'extension\n\nTous les gestionnaires reçoivent `ctx: ExtensionContext`.\n\n### ctx.ui\n\nMéthodes d’interface utilisateur pour l’interaction utilisateur. Voir [Custom UI](#custom-ui) pour plus de détails.\n\n### ctx.mode\n\nMode d'exécution actuel: `\"tui\"`, `\"rpc\"`, `\"json\"` ou `\"print\"`. Utilisez `ctx.mode === \"tui\"` pour protéger les fonctionnalités réservées au terminal telles que `custom()`, les usines de composants, l'entrée du terminal et le rendu direct TUI.\n\n### ctx.hasUI\n\n`true` en modes TUI et RPC. `false` en mode impression (`-p`) et JSON en mode. Utilisez-le pour protéger les méthodes de dialogue (`select`, `confirm`, `input`, `editor`) et les méthodes de déclenchement et d'oubli (`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`) qui fonctionnent à la fois en TUI et RPC modes. En mode RPC, certaines méthodes spécifiques à TUI ne fonctionnent pas ou renvoient des valeurs par défaut (voir [rpc.md](rpc.md#extension-ui-protocol)).\n\n### ctx.cwd\n\nRépertoire de travail actuel.\n\nUtilisez `CONFIG_DIR_NAME` au lieu de coder en dur `.pi` lors de la construction de chemins de configuration locaux du projet. Les distributions renommées peuvent utiliser un nom de répertoire de configuration différent.\n\n```typescript\nimport { CONFIG_DIR_NAME, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { join } from \"node:path\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, \"my-extension.json\");\n    // ...\n  });\n}\n```\n\n### ctx.isProjectTrusted()\n\nIndique si l'approbation locale du projet est active pour le contexte de session actuel. Cela inclut les décisions de confiance temporaires et les remplacements de confiance CLI, et pas seulement les décisions enregistrées dans le magasin de confiance global.\n\nUtilisez-le avant de lire la configuration de l'extension locale du projet qui ne doit être respectée que pour les projets approuvés.\n\n### ctx.sessionManager\n\nAccès en lecture seule à l'état de la session. Voir [Session Format](session-format.md) pour le SessionManager complet API et les types d'entrée.\n\nPour `tool_call`, cet état est synchronisé via le message de l'assistant actuel avant l'exécution des gestionnaires. En mode d'exécution d'outil parallèle, il n'est toujours pas garanti d'inclure les résultats des outils frères provenant du même message d'assistant.\n\n```typescript\nctx.sessionManager.getEntries()             // All entries\nctx.sessionManager.getBranch()              // Current branch\nctx.sessionManager.buildContextEntries()    // Active branch entries with compaction applied\nctx.sessionManager.getLeafId()              // Current leaf entry ID\n```\n\n### ctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels\n\nAccès aux modèles, aux fournisseurs et à l'authentification résolue. `ctx.modelRegistry.getProvider(id)` renvoie le fournisseur pi-ai effectif, tandis que `getProviderAuth(id)` résout son API key actuel, ses en-têtes, son URL de base et son environnement étendu au fournisseur sans nécessiter de modèle chargé. `ctx.model` est le modèle actif et `ctx.thinkingLevel` est son niveau de réflexion efficace actuel.\n\n`ctx.scopedModels` est la liste en lecture seule des modèles limités à la session en cours — le même ensemble que celui affiché par la commande `/scoped-models`. Il est résolu au début de la session à partir du drapeau `--models` CLI et du paramètre `enabledModels` (en comparaison avec le catalogue disponible avec une mini-match sur `provider/modelId` ou un simple `modelId`). Il est vide lorsqu'aucune portée n'est configurée, ce qui signifie que tous les modèles disponibles sont utilisables. Chaque entrée est `{ model, thinkingLevel? }`, où `thinkingLevel` est défini uniquement lorsqu'un motif l'a épinglée (par exemple `anthropic/*:high`). Utilisez-le pour remplir un sélecteur de modèle qui reflète celui intégré au lieu d'énumérer l'ensemble du catalogue via `ctx.modelRegistry.getAvailable()`.\n\n### signal ctx\n\nLe signal d'abandon de l'agent actuel, ou `undefined` lorsqu'aucun tour d'agent n'est actif.\n\nUtilisez-le pour les travaux imbriqués prenant en charge l'abandon démarrés par les gestionnaires d'extensions, par exemple:\n- `fetch(..., { signal: ctx.signal })`\n- appels de modèles qui acceptent `signal`\n- classer ou traiter les assistants qui acceptent `AbortSignal`\n\n`ctx.signal` est généralement défini lors d'événements de tour actifs tels que `tool_call`, `tool_result`, `message_update` et `turn_end`.\nIl s'agit généralement de `undefined` dans des contextes inactifs ou sans tour tels que les événements de session, les commandes d'extension et les raccourcis déclenchés lorsque pi est inactif.\n\n```typescript\npi.on(\"tool_result\", async (event, ctx) => {\n  const response = await fetch(\"https://example.com/api\", {\n    method: \"POST\",\n    body: JSON.stringify(event),\n    signal: ctx.signal,\n  });\n\n  const data = await response.json();\n  return { details: data };\n});\n```\n\n### ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()\n\nAides au flux de contrôle. `ctx.isIdle()` est faux tandis que Pi traite une exécution d'agent, une nouvelle tentative automatique, une nouvelle tentative de compactage automatique ou une continuation en file d'attente.\n\n### ctx.shutdown()\n\nDemandez un arrêt progressif de pi.\n\n- **Mode interactif:** Différé jusqu'à ce que l'agent devienne inactif (après avoir traité tous les messages de pilotage et de suivi en file d'attente).\n- **Mode RPC:** Différé jusqu'au prochain état d'inactivité (après avoir terminé la réponse à la commande actuelle, en attendant la commande suivante).\n- **Mode d'impression:** Aucune opération. Le processus se termine automatiquement lorsque toutes les invites sont traitées.\n\nÉmet l'événement `session_shutdown` à toutes les extensions avant de quitter. Disponible dans tous les contextes (gestionnaires d'événements, outils, commandes, raccourcis).\n\n```typescript\npi.on(\"tool_call\", (event, ctx) => {\n  if (isFatal(event.input)) {\n    ctx.shutdown();\n  }\n});\n```\n\n### ctx.getContextUsage()\n\nRenvoie l'utilisation actuelle du contexte pour le modèle actif. Utilise la dernière utilisation de l'assistant lorsqu'il est disponible, puis estime les jetons pour les messages de fin.\n\n```typescript\nconst usage = ctx.getContextUsage();\nif (usage && usage.tokens > 100_000) {\n  // ...\n}\n```\n\n### ctx.compact()\n\nDéclenchez le compactage sans attendre la fin. Utilisez `onComplete` et `onError` pour les actions de suivi.\n\n```typescript\nctx.compact({\n  customInstructions: \"Focus on recent changes\",\n  onComplete: (result) => {\n    ctx.ui.notify(\"Compaction completed\", \"info\");\n  },\n  onError: (error) => {\n    ctx.ui.notify(`Compaction failed: ${error.message}`, \"error\");\n  },\n});\n```\n\n### ctx.getSystemPrompt()\n\nRenvoie la chaîne d'invite système actuelle de Pi.\n\n- Pendant `before_agent_start`, cela reflète les modifications enchaînées des invites système effectuées jusqu'à présent pour le tour en cours.\n- Il n'inclut pas les mutations ultérieures du message `context`.\n- Il n'inclut pas les réécritures de charge utile `before_provider_request`.\n- Si des extensions chargées ultérieurement s'exécutent après la vôtre, elles peuvent toujours modifier ce qui est finalement envoyé.\n\n```typescript\npi.on(\"before_agent_start\", (event, ctx) => {\n  const prompt = ctx.getSystemPrompt();\n  console.log(`System prompt length: ${prompt.length}`);\n});\n```\n\n## ExtensionCommandContextExtensionCommandContext\n\nLes gestionnaires de commandes reçoivent `ExtensionCommandContext`, qui étend `ExtensionContext` avec les méthodes de contrôle de session. Ceux-ci ne sont disponibles que dans les commandes car ils peuvent se bloquer s'ils sont appelés à partir des gestionnaires d'événements.\n\n### ctx.getSystemPromptOptions()\n\nRenvoie les entrées de base que Pi utilise actuellement pour créer l'invite système.\n\n```typescript\nconst options = ctx.getSystemPromptOptions();\nconst contextPaths = options.contextFiles?.map((file) => file.path) ?? [];\n```\n\nCela a la même forme et la même mutabilité que `before_agent_start` `event.systemPromptOptions`: invite personnalisée, outils actifs, extraits d'outils, directives d'invite, texte d'invite système ajouté, cwd, context files chargé et compétences chargées. Il peut inclure le contenu complet du fichier de contexte, alors traitez-le comme des données sensibles locales d'extension et évitez de l'exposer via des listes de commandes, des journaux ou des métadonnées de saisie semi-automatique.\n\nCeci rapporte les entrées d'invite de base actuelles. Il n'inclut pas `before_agent_start` modifications d'invite système enchaînées par tour, `context` mutations ultérieures de message d'événement ou `before_provider_request` réécritures de charge utile.\n\n### ctx.waitForIdle()\n\nAttendez que l'agent se stabilise complètement, y compris les tentatives automatiques, les tentatives de compactage automatique et les continuations en file d'attente:\n\n```typescript\npi.registerCommand(\"my-cmd\", {\n  handler: async (args, ctx) => {\n    await ctx.waitForIdle();\n    // Agent is now idle, safe to modify session\n  },\n});\n```\n\n### ctx.newSession(options?)\n\nCréez une nouvelle session:\n\n```typescript\nconst parentSession = ctx.sessionManager.getSessionFile();\nconst kickoff = \"Continue in the replacement session\";\n\nconst result = await ctx.newSession({\n  parentSession,\n  setup: async (sm) => {\n    sm.appendMessage({\n      role: \"user\",\n      content: [{ type: \"text\", text: \"Context from previous session...\" }],\n      timestamp: Date.now(),\n    });\n  },\n  withSession: async (ctx) => {\n    // Use only the replacement-session ctx here.\n    await ctx.sendUserMessage(kickoff);\n  },\n});\n\nif (result.cancelled) {\n  // An extension cancelled the new session\n}\n```\n\nPossibilités:\n- `parentSession`: fichier de session parent à enregistrer dans le nouvel en-tête de session\n- `setup`: muter le `SessionManager` de la nouvelle session avant l'exécution de `withSession`\n- `withSession`: exécutez le travail post-changement dans un nouveau contexte de session de remplacement. N'utilisez pas l'ancien `pi` / commande `ctx` capturé; voir [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\n### ctx.fork(entryId, options?)\n\nFork à partir d'une entrée spécifique, créant un nouveau fichier de session:\n\n```typescript\nconst result = await ctx.fork(\"entry-id-123\", {\n  withSession: async (ctx) => {\n    // Use only the replacement-session ctx here.\n    ctx.ui.notify(\"Now in the forked session\", \"info\");\n  },\n});\nif (result.cancelled) {\n  // An extension cancelled the fork\n}\n\nconst cloneResult = await ctx.fork(\"entry-id-456\", { position: \"at\" });\nif (cloneResult.cancelled) {\n  // An extension cancelled the clone\n}\n```\n\nPossibilités:\n- `position`: `\"before\"` (par défaut) se situe avant le message utilisateur sélectionné, restaurant cette invite dans l'éditeur\n- `position`: `\"at\"` duplique le chemin actif via l'entrée sélectionnée sans restaurer le texte de l'éditeur\n- `withSession`: exécutez le travail post-changement dans un nouveau contexte de session de remplacement. N'utilisez pas l'ancien `pi` / commande `ctx` capturé; voir [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\n### ctx.navigateTree(targetId, options?)\n\nAccédez à un autre point dans le session tree:\n\n```typescript\nconst result = await ctx.navigateTree(\"entry-id-456\", {\n  summarize: true,\n  customInstructions: \"Focus on error handling changes\",\n  replaceInstructions: false, // true = replace default prompt entirely\n  label: \"review-checkpoint\",\n});\n```\n\nPossibilités:\n- `summarize`: s'il faut générer un résumé de la branche abandonnée\n- `customInstructions`: Instructions personnalisées pour le résumé\n- `replaceInstructions`: si vrai, `customInstructions` remplace l'invite par défaut au lieu d'être ajoutée\n- `label`: Libellé à attacher à l'entrée récapitulative de la branche (ou à l'entrée cible si elle ne résume pas)\n\n### ctx.switchSession (sessionPath, options?)\n\nBasculez vers un autre fichier de session:\n\n```typescript\nconst result = await ctx.switchSession(\"/path/to/session.jsonl\", {\n  withSession: async (ctx) => {\n    await ctx.sendUserMessage(\"Resume work in the replacement session\");\n  },\n});\nif (result.cancelled) {\n  // An extension cancelled the switch via session_before_switch\n}\n```\n\nPossibilités:\n- `withSession`: exécutez le travail post-changement dans un nouveau contexte de session de remplacement. N'utilisez pas l'ancien `pi` / commande `ctx` capturé; voir [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\nPour découvrir les sessions disponibles, utilisez les méthodes statiques `SessionManager.list()` ou `SessionManager.listAll()`:\n\n```typescript\nimport { SessionManager } from \"@earendil-works/pi-coding-agent\";\n\npi.registerCommand(\"switch\", {\n  description: \"Switch to another session\",\n  handler: async (args, ctx) => {\n    const sessions = await SessionManager.list(ctx.cwd);\n    if (sessions.length === 0) return;\n    const choice = await ctx.ui.select(\n      \"Pick session:\",\n      sessions.map(s => s.file),\n    );\n    if (choice) {\n      await ctx.switchSession(choice, {\n        withSession: async (ctx) => {\n          ctx.ui.notify(\"Switched session\", \"info\");\n        },\n      });\n    }\n  },\n});\n```\n\n### Cycle de vie de remplacement de session et armes à pied\n\n`withSession` reçoit un nouveau `ReplacedSessionContext`, qui étend `ExtensionCommandContext` avec les assistants asynchrones `sendMessage()` et `sendUserMessage()` liés à la session de remplacement.\n\nCycle de vie et armes à pied:\n- `withSession` ne s'exécute qu'après que l'ancienne session a émis `session_shutdown`, que l'ancien moteur d'exécution a été démoli, que la session de remplacement a été rebondie et que la nouvelle instance d'extension a déjà reçu `session_start`.\n- Le rappel s'exécute toujours dans la fermeture d'origine, pas dans la nouvelle instance d'extension. Cela signifie que votre ancienne instance d'extension a peut-être déjà exécuté son nettoyage d'arrêt avant le début de `withSession`.\n- Les anciens objets liés à la session `pi` / ancienne commande `ctx` capturés sont obsolètes après leur remplacement et seront lancés s'ils sont utilisés. Utilisez uniquement le `ctx` passé à `withSession` pour le travail lié à la session.\n- Les objets bruts précédemment extraits restent sous votre responsabilité. Par exemple, si vous capturez `const sm = ctx.sessionManager` avant le remplacement, `sm` est toujours l'ancien objet `SessionManager`. Ne le réutilisez pas après le remplacement.\n- Le code dans `withSession` devrait supposer que tout état invalidé par votre gestionnaire `session_shutdown` a déjà disparu. Capturez uniquement les données simples qui survivent proprement à l'arrêt, telles que les chaînes, les identifiants et la configuration sérialisée.\n\nModèle sécurisé:\n\n```typescript\npi.registerCommand(\"handoff\", {\n  handler: async (_args, ctx) => {\n    const kickoff = \"Continue from the replacement session\";\n    await ctx.newSession({\n      withSession: async (ctx) => {\n        await ctx.sendUserMessage(kickoff);\n      },\n    });\n  },\n});\n```\n\nModèle dangereux:\n\n```typescript\npi.registerCommand(\"handoff\", {\n  handler: async (_args, ctx) => {\n    const oldSessionManager = ctx.sessionManager;\n    await ctx.newSession({\n      withSession: async (_ctx) => {\n        // stale old objects: do not do this\n        oldSessionManager.getSessionFile();\n        pi.sendUserMessage(\"wrong\");\n      },\n    });\n  },\n});\n```\n\n### ctx.reload()\n\nExécutez le même flux de rechargement que `/reload`.\n\n```typescript\npi.registerCommand(\"reload-runtime\", {\n  description: \"Reload extensions, skills, prompts, themes, and context files\",\n  handler: async (_args, ctx) => {\n    await ctx.reload();\n    return;\n  },\n});\n```\n\nComportement important:\n- `await ctx.reload()` émet `session_shutdown` pour le runtime actuel de l'extension\n- Il recharge ensuite les ressources et émet `session_start` avec `reason: \"reload\"` et `resources_discover` avec raison `\"reload\"`\n- Le gestionnaire de commandes en cours d'exécution continue toujours dans l'ancien cadre d'appel\n- Le code après `await ctx.reload()` fonctionne toujours à partir de la version de pré-rechargement\n- Le code après `await ctx.reload()` ne doit pas supposer que l'ancien état d'extension en mémoire est toujours valide\n- Après le retour du gestionnaire, les futurs appels de commandes/événements/outils utilisent la nouvelle version de l'extension\n\nPour un comportement prévisible, traitez le rechargement comme un terminal pour ce gestionnaire (`await ctx.reload(); return;`).\n\nLes outils fonctionnent avec `ExtensionContext`, ils ne peuvent donc pas appeler directement `ctx.reload()`. Utilisez une commande comme point d’entrée de rechargement, puis exposez un outil qui met cette commande en file d’attente en tant que message utilisateur de suivi.\n\nExemple d'outil que le LLM peut appeler pour déclencher le rechargement:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"reload-runtime\", {\n    description: \"Reload extensions, skills, prompts, themes, and context files\",\n    handler: async (_args, ctx) => {\n      await ctx.reload();\n      return;\n    },\n  });\n\n  pi.registerTool({\n    name: \"reload_runtime\",\n    label: \"Reload Runtime\",\n    description: \"Reload extensions, skills, prompts, themes, and context files\",\n    parameters: Type.Object({}),\n    async execute() {\n      pi.sendUserMessage(\"/reload-runtime\", { deliverAs: \"followUp\" });\n      return {\n        content: [{ type: \"text\", text: \"Queued /reload-runtime as a follow-up command.\" }],\n      };\n    },\n  });\n}\n```\n\n## Méthodes ExtensionAPI\n\n### pi.on(événement, gestionnaire)\n\nAbonnez-vous aux événements. Voir [Events](#events) pour les types d'événements et les valeurs de retour.\n\n### pi.registerTool (définition)\n\nEnregistrez un outil personnalisé appelable par le LLM. Voir [Custom Tools](#custom-tools) pour plus de détails.\n\n`pi.registerTool()` fonctionne à la fois pendant le chargement de l'extension et après le démarrage. Vous pouvez l'appeler à l'intérieur de `session_start`, de gestionnaires de commandes ou d'autres gestionnaires d'événements. Les nouveaux outils sont actualisés immédiatement dans la même session, ils apparaissent donc en `pi.getAllTools()` et sont appelables par le LLM sans `/reload`.\n\nUtilisez `pi.setActiveTools()` pour activer ou désactiver les outils (y compris les outils ajoutés dynamiquement) au moment de l'exécution.\n\nUtilisez `promptSnippet` pour opter pour un outil personnalisé dans une entrée d'une seule ligne dans `Available tools` et `promptGuidelines` pour ajouter des puces spécifiques à l'outil à la section par défaut `Guidelines` lorsque l'outil est actif.\n\n**Important:** Les puces `promptGuidelines` sont ajoutées à plat à la section `Guidelines` sans préfixe de nom d'outil. Chaque ligne directrice doit nommer l'outil auquel elle fait référence – évitez « Utilisez cet outil lorsque… » car le LLM ne peut pas dire à quel outil « ce » signifie. Écrivez plutôt \"Utiliser my_tool quand...\".\n\nVoir [dynamic-tools.ts](../examples/extensions/dynamic-tools.ts) pour un exemple complet.\n\n```typescript\nimport { Type } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\n\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"What this tool does\",\n  promptSnippet: \"Summarize or transform text according to action\",\n  promptGuidelines: [\"Use my_tool when the user asks to summarize previously generated text.\"],\n  parameters: Type.Object({\n    action: StringEnum([\"list\", \"add\"] as const),\n    text: Type.Optional(Type.String()),\n  }),\n  prepareArguments(args) {\n    // Optional compatibility shim. Runs before schema validation.\n    // Return the current schema shape, for example to fold legacy fields\n    // into the modern parameter object.\n    return args;\n  },\n\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // Stream progress\n    onUpdate?.({ content: [{ type: \"text\", text: \"Working...\" }] });\n\n    return {\n      content: [{ type: \"text\", text: \"Done\" }],\n      details: { result: \"...\" },\n    };\n  },\n\n  // Optional: Custom rendering\n  renderCall(args, theme, context) { ... },\n  renderResult(result, options, theme, context) { ... },\n});\n```\n\n### pi.sendMessage(message, options?)\n\nInjectez un message personnalisé dans la session. Les messages personnalisés participent au contexte LLM. Pour un contenu durable uniquement TUI qui ne doit pas être envoyé au LLM, utilisez [`pi.appendEntry()`](#piappendentrycustomtype-data) avec [`pi.registerEntryRenderer()`](#piregisterentryrenderercustomtype-renderer).\n\n```typescript\npi.sendMessage({\n  customType: \"my-extension\",\n  content: \"Message text\",\n  display: true,\n  details: { ... },\n}, {\n  triggerTurn: true,\n  deliverAs: \"steer\",\n});\n```\n\n**Options:**\n- `deliverAs` - Mode de livraison:\n  - `\"steer\"` (par défaut) - Met le message en file d'attente pendant la diffusion. Livré après que le tour d'assistant en cours ait fini d'exécuter ses appels d'outil, avant le prochain appel LLM.\n  - `\"followUp\"` - Attend la fin de l'agent. Distribué uniquement lorsque l'agent n'a plus d'appels d'outil.\n  - `\"nextTurn\"`: mis en file d'attente pour la prochaine invite utilisateur. N'interrompt ni ne déclenche rien.\n- `triggerTurn: true` - Si l'agent est inactif, déclenchez immédiatement une réponse LLM. S'applique uniquement aux modes `\"steer\"` et `\"followUp\"` (ignoré pour `\"nextTurn\"`).\n\n### pi.sendUserMessage (contenu, options?)\n\nEnvoyez un message utilisateur à l'agent. Contrairement à `sendMessage()` qui envoie des messages personnalisés, cela envoie un message utilisateur réel qui apparaît comme s'il avait été tapé par l'utilisateur. Déclenche toujours un tour.\n\n```typescript\n// Simple text message\npi.sendUserMessage(\"What is 2+2?\");\n\n// With content array (text + images)\npi.sendUserMessage([\n  { type: \"text\", text: \"Describe this image:\" },\n  { type: \"image\", source: { type: \"base64\", mediaType: \"image/png\", data: \"...\" } },\n]);\n\n// During streaming - must specify delivery mode\npi.sendUserMessage(\"Focus on error handling\", { deliverAs: \"steer\" });\npi.sendUserMessage(\"And then summarize\", { deliverAs: \"followUp\" });\n```\n\n**Options:**\n- `deliverAs` - Obligatoire lorsque l'agent diffuse:\n  - `\"steer\"` - Met le message en file d'attente pour livraison une fois que le tour de l'assistant actuel a fini d'exécuter ses appels d'outil\n  - `\"followUp\"` - Attend que l'agent ait terminé tous les outils\n\nLorsqu'il n'est pas diffusé, le message est envoyé immédiatement et déclenche un nouveau tour. Lors d'une diffusion sans `deliverAs`, génère une erreur.\n\nVoir [send-user-message.ts](../examples/extensions/send-user-message.ts) pour un exemple complet.\n\n### pi.appendEntry(customType, données?)\n\nConserver les données d’extension. Les entrées personnalisées ne participent PAS au contexte LLM. En mode interactif, ils peuvent également s'afficher dans la transcription du chat lorsqu'ils sont associés à `pi.registerEntryRenderer()`.\n\n```typescript\npi.appendEntry(\"my-state\", { count: 42 });\npi.appendEntry(\"status-card\", { title: \"Indexed files\", count: 17 });\n\n// Restore on reload\npi.on(\"session_start\", async (_event, ctx) => {\n  for (const entry of ctx.sessionManager.getEntries()) {\n    if (entry.type === \"custom\" && entry.customType === \"my-state\") {\n      // Reconstruct from entry.data\n    }\n  }\n});\n```\n\n### pi.setSessionName(nom)\n\nDéfinissez le nom d'affichage de la session (affiché dans le sélecteur de session au lieu du premier message).\n\n```typescript\npi.setSessionName(\"Refactor auth module\");\n```\n\n### pi.getSessionName()\n\nObtenez le nom de la session actuelle, s'il est défini.\n\n```typescript\nconst name = pi.getSessionName();\nif (name) {\n  console.log(`Session: ${name}`);\n}\n```\n\n### pi.setLabel (entryId, étiquette)\n\nDéfinir ou effacer une étiquette sur une entrée. Les étiquettes sont des marqueurs définis par l'utilisateur pour la création de signets et la navigation (affichés dans le sélecteur `/tree`).\n\n```typescript\n// Set a label\npi.setLabel(entryId, \"checkpoint-before-refactor\");\n\n// Clear a label\npi.setLabel(entryId, undefined);\n\n// Read labels via sessionManager\nconst label = ctx.sessionManager.getLabel(entryId);\n```\n\nLes étiquettes persistent dans la session et survivent aux redémarrages. Utilisez-les pour marquer les points importants (virages, points de contrôle) dans l'arbre de conversation.\n\n### pi.registerCommand(nom, options)\n\nEnregistrez une commande.\n\nSi plusieurs extensions enregistrent le même nom de commande, pi les conserve toutes et attribue des suffixes d'appel numériques dans l'ordre de chargement, par exemple `/review:1` et `/review:2`.\n\n```typescript\npi.registerCommand(\"stats\", {\n  description: \"Show session statistics\",\n  handler: async (args, ctx) => {\n    const count = ctx.sessionManager.getEntries().length;\n    ctx.ui.notify(`${count} entries`, \"info\");\n  }\n});\n```\n\nFacultatif: ajoutez l'auto-complétion de l'argument pour `/command...`:\n\n```typescript\nimport type { AutocompleteItem } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"deploy\", {\n  description: \"Deploy to an environment\",\n  getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {\n    const envs = [\"dev\", \"staging\", \"prod\"];\n    const items = envs.map((e) => ({ value: e, label: e }));\n    const filtered = items.filter((i) => i.value.startsWith(prefix));\n    return filtered.length > 0 ? filtered : null;\n  },\n  handler: async (args, ctx) => {\n    ctx.ui.notify(`Deploying: ${args}`, \"info\");\n  },\n});\n```\n\n### pi.getCommands()\n\nObtenez le slash commands disponible pour invocation via `prompt` dans la session en cours. Comprend les commandes d'extension, prompt templates et les commandes de compétences.\nLa liste correspond à l'ordre RPC `get_commands`: les extensions d'abord, puis les modèles, puis les compétences.\n\n```typescript\nconst commands = pi.getCommands();\nconst bySource = commands.filter((command) => command.source === \"extension\");\nconst userScoped = commands.filter((command) => command.sourceInfo.scope === \"user\");\n```\n\nChaque entrée a cette forme:\n\n```typescript\n{\n  name: string; // Invokable command name without the leading slash. May be suffixed like \"review:1\"\n  description?: string;\n  source: \"extension\" | \"prompt\" | \"skill\";\n  sourceInfo: {\n    path: string;\n    source: string;\n    scope: \"user\" | \"project\" | \"temporary\";\n    origin: \"package\" | \"top-level\";\n    baseDir?: string;\n  };\n}\n```\n\nUtilisez `sourceInfo` comme champ de provenance canonique. Ne déduisez pas la propriété à partir des noms de commandes ou de l’analyse de chemin ad hoc.\n\nLes commandes interactives intégrées (comme `/model` et `/settings`) ne sont pas incluses ici. Ils sont traités uniquement de manière interactive\nmode et ne s'exécuterait pas s'il était envoyé via `prompt`.\n\n### pi.registerMessageRenderer (customType, moteur de rendu)\n\nEnregistrez un moteur de rendu TUI personnalisé pour les messages personnalisés avec votre `customType`. Les messages personnalisés sont créés avec `pi.sendMessage()` et participent au contexte LLM. Voir [Custom UI](#custom-ui).\n\n### pi.registerMarkdownTransformateur(transformateur)\n\nEnregistrez un transformateur pour le Markdown dans le texte utilisateur normal, le texte de l'assistant et les blocs de réflexion. Les transformateurs fonctionnent dans l'ordre de charge d'extension et chaque transformateur reçoit le Markdown renvoyé par le transformateur précédent. Une fois la chaîne terminée, Pi restitue le contenu transformé avec son moteur de rendu intégré.\n\nLe transformateur reçoit la chaîne Markdown et un contexte avec:\n\n- `messageType` — `\"user\"`, `\"assistant\"` ou `\"assistant-thinking\"`\n- `isStreaming` — `true` pour les mises à jour partielles de l'assistant; `false` pour l'utilisateur, l'assistant finalisé et les messages restaurés\n- `availableWidth` — colonnes terminales exactes disponibles pour le contenu Markdown transformé\n\nRenvoyez le Markdown transformé:\n\n```typescript\npi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {\n  if (isStreaming || messageType === \"assistant-thinking\") return markdown;\n  return markdown.replaceAll(\"-->\", \"→\");\n});\n```\n\nSi un transformateur lance, Pi conserve le Markdown produit jusqu'à présent et continue avec le transformateur suivant. Le hook est en affichage uniquement: le message d'origine reste inchangé dans le contexte de la session et du modèle. Il fonctionne pour les nouveaux messages utilisateur, les mises à jour en streaming de l'assistant, les messages de session restaurés et les changements de largeur de terminal, de sorte que les transformateurs doivent rester synchrones et peu coûteux.\n\n### pi.registerEntryRenderer (customType, moteur de rendu)\n\nEnregistrez un moteur de rendu TUI personnalisé pour les entrées personnalisées avec votre `customType`. Les entrées personnalisées sont créées avec `pi.appendEntry()` et ne participent pas au contexte LLM.\n\n```typescript\nimport { Box, Text } from \"@earendil-works/pi-tui\";\n\npi.registerEntryRenderer(\"status-card\", (entry, { expanded }, theme) => {\n  const data = entry.data as { title: string; count: number };\n  const box = new Box(1, 1, (text) => theme.bg(\"customMessageBg\", text));\n  box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));\n  if (expanded) {\n    box.addChild(new Text(theme.fg(\"dim\", JSON.stringify(data, null, 2))));\n  }\n  return box;\n});\n\npi.appendEntry(\"status-card\", { title: \"Indexed files\", count: 17 });\n```\n\n### pi.registerShortcut (raccourci, options)\n\nEnregistrez un raccourci clavier. Voir [keybindings.md](keybindings.md) pour le format de raccourci et les raccourcis clavier intégrés.\n\n```typescript\npi.registerShortcut(\"ctrl+shift+p\", {\n  description: \"Toggle plan mode\",\n  handler: async (ctx) => {\n    ctx.ui.notify(\"Toggled!\");\n  },\n});\n```\n\n### pi.registerFlag(nom, options)\n\nEnregistrez un drapeau CLI.\n\n```typescript\npi.registerFlag(\"plan\", {\n  description: \"Start in plan mode\",\n  type: \"boolean\",\n  default: false,\n});\n\n// Check value\nif (pi.getFlag(\"plan\")) {\n  // Plan mode enabled\n}\n```\n\n### pi.exec(commande, arguments, options?)\n\nExécutez une commande shell.\n\n```typescript\nconst result = await pi.exec(\"git\", [\"status\"], { signal, timeout: 5000 });\n// result.stdout, result.stderr, result.code, result.killed\n```\n\n### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(noms)\n\nGérer les outils actifs. Cela fonctionne à la fois pour les outils intégrés et les outils enregistrés dynamiquement. `pi.getActiveTools()` renvoie les noms d'outils actifs sous la forme `string[]`; `pi.getAllTools()` renvoie les métadonnées de tous les outils configurés.\n\n```typescript\nconst active = pi.getActiveTools(); // [\"read\", \"bash\", ...]\nconst all = pi.getAllTools();\n// all = [{\n//   name: \"read\",\n//   description: \"Read file contents...\",\n//   parameters: ...,\n//   promptGuidelines: [\"Use read to examine files instead of cat or sed.\"],\n//   sourceInfo: { path: \"<builtin:read>\", source: \"builtin\", scope: \"temporary\", origin: \"top-level\" }\n// }, ...]\nconst builtinTools = all.filter((t) => t.sourceInfo.source === \"builtin\");\nconst extensionTools = all.filter((t) => t.sourceInfo.source !== \"builtin\" && t.sourceInfo.source !== \"sdk\");\npi.setActiveTools([...new Set([...active, \"my_custom_tool\"])]); // Keep current tools and enable my_custom_tool\npi.setActiveTools([\"read\", \"bash\"]); // Switch to read-only\n```\n\n`pi.getAllTools()` renvoie `name`, `description`, `parameters`, `promptGuidelines` et `sourceInfo`.\n\nValeurs `sourceInfo.source` typiques:\n- `builtin` pour les outils intégrés\n- `sdk` pour les outils passés via `createAgentSession({ customTools })`\n- métadonnées de la source d'extension pour les outils enregistrés par les extensions\n\n### pi.setModel (modèle)\n\nDéfinissez le modèle actuel. Renvoie `false` si aucun API key n'est disponible pour le modèle. Voir [models.md](models.md) pour configurer des modèles personnalisés.\n\n```typescript\nconst model = ctx.modelRegistry.find(\"anthropic\", \"claude-sonnet-4-5\");\nif (model) {\n  const success = await pi.setModel(model);\n  if (!success) {\n    ctx.ui.notify(\"No API key for this model\", \"error\");\n  }\n}\n```\n\n### pi.getThinkingLevel() / pi.setThinkingLevel(niveau)\n\nObtenez ou définissez le niveau de réflexion. Le niveau est limité aux capacités du modèle (les modèles sans raisonnement utilisent toujours \"off\"). Les changements émettent `thinking_level_select`.\n\n```typescript\nconst current = pi.getThinkingLevel();  // \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\"\npi.setThinkingLevel(\"high\");\n```\n\n### pi.événements\n\nBus d’événements partagé pour la communication entre extensions:\n\n```typescript\npi.events.on(\"my:event\", (data) => { ... });\npi.events.emit(\"my:event\", { ... });\n```\n\n### pi.registerProvider(nom, configuration)\n\nEnregistrez ou remplacez un fournisseur de modèles de manière dynamique. Utile pour les proxys, les points de terminaison personnalisés ou les configurations de modèles à l'échelle de l'équipe.\n\nLes appels effectués pendant la fonction d'usine d'extension sont mis en file d'attente et appliqués une fois que le programme d'exécution s'initialise. Les appels effectués par la suite – par exemple à partir d'un gestionnaire de commandes suivant un flux de configuration utilisateur – prennent effet immédiatement sans nécessiter un `/reload`.\n\nLes fournisseurs dynamiques peuvent implémenter `refreshModels`. Pi l'appelle lors de l'actualisation du modèle, publie la liste renvoyée de manière synchrone via le fournisseur et transmet le contexte d'informations d'identification canonique/catalogue stocké/réseau/signal. L'extension décide si elle doit conserver les métadonnées du catalogue via la vérification de génération `context.publish({ persist: entry })`; les serveurs live tels que llama.cpp peuvent renvoyer des modèles sans les conserver.\n\n`context.signal` est toujours un signal concret et les rappels du fournisseur doivent le transmettre au blocage des E/S. Les appels publics `ModelRuntime.refresh()` et `ModelRegistry.refresh()` acceptent un signal facultatif et sont illimités lorsqu'il est omis; les extensions et les candidatures choisissent leurs propres délais. L'annulation arrête l'appelant en attente même si un fournisseur ignore le signal, mais une coopération est toujours nécessaire pour arrêter le travail sous-jacent.\n\nExtensions qui nécessitent une authentification, un filtrage, une actualisation ou un comportement de flux natif du fournisseur peuvent enregistrer un `Provider` complet à partir de `@earendil-works/pi-ai`. Le fournisseur devient la base de composition et les remplacements `models.json` s'appliquent toujours au-dessus.\n\n```typescript\nimport { createProvider, openAICompletionsApi } from \"@earendil-works/pi-ai\";\n\nconst provider = createProvider({\n  id: \"local-server\",\n  name: \"Local Server\",\n  baseUrl: \"http://localhost:8080/v1\",\n  auth: {\n    apiKey: {\n      name: \"Local server setup\",\n      async login(interaction) {\n        return {\n          type: \"api_key\",\n          key: await interaction.prompt({ type: \"secret\", message: \"API key\" }),\n        };\n      },\n      async resolve({ credential }) {\n        return credential?.key\n          ? { auth: { apiKey: credential.key }, source: \"stored API key\" }\n          : undefined;\n      },\n    },\n  },\n  models: [],\n  api: openAICompletionsApi(),\n});\n\npi.registerProvider(provider);\n\n// Register a new provider with custom models\npi.registerProvider(\"my-proxy\", {\n  name: \"My Proxy\",\n  baseUrl: \"https://proxy.example.com\",\n  apiKey: \"$PROXY_API_KEY\",  // env var reference\n  api: \"anthropic-messages\",\n  models: [\n    {\n      id: \"claude-sonnet-4-20250514\",\n      name: \"Claude 4 Sonnet (proxy)\",\n      reasoning: false,\n      input: [\"text\", \"image\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n\n// Register a live llama.cpp catalog without persisting discovered models\npi.registerProvider(\"llama.cpp\", {\n  baseUrl: \"http://localhost:8080/v1\",\n  apiKey: \"local\",\n  api: \"openai-completions\",\n  async refreshModels({ signal }) {\n    const response = await fetch(\"http://localhost:8080/v1/models\", { signal });\n    const { data } = await response.json();\n    return data.map(({ id }) => ({\n      id,\n      name: id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 128000,\n      maxTokens: 16384\n    }));\n  }\n});\n\n// Override baseUrl for an existing provider (keeps all models)\npi.registerProvider(\"anthropic\", {\n  baseUrl: \"https://proxy.example.com\"\n});\n\n// Register provider with OAuth support for /login\npi.registerProvider(\"corporate-ai\", {\n  baseUrl: \"https://ai.corp.com\",\n  api: \"openai-responses\",\n  models: [...],\n  oauth: {\n    name: \"Corporate AI (SSO)\",\n    async login(callbacks) {\n      // Custom OAuth flow\n      callbacks.onAuth({ url: \"https://sso.corp.com/...\" });\n      const code = await callbacks.onPrompt({ message: \"Enter code:\" });\n      return { refresh: code, access: code, expires: Date.now() + 3600000 };\n    },\n    async refreshToken(credentials, signal) {\n      signal.throwIfAborted();\n      // Refresh logic\n      return credentials;\n    },\n    getApiKey(credentials) {\n      return credentials.access;\n    }\n  }\n});\n```\n\nLe formulaire objet accepte un pi-ai `Provider` complet, y compris les comportements natifs `auth`, `getModels`, `refreshModels`, `filterModels`, `stream` et `streamSimple`.\n\n**Options de configuration héritées:**\n- `name` - Nom d'affichage du fournisseur dans l'interface utilisateur, tel que `/login`.\n- `baseUrl` - API URL du point de terminaison. Obligatoire lors de la définition des modèles.\n- `apiKey` - API key littéral, interpolation d'environnement (`$ENV_VAR` ou `${ENV_VAR}`), ou leader `!command`. Requis lors de la définition des modèles (sauf si `oauth` est fourni). `$` échappe à ``apiKey` - API key littéral, interpolation d'environnement (`$ENV_VAR` ou `${ENV_VAR}`), ou leader `!command`. Requis lors de la définition des modèles (sauf si `oauth` est fourni). `$` échappe à  et `$!` échappe à un `!` littéral sans déclencher l'exécution de la commande.\n- `api` - API tapez: `\"anthropic-messages\"`, `\"openai-completions\"`, `\"openai-responses\"`, etc.\n- `headers` - En-têtes personnalisés à inclure dans les requêtes.\n- `authHeader` - Si vrai, ajoute automatiquement l'en-tête `Authorization: Bearer`.\n- `models` - Tableau de définitions de modèles. S'il est fourni, remplace tous les modèles existants pour ce fournisseur. Les définitions de modèle peuvent définir `baseUrl` pour remplacer le point de terminaison du fournisseur pour ce modèle.\n- `refreshModels` - Rappel de découverte dynamique asynchrone. Ses modèles renvoyés remplacent les modèles fournis par l'extension. `context.stored` contient l'instantané persistant du fournisseur; utilisez la génération vérifiée `context.publish({ persist: entry })` uniquement lorsque les données du catalogue mises à jour doivent persister. Utilisez `persist: null` pour supprimer cet instantané.\n- `oauth` - OAuth configuration du fournisseur pour le support `/login`. Lorsqu'il est fourni, le fournisseur apparaît dans le menu de connexion.\n- `streamSimple` - Implémentation de streaming personnalisé pour les API non standard.\n\nVoir [custom-provider.md](custom-provider.md) pour les sujets avancés: APIs de streaming personnalisé, détails OAuth, référence de définition de modèle.\n\n### pi.unregisterProvider(nom)\n\nSupprimez un fournisseur précédemment enregistré et ses modèles. Les modèles intégrés qui ont été remplacés par le fournisseur sont restaurés. N'a aucun effet si le fournisseur n'est pas enregistré.\n\nComme `registerProvider`, cela prend effet immédiatement lorsqu'il est appelé après la phase de chargement initiale, donc un `/reload` n'est pas requis.\n\n```typescript\npi.registerCommand(\"my-setup-teardown\", {\n  description: \"Remove the custom proxy provider\",\n  handler: async (_args, _ctx) => {\n    pi.unregisterProvider(\"my-proxy\");\n  },\n});\n```\n\n## Gestion de l'État\n\nExtensions with state doit le stocker dans le résultat de l'outil `details` pour une prise en charge appropriée des branchements:\n\n```typescript\nexport default function (pi: ExtensionAPI) {\n  let items: string[] = [];\n\n  // Reconstruct state from session\n  pi.on(\"session_start\", async (_event, ctx) => {\n    items = [];\n    for (const entry of ctx.sessionManager.getBranch()) {\n      if (entry.type === \"message\" && entry.message.role === \"toolResult\") {\n        if (entry.message.toolName === \"my_tool\") {\n          items = entry.message.details?.items ?? [];\n        }\n      }\n    }\n  });\n\n  pi.registerTool({\n    name: \"my_tool\",\n    // ...\n    async execute(toolCallId, params, signal, onUpdate, ctx) {\n      items.push(\"new item\");\n      return {\n        content: [{ type: \"text\", text: \"Added\" }],\n        details: { items: [...items] },  // Store for reconstruction\n      };\n    },\n  });\n}\n```\n\n## Outils personnalisés\n\nEnregistrez les outils que le LLM peut appeler via `pi.registerTool()`. Les outils apparaissent dans l'invite système et peuvent avoir un rendu personnalisé.\n\nUtilisez `promptSnippet` pour une courte entrée d'une ligne dans la section `Available tools` de l'invite système par défaut. En cas d'omission, les outils personnalisés sont exclus de cette section.\n\nUtilisez `promptGuidelines` pour ajouter des puces spécifiques à l'outil à la section `Guidelines` de l'invite système par défaut. Ces puces sont incluses uniquement lorsque l'outil est actif (par exemple, après `pi.setActiveTools([...])`).\n\n**Important:** Les puces `promptGuidelines` sont ajoutées à plat à la section `Guidelines` sans préfixe ni regroupement de nom d'outil. Chaque ligne directrice doit nommer l'outil auquel elle fait référence – évitez « Utilisez cet outil lorsque… » car le LLM ne peut pas dire à quel outil « ce » signifie. Écrivez plutôt \"Utiliser my_tool quand...\".\n\nRemarque: Certains modèles sont idiots et incluent le préfixe @ dans les arguments du chemin d'outil. Les outils intégrés suppriment un premier @ avant de résoudre les chemins. Si votre outil personnalisé accepte un chemin, normalisez également un @ initial.\n\nSi votre outil personnalisé mute les fichiers, utilisez `withFileMutationQueue()` afin qu'il participe à la même file d'attente par fichier que les `edit` et `write` intégrés. Cela est important car les appels d’outils s’exécutent en parallèle par défaut. Sans la file d'attente, deux outils peuvent lire le même ancien contenu de fichier, calculer différentes mises à jour, puis la dernière écriture écrase l'autre.\n\nExemple de cas d'échec: votre outil personnalisé modifie `foo.ts` tandis que le `edit` intégré modifie également `foo.ts` dans le même tour d'assistant. Si votre outil ne participe pas à la file d'attente, les deux peuvent lire le `foo.ts` original, appliquer des modifications distinctes, et l'une de ces modifications est perdue.\n\nTransmettez le chemin réel du fichier cible à `withFileMutationQueue()`, pas l'argument utilisateur brut. Résolvez-le d'abord en un chemin absolu, par rapport à `ctx.cwd` ou au répertoire de travail de votre outil. Pour les fichiers existants, l'assistant canonise via `realpath()`, donc les alias de liens symboliques pour le même fichier partagent une file d'attente. Pour les nouveaux fichiers, il revient au chemin absolu résolu car il n'y a encore rien à `realpath()`.\n\nMettez en file d'attente toute la fenêtre de mutation sur ce chemin cible. Cela inclut la logique de lecture-modification-écriture, pas seulement l'écriture finale.\n\n```typescript\nimport { withFileMutationQueue } from \"@earendil-works/pi-coding-agent\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\n\nasync execute(_toolCallId, params, _signal, _onUpdate, ctx) {\n  const absolutePath = resolve(ctx.cwd, params.path);\n\n  return withFileMutationQueue(absolutePath, async () => {\n    await mkdir(dirname(absolutePath), { recursive: true });\n    const current = await readFile(absolutePath, \"utf8\");\n    const next = current.replace(params.oldText, params.newText);\n    await writeFile(absolutePath, next, \"utf8\");\n\n    return {\n      content: [{ type: \"text\", text: `Updated ${params.path}` }],\n      details: {},\n    };\n  });\n}\n```\n\n### Définition de l'outil\n\n```typescript\nimport { Type } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\nimport { Text } from \"@earendil-works/pi-tui\";\n\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"What this tool does (shown to LLM)\",\n  promptSnippet: \"List or add items in the project todo list\",\n  promptGuidelines: [\n    \"Use my_tool for todo planning instead of direct file edits when the user asks for a task list.\"\n  ],\n  parameters: Type.Object({\n    action: StringEnum([\"list\", \"add\"] as const),  // Use StringEnum for Google compatibility\n    text: Type.Optional(Type.String()),\n  }),\n  prepareArguments(args) {\n    if (!args || typeof args !== \"object\") return args;\n    const input = args as { action?: string; oldAction?: string };\n    if (typeof input.oldAction === \"string\" && input.action === undefined) {\n      return { ...input, action: input.oldAction };\n    }\n    return args;\n  },\n\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // Check for cancellation\n    if (signal?.aborted) {\n      return { content: [{ type: \"text\", text: \"Cancelled\" }] };\n    }\n\n    // Stream progress updates\n    onUpdate?.({\n      content: [{ type: \"text\", text: \"Working...\" }],\n      details: { progress: 50 },\n    });\n\n    // Run commands via pi.exec (captured from extension closure)\n    const result = await pi.exec(\"some-command\", [], { signal });\n\n    // Return result\n    return {\n      content: [{ type: \"text\", text: \"Done\" }],  // Sent to LLM\n      details: { data: result },                   // For rendering & state\n      // usage: nestedModelResponse.usage,          // Optional nested LLM usage\n      // Optional: stop after this tool batch when every finalized tool result\n      // in the batch also returns terminate: true.\n      terminate: true,\n    };\n  },\n\n  // Optional: Custom rendering\n  renderCall(args, theme, context) { ... },\n  renderResult(result, options, theme, context) { ... },\n});\n```\n\n**Comptabilité d'utilisation:** Si un outil effectue des appels LLM imbriqués, renvoyez leur `Usage` combiné sous la forme `usage`. Pi le conserve dans le résultat de l'outil et l'inclut dans les totaux de session `/session` et RPC. `tool_result` les gestionnaires peuvent inspecter ou remplacer cette valeur.\n\n**Erreurs de signalisation:** Pour marquer l'exécution d'un outil comme ayant échoué (définit `isError: true` sur le résultat et le signale au LLM), lancez une erreur à partir de `execute`. Le renvoi d'une valeur ne définit jamais l'indicateur d'erreur, quelles que soient les propriétés que vous incluez dans l'objet de retour.\n\n**Résiliation anticipée:** Renvoyez `terminate: true` de `execute()` pour indiquer que l'appel LLM de suivi automatique doit être ignoré après le lot d'outils en cours. Cela ne prend effet que lorsque chaque résultat d'outil finalisé dans ce lot se termine. Voir [examples/extensions/structured-output.ts](../examples/extensions/structured-output.ts) pour un exemple minimal où l'agent se termine sur un dernier appel d'outil à sortie structurée.\n\n```typescript\n// Correct: throw to signal an error\nasync execute(toolCallId, params) {\n  if (!isValid(params.input)) {\n    throw new Error(`Invalid input: ${params.input}`);\n  }\n  return { content: [{ type: \"text\", text: \"OK\" }], details: {} };\n}\n```\n\n**Important:** Utilisez `StringEnum` à partir de `@earendil-works/pi-ai` pour les énumérations de chaînes. `Type.Union`/`Type.Literal` ne fonctionne pas avec le API de Google.\n\n**Préparation des arguments:** `prepareArguments(args)` est facultatif. S'il est défini, il s'exécute avant la validation du schéma et avant le `execute()`. Utilisez-le pour imiter une ancienne forme d'entrée acceptée lorsque pi reprend une ancienne session dont les arguments d'appel d'outil stockés ne correspondent plus au schéma actuel. Renvoyez l'objet que vous souhaitez valider par rapport à `parameters`. Gardez le schéma public strict. N'ajoutez pas de champs de compatibilité obsolètes à `parameters` juste pour que les anciennes sessions reprises fonctionnent.\n\nExemple: une ancienne session peut contenir un appel d'outil `edit` avec des niveaux supérieurs `oldText` et `newText`, alors que le schéma actuel n'accepte que `edits: [{ oldText, newText }]`.\n\n```typescript\npi.registerTool({\n  name: \"edit\",\n  label: \"Edit\",\n  description: \"Edit a single file using exact text replacement\",\n  parameters: Type.Object({\n    path: Type.String(),\n    edits: Type.Array(\n      Type.Object({\n        oldText: Type.String(),\n        newText: Type.String(),\n      }),\n    ),\n  }),\n  prepareArguments(args) {\n    if (!args || typeof args !== \"object\") return args;\n\n    const input = args as {\n      path?: string;\n      edits?: Array<{ oldText: string; newText: string }>;\n      oldText?: unknown;\n      newText?: unknown;\n    };\n\n    if (typeof input.oldText !== \"string\" || typeof input.newText !== \"string\") {\n      return args;\n    }\n\n    return {\n      ...input,\n      edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],\n    };\n  },\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // params now matches the current schema\n    return {\n      content: [{ type: \"text\", text: `Applying ${params.edits.length} edit block(s)` }],\n      details: {},\n    };\n  },\n});\n```\n\n### Remplacement des outils intégrés\n\nExtensions peut remplacer les outils intégrés (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) en enregistrant un outil avec le même nom. Le mode interactif affiche un avertissement lorsque cela se produit.\n\n```bash\n# Extension's read tool replaces built-in read\npi -e ./tool-override.ts\n```\n\nVous pouvez également utiliser `--no-builtin-tools` pour démarrer sans aucun outil intégré tout en gardant les outils d'extension activés:\n```bash\n# No built-in tools, only extension tools\npi --no-builtin-tools -e ./my-extension.ts\n```\n\nVoir [examples/extensions/tool-override.ts](../examples/extensions/tool-override.ts) pour un exemple complet qui remplace `read` avec la journalisation et le contrôle d'accès.\n\n**Rendu:** L'héritage du moteur de rendu intégré est résolu par emplacement. Le remplacement de l'exécution et le remplacement du rendu sont indépendants. Si votre remplacement omet `renderCall`, le `renderCall` intégré est utilisé. Si votre remplacement omet `renderResult`, le `renderResult` intégré est utilisé. Si votre remplacement omet les deux, le moteur de rendu intégré est utilisé automatiquement (surbrillance de la syntaxe, différences, etc.). Cela vous permet d'encapsuler des outils intégrés pour la journalisation ou le contrôle d'accès sans réimplémenter l'interface utilisateur.\n\n**Métadonnées d'invite:** `promptSnippet` et `promptGuidelines` ne sont pas héritées de l'outil intégré. Si votre remplacement doit conserver ces instructions d'invite, définissez-les explicitement sur le remplacement.\n\n**Votre implémentation doit correspondre à la forme exacte du résultat**, y compris le type `details`. L'interface utilisateur et la logique de session dépendent de ces formes pour le rendu et le suivi de l'état.\n\nImplémentations d'outils intégrés:\n- [read.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`\n- [bash.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`\n- [edit.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/edit.ts)\n- [write.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/write.ts)\n- [grep.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`\n- [find.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/find.ts) - `FindToolDetails`\n- [ls.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/ls.ts) - `LsToolDetails`\n\n### Exécution à distance\n\nLes outils intégrés prennent en charge les opérations enfichables pour la délégation à des systèmes distants (SSH, conteneurs, etc.):\n\n```typescript\nimport { createReadTool, createBashTool, type ReadOperations } from \"@earendil-works/pi-coding-agent\";\n\n// Create tool with custom operations\nconst remoteRead = createReadTool(cwd, {\n  operations: {\n    readFile: (path) => sshExec(remote, `cat ${path}`),\n    access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),\n  }\n});\n\n// Register, checking flag at execution time\npi.registerTool({\n  ...remoteRead,\n  async execute(id, params, signal, onUpdate, _ctx) {\n    const ssh = getSshConfig();\n    if (ssh) {\n      const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });\n      return tool.execute(id, params, signal, onUpdate);\n    }\n    return localRead.execute(id, params, signal, onUpdate);\n  },\n});\n```\n\n**Interfaces d'opérations:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`\n\nPour `user_bash`, les extensions peuvent réutiliser le backend du shell local de pi via `createLocalBashOperations()` au lieu de réimplémenter la génération de processus locaux, la résolution du shell et la terminaison de l'arborescence des processus.\n\nL'outil bash prend également en charge un hook d'apparition pour ajuster la commande, cwd ou env avant l'exécution:\n\n```typescript\nimport { createBashTool } from \"@earendil-works/pi-coding-agent\";\n\nconst bashTool = createBashTool(cwd, {\n  spawnHook: ({ command, cwd, env }) => ({\n    command: `source ~/.profile\\n${command}`,\n    cwd: `/mnt/sandbox${cwd}`,\n    env: { ...env, CI: \"1\" },\n  }),\n});\n```\n\n`createBashTool()` expose la session en cours aux commandes via `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL` et `PI_REASONING_LEVEL`. L'injection a lieu avant `spawnHook`, donc les hooks reçoivent ces valeurs en `env` et les préservent lorsqu'ils propagent l'environnement existant comme ci-dessus. Définissez `exposeSessionEnvironment: false` pour les désactiver:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n});\n```\n\nVoir [Bash tool session environment](environment-variables.md#bash-tool-session-environment) pour la sémantique des variables. Voir [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) pour un exemple complet SSH avec l'indicateur `--ssh`.\n\n### Troncature de sortie\n\n**Les outils DOIVENT tronquer leur sortie** pour éviter de surcharger le contexte LLM. Des sorties importantes peuvent provoquer:\n- Erreurs de dépassement de contexte (invite trop longue)\n- Échecs de compactage\n- Performances du modèle dégradées\n\nLa limite intégrée est de **50 Ko** (~ 10 000 jetons) et de **2 000 lignes**, selon la première éventualité atteinte. Utilisez les utilitaires de troncature exportés:\n\n```typescript\nimport {\n  truncateHead,      // Keep first N lines/bytes (good for file reads, search results)\n  truncateTail,      // Keep last N lines/bytes (good for logs, command output)\n  truncateLine,      // Truncate a single line to maxBytes with ellipsis\n  formatSize,        // Human-readable size (e.g., \"50KB\", \"1.5MB\")\n  DEFAULT_MAX_BYTES, // 50KB\n  DEFAULT_MAX_LINES, // 2000\n} from \"@earendil-works/pi-coding-agent\";\n\nasync execute(toolCallId, params, signal, onUpdate, ctx) {\n  const output = await runCommand();\n\n  // Apply truncation\n  const truncation = truncateHead(output, {\n    maxLines: DEFAULT_MAX_LINES,\n    maxBytes: DEFAULT_MAX_BYTES,\n  });\n\n  let result = truncation.content;\n\n  if (truncation.truncated) {\n    // Write full output to temp file\n    const tempFile = writeTempFile(output);\n\n    // Inform the LLM where to find complete output\n    result += `\\n\\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;\n    result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;\n    result += ` Full output saved to: ${tempFile}]`;\n  }\n\n  return { content: [{ type: \"text\", text: result }] };\n}\n```\n\n**Points clés:**\n- Utilisez `truncateHead` pour le contenu dont le début est important (résultats de recherche, lectures de fichiers)\n- Utilisez `truncateTail` pour le contenu où la fin compte (journaux, sortie de commande)\n- Informez toujours le LLM lorsque la sortie est tronquée et où trouver la version complète\n- Documentez les limites de troncature dans la description de votre outil\n\nVoir [examples/extensions/truncated-tool.ts](../examples/extensions/truncated-tool.ts) pour un exemple complet d'encapsulation de `rg` (ripgrep) avec une troncature appropriée.\n\n### Plusieurs outils\n\nUne extension peut enregistrer plusieurs outils avec un état partagé:\n\n```typescript\nexport default function (pi: ExtensionAPI) {\n  let connection = null;\n\n  pi.registerTool({ name: \"db_connect\", ... });\n  pi.registerTool({ name: \"db_query\", ... });\n  pi.registerTool({ name: \"db_close\", ... });\n\n  pi.on(\"session_shutdown\", async () => {\n    connection?.close();\n  });\n}\n```\n\n### Rendu personnalisé\n\nLes outils peuvent fournir `renderCall` et `renderResult` pour un affichage personnalisé TUI. Voir [tui.md](tui.md) pour le composant complet API et [tool-execution.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) pour la façon dont les lignes d'outils sont composées.\n\nPar défaut, la sortie de l'outil est entourée d'un `Box` qui gère le remplissage et l'arrière-plan. Un `renderCall` ou un `renderResult` défini doit renvoyer un `Component`. Si aucun moteur de rendu d'emplacement n'est défini, `tool-execution.ts` utilise le rendu de secours pour cet emplacement.\n\nDéfinissez `renderShell: \"self\"` lorsque l'outil doit restituer son propre shell au lieu d'utiliser le `Box` par défaut. Ceci est utile pour les outils qui nécessitent un contrôle complet sur le cadrage ou le comportement de l'arrière-plan, par exemple les grands aperçus qui doivent rester visuellement stables une fois l'outil installé.\n\n```typescript\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"Custom shell example\",\n  parameters: Type.Object({}),\n  renderShell: \"self\",\n  async execute() {\n    return { content: [{ type: \"text\", text: \"ok\" }], details: undefined };\n  },\n  renderCall(args, theme, context) {\n    return new Text(theme.fg(\"accent\", \"my custom shell\"), 0, 0);\n  },\n});\n```\n\n`renderCall` et `renderResult` reçoivent chacun un objet `context` avec:\n- `args` - les arguments d'appel d'outil actuels\n- `state` - état local de ligne partagé entre `renderCall` et `renderResult`\n- `lastComponent` - le composant précédemment renvoyé pour cet emplacement, le cas échéant\n- `invalidate()` - demander un nouveau rendu de cette ligne d'outils\n- `toolCallId`, `cwd`, `executionStarted`, `argsComplete`, `isPartial`, `expanded`, `showImages`, `isError`\n\nUtilisez `context.state` pour l'état partagé entre plusieurs emplacements. Conservez les caches locaux sur l'instance de composant renvoyée lorsque vous souhaitez réutiliser et muter le même composant entre les rendus.\n\n#### rendreAppel\n\nAffiche l'appel ou l'en-tête de l'outil:\n\n```typescript\nimport { Text } from \"@earendil-works/pi-tui\";\n\nrenderCall(args, theme, context) {\n  const text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n  let content = theme.fg(\"toolTitle\", theme.bold(\"my_tool \"));\n  content += theme.fg(\"muted\", args.action);\n  if (args.text) {\n    content += \" \" + theme.fg(\"dim\", `\"${args.text}\"`);\n  }\n  text.setText(content);\n  return text;\n}\n```\n\n#### renduRésultat\n\nRend le résultat ou la sortie de l'outil:\n\n```typescript\nrenderResult(result, { expanded, isPartial }, theme, context) {\n  if (isPartial) {\n    return new Text(theme.fg(\"warning\", \"Processing...\"), 0, 0);\n  }\n\n  if (result.details?.error) {\n    return new Text(theme.fg(\"error\", `Error: ${result.details.error}`), 0, 0);\n  }\n\n  let text = theme.fg(\"success\", \"✓ Done\");\n  if (expanded && result.details?.items) {\n    for (const item of result.details.items) {\n      text += \"\\n  \" + theme.fg(\"dim\", item);\n    }\n  }\n  return new Text(text, 0, 0);\n}\n```\n\nSi un emplacement n'a intentionnellement aucun contenu visible, renvoyez un `Component` vide tel qu'un `Container` vide.\n\n#### Conseils pour les raccourcis clavier\n\nUtilisez `keyHint()` pour afficher des astuces de raccourcis clavier qui respectent la configuration de raccourcis clavier active:\n\n```typescript\nimport { keyHint } from \"@earendil-works/pi-coding-agent\";\n\nrenderResult(result, { expanded }, theme, context) {\n  let text = theme.fg(\"success\", \"✓ Done\");\n  if (!expanded) {\n    text += ` (${keyHint(\"app.tools.expand\", \"to expand\")})`;\n  }\n  return new Text(text, 0, 0);\n}\n```\n\nFonctions disponibles:\n- `keyHint(keybinding, description)` - Formate un identifiant de liaison de touches configuré tel que `\"app.tools.expand\"` ou `\"tui.select.confirm\"`\n- `keyText(keybinding)` - Renvoie le texte de clé brut configuré pour un identifiant de liaison de touches\n- `rawKeyHint(key, description)` - Formater une chaîne de clé brute\n\nUtilisez des identifiants de raccourcis clavier avec espace de noms:\n- Les identifiants d'agent de codage utilisent l'espace de noms `app.*`, par exemple `app.tools.expand`, `app.editor.external`, `app.session.rename`\n- Les identifiants TUI partagés utilisent l'espace de noms `tui.*`, par exemple `tui.select.confirm`, `tui.select.cancel`, `tui.input.tab`\n\nPour la liste exhaustive des identifiants de raccourcis clavier et des valeurs par défaut, voir [keybindings.md](keybindings.md). `keybindings.json` utilise ces mêmes identifiants d'espace de noms.\n\nLes éditeurs personnalisés et les composants `ctx.ui.custom()` reçoivent `keybindings: KeybindingsManager` comme argument injecté. Ils devraient utiliser directement ce gestionnaire injecté au lieu d'appeler `getKeybindings()` ou `setKeybindings()`.\n\n#### Meilleures pratiques\n\n- Utilisez `Text` avec un remplissage `(0, 0)`. La boîte par défaut gère le remplissage.\n- Utilisez `\\n` pour le contenu multiligne.\n- Gérez `isPartial` pour la progression du streaming.\n- Supportez `expanded` pour plus de détails sur demande.\n- Gardez la vue par défaut compacte.\n- Lisez `context.args` dans `renderResult` au lieu de copier les arguments dans `context.state`.\n- Utilisez `context.state` uniquement pour les données qui doivent être partagées entre les emplacements d'appel et de résultat.\n- Réutilisez `context.lastComponent` lorsque la même instance de composant peut être mise à jour sur place.\n- Utilisez `renderShell: \"self\"` uniquement lorsque le shell en boîte par défaut gêne. En mode self-shell, l'outil est responsable de son propre cadrage, remplissage et arrière-plan.\n\n#### Retomber\n\nSi un moteur de rendu de slot n'est pas défini ou génère:\n- `renderCall`: affiche le nom de l'outil\n- `renderResult`: affiche le texte brut de `content`\n\n### Chargement dynamique des outils\n\nExtensions peut enregistrer de nombreux outils tout en ne gardant actif qu'un petit ensemble initial. Un outil peut alors ajouter d'autres outils avec `pi.setActiveTools()` lors de l'exécution. Pi détecte les modifications purement additives, enregistre les noms d'outils nouvellement disponibles sur ce résultat d'outil et applique l'ensemble actif mis à jour avant la prochaine demande de modèle.\n\nCela fonctionne avec tous les modèles. Models avec la prise en charge native du chargement différé préserve le préfixe d'invite stable et charge les nouvelles définitions à la position du résultat de l'outil. D'autres modèles utilisent la solution de secours décrite ci-dessous.\n\nLe cycle de vie est:\n\n1. Enregistrez chaque outil avec `pi.registerTool()` pour qu'il apparaisse dans `pi.getAllTools()`.\n2. Gardez les outils de chargement, tels que `search_tools`, actifs et laissez les outils de recherche inactifs.\n3. Pendant l'exécution du chargeur, appelez `pi.setActiveTools([...currentTools,...matchingTools])`. Le changement doit être additif: ne supprimez pas les outils actuellement actifs dans le même appel.\n4. Pi enregistre les outils qui ont été ajoutés au résultat de l'outil du chargeur.\n5. Avant la réponse suivante du modèle, Pi expose les définitions ajoutées en utilisant le chargement différé natif lorsqu'il est pris en charge, ou la liste d'outils actifs normaux dans le cas contraire.\n\nVous n'avez pas besoin de renvoyer des références d'outils spécifiques au fournisseur ni de marquer le chargeur comme outil de recherche spécial. Le changement d'outil actif est le signal. Les noms passés à `pi.setActiveTools()` doivent déjà être enregistrés; les noms inconnus sont ignorés.\n\n#### Models avec chargement différé natif\n\n- **Anthropique**\n  - **Models:** Sonnet, Opus, Fable version 4.5 ou plus récente (sans Haiku)\n  - **Représentation native:** Les définitions différées utilisent `defer_loading`; le point de chargement utilise le contenu `tool_reference`.\n- **OpenAI**\n  - **Models:** `gpt-5.4` et famille plus récente\n  - **Représentation native:** Pi ajoute les éléments client `tool_search_call` et `tool_search_output` terminés au point de chargement.\n\nPour un modèle personnalisé ou un proxy vérifié, la gestion native peut être activée avec `compat.supportsToolReferences: true` pour `anthropic-messages`, ou `compat.supportsToolSearch: true` pour `openai-responses` et `openai-codex-responses`. Laissez-les désactivés à moins que le point de terminaison et le modèle acceptent le protocole natif correspondant.\n\n#### Comportement de repli\n\nPour tous les autres modèles et fournisseurs, l'activation dynamique fonctionne toujours: Pi envoie normalement la liste complète des outils actifs actuels lors de la prochaine demande. Le modèle peut appeler les outils nouvellement activés, mais l'ajout de leurs définitions peut invalider le préfixe d'invite mis en cache du fournisseur.\n\nPi utilise également cette solution de repli sûre lorsque l'ensemble actif n'est pas purement additif, comme le remplacement d'un groupe d'outils par un autre. Les suppressions d'outils fonctionnent donc, mais elles ne font pas appel au chargement différé.\n\nPour un meilleur comportement du cache, gardez l'outil de chargement actif pendant toute la session et ajoutez des outils au lieu de remplacer l'ensemble actif. Notez également que l'activation d'un outil avec `promptSnippet` ou `promptGuidelines` reconstruit l'invite système; cette modification à l'invite du système peut invalider le préfixe même lorsque le fournisseur prend en charge les schémas différés. Les outils chargés paresseusement doivent généralement s'appuyer sur leur outil `description` et omettre les métadonnées d'invite actives uniquement.\n\n#### Exemple d'outil de recherche\n\nL'extension suivante enregistre deux outils consultables, les supprime de l'ensemble actif initial et ne conserve que `search_tools` comme chargeur. L'exemple utilise une simple correspondance de mots clés, mais l'implémentation de la recherche peut utiliser BM25, des intégrations, un catalogue distant ou un routage spécifique au projet.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nconst SEARCHABLE_TOOL_NAMES = new Set([\"lookup_weather\", \"search_issues\"]);\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerTool({\n    name: \"lookup_weather\",\n    label: \"Lookup Weather\",\n    description: \"Look up the current weather for a city\",\n    parameters: Type.Object({ city: Type.String() }),\n    async execute(_toolCallId, params) {\n      return {\n        content: [{ type: \"text\", text: `Weather for ${params.city}: sunny` }],\n        details: {},\n      };\n    },\n  });\n\n  pi.registerTool({\n    name: \"search_issues\",\n    label: \"Search Issues\",\n    description: \"Search project issues by keyword\",\n    parameters: Type.Object({ query: Type.String() }),\n    async execute(_toolCallId, params) {\n      return {\n        content: [{ type: \"text\", text: `No open issues matching ${params.query}` }],\n        details: {},\n      };\n    },\n  });\n\n  pi.registerTool({\n    name: \"search_tools\",\n    label: \"Search Tools\",\n    description: \"Search for and enable tools relevant to a task\",\n    promptSnippet: \"Search for additional tools when the active tools cannot perform the task\",\n    promptGuidelines: [\n      \"Use search_tools when a task requires a capability that is not currently available.\",\n    ],\n    parameters: Type.Object({\n      query: Type.String({ description: \"Capability or task to search for\" }),\n      limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),\n    }),\n    async execute(_toolCallId, params) {\n      const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);\n      const matches = pi.getAllTools()\n        .filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))\n        .map((tool) => ({\n          tool,\n          score: terms.reduce(\n            (score, term) =>\n              score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),\n            0,\n          ),\n        }))\n        .filter((match) => match.score > 0)\n        .sort((a, b) => b.score - a.score)\n        .slice(0, params.limit ?? 3)\n        .map((match) => match.tool.name);\n\n      if (matches.length === 0) {\n        return {\n          content: [{ type: \"text\", text: `No tools found for: ${params.query}` }],\n          details: { matches: [] },\n        };\n      }\n\n      const active = pi.getActiveTools();\n      const added = matches.filter((name) => !active.includes(name));\n      pi.setActiveTools([...new Set([...active, ...added])]);\n\n      return {\n        content: [{\n          type: \"text\",\n          text: added.length > 0\n            ? `Loaded tools: ${added.join(\", \")}`\n            : `Matching tools already active: ${matches.join(\", \")}`,\n        }],\n        details: { matches, added },\n      };\n    },\n  });\n\n  pi.on(\"session_start\", () => {\n    // Keep searchable tools registered but initially inactive. Preserve built-ins\n    // and tools owned by other extensions, and keep the loader itself active.\n    const initialTools = pi.getActiveTools().filter(\n      (name) => !SEARCHABLE_TOOL_NAMES.has(name),\n    );\n    pi.setActiveTools([...new Set([...initialTools, \"search_tools\"])]);\n  });\n}\n```\n\nLorsque `search_tools` ajoute une correspondance, le modèle reçoit cette définition lors de la requête immédiatement suivante. Sur un modèle compatible natif, la définition est ancrée après le résultat de la recherche sans modifier le préfixe initial du schéma d'outil. Sur d'autres modèles, il apparaît dans la liste d'outils normale sur la même demande suivante.\n\n## Interface utilisateur personnalisée\n\nExtensions peut interagir avec les utilisateurs via les méthodes `ctx.ui` et personnaliser le rendu des messages/outils.\n\n**Pour les composants personnalisés, voir [tui.md](tui.md)** qui propose des modèles de copier-coller pour:\n- Boîtes de dialogue de sélection (SelectList)\n- Opérations asynchrones avec annulation (BorderedLoader)\n- Bascule les paramètres (SettingsList)\n- Indicateurs d'état (setStatus)\n- Message de travail, visibilité et indicateur pendant le streaming (`setWorkingMessage`, `setWorkingVisible`, `setWorkingIndicator`)\n- Éditeur de widgets au-dessus/en-dessous (setWidget)\n- Fournisseurs de saisie semi-automatique superposés à la complétion de chemin/slash intégrée (addAutocompleteProvider)\n- Pieds de page personnalisés (setFooter)\n\n### Boîtes de dialogue\n\n```typescript\n// Select from options\nconst choice = await ctx.ui.select(\"Pick one:\", [\"A\", \"B\", \"C\"]);\n\n// Confirm dialog\nconst ok = await ctx.ui.confirm(\"Delete?\", \"This cannot be undone\");\n\n// Text input\nconst name = await ctx.ui.input(\"Name:\", \"placeholder\");\n\n// Multi-line editor\nconst text = await ctx.ui.editor(\"Edit:\", \"prefilled text\");\n\n// Notification (non-blocking)\nctx.ui.notify(\"Done!\", \"info\");  // \"info\" | \"warning\" | \"error\"\n```\n\n#### Dialogues chronométrés avec compte à rebours\n\nLes boîtes de dialogue prennent en charge une option `timeout` qui se ferme automatiquement avec un affichage de compte à rebours en direct:\n\n```typescript\n// Dialog shows \"Title (5s)\" → \"Title (4s)\" → ... → auto-dismisses at 0\nconst confirmed = await ctx.ui.confirm(\n  \"Timed Confirmation\",\n  \"This dialog will auto-cancel in 5 seconds. Confirm?\",\n  { timeout: 5000 }\n);\n\nif (confirmed) {\n  // User confirmed\n} else {\n  // User cancelled or timed out\n}\n```\n\n**Valeurs renvoyées en cas d'expiration:**\n- `select()` renvoie `undefined`\n- `confirm()` renvoie `false`\n- `input()` renvoie `undefined`\n\n#### Licenciement manuel avec AbortSignal\n\nPour plus de contrôle (par exemple, pour distinguer le délai d'attente de l'annulation par l'utilisateur), utilisez `AbortSignal`:\n\n```typescript\nconst controller = new AbortController();\nconst timeoutId = setTimeout(() => controller.abort(), 5000);\n\nconst confirmed = await ctx.ui.confirm(\n  \"Timed Confirmation\",\n  \"This dialog will auto-cancel in 5 seconds. Confirm?\",\n  { signal: controller.signal }\n);\n\nclearTimeout(timeoutId);\n\nif (confirmed) {\n  // User confirmed\n} else if (controller.signal.aborted) {\n  // Dialog timed out\n} else {\n  // User cancelled (pressed Escape or selected \"No\")\n}\n```\n\nVoir [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) pour des exemples complets.\n\n### Widgets, statut et pied de page\n\n```typescript\n// Status in footer (persistent until cleared)\nctx.ui.setStatus(\"my-ext\", \"Processing...\");\nctx.ui.setStatus(\"my-ext\", undefined);  // Clear\n\n// Working loader (shown during streaming)\nctx.ui.setWorkingMessage(\"Thinking deeply...\");\nctx.ui.setWorkingMessage();  // Restore default\nctx.ui.setWorkingVisible(false);  // Hide the built-in working loader row entirely\nctx.ui.setWorkingVisible(true);   // Show the built-in working loader row\n\n// Working indicator (shown during streaming)\nctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg(\"accent\", \"●\")] });  // Static dot\nctx.ui.setWorkingIndicator({\n  frames: [\n    ctx.ui.theme.fg(\"dim\", \"·\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n    ctx.ui.theme.fg(\"accent\", \"●\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n  ],\n  intervalMs: 120,\n});\nctx.ui.setWorkingIndicator({ frames: [] });  // Hide indicator\nctx.ui.setWorkingIndicator();  // Restore default spinner\n\n// Widget above editor (default)\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"]);\n// Widget below editor\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"], { placement: \"belowEditor\" });\nctx.ui.setWidget(\"my-widget\", (tui, theme) => new Text(theme.fg(\"accent\", \"Custom\"), 0, 0));\nctx.ui.setWidget(\"my-widget\", undefined);  // Clear\n\n// Custom footer (replaces built-in footer entirely)\nctx.ui.setFooter((tui, theme) => ({\n  render(width) { return [theme.fg(\"dim\", \"Custom footer\")]; },\n  invalidate() {},\n}));\nctx.ui.setFooter(undefined);  // Restore built-in footer\n\n// Terminal title\nctx.ui.setTitle(\"pi - my-project\");\n\n// Editor text\nctx.ui.setEditorText(\"Prefill text\");\nconst current = ctx.ui.getEditorText();\n\n// Paste into editor (triggers paste handling, including collapse for large content)\nctx.ui.pasteToEditor(\"pasted content\");\n\n// Stack custom autocomplete behavior on top of the built-in provider\nctx.ui.addAutocompleteProvider((current) => ({\n  triggerCharacters: [\"#\"],\n  async getSuggestions(lines, line, col, options) {\n    const beforeCursor = (lines[line] ?? \"\").slice(0, col);\n    const match = beforeCursor.match(/(?:^|[ \\t])#([^\\s#]*)$/);\n    if (!match) {\n      return current.getSuggestions(lines, line, col, options);\n    }\n\n    return {\n      prefix: `#${match[1] ?? \"\"}`,\n      items: [{ value: \"#2983\", label: \"#2983\", description: \"Extension API for autocomplete\" }],\n    };\n  },\n  applyCompletion(lines, line, col, item, prefix) {\n    return current.applyCompletion(lines, line, col, item, prefix);\n  },\n  shouldTriggerFileCompletion(lines, line, col) {\n    return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true;\n  },\n}));\n\n// Tool output expansion\nconst wasExpanded = ctx.ui.getToolsExpanded();\nctx.ui.setToolsExpanded(true);\nctx.ui.setToolsExpanded(wasExpanded);\n\n// Custom editor (vim mode, emacs mode, etc.)\nctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));\nconst currentEditor = ctx.ui.getEditorComponent();\nctx.ui.setEditorComponent((tui, theme, keybindings) =>\n  new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))\n);\nctx.ui.setEditorComponent(undefined);  // Restore default editor\n\n// Theme management (see themes.md for creating themes)\nconst themes = ctx.ui.getAllThemes();  // [{ name: \"dark\", path: \"/...\" | undefined }, ...]\nconst lightTheme = ctx.ui.getTheme(\"light\");  // Load without switching\nconst result = ctx.ui.setTheme(\"light\");  // Switch by name\nif (!result.success) {\n  ctx.ui.notify(`Failed: ${result.error}`, \"error\");\n}\nctx.ui.setTheme(lightTheme!);  // Or switch by Theme object\nctx.ui.theme.fg(\"accent\", \"styled text\");  // Access current theme\n```\n\nLes cadres d’indicateurs de travail personnalisés sont rendus textuellement. Si vous voulez des couleurs, ajoutez-les vous-même aux chaînes du cadre, par exemple avec `ctx.ui.theme.fg(...)`.\n\n### Saisie semi-automatique Providers\n\nUtilisez `ctx.ui.addAutocompleteProvider()` pour empiler une logique de saisie semi-automatique personnalisée au-dessus de la commande slash et du fournisseur de chemin intégrés. Définissez `triggerCharacters` pour les déclencheurs naturels personnalisés tels que `Utilisez `ctx.ui.addAutocompleteProvider()` pour empiler une logique de saisie semi-automatique personnalisée au-dessus de la commande slash et du fournisseur de chemin intégrés. Définissez `triggerCharacters` pour les déclencheurs naturels personnalisés tels que.\n\nModèle typique:\n\n- inspecter le texte avant le curseur\n- renvoie vos propres suggestions lorsque la syntaxe spécifique à votre extension correspond\n- sinon déléguez à `current.getSuggestions(...)`\n- déléguez `applyCompletion(...)` sauf si vous avez besoin d'un comportement d'insertion personnalisé\n\n```typescript\npi.on(\"session_start\", (_event, ctx) => {\n  ctx.ui.addAutocompleteProvider((current) => ({\n    triggerCharacters: [\"#\"],\n    async getSuggestions(lines, cursorLine, cursorCol, options) {\n      const line = lines[cursorLine] ?? \"\";\n      const beforeCursor = line.slice(0, cursorCol);\n      const match = beforeCursor.match(/(?:^|[ \\t])#([^\\s#]*)$/);\n      if (!match) {\n        return current.getSuggestions(lines, cursorLine, cursorCol, options);\n      }\n\n      return {\n        prefix: `#${match[1] ?? \"\"}`,\n        items: [\n          { value: \"#2983\", label: \"#2983\", description: \"Extension API for registering custom @ autocomplete providers\" },\n          { value: \"#2753\", label: \"#2753\", description: \"Reload stale resource settings\" },\n        ],\n      };\n    },\n\n    applyCompletion(lines, cursorLine, cursorCol, item, prefix) {\n      return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);\n    },\n\n    shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {\n      return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;\n    },\n  }));\n});\n```\n\nVoir [github-issue-autocomplete.ts](../examples/extensions/github-issue-autocomplete.ts) pour un exemple complet qui précharge les derniers problèmes ouverts GitHub avec `gh issue list` et les filtre localement pour une achèvement rapide `#...`. Cela nécessite GitHub CLI (`gh`) et une extraction de référentiel GitHub.\n\n### Composants personnalisés\n\nPour une interface utilisateur complexe, utilisez `ctx.ui.custom()`. Cela remplace temporairement l'éditeur par votre composant jusqu'à ce que `done()` soit appelé:\n\n```typescript\nimport { Text, Component } from \"@earendil-works/pi-tui\";\n\nconst result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {\n  const text = new Text(\"Press Enter to confirm, Escape to cancel\", 1, 1);\n\n  text.onKey = (key) => {\n    if (key === \"return\") done(true);\n    if (key === \"escape\") done(false);\n    return true;\n  };\n\n  return text;\n});\n\nif (result) {\n  // User pressed Enter\n}\n```\n\nLe rappel reçoit:\n- Instance `tui` - TUI (pour les dimensions de l'écran, gestion du focus)\n- `theme` - Thème actuel pour le style\n- `keybindings` - Gestionnaire de raccourcis clavier d'application (pour vérifier les raccourcis)\n- `done(value)` - Appel pour fermer le composant et renvoyer la valeur\n\nVoir [tui.md](tui.md) pour le composant complet API.\n\n#### Mode superposition (expérimental)\n\nPassez `{ overlay: true }` pour afficher le composant sous forme modale flottante au-dessus du contenu existant, sans effacer l'écran:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),\n  { overlay: true }\n);\n```\n\nPour un positionnement avancé (ancres, marges, pourcentages, visibilité réactive), passez `overlayOptions`. Utilisez `onHandle` pour contrôler la mise au point ou la visibilité par programmation:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),\n  {\n    overlay: true,\n    overlayOptions: { anchor: \"top-right\", width: \"50%\", margin: 2 },\n    onHandle: (handle) => {\n      handle.focus(); // focus this overlay and bring it to the visual front\n      // handle.unfocus({ target: editorComponent }); // release input to a specific component\n      // handle.setHidden(true/false); // toggle visibility\n      // handle.hide(); // permanently remove\n    }\n  }\n);\n```\n\nUne superposition visible ciblée peut récupérer les entrées après la fermeture temporaire de l'interface utilisateur personnalisée sans superposition. Si vous souhaitez intentionnellement qu'un autre composant conserve l'entrée pendant que la superposition reste visible, appelez `handle.unfocus({ target })`. Passer `{ target: null }` libère la superposition sans focaliser un autre composant.\n\nVoir [tui.md](tui.md) pour les `OverlayOptions` et `OverlayHandle` API et [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) complets pour des exemples.\n\n### Éditeur personnalisé\n\nRemplacez l'éditeur d'entrée principal par une implémentation personnalisée (mode vim, mode emacs, etc.):\n\n```typescript\nimport { CustomEditor, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { matchesKey } from \"@earendil-works/pi-tui\";\n\nclass VimEditor extends CustomEditor {\n  private mode: \"normal\" | \"insert\" = \"insert\";\n\n  handleInput(data: string): void {\n    if (matchesKey(data, \"escape\") && this.mode === \"insert\") {\n      this.mode = \"normal\";\n      return;\n    }\n    if (this.mode === \"normal\" && data === \"i\") {\n      this.mode = \"insert\";\n      return;\n    }\n    super.handleInput(data);  // App keybindings + text editing\n  }\n}\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    ctx.ui.setEditorComponent((tui, theme, keybindings) =>\n      new VimEditor(tui, theme, keybindings)\n    );\n  });\n}\n```\n\n**Points clés:**\n- Étendez `CustomEditor` (pas la base `Editor`) pour obtenir les raccourcis clavier de l'application (échappement pour abandonner, ctrl+d, changement de modèle)\n- Appelez le `super.handleInput(data)` pour les clés que vous ne gérez pas\n- L'usine reçoit `tui`, `theme` et `keybindings` de l'application\n- Utilisez `ctx.ui.getEditorComponent()` avant `setEditorComponent()` pour envelopper l'éditeur personnalisé précédemment configuré\n- Passez `undefined` pour restaurer la valeur par défaut: `ctx.ui.setEditorComponent(undefined)`\n\nPour composer avec une autre extension qui a déjà remplacé l'éditeur, capturez l'usine précédente avant de paramétrer la vôtre:\n\n```typescript\nconst previous = ctx.ui.getEditorComponent();\nctx.ui.setEditorComponent((tui, theme, keybindings) =>\n  new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })\n);\n```\n\nVoir [tui.md](tui.md) Modèle 7 pour un exemple complet avec indicateur de mode.\n\n### Rendu des messages et des entrées\n\nEnregistrez un moteur de rendu personnalisé pour les messages avec votre `customType`. Utilisez des moteurs de rendu de messages pour le contenu qui doit participer au contexte LLM:\n\n```typescript\nimport { Text } from \"@earendil-works/pi-tui\";\n\npi.registerMessageRenderer(\"my-extension\", (message, options, theme) => {\n  const { expanded, outputPad } = options;\n  let text = theme.fg(\"accent\", `[${message.customType}] `);\n  text += message.content;\n\n  if (expanded && message.details) {\n    text += \"\\n\" + theme.fg(\"dim\", JSON.stringify(message.details, null, 2));\n  }\n\n  return new Text(text, outputPad, 0);\n});\n```\n\nLes messages sont envoyés via `pi.sendMessage()`:\n\n```typescript\npi.sendMessage({\n  customType: \"my-extension\",  // Matches registerMessageRenderer\n  content: \"Status update\",\n  display: true,               // Show in TUI\n  details: { ... },            // Available in renderer\n});\n```\n\nPour le contenu TUI uniquement qui ne doit pas être envoyé au LLM, affichez plutôt les entrées personnalisées:\n\n```typescript\npi.registerEntryRenderer(\"my-card\", (entry, options, theme) => {\n  return new Text(theme.fg(\"accent\", JSON.stringify(entry.data)));\n});\n\npi.appendEntry(\"my-card\", { status: \"done\" });\n```\n\n### Couleurs du thème\n\nToutes les fonctions de rendu reçoivent un objet `theme`. Voir [themes.md](themes.md) pour créer des thèmes personnalisés et la palette de couleurs complète.\n\n```typescript\n// Foreground colors\ntheme.fg(\"toolTitle\", text)   // Tool names\ntheme.fg(\"accent\", text)      // Highlights\ntheme.fg(\"success\", text)     // Success (green)\ntheme.fg(\"error\", text)       // Errors (red)\ntheme.fg(\"warning\", text)     // Warnings (yellow)\ntheme.fg(\"muted\", text)       // Secondary text\ntheme.fg(\"dim\", text)         // Tertiary text\n\n// Text styles\ntheme.bold(text)\ntheme.italic(text)\ntheme.strikethrough(text)\n```\n\nPour la coloration syntaxique dans les moteurs de rendu d'outils personnalisés:\n\n```typescript\nimport { highlightCode, getLanguageFromPath } from \"@earendil-works/pi-coding-agent\";\n\n// Highlight code with explicit language\nconst highlighted = highlightCode(\"const x = 1;\", \"typescript\", theme);\n\n// Auto-detect language from file path\nconst lang = getLanguageFromPath(\"/path/to/file.rs\");  // \"rust\"\nconst highlighted = highlightCode(code, lang, theme);\n```\n\n## Gestion des erreurs\n\n- Les erreurs d'extension sont enregistrées, l'agent continue\n- Les erreurs `tool_call` bloquent l'outil (sécurité intégrée)\n- Les erreurs de l'outil `execute` doivent être signalées par un lancer; l'erreur générée est détectée, signalée au LLM avec `isError: true` et l'exécution continue\n\n## Comportement des modes\n\n| Mode | `ctx.mode` | `ctx.hasUI` | Remarques |\n|------|------------|-------------|-------|\n| Interactif | `\"tui\"` | `true` | Complet TUI avec rendu du terminal |\n| RPC (`--mode rpc`) | `\"rpc\"` | `true` | Dialogues et notifications via le protocole JSON; `custom()` renvoie `undefined`. Voir [rpc.md](rpc.md) |\n| JSON (`--mode json`) | `\"json\"` | `false` | Flux d'événements vers stdout; Les méthodes d'interface utilisateur ne fonctionnent pas |\n| Imprimer (`-p`) | `\"print\"` | `false` | Extensions exécuté mais ne peut pas demander |\n\nUtilisez `ctx.mode === \"tui\"` avant les fonctionnalités spécifiques à TUI (`custom()`, usines de composants, entrée de terminal). Utilisez `ctx.hasUI` avant les méthodes de dialogue et de notification qui fonctionnent dans les modes TUI et RPC.\n\n## Exemples de référence\n\nTous les exemples en [examples/extensions/](../examples/extensions/).\n\n| Exemple | Description | Touche API |\n|---------|-------------|----------|\n| **Outils** |  |  |\n| `hello.ts` | Enregistrement minimal des outils | `registerTool` |\n| `question.ts` | Outil avec interaction utilisateur | `registerTool`, `ui.select` |\n| `questionnaire.ts` | Outil d'assistant en plusieurs étapes | `registerTool`, `ui.custom` |\n| `todo.ts` | Outil avec état avec persistance | `registerTool`, `appendEntry`, `renderResult`, événements de session |\n| `dynamic-tools.ts` | Enregistrez les outils après le démarrage et pendant les commandes | `registerTool`, `session_start`, `registerCommand` |\n| `structured-output.ts` | Outil de sortie structurée final avec `terminate: true` | `registerTool`, fin des résultats de l'outil |\n| `truncated-tool.ts` | Exemple de troncature de sortie | `registerTool`, `truncateHead` |\n| `tool-override.ts` | Remplacer l'outil de lecture intégré | `registerTool` (même nom que celui intégré) |\n| **Commandes** |  |  |\n| `pirate.ts` | Modifier l'invite du système par tour | `registerCommand`, `before_agent_start` |\n| `summarize.ts` | Commande de résumé de conversation | `registerCommand`, `ui.custom` |\n| `handoff.ts` | Transfert de modèle entre fournisseurs | `registerCommand`, `ui.editor`, `ui.custom` |\n| `qna.ts` | Questions et réponses avec interface utilisateur personnalisée | `registerCommand`, `ui.custom`, `setEditorText` |\n| `send-user-message.ts` | Injecter les messages des utilisateurs | `registerCommand`, `sendUserMessage` |\n| `reload-runtime.ts` | Commande de rechargement et transfert de l'outil LLM | `registerCommand`, `ctx.reload()`, `sendUserMessage` |\n| `shutdown-command.ts` | Commande d'arrêt progressif | `registerCommand`, `shutdown()` |\n| **Événements et portes** |  |  |\n| `permission-gate.ts` | Bloquer les commandes dangereuses | `on(\"tool_call\")`, `ui.confirm` |\n| `project-trust.ts` | Décider ou différer l'approbation du projet par un utilisateur/global ou une extension CLI | `on(\"project_trust\")`, confiance dans l'interface utilisateur, résultat de confiance requis |\n| `protected-paths.ts` | Bloquer les écritures sur des chemins spécifiques | `on(\"tool_call\")` |\n| `confirm-destructive.ts` | Confirmer les modifications de session | `on(\"session_before_switch\")`, `on(\"session_before_fork\")` |\n| `dirty-repo-guard.ts` | Avertir en cas de dépôt git sale | `on(\"session_before_*\")`, `exec` |\n| `input-transform.ts` | Transformer la saisie de l'utilisateur | `on(\"input\")` |\n| `input-transform-streaming.ts` | Transformation d'entrée compatible avec le streaming | `on(\"input\")`, `streamingBehavior` |\n| `model-status.ts` | React pour modéliser les changements | `on(\"model_select\")`, `setStatus` |\n| `provider-payload.ts` | Inspecter les charges utiles et les en-têtes de réponse du fournisseur | `on(\"before_provider_request\")`, `on(\"after_provider_response\")` |\n| `system-prompt-header.ts` | Afficher les informations d'invite du système | `on(\"agent_start\")`, `getSystemPrompt` |\n| `claude-rules.ts` | Charger des règles à partir de fichiers | `on(\"session_start\")`, `on(\"before_agent_start\")` |\n| `prompt-customizer.ts` | Ajoutez des conseils d'outils contextuels à l'aide de `systemPromptOptions` | `on(\"before_agent_start\")`, `BuildSystemPromptOptions` |\n| `file-trigger.ts` | L'observateur de fichiers déclenche des messages | `sendMessage` |\n| **Compactage et séances** |  |  |\n| `custom-compaction.ts` | Résumé du compactage personnalisé | `on(\"session_before_compact\")` |\n| `trigger-compact.ts` | Déclencher le compactage manuellement | `compact()` |\n| `git-checkpoint.ts` | Git réserve aux tours | `on(\"turn_start\")`, `on(\"session_before_fork\")`, `exec` |\n| `git-merge-and-resolve.ts` | Récupérer, fusionner et résoudre les conflits | `on(\"agent_end\")`, `exec`, `sendUserMessage` |\n| `auto-commit-on-exit.ts` | S'engager à l'arrêt | `on(\"session_shutdown\")`, `exec` |\n| **Composants de l'interface utilisateur** |  |  |\n| `status-line.ts` | Indicateur d'état du pied de page | `setStatus`, événements de session |\n| `working-indicator.ts` | Personnaliser l'indicateur de fonctionnement du streaming | `setWorkingIndicator`, `registerCommand` |\n| `github-issue-autocomplete.ts` | Ajoutez la complétion de problèmes `#1234` en plus de la saisie semi-automatique intégrée en préchargeant les problèmes ouverts récents à partir de `gh issue list` | `addAutocompleteProvider`, `on(\"session_start\")`, `exec` |\n| `custom-footer.ts` | Remplacer entièrement le pied de page | `registerCommand`, `setFooter` |\n| `custom-header.ts` | Remplacer l'en-tête de démarrage | `on(\"session_start\")`, `setHeader` |\n| `modal-editor.ts` | Éditeur modal de style Vim | `setEditorComponent`, `CustomEditor` |\n| `rainbow-editor.ts` | Style d'éditeur personnalisé | `setEditorComponent` |\n| `widget-placement.ts` | Éditeur de widget au-dessus/en-dessous | `setWidget` |\n| `overlay-test.ts` | Composants de superposition | `ui.custom` avec options de superposition |\n| `overlay-qa-tests.ts` | Tests de superposition complets | `ui.custom`, toutes les options de superposition |\n| `notify.ts` | Notifications simples | `ui.notify` |\n| `timed-confirm.ts` | Boîtes de dialogue avec timeout | `ui.confirm` avec délai d'attente/signal |\n| `mac-system-theme.ts` | Thème de changement automatique | `setTheme`, `exec` |\n| **Complexe Extensions** |  |  |\n| `plan-mode/` | Implémentation du mode plan complet | Tous les types d'événements, `registerCommand`, `registerShortcut`, `registerFlag`, `setStatus`, `setWidget`, `sendMessage`, `setActiveTools` |\n| `preset.ts` | Préréglages enregistrables (modèle, outils, réflexion) | `registerCommand`, `registerShortcut`, `registerFlag`, `setModel`, `setActiveTools`, `setThinkingLevel`, `appendEntry` |\n| `tools.ts` | Activer/désactiver les outils de l'interface utilisateur | `registerCommand`, `setActiveTools`, `SettingsList`, événements de session |\n| **À distance et bac à sable** |  |  |\n| `ssh.ts` | SSH exécution à distance | `registerFlag`, `on(\"user_bash\")`, `on(\"before_agent_start\")`, opérations d'outil |\n| `interactive-shell.ts` | Session shell persistante | `on(\"user_bash\")` |\n| `sandbox/` | Exécution d'outils en bac à sable | Opérations sur les outils |\n| `gondolin/` | Acheminez les outils intégrés et les commandes `!` vers une micro-VM Gondolin | Opérations sur les outils, remplacements d'outils intégrés, `on(\"user_bash\")` |\n| `subagent/` | Générer des sous-agents | `registerTool`, `exec` |\n| **Jeux** |  |  |\n| `snake.ts` | Jeu de serpent | `registerCommand`, `ui.custom`, manipulation du clavier |\n| `space-invaders.ts` | Jeu Space Invaders | `registerCommand`, `ui.custom` |\n| `doom-overlay/` | Doom en superposition | `ui.custom` avec superposition |\n| **Providers** |  |  |\n| `custom-provider-anthropic/` | Proxy anthropique personnalisé | `registerProvider` |\n| `custom-provider-gitlab-duo/` | GitIntégration Lab Duo | `registerProvider` avec OAuth |\n| **Messages et communications** |  |  |\n| `message-renderer.ts` | Rendu des messages personnalisé | `registerMessageRenderer`, `sendMessage` |\n| `entry-renderer.ts` | Rendu d'entrée personnalisé TUI uniquement | `registerEntryRenderer`, `appendEntry` |\n| `event-bus.ts` | Événements inter-extensions | `pi.events` |\n| **Métadonnées de session** |  |  |\n| `session-name.ts` | Nommer les sessions pour le sélecteur | `setSessionName`, `getSessionName` |\n| `bookmark.ts` | Entrées de favoris pour /tree | `setLabel` |\n| **Divers** |  |  |\n| `inline-bash.ts` | Inline bash dans les appels d'outils | `on(\"tool_call\")` |\n| `bash-spawn-hook.ts` | Ajustez la commande bash, cwd et env avant l'exécution | `createBashTool`, `spawnHook` |\n| `with-deps/` | Extension avec npm dépendances | Structure du paquet avec `package.json` |","sourceFile":"extensions.md"},"index":{"title":"Pi Documentation","markdown":"Pi est un faisceau de codage de terminal minimal. Il est conçu pour rester petit à la base tout en étant étendu via TypeScript extensions, compétences, prompt templates, thèmes et packages pi.\n\n## Démarrage rapide\n\nInstallez Pi avec npm:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` désactive les scripts de cycle de vie des dépendances pendant l'installation. Pi ne nécessite pas de scripts d'installation pour les installations npm normales.\n\nSous Linux ou macOS, vous pouvez également utiliser l'installateur:\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\nPour désinstaller pi lui-même, utilisez npm pour les installations curl et npm:\n\n```bash\nnpm uninstall -g @earendil-works/pi-coding-agent\n```\n\nPour les installations pnpm, Yarn ou Bun, utilisez la commande de suppression globale correspondante: `pnpm remove -g @earendil-works/pi-coding-agent`, `yarn global remove @earendil-works/pi-coding-agent` ou `bun uninstall -g @earendil-works/pi-coding-agent`.\n\nEnsuite, exécutez-le dans un répertoire de projet:\n\n```bash\npi\n```\n\nAuthentifiez-vous avec `/login` pour subscription providers, ou définissez un API key tel que `ANTHROPIC_API_KEY` avant de démarrer pi.\n\nPour le flux complet de première exécution, voir [Quickstart](quickstart.md).\n\n## Commencez ici\n\n- [Quickstart](quickstart.md) - installez, authentifiez-vous et exécutez une première session.\n- [Using Pi](usage.md) - mode interactif, référence slash commands, context files et CLI.\n- [Providers](providers.md) - abonnement et configuration de la touche API pour les fournisseurs intégrés.\n- [llama.cpp](llama-cpp.md) - exécutez un routeur local et gérez les modèles avec `/llama`.\n- [Security](security.md) - confiance dans le projet, limites sandbox et rapports de vulnérabilité.\n- [Containerization](containerization.md) - sandbox pi avec Gondolin, Docker ou OpenShell.\n- [Settings](settings.md) - paramètres globaux et du projet.\n- [Keybindings](keybindings.md) - raccourcis par défaut et raccourcis clavier personnalisés.\n- [Sessions](sessions.md) - gestion de session, branchement et navigation dans l'arborescence.\n- [Compaction](compaction.md) - context compaction et branch summarization.\n\n## Personnalisation\n\n- [Extensions](extensions.md) - TypeScript modules pour les outils, les commandes, les événements et l'interface utilisateur personnalisée.\n- [Skills](skills.md) - Agent Skills pour des fonctionnalités réutilisables à la demande.\n- [Prompt templates](prompt-templates.md) - invites réutilisables qui s'étendent à partir de slash commands.\n- [Themes](themes.md) - terminal themes intégré et personnalisé.\n- [Pi packages](packages.md) - regroupez et partagez des extensions, des compétences, des invites et des thèmes.\n- [Custom models](models.md) - ajoute des entrées de modèle pour les fournisseurs pris en charge APIs.\n- [Custom providers](custom-provider.md) - implémentez des flux API et OAuth personnalisés.\n\n## Utilisation programmatique\n\n- [SDK](sdk.md) - intégrer pi dans les applications Node.js.\n- [RPC mode](rpc.md) - intégrer sur stdin/stdout JSONL.\n- [JSON event stream mode](json.md) - mode d'impression avec événements structurés.\n- [TUI components](tui.md) - créez une interface utilisateur de terminal personnalisée pour les extensions.\n\n## Référence\n\n- [Environment variables](environment-variables.md) - Pi configuration du processus et métadonnées de session disponibles pour bash outils.\n- [Session format](session-format.md) - JSONL format de fichier de session, types d'entrée et SessionManager API.\n\n## Configuration de la plateforme\n\n- [Windows](windows.md)\n- [Termux on Android](termux.md)\n- [tmux](tmux.md)\n- [Terminal setup](terminal-setup.md)\n- [Shell aliases](shell-aliases.md)\n\n## Développement\n\n- [Development](development.md) - configuration locale, structure du projet et débogage.","sourceFile":"index.md"},"json":{"title":"JSON Mode flux d'événements","markdown":"```bash\npi --mode json \"Your prompt\"\n```\n\nAffiche tous les événements de session sous forme de lignes JSON à stdout. Utile pour intégrer pi dans d'autres outils ou interfaces utilisateur personnalisées.\n\n## Types d'événements\n\nLes événements filaires utilisent `JsonAgentSessionEvent`. Cela correspond\n[`AgentSessionEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/agent-session.ts)\nsauf que les mises à jour des messages en streaming omettent les instantanés cumulés:\n\n```typescript\ntype WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, \"partial\"> : T;\n\ntype JsonAgentSessionEvent =\n  | Exclude<AgentSessionEvent, { type: \"message_update\" }>\n  | {\n      type: \"message_update\";\n      assistantMessageEvent: WithoutPartial<AssistantMessageEvent>;\n    };\n```\n\n`queue_update` émet l'intégralité des files d'attente de pilotage et de suivi en attente à chaque fois qu'elles changent. `compaction_start` et `compaction_end` couvrent à la fois le compactage manuel et automatique.\n\nD'autres événements de base proviennent de\n[`AgentEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts):\n\n```typescript\ntype AgentEvent =\n  // Agent lifecycle\n  | { type: \"agent_start\" }\n  | { type: \"agent_end\"; messages: AgentMessage[] }\n  // Turn lifecycle\n  | { type: \"turn_start\" }\n  | { type: \"turn_end\"; message: AgentMessage; toolResults: ToolResultMessage[] }\n  // Message lifecycle\n  | { type: \"message_start\"; message: AgentMessage }\n  | { type: \"message_update\"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }\n  | { type: \"message_end\"; message: AgentMessage }\n  // Tool execution\n  | { type: \"tool_execution_start\"; toolCallId: string; toolName: string; args: any }\n  | { type: \"tool_execution_update\"; toolCallId: string; toolName: string; args: any; partialResult: any }\n  | { type: \"tool_execution_end\"; toolCallId: string; toolName: string; result: any; isError: boolean };\n```\n\n## Types de messages\n\nMessages de base de [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts#L134):\n- `UserMessage` (ligne 134)\n- `AssistantMessage` (ligne 140)\n- `ToolResultMessage` (ligne 152)\n\nMessages étendus de [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts#L29):\n- `BashExecutionMessage` (ligne 29)\n- `CustomMessage` (ligne 46)\n- `BranchSummaryMessage` (ligne 55)\n- `CompactionSummaryMessage` (ligne 62)\n\n## Format de sortie\n\nChaque ligne est un objet JSON. La première ligne est l'en-tête de la session:\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"...\",\"cwd\":\"/path\"}\n```\n\nSuivi des événements au fur et à mesure qu'ils se produisent:\n\n```json\n{\"type\":\"agent_start\"}\n{\"type\":\"turn_start\"}\n{\"type\":\"message_start\",\"message\":{\"role\":\"assistant\",\"content\":[],...}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_end\",\"message\":{...}}\n{\"type\":\"turn_end\",\"message\":{...},\"toolResults\":[]}\n{\"type\":\"agent_end\",\"messages\":[...]}\n```\n\nLes enregistrements `message_update` sont uniquement delta. Ils omettent à la fois le champ cumulatif `message` et\n`assistantMessageEvent.partial` pour conserver la taille du flux linéaire. Utilisez `contentIndex` et `delta`\npour assembler du texte en direct, des réflexions ou des arguments d'appel d'outils si nécessaire. `message_end` contient\nle message final faisant autorité.\n\n## Exemple\n\n```bash\npi --mode json \"List files\" 2>/dev/null | jq -c 'select(.type == \"message_end\")'\n```","sourceFile":"json.md"},"keybindings":{"title":"Raccourcis clavier","markdown":"Tous les raccourcis clavier peuvent être personnalisés via `~/.pi/agent/keybindings.json`. Chaque action peut être liée à une ou plusieurs touches.\n\nLe fichier de configuration utilise les mêmes identifiants de liaison de touches avec espace de noms que pi utilise en interne et que les auteurs d'extensions utilisent dans les gestionnaires `keyHint()` et `keybindings` injectés.\n\nLes configurations plus anciennes utilisant des identifiants pré-espaces de noms tels que `cursorUp` ou `expandTools` sont automatiquement migrées vers les identifiants avec espace de noms au démarrage.\n\nAprès avoir modifié `keybindings.json`, exécutez `/reload` dans pi pour appliquer les modifications sans redémarrer la session.\n\n## Format de clé\n\n`modifier+key` où les modificateurs sont `ctrl`, `shift`, `alt`, `super` (combinables) et les clés sont:\n\n- **Lettres:** `a-z`\n- **Chiffres:** `0-9`\n- **Touches spéciales:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`\n- **Touches de fonction:** `f1`-`f12`\n- **Symboles:** `` ` ``, `-`, `=`, `[`, `]`, `\\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`\n\nCombinaisons de modificateurs: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `super+k`, `ctrl+super+k`, `ctrl+1`, etc.\n\nLes liaisons `super` nécessitent un terminal qui signale le modificateur séparément, généralement via le protocole du clavier Kitty. Ils pourraient ne pas fonctionner dans les terminaux sans ce support.\n\n## Toutes les actions\n\n### TUI Mouvement du curseur de l'éditeur\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `tui.editor.cursorUp` | `up` | Déplacez le curseur vers le haut et parcourez l'historique plus ancien en haut |\n| `tui.editor.cursorDown` | `down` | Déplacez le curseur vers le bas et parcourez l'historique le plus récent en bas. |\n| `tui.editor.historyPrevious` | *(aucun)* | Sélectionnez l'entrée précédente de l'historique des invites |\n| `tui.editor.historyNext` | *(aucun)* | Sélectionnez la prochaine entrée de l'historique des invites |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | Déplacer le curseur vers la gauche |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | Déplacer le curseur vers la droite |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | Déplacer le mot curseur vers la gauche |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | Déplacer le mot curseur vers la droite |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+home`, `ctrl+a` | Passer au début de la ligne |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+end`, `ctrl+e` | Passer à la fin de la ligne |\n| `tui.editor.jumpForward` | `ctrl+]` | Avancer vers le personnage |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | Revenir en arrière jusqu'au personnage |\n| `tui.editor.pageUp` | `pageUp`, `ctrl+pageUp` | Faire défiler la page vers le haut |\n| `tui.editor.pageDown` | `pageDown`, `ctrl+pageDown` | Faites défiler par page |\n\nLes actions d'historique dédiées modifient toujours les entrées de l'historique, quelle que soit la position du curseur dans une invite multiligne. Les liaisons d'historique explicites ont priorité sur les actions de l'application lorsque l'éditeur principal est ciblé, donc la liaison de `tui.editor.historyPrevious` à `ctrl+p` remplace le cycle de modèle dans ce contexte sans modifier `Ctrl+P` dans les sélecteurs.\n\n### TUI Suppression de l'éditeur\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `tui.editor.deleteCharBackward` | `backspace` | Supprimer un caractère vers l'arrière |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | Supprimer le caractère vers l'avant |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | Supprimer le mot à l'envers |\n| `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | Supprimer le mot en avant |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | Supprimer au début de la ligne |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | Supprimer jusqu'à la fin de la ligne |\n\n### TUI Entrée\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `tui.input.newLine` | `shift+enter`, `ctrl+j` | Insérer une nouvelle ligne |\n| `tui.input.submit` | `enter` | Soumettre la contribution |\n| `tui.input.tab` | `tab` | Onglet / saisie semi-automatique |\n\n### TUI Tuer l'anneau\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `tui.editor.yank` | `ctrl+y` | Coller le texte le plus récemment supprimé |\n| `tui.editor.yankPop` | `alt+y` | Parcourez le texte supprimé après un coup sec |\n| `tui.editor.undo` | `ctrl+-` | Annuler la dernière modification |\n\n### TUI Presse-papiers et sélection\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `tui.input.copy` | `ctrl+c` | Copier la sélection |\n| `tui.select.up` | `up` | Déplacer la sélection vers le haut |\n| `tui.select.down` | `down` | Déplacer la sélection vers le bas |\n| `tui.select.pageUp` | `pageUp` | Page précédente dans la liste |\n| `tui.select.pageDown` | `pageDown` | Page suivante dans la liste |\n| `tui.select.confirm` | `enter` | Confirmer la sélection |\n| `tui.select.cancel` | `escape`, `ctrl+c` | Annuler la sélection |\n\n### TUI Fenêtre plein écran\n\nCes actions s'appliquent lorsque le mode interactif utilise `--tui-mode fullscreen` et cible la région de défilement de transcription principale. Le trackpad à deux doigts et la molette de la souris font défiler la région sous le pointeur, en revenant à la transcription sur le dock fixe éditeur/statut/pied de page. Cliquer sur un lien hypertexte OSC 8 l'ouvre dans le gestionnaire par défaut. Faire glisser avec le bouton principal de la souris sélectionne le texte et le copie dans le presse-papiers; en maintenant le bord supérieur ou inférieur de la transcription, vous faites défiler automatiquement le contenu hors écran.\n\nLes liaisons de transcription plein écran ont priorité sur les liaisons de l'éditeur. Les touches de navigation par défaut non modifiées contrôlent donc la transcription en mode plein écran, tandis que leurs variantes `ctrl` continuent de contrôler l'éditeur. En dehors du mode plein écran, les deux variantes contrôlent l'éditeur.\n\n| Clé | Mode par défaut | Mode plein écran |\n|-----|--------------|-----------------|\n| `home`, `end` | Éditeur | Transcription |\n| `ctrl+home`, `ctrl+end` | Éditeur | Éditeur |\n| `pageUp`, `pageDown` | Éditeur | Transcription |\n| `ctrl+pageUp`, `ctrl+pageDown` | Éditeur | Éditeur |\n\nCe routage reste configurable via les liaisons d'actions ordinaires. Par exemple, `\"tui.altScreen.pageUp\": \"ctrl+pageUp\"` permet à `pageUp` de contrôler l'éditeur et à `ctrl+pageUp` de contrôler la transcription en mode plein écran. Liez `tui.altScreen.halfPageUp` et `tui.altScreen.halfPageDown` pour des étapes de transcription plus petites tout en conservant les liaisons pleine page. Le paramètre `\"tui.altScreen.pageUp\": []` désactive entièrement ce raccourci de transcription. Les liaisons utilisateur remplacent les valeurs par défaut pour cette action.\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `tui.altScreen.pageUp` | `pageUp` | Faites défiler la transcription d'une page vers le haut |\n| `tui.altScreen.pageDown` | `pageDown` | Faites défiler la transcription d'une page vers le bas |\n| `tui.altScreen.halfPageUp` | *(aucun)* | Faites défiler la transcription d'une demi-page vers le haut |\n| `tui.altScreen.halfPageDown` | *(aucun)* | Faites défiler la transcription d'une demi-page vers le bas |\n| `tui.altScreen.previousPrompt` | `ctrl+shift+up` | Passer au message marqué précédent |\n| `tui.altScreen.nextPrompt` | `ctrl+shift+down` | Passer au message marqué suivant |\n| `tui.altScreen.top` | `home` | Faites défiler jusqu'au début de la transcription |\n| `tui.altScreen.bottom` | `end` | Faites défiler jusqu'à la fin de la transcription et suivez la nouvelle sortie |\n\n### Application\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `app.interrupt` | `escape` | Annuler / abandonner |\n| `app.clear` | `ctrl+c` | Effacer l'éditeur (premier) / quitter (deuxième) |\n| `app.exit` | `ctrl+d` | Quitter (quand l'éditeur est vide) |\n| `app.suspend` | `ctrl+z` (aucun sous Windows) | Suspendre en arrière-plan |\n| `app.editor.external` | `ctrl+g` | Ouvrir dans un éditeur externe (`externalEditor`, `$VISUAL`, `$EDITOR`, Bloc-notes sous Windows ou `nano` ailleurs) |\n| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` sous Windows) | Coller une image ou du texte à partir du presse-papiers |\n\n### Séances\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `app.session.new` | *(aucun)* | Démarrer une nouvelle session (`/new`) |\n| `app.session.tree` | *(aucun)* | Ouvrez le navigateur session tree (`/tree`) |\n| `app.session.fork` | *(aucun)* | Session en cours de fourche (`/fork`) |\n| `app.session.resume` | *(aucun)* | Sélecteur de CV de session ouverte (`/resume`) |\n| `app.session.togglePath` | `ctrl+p` | Basculer l'affichage du chemin |\n| `app.session.toggleSort` | `ctrl+s` | Basculer le mode de tri |\n| `app.session.toggleNamedFilter` | `ctrl+n` | Activer/désactiver le filtre nommé uniquement |\n| `app.session.rename` | `ctrl+r` | Renommer la session |\n| `app.session.delete` | `ctrl+d` | Supprimer la séance |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | Supprimer la session lorsque la requête est vide |\n\n### Models et réflexion\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `app.model.select` | `ctrl+l` | Ouvrir le sélecteur de modèle |\n| `app.model.cycleForward` | `ctrl+p` | Passer au modèle suivant |\n| `app.model.cycleBackward` | `shift+ctrl+p` | Passer au modèle précédent |\n| `app.thinking.cycle` | `shift+tab` | Niveau de réflexion cyclique |\n| `app.thinking.toggle` | `ctrl+t` | Réduire ou développer les blocs de réflexion |\n\n### Affichage et file d'attente des messages\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `app.tools.expand` | `ctrl+o` | Réduire ou développer la sortie de l'outil |\n| `app.message.copy` | `ctrl+x` | Copiez le dernier message de l'assistant ou le message sélectionné dans `/tree` |\n| `app.message.followUp` | `alt+enter` | Message de suivi de file d'attente |\n| `app.message.dequeue` | `alt+up` | Restaurer les messages en file d'attente dans l'éditeur |\n\n### Navigation dans l'arborescence\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | Pliez le segment de branche actuel ou passez au début du segment précédent |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | Dépliez le segment de branche actuel ou passez au début ou à la fin du segment suivant. |\n| `app.tree.editLabel` | `shift+l` | Modifier l'étiquette sur le nœud d'arborescence sélectionné |\n| `app.tree.toggleLabelTimestamp` | `shift+t` | Basculer les horodatages des étiquettes dans l'arborescence |\n| `app.tree.filter.default` | `ctrl+d` | Définir le filtre d'arborescence sur la vue par défaut |\n| `app.tree.filter.noTools` | `ctrl+t` | Activer le filtre d'arborescence qui masque les résultats des outils |\n| `app.tree.filter.userOnly` | `ctrl+u` | Basculer le filtre d'arborescence qui affiche uniquement les messages des utilisateurs |\n| `app.tree.filter.labeledOnly` | `ctrl+l` | Basculer le filtre d'arborescence qui affiche uniquement les entrées étiquetées |\n| `app.tree.filter.all` | `ctrl+a` | Basculer le filtre d'arborescence qui affiche toutes les entrées |\n| `app.tree.filter.cycleForward` | `ctrl+o` | Filtrer l'arborescence en avant |\n| `app.tree.filter.cycleBackward` | `shift+ctrl+o` | Filtrer l'arborescence en arrière |\n\n### Sélecteur de portée Models\n\nUtilisé dans le sélecteur de modèles étendus (ouvert via `/scoped-models`).\n\n| ID de liaison de clé | Défaut | Description |\n|--------|---------|-------------|\n| `app.models.save` | `ctrl+s` | Enregistrer la sélection actuelle du modèle dans les paramètres |\n| `app.models.enableAll` | `ctrl+a` | Activer tous les modèles (ou tous correspondant à la recherche en cours) |\n| `app.models.clearAll` | `ctrl+x` | Effacer tous les modèles (ou tous correspondant à la recherche en cours) |\n| `app.models.toggleProvider` | `ctrl+p` | Basculer tous les modèles pour le fournisseur actuel |\n| `app.models.reorderUp` | `alt+up` | Déplacer le modèle sélectionné vers le haut dans l'ordre du cycle |\n| `app.models.reorderDown` | `alt+down` | Déplacer le modèle sélectionné vers le bas dans l'ordre du cycle |\n\n## Configuration personnalisée\n\nCréez `~/.pi/agent/keybindings.json`:\n\n```json\n{\n  \"tui.editor.historyPrevious\": \"ctrl+p\",\n  \"tui.editor.historyNext\": \"ctrl+n\",\n  \"tui.editor.deleteWordBackward\": [\"ctrl+w\", \"alt+backspace\"]\n}\n```\n\nChaque action peut avoir une seule clé ou un tableau de clés. La configuration utilisateur remplace les valeurs par défaut.\n\nSous Windows natif, `app.suspend` n'a pas de liaison par défaut car les terminaux Windows ne prennent pas en charge le contrôle des tâches Unix. Si vous le liez manuellement, pi affiche un message d'état au lieu de le suspendre. Dans WSL, le comportement normal de Linux `ctrl+z`/`fg` s'applique toujours.\n\n### Exemple Emacs\n\n```json\n{\n  \"tui.editor.historyPrevious\": \"ctrl+p\",\n  \"tui.editor.historyNext\": \"ctrl+n\",\n  \"tui.editor.cursorLeft\": [\"left\", \"ctrl+b\"],\n  \"tui.editor.cursorRight\": [\"right\", \"ctrl+f\"],\n  \"tui.editor.cursorWordLeft\": [\"alt+left\", \"alt+b\"],\n  \"tui.editor.cursorWordRight\": [\"alt+right\", \"alt+f\"],\n  \"tui.editor.deleteCharForward\": [\"delete\", \"ctrl+d\"],\n  \"tui.editor.deleteCharBackward\": [\"backspace\", \"ctrl+h\"],\n  \"tui.input.newLine\": [\"shift+enter\", \"ctrl+j\"]\n}\n```\n\n### Exemple Vim\n\n```json\n{\n  \"tui.editor.cursorUp\": [\"up\", \"alt+k\"],\n  \"tui.editor.cursorDown\": [\"down\", \"alt+j\"],\n  \"tui.editor.cursorLeft\": [\"left\", \"alt+h\"],\n  \"tui.editor.cursorRight\": [\"right\", \"alt+l\"],\n  \"tui.editor.cursorWordLeft\": [\"alt+left\", \"alt+b\"],\n  \"tui.editor.cursorWordRight\": [\"alt+right\", \"alt+w\"]\n}\n```","sourceFile":"keybindings.md"},"llama-cpp":{"title":"llama.cpp","markdown":"Pi prend en charge le serveur de routeur [llama.cpp](https://github.com/ggml-org/llama.cpp). Le routeur découvre plusieurs modèles GGUF et les charge ou les décharge à la demande.\n\nUtilisez une version llama.cpp actuelle avec prise en charge du routeur. Suivez le [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) ou installez un [prebuilt release](https://github.com/ggml-org/llama.cpp/releases) pour votre plateforme.\n\n## Démarrez le routeur\n\nCommencez `llama-server` sans `--model` ou `-m`. La transmission d'un modèle démarre le mode modèle unique au lieu du mode routeur.\n\n```bash\nllama-server \\\n  --models-dir ~/models \\\n  --no-models-autoload \\\n  --jinja \\\n  --host 127.0.0.1 \\\n  --port 8080 \\\n  -ngl 999 \\\n  -c 32768\n```\n\nOptions importantes:\n\n- `--models-dir ~/models` découvre les fichiers GGUF locaux.\n- `--no-models-autoload` continue de se charger de manière explicite jusqu'à `/llama`.\n- `--jinja` active les modèles de discussion et les appels d'outils compatibles.\n- `-ngl 999` décharge autant de couches que possible sur le GPU.\n- `-c 32768` définit la fenêtre contextuelle pour chaque modèle chargé. Omettez-le pour utiliser le contexte natif du modèle, ce qui peut nécessiter beaucoup plus de mémoire.\n\nUn modèle à fichier unique peut se trouver directement dans le répertoire du modèle. Placez les modèles multimodaux et multi-fragments dans des sous-répertoires distincts:\n\n```text\n~/models/\n├── llama-3.2-1b-Q4_K_M.gguf\n├── gemma-3-4b-it-Q4_K_M/\n│   ├── gemma-3-4b-it-Q4_K_M.gguf\n│   └── mmproj-F16.gguf\n└── large-model-Q4_K_M/\n    ├── large-model-Q4_K_M-00001-of-00003.gguf\n    ├── large-model-Q4_K_M-00002-of-00003.gguf\n    └── large-model-Q4_K_M-00003-of-00003.gguf\n```\n\nRedémarrez le routeur après avoir ajouté manuellement des fichiers. Pour les tailles de contexte par modèle et d'autres options, utilisez [llama.cpp model presets](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets).\n\n## Configurer Pi\n\nDémarrez Pi et configurez le fournisseur:\n\n```text\n/login llama.cpp\n```\n\nEntrez l'URL du routeur et API key facultatif. L'URL par défaut est `http://127.0.0.1:8080`.\n\nLes variables d'environnement peuvent configurer les mêmes valeurs sans `/login`:\n\n```bash\nexport LLAMA_BASE_URL=http://127.0.0.1:8080\nexport LLAMA_API_KEY=optional-secret\npi\n```\n\nSi le serveur utilise un API key, commencez `llama-server` avec la valeur `--api-key` correspondante. Conservez `--host 127.0.0.1` pour un accès local uniquement.\n\n## Gérer les modèles\n\nCourir:\n\n```text\n/llama\n```\n\n- Sélectionnez un modèle déchargé pour le charger.\n- Sélectionnez un modèle chargé pour le décharger.\n- Sélectionnez **Télécharger le modèle…**, recherchez Hugging Face, puis choisissez un référentiel et une quantification. Les valeurs exactes `owner/repository[:quant]` fonctionnent également.\n- Appuyez sur Échap pendant un chargement ou un téléchargement pour confirmer l'annulation.\n\nLa recherche Hugging Face utilise `HF_TOKEN` lorsqu'elle est définie, puis vérifie `$HF_TOKEN_PATH`, `$HF_HOME/token`, `$XDG_CACHE_HOME/huggingface/token` et `~/.cache/huggingface/token`. La recherche fonctionne également sans authentification, sous réserve de limites de débit inférieures. Pi avertit avant de télécharger des référentiels sécurisés et des liens vers leur page d'accès. Le serveur llama.cpp effectue le téléchargement, son processus doit donc également avoir `HF_TOKEN` lorsque le référentiel sélectionné nécessite un accès.\n\nSi d'autres modèles sont chargés, Pi demande s'il faut les décharger en premier ou les garder chargés. Pi ne décharge pas silencieusement les modèles et ne supprime jamais les fichiers de modèle. Le routeur peut être partagé avec d'autres clients, donc `/llama` affiche toujours l'état actuel du routeur.\n\nSeuls les modèles chargés apparaissent dans `/model`. Après avoir chargé un modèle, exécutez `/model` pour le sélectionner pour la session Pi en cours.\n\nSi le routeur se déconnecte, `/llama` affiche **Réessayer** et **Fermer**. Réessayez de vous reconnecter et d'actualiser l'état du modèle sans rejouer l'opération interrompue.\n\n## Dépannage\n\nVérifiez que le routeur est accessible:\n\n```bash\ncurl http://127.0.0.1:8080/health\ncurl http://127.0.0.1:8080/models\n```\n\n- **Aucun modèle dans `/llama`:** Vérifiez `--models-dir`, la disposition du répertoire et redémarrez le routeur.\n- **Modèle manquant dans `/model`:** Chargez-le d'abord avec `/llama`.\n- **Le chargement échoue ou utilise trop de mémoire:** Réduisez `-c` ou déchargez un autre modèle.\n- **Le serveur n'est pas en mode routeur:** Démarrez-le sans `--model`, `-m` ou `-hf`.","sourceFile":"llama-cpp.md"},"models":{"title":"Personnalisé Models","markdown":"Ajoutez des fournisseurs et des modèles personnalisés (Ollama, vLLM, LM Studio, proxys) via `~/.pi/agent/models.json`.\n\n## Table des matières\n\n- [Minimal Example](#minimal-example)\n- [Full Example](#full-example)\n- [Supported APIs](#supported-apis)\n- [Provider Configuration](#provider-configuration)\n- [Model Configuration](#model-configuration)\n- [Overriding Built-in Providers](#overriding-built-in-providers)\n- [Per-model Overrides](#per-model-overrides)\n- [Anthropic Messages Compatibility](#anthropic-messages-compatibility)\n- [OpenAI Compatibility](#openai-compatibility)\n\n## Exemple minimal\n\nPour les modèles locaux (Ollama, LM Studio, vLLM), seul `id` est requis par modèle:\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        { \"id\": \"llama3.1:8b\" },\n        { \"id\": \"qwen2.5-coder:7b\" }\n      ]\n    }\n  }\n}\n```\n\nLa valeur `apiKey` est un espace réservé car Ollama l'ignore. pi traite toujours les modèles comme nécessitant une authentification avant d'apparaître dans `/model`, donc les serveurs locaux sans clé doivent conserver une valeur factice, enregistrer une clé pour ce fournisseur avec `/login` ou transmettre `--api-key` lors de la sélection du modèle.\n\nCertains serveurs compatibles OpenAI ne comprennent pas le rôle `developer` utilisé pour les modèles capables de raisonner. Pour ces fournisseurs, définissez `compat.supportsDeveloperRole` sur `false` afin que pi envoie l'invite système sous forme de message `system` à la place. Si le serveur ne prend pas non plus en charge `reasoning_effort`, définissez également `compat.supportsReasoningEffort` sur `false`.\n\nVous pouvez définir `compat` au niveau du fournisseur pour l'appliquer à tous les modèles, ou au niveau du modèle pour remplacer un modèle spécifique. Cela s'applique généralement aux serveurs Ollama, vLLM, SGLang et similaires compatibles OpenAI.\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"compat\": {\n        \"supportsDeveloperRole\": false,\n        \"supportsReasoningEffort\": false\n      },\n      \"models\": [\n        {\n          \"id\": \"gpt-oss:20b\",\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\n## Exemple complet\n\nRemplacez les valeurs par défaut lorsque vous avez besoin de valeurs spécifiques:\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        {\n          \"id\": \"llama3.1:8b\",\n          \"name\": \"Llama 3.1 8B (Local)\",\n          \"reasoning\": false,\n          \"input\": [\"text\"],\n          \"contextWindow\": 128000,\n          \"maxTokens\": 32000,\n          \"cost\": { \"input\": 0, \"output\": 0, \"cacheRead\": 0, \"cacheWrite\": 0 }\n        }\n      ]\n    }\n  }\n}\n```\n\nLe fichier se recharge à chaque fois que vous ouvrez `/model`. Modifier pendant la session; aucun redémarrage n'est nécessaire.\n\n## Exemple de Google AI Studio\n\nUtilisez `google-generative-ai` avec un `baseUrl` pour ajouter des modèles de Google AI Studio, y compris des entrées Gemma 4 personnalisées:\n\n```json\n{\n  \"providers\": {\n    \"my-google\": {\n      \"baseUrl\": \"https://generativelanguage.googleapis.com/v1beta\",\n      \"api\": \"google-generative-ai\",\n      \"apiKey\": \"$GEMINI_API_KEY\",\n      \"models\": [\n        {\n          \"id\": \"gemma-4-31b-it\",\n          \"name\": \"Gemma 4 31B\",\n          \"input\": [\"text\", \"image\"],\n          \"contextWindow\": 262144,\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\nLe `baseUrl` est requis lors de l'ajout de modèles personnalisés au type `google-generative-ai` API.\n\n## API pris en charge\n\n| API | Description |\n|-----|-------------|\n| `openai-completions` | Achèvements de chat OpenAI (les plus compatibles) |\n| `openai-responses` | Réponses OpenAI API |\n| `anthropic-messages` | Messages anthropiques API |\n| `google-generative-ai` | IA générative de Google |\n\nDéfinissez `api` au niveau du fournisseur (par défaut pour tous les modèles) ou au niveau du modèle (remplacement par modèle).\n\n## Configuration du fournisseur\n\n| Champ | Description |\n|-------|-------------|\n| `baseUrl` | API URL du point de terminaison |\n| `api` | Tapez API (voir ci-dessus) |\n| `apiKey` | Configuration API key facultative (voir résolution de valeur ci-dessous). Omettez-le lorsque l'authentification est fournie par `/login`/`auth.json` ou CLI `--api-key`. |\n| `oauth` | Type de fournisseur dynamique OAuth. Prend actuellement en charge `\"radius\"`; nécessite la passerelle `baseUrl`. |\n| `headers` | En-têtes personnalisés (voir la résolution des valeurs ci-dessous) |\n| `authHeader` | Définissez `true` pour ajouter `Authorization: Bearer <apiKey>` automatiquement |\n| `models` | Tableau de configurations de modèles |\n| `modelOverrides` | Remplacements par modèle pour les modèles intégrés ou enregistrés avec une extension sur ce fournisseur |\n\nPour les fournisseurs avec `models`, les configurations de fournisseur non intégrées ont besoin de `baseUrl` et d'une valeur `api` au niveau du fournisseur ou du modèle. `apiKey` n'est pas requis pour charger le fichier: les modèles deviennent disponibles lorsque l'authentification est configurée via `/login`/`auth.json`, CLI `--api-key` ou le fournisseur `apiKey`. Si aucune authentification n'est configurée, les modèles se chargent mais restent indisponibles en `/model` et `--list-models`.\n\n### Résolution de valeur\n\nLes champs `apiKey` et `headers` prennent en charge l'exécution de commandes, l'interpolation d'environnement et les littéraux:\n\n- **Commande Shell:** `\"!command\"` au début exécute la valeur entière en tant que commande et utilise stdout\n  ```json\n  \"apiKey\": \"!security find-generic-password -ws 'anthropic'\"\n  \"apiKey\": \"!op read 'op://vault/item/credential'\"\n  ```\n- **Interpolation d'environnement:** `\"$ENV_VAR\"` ou `\"${ENV_VAR}\"` utilise la valeur de la variable nommée. L'interpolation fonctionne à l'intérieur de littéraux plus grands.\n  ```json\n  \"apiKey\": \"$MY_API_KEY\"\n  \"apiKey\": \"${KEY_PREFIX}_${KEY_SUFFIX}\"\n  ```\n  `$FOO_BAR` est la variable `FOO_BAR`; utilisez `${FOO}_BAR` lorsque `BAR` est un texte littéral. Les variables d'environnement manquantes rendent la valeur non résolue.\n- **Échappe:** `\"$\"` émet un `\"$\"` littéral; `\"$!\"` émet un `\"!\"` littéral sans déclencher l'exécution de la commande.\n  ```json\n  \"apiKey\": \"$$literal-dollar-prefix\"\n  \"apiKey\": \"$!literal-bang-prefix\"\n  ```\n- **Valeur littérale:** Utilisé directement. Les chaînes majuscules simples telles que `MY_API_KEY` sont des littéraux; utilisez `$MY_API_KEY` pour les variables d'environnement.\n  ```json\n  \"apiKey\": \"sk-...\"\n  ```\n\nPour `models.json`, les commandes shell sont résolues au moment de la demande. pi n'applique pas intentionnellement le TTL intégré, la réutilisation obsolète ou la logique de récupération pour les commandes arbitraires. Différentes commandes nécessitent différentes stratégies de mise en cache et de défaillance, et pi ne peut pas déduire la bonne.\n\nSi votre commande est lente, coûteuse, limitée en débit ou si elle doit continuer à utiliser une valeur précédente en cas d'échecs transitoires, enveloppez-la dans votre propre script ou commande qui implémente la mise en cache ou le comportement TTL souhaité.\n\nLes contrôles de disponibilité `/model` utilisent la présence d'authentification configurée et n'exécutent pas de commandes shell.\n\n### En-têtes personnalisés\n\n```json\n{\n  \"providers\": {\n    \"custom-proxy\": {\n      \"baseUrl\": \"https://proxy.example.com/v1\",\n      \"apiKey\": \"$MY_API_KEY\",\n      \"api\": \"anthropic-messages\",\n      \"headers\": {\n        \"x-portkey-api-key\": \"$PORTKEY_API_KEY\",\n        \"x-secret\": \"!op read 'op://vault/item/secret'\"\n      },\n      \"models\": [...]\n    }\n  }\n}\n```\n\n## Configuration du modèle\n\n| Champ | Requis | Défaut | Description |\n|-------|----------|---------|-------------|\n| `id` | Oui | — | Identifiant du modèle (passé au API) |\n| `name` | Non | `id` | Étiquette de modèle lisible par l'homme. Utilisé pour la correspondance (modèles `--model`) et affiché comme texte de détail du modèle secondaire. |\n| `api` | Non | `api` du fournisseur | Remplacer le API du fournisseur pour ce modèle |\n| `reasoning` | Non | `false` | Prend en charge la réflexion étendue |\n| `thinkingLevelMap` | Non | omis | Mappe les niveaux de réflexion Pi aux valeurs du fournisseur et marque les niveaux non pris en charge (voir ci-dessous) |\n| `input` | Non | `[\"text\"]` | Types d'entrée: `[\"text\"]` ou `[\"text\", \"image\"]` |\n| `contextWindow` | Non | `128000` | Taille de la fenêtre contextuelle en jetons |\n| `maxTokens` | Non | `16384` | Jetons de sortie maximale |\n| `samplingParams` | Non | omis | Paramètres d'échantillonnage fusionnés textuellement dans chaque corps de requête (voir ci-dessous) |\n| `cost` | Non | tous les zéros | Tarifs par million de jetons avec niveaux de tarification d'entrée facultatifs à l'échelle de la demande |\n| `compat` | Non | fournisseur `compat` | Remplacements de compatibilité du fournisseur. Fusionné avec le niveau du fournisseur `compat` lorsque les deux sont définis. |\n\nUn niveau de coût fournit un ensemble complet de tarifs alternatifs et s'applique à la demande complète lorsque l'utilisation totale des entrées (`input + cacheRead + cacheWrite`) dépasse `inputTokensAbove`. Lorsque plusieurs niveaux correspondent, le seuil le plus élevé l’emporte.\n\n```json\n{\n  \"cost\": {\n    \"input\": 5,\n    \"output\": 30,\n    \"cacheRead\": 0.5,\n    \"cacheWrite\": 6.25,\n    \"tiers\": [\n      {\n        \"inputTokensAbove\": 272000,\n        \"input\": 10,\n        \"output\": 45,\n        \"cacheRead\": 1,\n        \"cacheWrite\": 12.5\n      }\n    ]\n  }\n}\n```\n\nComportement actuel:\n- `/model`, `--list-models` et le pied de page interactif affichent les entrées par modèle `id`.\n- Le `name` configuré est utilisé pour la correspondance du modèle et le texte détaillé du modèle secondaire. Il ne remplace pas l’identifiant du modèle de pied de page/barre d’état.\n\n### Paramètres d'échantillonnage\n\n`samplingParams` est un objet de forme libre fusionné textuellement dans chaque corps de requête pour le modèle, une fois que les champs pi se sont définis, de sorte que ses clés gagnent. Utilisez-le pour envoyer des paramètres d'échantillonnage que pi ne modélise pas, y compris ceux spécifiques au serveur comme le `min_p` de llama.cpp ou le `top_k` de vLLM:\n\n```json\n{\n  \"id\": \"deepseek-v4-flash\",\n  \"samplingParams\": {\n    \"temperature\": 1.0,\n    \"top_p\": 0.95,\n    \"top_k\": 0,\n    \"min_p\": 0.0\n  }\n}\n```\n\nSeuls les API compatibles OpenAI l'appliquent (`openai-completions`, `openai-responses`, `azure-openai-responses`); les autres API l'ignorent. Les clés remplacent les champs de requête nommés de pi (par exemple, une touche `temperature` bat ici la température au niveau de la requête), préférez-la donc comme source unique de vérité d'échantillonnage pour un modèle. Dans `modelOverrides`, `samplingParams` fusionne par clé avec la valeur du modèle de base.\n\n### Carte des niveaux de réflexion\n\nUtilisez `thinkingLevelMap` sur un modèle pour décrire les contrôles de réflexion spécifiques au modèle. Les clés sont les niveaux de pensée Pi: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Les cartes peuvent contenir des trous; par exemple, un modèle peut exposer `high` et `max` sans exposer `xhigh`.\n\nLes valeurs sont à trois états:\n\n| Valeur | Signification |\n|-------|---------|\n| omis | Les niveaux standard jusqu'à `high` utilisent le mappage par défaut du fournisseur; Les niveaux étendus `xhigh` et `max` ne sont pas pris en charge |\n| chaîne | Le niveau est pris en charge et cette valeur est envoyée au fournisseur |\n| `null` | Le niveau n'est pas pris en charge et est masqué/ignoré/bloqué |\n\nExemple pour un modèle qui prend uniquement en charge les raisonnements off, high et max:\n\n```json\n{\n  \"id\": \"deepseek-v4-pro\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"minimal\": null,\n    \"low\": null,\n    \"medium\": null,\n    \"high\": \"high\",\n    \"xhigh\": null,\n    \"max\": \"max\"\n  }\n}\n```\n\nExemple de modèle où la pensée ne peut pas être désactivée:\n\n```json\n{\n  \"id\": \"always-thinking-model\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"off\": null\n  }\n}\n```\n\nMigration: les anciennes configurations qui utilisaient `compat.reasoningEffortMap` devraient déplacer ce mappage au niveau du modèle `thinkingLevelMap`. Utilisez `null` pour les niveaux qui ne doivent pas apparaître dans l'interface utilisateur.\n\n## Remplacement du Providers intégré\n\nAcheminez un fournisseur intégré via un proxy sans redéfinir les modèles:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\"\n    }\n  }\n}\n```\n\nTous les modèles Anthropic intégrés restent disponibles. L'authentification OAuth ou API key existante continue de fonctionner.\n\nPour fusionner des modèles personnalisés dans un fournisseur intégré, incluez le tableau `models`:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"api\": \"anthropic-messages\",\n      \"models\": [...]\n    }\n  }\n}\n```\n\nFusionner la sémantique:\n- Les modèles intégrés sont conservés.\n- Les modèles personnalisés sont remplacés par `id` au sein du fournisseur.\n- Si un modèle personnalisé `id` correspond à un modèle intégré `id`, le modèle personnalisé remplace ce modèle intégré.\n- Si un modèle personnalisé `id` est nouveau, il est ajouté aux côtés des modèles intégrés.\n\n## Remplacements par modèle\n\nUtilisez `modelOverrides` pour personnaliser les modèles intégrés et les modèles correspondants enregistrés avec l'extension sans remplacer la liste complète des modèles du fournisseur.\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": {\n      \"modelOverrides\": {\n        \"anthropic/claude-sonnet-4\": {\n          \"name\": \"Claude Sonnet 4 (Bedrock Route)\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"only\": [\"amazon-bedrock\"]\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n`modelOverrides` prend en charge ces champs par modèle: `name`, `reasoning`, `thinkingLevelMap`, `input`, `cost` (partiel), `contextWindow`, `maxTokens`, `samplingParams` (fusionné par clé), `headers`, `compat`.\n\nDirect OpenAI GPT-5.6 Sol, Terra et Luna par défaut sur une fenêtre contextuelle `272000` afin que les demandes restent dans le niveau tarifaire à contexte court d'OpenAI. Pour activer la fenêtre contextuelle de 1,05 million d'OpenAI, augmentez-la pour chaque modèle que vous utilisez:\n\n```json\n{\n  \"providers\": {\n    \"openai\": {\n      \"modelOverrides\": {\n        \"gpt-5.6-sol\": {\n          \"contextWindow\": 1050000\n        }\n      }\n    }\n  }\n}\n```\n\nLe remplacement préserve les métadonnées de tarification intégrées. Les requêtes comportant plus de 272 000 jetons d'entrée au total utilisent les taux de contexte long de GPT-5.6 pour l'intégralité de la requête. Appliquez le même remplacement à `gpt-5.6-terra` ou `gpt-5.6-luna` si nécessaire.\n\nNotes de comportement:\n- `modelOverrides` sont appliqués aux modèles de fournisseur intégrés et aux modèles de fournisseur enregistrés par extension correspondants.\n- Les ID de modèle inconnus sont ignorés.\n- Vous pouvez combiner `baseUrl`/`headers` au niveau du fournisseur avec `modelOverrides`.\n- Le remplacement de `name` modifie uniquement la correspondance du modèle et le texte des détails secondaires; les listes de pied de page et de modèles principaux continuent d'afficher le modèle `id`.\n- Si `models` est également défini pour un fournisseur, les modèles personnalisés sont fusionnés après les remplacements intégrés. Un modèle personnalisé avec le même `id` remplace l'entrée de modèle intégrée remplacée.\n\n## Compatibilité des messages anthropiques\n\nPour les fournisseurs ou les proxys utilisant `api: \"anthropic-messages\"`, utilisez `compat` pour contrôler la compatibilité des requêtes spécifiques à Anthropic.\n\nPar défaut, pi envoie par outil `eager_input_streaming: true`. Si un proxy ou un backend compatible Anthropic rejette ce champ, définissez `supportsEagerToolInputStreaming` sur `false`. Pi omettra `tools[].eager_input_streaming` et enverra à la place l'ancien en-tête bêta `fine-grained-tool-streaming-2025-05-14` pour les requêtes activées par les outils.\n\nCertains modèles anthropiques nécessitent une pensée adaptative (`thinking.type: \"adaptive\"` plus `output_config.effort`) au lieu de la charge utile de réflexion traditionnelle basée sur le budget. Les modèles intégrés le règlent automatiquement. Pour les fournisseurs personnalisés ou les alias qui redirigent vers ces modèles, définissez `forceAdaptiveThinking` sur `true`.\n\nCertains fournisseurs compatibles Anthropic émettent des blocs de réflexion avec des signatures vides et les attendent toujours lors de la relecture. Définissez `allowEmptySignature` sur `true` uniquement pour ces fournisseurs; Le véritable Anthropique rejette les signatures vides de sens.\n\nLes modèles anthropiques intégrés activent `supportsStrictTools` dans leurs métadonnées de modèle. Les modèles personnalisés compatibles Anthropic doivent le définir sur `true` lorsque leur point de terminaison accepte les définitions strictes de l'outil de schéma JSON.\n\n```json\n{\n  \"providers\": {\n    \"anthropic-proxy\": {\n      \"baseUrl\": \"https://proxy.example.com\",\n      \"api\": \"anthropic-messages\",\n      \"apiKey\": \"$ANTHROPIC_PROXY_KEY\",\n      \"compat\": {\n        \"supportsEagerToolInputStreaming\": false,\n        \"supportsLongCacheRetention\": true,\n        \"forceAdaptiveThinking\": true,\n        \"allowEmptySignature\": true\n      },\n      \"models\": [\n        {\n          \"id\": \"claude-opus-4-7\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"]\n        }\n      ]\n    }\n  }\n}\n```\n\n| Champ | Description |\n|-------|-------------|\n| `supportsEagerToolInputStreaming` | Si le fournisseur accepte par outil `eager_input_streaming`. Par défaut: `true`. Définissez sur `false` pour omettre ce champ et utiliser l'en-tête bêta de streaming d'outils à granularité fine hérité sur les requêtes activées par les outils. |\n| `supportsLongCacheRetention` | Indique si le fournisseur accepte la rétention de cache longue Anthropic (`cache_control.ttl: \"1h\"`) lorsque la rétention de cache est `long`. Par défaut: `true`. |\n| `sendSessionAffinityHeaders` | S'il faut envoyer `x-session-affinity` à partir de l'identifiant de session lorsque la mise en cache est activée. Par défaut: détecté automatiquement pour les fournisseurs connus. |\n| `supportsCacheControlOnTools` | Indique si le fournisseur accepte les marqueurs `cache_control` de style anthropique sur les définitions d'outils. Par défaut: `true`. |\n| `forceAdaptiveThinking` | S'il faut envoyer une pensée adaptative (`thinking.type: \"adaptive\"` plus `output_config.effort`) pour ce modèle. Les modèles adaptatifs intégrés règlent cela automatiquement. Par défaut: `false`. |\n| `allowEmptySignature` | S'il faut rejouer les signatures de pensée vides sous la forme `signature: \"\"` au lieu de convertir la pensée en texte. Par défaut: `false`. |\n| `supportsStrictTools` | Indique si le fournisseur accepte les définitions strictes des outils de schéma JSON. Par défaut: `false`; Les modèles anthropiques intégrés le permettent dans les métadonnées générées. |\n\n## Compatibilité OpenAI\n\nPour les fournisseurs offrant une compatibilité partielle avec OpenAI, utilisez le champ `compat`.\n\n- Au niveau du fournisseur `compat` applique les valeurs par défaut à tous les modèles sous ce fournisseur.\n- Au niveau du modèle `compat` remplace les valeurs au niveau du fournisseur pour ce modèle.\n\n```json\n{\n  \"providers\": {\n    \"local-llm\": {\n      \"baseUrl\": \"http://localhost:8080/v1\",\n      \"api\": \"openai-completions\",\n      \"compat\": {\n        \"supportsUsageInStreaming\": false,\n        \"maxTokensField\": \"max_tokens\"\n      },\n      \"models\": [...]\n    }\n  }\n}\n```\n\n| Champ | Description |\n|-------|-------------|\n| `supportsStore` | Le fournisseur prend en charge le champ `store` |\n| `supportsDeveloperRole` | Utilisez le rôle `developer` vs `system` |\n| `supportsReasoningEffort` | Prise en charge du paramètre `reasoning_effort` |\n| `supportsUsageInStreaming` | Prend en charge `stream_options: { include_usage: true }` (par défaut: `true`) |\n| `supportsFinishReason` | Si les réponses diffusées incluent `finish_reason`. Lorsque `false`, pi déduit `stop` ou `toolUse` lorsque le flux se termine. Par défaut: `true`. |\n| `maxTokensField` | Utilisez `max_completion_tokens` ou `max_tokens` |\n| `requiresToolResultName` | Incluez `name` dans les messages de résultat de l'outil |\n| `requiresAssistantAfterToolResult` | Insérer un message d'assistant avant un message utilisateur après les résultats de l'outil |\n| `requiresThinkingAsText` | Convertir les blocs de réflexion en texte brut |\n| `requiresReasoningContentOnAssistantMessages` | Incluez un `reasoning_content` vide sur tous les messages de l'assistant rejoués lorsque le raisonnement est activé |\n| `thinkingFormat` | Utilisez les paramètres de réflexion `reasoning_effort`, `openrouter`, `deepseek`, `together`, `baseten`, `zai`, `qwen`, `chat-template` ou `qwen-chat-template` |\n| `chatTemplateKwargs` | Valeurs `chat_template_kwargs` pour `thinkingFormat: \"chat-template\"`; utilisez `{ \"$var\": \"thinking.enabled\" }` ou `{ \"$var\": \"thinking.effort\" }` pour les valeurs de pensée contrôlées par pi |\n| `chatTemplateArgs` | Valeurs `chat_template_args` pour `thinkingFormat: \"baseten\"`; utilisez `{ \"$var\": \"thinking.enabled\" }` ou `{ \"$var\": \"thinking.effort\" }` pour les valeurs de pensée contrôlées par pi |\n| `cacheControlFormat` | Utilisez des marqueurs `cache_control` de style anthropique sur l'invite système, la dernière définition d'outil et le contenu textuel du dernier utilisateur, assistant ou résultat de l'outil. Actuellement, seul `anthropic` est pris en charge. |\n| `sendSessionAffinityHeaders` | Pour `openai-completions`, envoyez les en-têtes d'affinité de session à partir de l'identifiant de session lorsque la mise en cache est activée. Par défaut: `false`. |\n| `sessionAffinityFormat` | Pour `openai-completions` et `openai-responses`, le format d'en-tête d'affinité de session: `openai` envoie `session_id`/`x-client-request-id` (les complétions également `x-session-affinity`), `openai-nosession` omet l'en-tête `session_id` contenant le trait de soulignement, `openrouter` envoie `x-session-id`. N'affecte pas le paramètre de corps `prompt_cache_key`. Par défaut: détection automatique. |\n| `supportsStrictMode` | Si le fournisseur accepte les définitions strictes des outils de fonction de schéma JSON. Les valeurs par défaut dépendent du API; Les modèles OpenAI intégrés contiennent des métadonnées de capacités explicites. |\n| `supportsOpenAIGrammarTools` | Si les API compatibles OpenAI émettent des outils de grammaire Lark/regex personnalisés. Lorsque `false`, les outils contraints par la grammaire reviennent aux outils de fonction normaux. Par défaut: `false`; le catalogue de modèles intégré le permet pour les modèles GPT-5+ sur OpenAI, OpenAI Codex, Azure OpenAI, GitHub Copilot, opencode et Cloudflare AI Gateway. |\n| `deferredToolsMode` | Utilisez la sérialisation différée des outils spécifique au fournisseur. Actuellement, seul `\"kimi\"` est pris en charge pour le format de complétion de discussion compatible OpenAI de Kimi. |\n| `supportsLongCacheRetention` | Indique si le fournisseur accepte une longue conservation du cache lorsque la rétention du cache est `long`: `prompt_cache_retention: \"24h\"` pour la mise en cache des invites OpenAI, ou `cache_control.ttl: \"1h\"` lorsque `cacheControlFormat` est `anthropic`. Par défaut: `true`. |\n| `openRouterRouting` | Préférences de routage du fournisseur OpenRouter. Cet objet est envoyé tel quel dans le champ `provider` du [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). |\n| `vercelGatewayRouting` | Configuration de routage Vercel AI Gateway pour la sélection du fournisseur (`only`, `order`) |\n\n`openrouter` utilise `reasoning: { effort }`. `together` utilise `reasoning: { enabled }` et également `reasoning_effort` lorsque `supportsReasoningEffort` est activé. `qwen` utilise le niveau supérieur `enable_thinking`. Utilisez `qwen-chat-template` pour les serveurs locaux compatibles Qwen qui nécessitent `chat_template_kwargs.enable_thinking` et `preserve_thinking`. Utilisez `chat-template` pour les modèles de discussion vLLM/Hugging Face qui nécessitent un `chat_template_kwargs` configurable, tel que `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }` pour les modèles DeepSeek V3.x. Utilisez `thinkingFormat: \"baseten\"` avec `chatTemplateArgs` pour les fournisseurs qui exposent des contrôles à bascule via `chat_template_args` et prennent éventuellement en charge `reasoning_effort` de niveau supérieur.\n\n`cacheControlFormat: \"anthropic\"` est destiné aux fournisseurs compatibles OpenAI qui exposent la mise en cache des invites de style Anthropic via des marqueurs `cache_control` sur le contenu du texte et les définitions d'outils.\n\nExemple:\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": {\n      \"baseUrl\": \"https://openrouter.ai/api/v1\",\n      \"apiKey\": \"$OPENROUTER_API_KEY\",\n      \"api\": \"openai-completions\",\n      \"models\": [\n        {\n          \"id\": \"openrouter/anthropic/claude-3.5-sonnet\",\n          \"name\": \"OpenRouter Claude 3.5 Sonnet\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"allow_fallbacks\": true,\n              \"require_parameters\": false,\n              \"data_collection\": \"deny\",\n              \"zdr\": true,\n              \"enforce_distillable_text\": false,\n              \"order\": [\"anthropic\", \"amazon-bedrock\", \"google-vertex\"],\n              \"only\": [\"anthropic\", \"amazon-bedrock\"],\n              \"ignore\": [\"gmicloud\", \"friendli\"],\n              \"quantizations\": [\"fp16\", \"bf16\"],\n              \"sort\": {\n                \"by\": \"price\",\n                \"partition\": \"model\"\n              },\n              \"max_price\": {\n                \"prompt\": 10,\n                \"completion\": 20\n              },\n              \"preferred_min_throughput\": {\n                \"p50\": 100,\n                \"p90\": 50\n              },\n              \"preferred_max_latency\": {\n                \"p50\": 1,\n                \"p90\": 3,\n                \"p99\": 5\n              }\n            }\n          }\n        }\n      ]\n    }\n  }\n}\n```\n\nExemple de Vercel AI Gateway:\n\n```json\n{\n  \"providers\": {\n    \"vercel-ai-gateway\": {\n      \"baseUrl\": \"https://ai-gateway.vercel.sh/v1\",\n      \"apiKey\": \"$AI_GATEWAY_API_KEY\",\n      \"api\": \"openai-completions\",\n      \"models\": [\n        {\n          \"id\": \"moonshotai/kimi-k2.5\",\n          \"name\": \"Kimi K2.5 (Fireworks via Vercel)\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"],\n          \"cost\": { \"input\": 0.6, \"output\": 3, \"cacheRead\": 0, \"cacheWrite\": 0 },\n          \"contextWindow\": 262144,\n          \"maxTokens\": 262144,\n          \"compat\": {\n            \"vercelGatewayRouting\": {\n              \"only\": [\"fireworks\", \"novita\"],\n              \"order\": [\"fireworks\", \"novita\"]\n            }\n          }\n        }\n      ]\n    }\n  }\n}\n```","sourceFile":"models.md"},"packages":{"title":"Pi Packages","markdown":"> pi peut vous aider à créer des packages pi. Demandez-lui de regrouper vos extensions, compétences, prompt templates ou thèmes.\n\n\nLes packages Pi regroupent des extensions, des compétences, prompt templates et des thèmes afin que vous puissiez les partager via npm ou git. Un package peut déclarer des ressources en `package.json` sous la clé `pi`, ou utiliser des répertoires conventionnels.\n\n## Table des matières\n\n- [Install and Manage](#install-and-manage)\n- [Package Sources](#package-sources)\n- [Creating a Pi Package](#creating-a-pi-package)\n- [Package Structure](#package-structure)\n- [Dependencies](#dependencies)\n- [Package Filtering](#package-filtering)\n- [Enable and Disable Resources](#enable-and-disable-resources)\n- [Scope and Deduplication](#scope-and-deduplication)\n\n## Installer et gérer\n\n> **Sécurité:** Les packages Pi s'exécutent avec un accès complet au système. Extensions exécute du code arbitraire et les compétences peuvent demander au modèle d'effectuer n'importe quelle action, y compris l'exécution d'exécutables. Vérifiez le code source avant d'installer des packages tiers.\n\n```bash\npi install npm:@foo/bar@1.0.0\npi install git:github.com/user/repo@v1\npi install https://github.com/user/repo  # raw URLs work too\npi install /absolute/path/to/package\npi install ./relative/path/to/package\n\npi remove npm:@foo/bar\npi list                     # show installed packages from settings\npi update                   # update pi only\npi update --all             # update pi, update packages, and reconcile pinned git refs\npi update --extensions      # update packages and reconcile pinned git refs only\npi update --models          # refresh model catalogs only\npi update --self            # update pi only\npi update --self --force    # reinstall pi even if current\npi update npm:@foo/bar      # update one package\npi update --extension npm:@foo/bar\n```\n\nCes commandes gèrent les packages pi et `pi update` peuvent mettre à jour l'installation de pi CLI. Pour désinstaller pi lui-même, voir [Quickstart](quickstart.md#uninstall).\n\nPar défaut, `install` et `remove` écrivent dans les paramètres utilisateur (`~/.pi/agent/settings.json`). Utilisez plutôt `-l` pour écrire dans les paramètres du projet (`.pi/settings.json`). Les paramètres du projet peuvent être partagés avec votre équipe et pi installe automatiquement tous les packages manquants au démarrage une fois le projet approuvé.\n\nPour essayer un package sans l'installer, utilisez `--extension` ou `-e`. Ceci s'installe dans un répertoire temporaire pour l'exécution en cours uniquement:\n\n```bash\npi -e npm:@foo/bar\npi -e git:github.com/user/repo\n```\n\n## Sources des packages\n\nPi accepte trois types de sources dans les paramètres et `pi install`.\n\n### npm\n\n```\nnpm:@scope/pkg@1.2.3\nnpm:pkg\n```\n\n- Les spécifications versionnées sont épinglées et ignorées par les mises à jour des packages (`pi update --extensions`, `pi update --all`).\n- Les installations utilisateur passent sous `~/.pi/agent/npm/`.\n- Les installations du projet passent sous `.pi/npm/`.\n- Définissez `npmCommand` dans `settings.json` pour épingler npm la recherche de package et les opérations d'installation sur une commande wrapper spécifique telle que `mise` ou `asdf`.\n\nExemple:\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n### git\n\n```\ngit:github.com/user/repo@v1\ngit:git@github.com:user/repo@v1\nhttps://github.com/user/repo@v1\nssh://git@github.com/user/repo@v1\n```\n\n- Sans le préfixe `git:`, seules les URL de protocole sont acceptées (`https://`, `http://`, `ssh://`, `git://`).\n- Avec le préfixe `git:`, les formats abrégés sont acceptés, notamment `github.com/user/repo` et `git@github.com:user/repo`.\n- Les URL HTTPS et SSH sont toutes deux prises en charge.\n- Les URL SSH utilisent automatiquement vos clés SSH configurées (respecte `~/.ssh/config`).\n- Pour les exécutions non interactives (par exemple CI), vous pouvez définir `GIT_TERMINAL_PROMPT=0` pour désactiver les invites d'identification et définir `GIT_SSH_COMMAND` (par exemple `ssh -o BatchMode=yes -o ConnectTimeout=5`) pour échouer rapidement.\n- Les références sont des balises épinglées ou des commits. `pi update --extensions` et `pi update --all` ne les déplacent pas vers des références plus récentes, mais ils rapprochent un clone existant avec la référence configurée.\n- Utilisez `pi install git:host/user/repo@new-ref` pour mettre à jour les paramètres et déplacer un package existant vers une nouvelle référence épinglée.\n- Cloné à `~/.pi/agent/git/<host>/<path>` (global) ou `.pi/git/<host>/<path>` (projet).\n- Lorsque la réconciliation modifie la caisse, pi réinitialise et nettoie le clone, puis exécute `npm install` si `package.json` existe.\n\n**SSH exemples:**\n```bash\n# git@host:path shorthand (requires git: prefix)\npi install git:git@github.com:user/repo\n\n# ssh:// protocol format\npi install ssh://git@github.com/user/repo\n\n# With version ref\npi install git:git@github.com:user/repo@v1.0.0\n```\n\n### Chemins locaux\n\n```\n/absolute/path/to/package\n./relative/path/to/package\n```\n\nLes chemins locaux pointent vers des fichiers ou des répertoires sur le disque et sont ajoutés aux paramètres sans copie. Les chemins relatifs sont résolus par rapport au fichier de paramètres dans lequel ils apparaissent. Si le chemin est un fichier, il se charge comme une seule extension. S'il s'agit d'un répertoire, pi charge les ressources en utilisant les règles du package.\n\n## Création d'un package Pi\n\nAjoutez un manifeste `pi` à `package.json` ou utilisez des répertoires conventionnels. Incluez le mot-clé `pi-package` pour la découvrabilité.\n\n```json\n{\n  \"name\": \"my-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./extensions\"],\n    \"skills\": [\"./skills\"],\n    \"prompts\": [\"./prompts\"],\n    \"themes\": [\"./themes\"]\n  }\n}\n```\n\nLes chemins sont relatifs à la racine du package. Les tableaux prennent en charge les modèles globaux et `!exclusions`.\n\n### Métadonnées de la galerie\n\nLe [package gallery](https://pi.dev/packages) affiche les packages étiquetés avec `pi-package`. Ajoutez les champs `video` ou `image` pour afficher un aperçu:\n\n```json\n{\n  \"name\": \"my-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./extensions\"],\n    \"video\": \"https://example.com/demo.mp4\",\n    \"image\": \"https://example.com/screenshot.png\"\n  }\n}\n```\n\n- **vidéo**: MP4 uniquement. Sur le bureau, lecture automatique au survol. Cliquer ouvre un lecteur en plein écran.\n- **image**: PNG, JPEG, GIF ou WebP. Affiché sous forme d’aperçu statique.\n\nSi les deux sont définis, la vidéo est prioritaire.\n\n## Structure du paquet\n\n### Répertoires des congrès\n\nSi aucun manifeste `pi` n'est présent, pi découvre automatiquement les ressources de ces répertoires:\n\n- `extensions/` charge les fichiers `.ts` et `.js`\n- `skills/` trouve récursivement `SKILL.md` dossiers et charge `.md` fichiers de niveau supérieur en tant que compétences\n- `prompts/` charge `.md` fichiers\n- `themes/` charge `.json` fichiers\n\n## Dépendances\n\nLes dépendances d'exécution tierces appartiennent à `dependencies` dans `package.json`. Les dépendances qui n'enregistrent pas d'extensions, de compétences, de prompt templates ou de thèmes appartiennent également à `dependencies`. Lorsque pi installe un package à partir de npm ou de git, il exécute `npm install`, donc ces dépendances sont installées automatiquement.\n\nPi regroupe les packages de base pour les extensions et les compétences. Si vous importez l'un d'entre eux, répertoriez-les dans `peerDependencies` avec une plage `\"*\"` et ne les regroupez pas: `@earendil-works/pi-ai`, `@earendil-works/pi-agent-core`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`, `typebox`.\n\nLes autres packages pi doivent être regroupés dans votre archive tar. Ajoutez-les à `dependencies` et `bundledDependencies`, puis référencez leurs ressources via les chemins `node_modules/`. Pi charge les packages avec des racines de modules distinctes, afin que les installations distinctes n'entrent pas en collision ou ne partagent pas de modules.\n\nExemple:\n\n```json\n{\n  \"dependencies\": {\n    \"shitty-extensions\": \"^1.0.1\"\n  },\n  \"bundledDependencies\": [\"shitty-extensions\"],\n  \"pi\": {\n    \"extensions\": [\"extensions\", \"node_modules/shitty-extensions/extensions\"],\n    \"skills\": [\"skills\", \"node_modules/shitty-extensions/skills\"]\n  }\n}\n```\n\n## Filtrage des paquets\n\nFiltrez ce qu'un package charge à l'aide du formulaire objet dans les paramètres:\n\n```json\n{\n  \"packages\": [\n    \"npm:simple-pkg\",\n    {\n      \"source\": \"npm:my-package\",\n      \"extensions\": [\"extensions/*.ts\", \"!extensions/legacy.ts\"],\n      \"skills\": [],\n      \"prompts\": [\"prompts/review.md\"],\n      \"themes\": [\"+themes/legacy.json\"]\n    }\n  ]\n}\n```\n\n`+path` et `-path` sont des chemins exacts par rapport à la racine du package.\n\n- Omettez une clé pour charger tout ce type.\n- Utilisez `[]` pour ne charger aucun de ce type.\n- `!pattern` exclut les correspondances.\n- `+path` force-inclut un chemin exact.\n- `-path` force-exclut un chemin exact.\n- Couche de filtres au-dessus du manifeste. Ils limitent ce qui est déjà autorisé.\n\n## Activer et désactiver les ressources\n\nUtilisez `pi config` pour activer ou désactiver les extensions, les compétences, prompt templates et les thèmes des packages installés et des répertoires locaux. `pi config` démarre dans les paramètres globaux (`~/.pi/agent/settings.json`); appuyez sur Tab pour basculer entre les modes global et local du projet. Utilisez `pi config -l` pour démarrer les remplacements de projet (`.pi/settings.json`) avec les ressources globales héritées grisées.\n\n## Portée et déduplication\n\nLes packages peuvent apparaître dans les paramètres globaux et du projet. Si le même package apparaît dans les deux, l'entrée de projet l'emporte sauf si l'entrée de projet a `autoload: false`, auquel cas elle est appliquée comme un delta sur l'entrée globale. L'identité est déterminée par:\n\n- npm: nom du package\n- git: URL du référentiel sans référence\n- local: chemin absolu résolu","sourceFile":"packages.md"},"prompt-templates":{"title":"Modèles d'invite","markdown":"> pi peut créer prompt templates. Demandez-lui d'en créer un pour votre flux de travail.\n\n\nLes modèles d'invites sont des extraits de Markdown qui se transforment en invites complètes. Tapez `/name` dans l'éditeur pour appeler un modèle, où `name` est le nom du fichier sans `.md`.\n\n## Emplacements\n\nPi charge prompt templates depuis:\n\n- Mondial: `~/.pi/agent/prompts/*.md`\n- Projet: `.pi/prompts/*.md` (uniquement une fois le projet approuvé)\n- Forfaits: `prompts/` répertoires ou `pi.prompts` entrées dans `package.json`\n- Paramètres: `prompts` tableau avec des fichiers ou des répertoires\n- CLI: `--prompt-template <path>` (répétable)\n\nDésactivez la découverte avec `--no-prompt-templates`.\n\n## Format\n\n```markdown\n---\ndescription: Review staged git changes\n---\nReview the staged changes (`git diff --cached`). Focus on:\n- Bugs and logic errors\n- Security issues\n- Error handling gaps\n```\n\n- Le nom du fichier devient le nom de la commande. `review.md` devient `/review`.\n- `description` est facultatif. En cas d'absence, la première ligne non vide est utilisée.\n- `argument-hint` est facultatif. Lorsqu'il est défini, l'indice s'affiche avant la description dans la liste déroulante de saisie semi-automatique.\n\n### Conseils d'argumentation\n\nUtilisez `argument-hint` en frontmatter pour afficher les arguments attendus en saisie semi-automatique. Utilisez `<angle brackets>` pour les arguments obligatoires et `[square brackets]` pour les arguments facultatifs:\n\n```markdown\n---\ndescription: Review PRs from URLs with structured issue and code analysis\nargument-hint: \"<PR-URL>\"\n---\n```\n\nCela s'affiche dans la liste déroulante de saisie semi-automatique comme:\n\n```\n→ pr   <PR-URL>       — Review PRs from URLs with structured issue and code analysis\n  is   <issue>        — Analyze GitHub issues (bugs or feature requests)\n  wr   [instructions] — Finish the current task end-to-end\n  cl   — Audit changelog entries before release\n```\n\n## Usage\n\nTapez `/` suivi du nom du modèle dans l'éditeur. La saisie semi-automatique affiche les modèles disponibles avec des descriptions.\n\n```\n/review                           # Expands review.md\n/component Button                 # Expands with argument\n/component Button \"click handler\" # Multiple arguments\n```\n\n## Arguments\n\nLes modèles prennent en charge les arguments de position, les valeurs par défaut et le découpage simple:\n\n- `$1`, `$2`,... arguments de position\n- `$@` ou `$ARGUMENTS` pour tous les arguments joints\n- `${1:-default}` utilise arg 1 lorsqu'il est présent/non vide, sinon `default`\n- `${@:-default}` ou `${ARGUMENTS:-default}` utilise tous les arguments lorsqu'ils sont présents/non vides, sinon `default`\n- `${@:N}` pour les arguments de la Nième position (indexé 1)\n- `${@:N:L}` pour `L` arguments commençant à N\n\nExemple:\n\n```markdown\n---\ndescription: Create a component\n---\nCreate a React component named $1 with features: $@\n```\n\nLes valeurs par défaut sont utiles pour les arguments facultatifs:\n\n```markdown\nSummarize the current state in ${1:-7} bullet points.\n```\n\nUtilisation: `/component Button \"onClick handler\" \"disabled support\"`\n\n## Règles de chargement\n\n- La découverte de modèles dans `prompts/` n'est pas récursive.\n- Si vous souhaitez des modèles dans des sous-répertoires, ajoutez-les explicitement via les paramètres `prompts` ou un manifeste de package.","sourceFile":"prompt-templates.md"},"providers":{"title":"Providers","markdown":"Pi prend en charge les fournisseurs par abonnement via les fournisseurs OAuth et API key via des variables d'environnement ou un fichier d'authentification. Les catalogues intégrés sont livrés avec pi; les fournisseurs configurés peuvent actualiser les catalogues les plus récents et les mettre en cache dans `~/.pi/agent/models-store.json` pour une utilisation hors ligne.\n\n## Table des matières\n\n- [Subscriptions](#subscriptions)\n- [API Keys](#api-keys)\n- [Auth File](#auth-file)\n- [Cloud Providers](#cloud-providers)\n- [llama.cpp](#llamacpp)\n- [Custom Providers](#custom-providers)\n- [Resolution Order](#resolution-order)\n\n## Abonnements\n\nUtilisez `/login` en mode interactif, puis sélectionnez un fournisseur:\n\n- ChatGPT Plus/Pro (Codex)\n- Claude Pro/Max\n- GitHub Copilote\n- xAI (abonnement Grok/X)\n- OpenRouter (OAuth-minted API key facturé à partir des crédits OpenRouter)\n- Rayon\n\nUtilisez `/logout` pour effacer les informations d'identification. Les jetons sont stockés dans `~/.pi/agent/auth.json` et s'actualisent automatiquement une fois expirés. OpenRouter crée à la place un API key contrôlé par l'utilisateur qui n'expire pas automatiquement.\n\n### Codex OpenAI\n\n- Nécessite un abonnement ChatGPT Plus ou Pro\n- Officiellement approuvé par OpenAI: [Codex for OSS](https://developers.openai.com/community/codex-for-oss)\n\n### Claude Pro/Max\n\nL'authentification d'abonnement Anthropic est active pour les comptes Claude Pro/Max. L'utilisation du harnais tiers provient de [extra usage](https://claude.ai/settings/usage) et est facturée par jeton, et non par rapport aux limites du plan Claude.\n\n### GitHub Copilote\n\n- Appuyez sur Entrée pour github.com ou entrez votre domaine Enterprise Server GitHub\n- Si vous obtenez « modèle non pris en charge », activez-le dans VS Code: Copilot Chat → sélecteur de modèle → sélectionnez le modèle → « Activer »\n\n### xAI (abonnement Grok/X)\n\n- Exécutez `/login xai`, puis sélectionnez **Utiliser un abonnement**\n- `XAI_API_KEY` reste disponible via **Utilisez un API key**\n\n### OuvrirRouter\n\n- Exécutez `/login openrouter`, puis sélectionnez **Connectez-vous avec OpenRouter** pour ouvrir le flux d'autorisation OpenRouter PKCE.\n- L'autorisation crée un OpenRouter contrôlé par l'utilisateur API key facturé à partir de vos crédits OpenRouter\n- Sur les machines distantes/sans tête (par exemple au-dessus de SSH), le navigateur ne peut pas atteindre le rappel de bouclage; collez plutôt l'URL de redirection finale (ou le code d'autorisation) dans l'invite de connexion\n- `OPENROUTER_API_KEY` reste disponible via **Utilisez un API key**\n\n### Rayon\n\nRadius est une passerelle `pi-messages` dynamique. `/login radius` stocke OAuth jetons dans `auth.json`; le catalogue de la passerelle est actualisé indépendamment et mis en cache dans `models-store.json`. Les passerelles Custom Radius peuvent être déclarées en `models.json` avec `\"oauth\": \"radius\"` et une passerelle `baseUrl`.\n\n## API Touches\n\n### Variables d'environnement ou fichier d'authentification\n\nUtilisez `/login` en mode interactif et sélectionnez un fournisseur pour stocker un API key dans `auth.json`, ou définissez les informations d'identification via une variable d'environnement:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\n| Fournisseur | Variable d'environnement | Touche `auth.json` |\n|----------|----------------------|------------------|\n| Anthropique | `ANTHROPIC_API_KEY` | `anthropic` |\n| Fourmi Ling | `ANT_LING_API_KEY` | `ant-ling` |\n| Réponses Azure OpenAI | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |\n| OpenAI | `OPENAI_API_KEY` | `openai` |\n| Recherche profonde | `DEEPSEEK_API_KEY` | `deepseek` |\n| NIM NVIDIA | `NVIDIA_API_KEY` | `nvidia` |\n| Google Gémeaux | `GEMINI_API_KEY` | `google` |\n| Socle amazonien | `AWS_BEARER_TOKEN_BEDROCK` | `amazon-bedrock` |\n| Mistral | `MISTRAL_API_KEY` | `mistral` |\n| Groq | `GROQ_API_KEY` | `groq` |\n| Cérébraux | `CEREBRAS_API_KEY` | `cerebras` |\n| Passerelle IA Cloudflare | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_GATEWAY_ID`) | `cloudflare-ai-gateway` |\n| IA des travailleurs Cloudflare | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`) | `cloudflare-workers-ai` |\n| xAI | `XAI_API_KEY` | `xai` |\n| OuvrirRouter | `OPENROUTER_API_KEY` | `openrouter` |\n| Passerelle IA Vercel | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |\n| Plan de codage ZAI (mondial) | `ZAI_API_KEY` | `zai` |\n| Plan de codage ZAI (Chine) | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` |\n| OpenCode Zen | `OPENCODE_API_KEY` | `opencode` |\n| OpenCode Aller | `OPENCODE_API_KEY` | `opencode-go` |\n| Rayon | `RADIUS_API_KEY` | `radius` |\n| Hugging Face | `HF_TOKEN` | `huggingface` |\n| Feux d'artifice | `FIREWORKS_API_KEY` | `fireworks` |\n| Ensemble IA | `TOGETHER_API_KEY` | `together` |\n| Baseten | `BASETEN_API_KEY` | `baseten` |\n| Kimi pour le codage | `KIMI_API_KEY` | `kimi-coding` |\n| MiniMax | `MINIMAX_API_KEY` | `minimax` |\n| MiniMax (Chine) | `MINIMAX_CN_API_KEY` | `minimax-cn` |\n| Plan de jetons Qwen (catalogue existant) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan` |\n| Plan de jetons Qwen (individuel) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan-individual` |\n| Plan de jetons Qwen (Chine) | `QWEN_TOKEN_PLAN_CN_API_KEY` | `qwen-token-plan-cn` |\n| Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` |\n| Plan de jetons Xiaomi MiMo (Chine) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | `xiaomi-token-plan-cn` |\n| Plan de jetons Xiaomi MiMo (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | `xiaomi-token-plan-ams` |\n| Plan de jetons Xiaomi MiMo (Singapour) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | `xiaomi-token-plan-sgp` |\n\nRéférence pour les variables d'environnement et les clés `auth.json`: [`const envMap`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts) dans [`packages/ai/src/env-api-keys.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts).\n\n#### Fichier d'authentification\n\nStockez les informations d'identification dans `~/.pi/agent/auth.json`:\n\n```json\n{\n  \"anthropic\": { \"type\": \"api_key\", \"key\": \"sk-ant-...\" },\n  \"ant-ling\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"openai\": { \"type\": \"api_key\", \"key\": \"sk-...\" },\n  \"deepseek\": { \"type\": \"api_key\", \"key\": \"sk-...\" },\n  \"nvidia\": { \"type\": \"api_key\", \"key\": \"nvapi-...\" },\n  \"google\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"opencode\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"opencode-go\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"together\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"qwen-token-plan\":  { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"qwen-token-plan-individual\": { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"qwen-token-plan-cn\": { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"xiaomi\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-cn\":  { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-ams\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-sgp\": { \"type\": \"api_key\", \"key\": \"...\" }\n}\n```\n\n`qwen-token-plan-individual` utilise le même point de terminaison international et `QWEN_TOKEN_PLAN_API_KEY` que\n`qwen-token-plan`, mais limite le sélecteur aux modèles documentés pour les abonnements individuels. L'existant\nLe fournisseur conserve son catalogue plus large pour des raisons de compatibilité ascendante. Lorsque vous utilisez `auth.json`, stockez le\ninformations d'identification auprès du fournisseur que vous sélectionnez; une variable d'environnement est partagée par les deux fournisseurs internationaux.\n\nLe fichier est créé avec les autorisations `0600` (lecture/écriture utilisateur uniquement). Les informations d'identification du fichier d'authentification ont la priorité sur les variables d'environnement.\n\nAPI key les informations d'identification peuvent également inclure des valeurs d'environnement définies par le fournisseur. Ces valeurs sont utilisées avant les variables d'environnement de processus lors de la résolution de la clé d'identification, des en-têtes de fournisseur/modèle et de la configuration du fournisseur, telles que les ID de compte Cloudflare, les paramètres Azure OpenAI, le projet/emplacement Vertex, les paramètres Bedrock, `PI_CACHE_RETENTION` et `HTTP_PROXY`/`HTTPS_PROXY`.\n\n```json\n{\n  \"cloudflare-ai-gateway\": {\n    \"type\": \"api_key\",\n    \"key\": \"$CLOUDFLARE_API_KEY\",\n    \"env\": {\n      \"CLOUDFLARE_API_KEY\": \"...\",\n      \"CLOUDFLARE_ACCOUNT_ID\": \"account-id\",\n      \"CLOUDFLARE_GATEWAY_ID\": \"gateway-id\"\n    }\n  }\n}\n```\n\nUtilisez-le lorsque pi doit utiliser des paramètres de fournisseur différents de ceux de l'environnement shell du projet.\n\n### Résolution clé\n\nLe champ `key` prend en charge l'exécution de commandes, l'interpolation d'environnement et les littéraux:\n\n- **Commande Shell:** `\"!command\"` au début exécute la valeur entière en tant que commande et utilise stdout (mis en cache pour la durée de vie du processus)\n  ```json\n  { \"type\": \"api_key\", \"key\": \"!security find-generic-password -ws 'anthropic'\" }\n  { \"type\": \"api_key\", \"key\": \"!op read 'op://vault/item/credential'\" }\n  ```\n- **Interpolation d'environnement:** `\"$ENV_VAR\"` ou `\"${ENV_VAR}\"` utilise la valeur de la variable nommée. L'interpolation fonctionne à l'intérieur de littéraux plus grands.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$MY_ANTHROPIC_KEY\" }\n  { \"type\": \"api_key\", \"key\": \"${KEY_PREFIX}_${KEY_SUFFIX}\" }\n  ```\n  `$FOO_BAR` est la variable `FOO_BAR`; utilisez `${FOO}_BAR` lorsque `BAR` est un texte littéral. Les variables d'environnement manquantes rendent la valeur non résolue.\n- **Échappe:** `\"$\"` émet un `\"$\"` littéral; `\"$!\"` émet un `\"!\"` littéral sans déclencher l'exécution de la commande.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$$literal-dollar-prefix\" }\n  { \"type\": \"api_key\", \"key\": \"$!literal-bang-prefix\" }\n  ```\n- **Valeur littérale:** Utilisé directement. Les chaînes majuscules simples telles que `MY_API_KEY` sont des littéraux; utilisez `$MY_API_KEY` pour les variables d'environnement.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"sk-ant-...\" }\n  { \"type\": \"api_key\", \"key\": \"public\" }\n  ```\n\nLes informations d'identification OAuth sont également stockées ici après `/login` et gérées automatiquement.\n\n## Nuage Providers\n\n### Azure OpenAI\n\n```bash\nexport AZURE_OPENAI_API_KEY=...\nexport AZURE_OPENAI_BASE_URL=https://your-resource.ai.azure.com\n# also supported: https://your-resource.cognitiveservices.azure.com\n# also supported: https://your-resource.openai.azure.com\n# root endpoints are auto-normalized to /openai/v1\n# or use resource name instead of base URL\nexport AZURE_OPENAI_RESOURCE_NAME=your-resource\n\n# Optional\nexport AZURE_OPENAI_API_VERSION=2024-02-01\nexport AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-4=my-gpt4,gpt-4o=my-gpt4o\n```\n\n### Socle amazonien\n\nUtilisez `/login amazon-bedrock` pour stocker un Bedrock API key ou configurez l'une des sources d'informations d'identification AWS ambiantes ci-dessous:\n\n```bash\n# Option 1: AWS Profile\nexport AWS_PROFILE=your-profile\n\n# Option 2: IAM Keys\nexport AWS_ACCESS_KEY_ID=AKIA...\nexport AWS_SECRET_ACCESS_KEY=...\n\n# Option 3: Bearer Token\nexport AWS_BEARER_TOKEN_BEDROCK=...\n\n# Optional region (defaults to us-east-1)\nexport AWS_REGION=us-west-2\n```\n\nPrend également en charge les rôles de tâche ECS (`AWS_CONTAINER_CREDENTIALS_*`) et IRSA (`AWS_WEB_IDENTITY_TOKEN_FILE`).\n\n```bash\npi --provider amazon-bedrock --model us.anthropic.claude-sonnet-4-20250514-v1:0\n```\n\nLa mise en cache des invites est activée automatiquement pour les modèles Claude dont l'ID contient un nom de modèle reconnaissable (modèles de base et profils d'inférence définis par le système). Pour les profils d'inférence d'application (dont les ARN ne contiennent pas le nom du modèle), définissez `AWS_BEDROCK_FORCE_CACHE=1` pour activer les points de cache:\n\n```bash\nexport AWS_BEDROCK_FORCE_CACHE=1\npi --provider amazon-bedrock --model arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123\n```\n\nSi vous vous connectez à un proxy Bedrock API, les variables d'environnement suivantes peuvent être utilisées:\n\n```bash\n# Set the URL for the Bedrock proxy (standard AWS SDK env var)\nexport AWS_ENDPOINT_URL_BEDROCK_RUNTIME=https://my.corp.proxy/bedrock\n\n# Set if your proxy does not require authentication\nexport AWS_BEDROCK_SKIP_AUTH=1\n\n# Set if your proxy only supports HTTP/1.1\nexport AWS_BEDROCK_FORCE_HTTP1=1\n```\n\n### Passerelle IA Cloudflare\n\n`CLOUDFLARE_API_KEY` peut être réglé via `/login`. L'ID de compte et le slug de passerelle peuvent être définis en tant que variables d'environnement ou dans l'objet `env` des informations d'identification API key dans `auth.json`.\n\n```bash\nexport CLOUDFLARE_API_KEY=...           # or use /login\nexport CLOUDFLARE_ACCOUNT_ID=...\nexport CLOUDFLARE_GATEWAY_ID=...        # create at dash.cloudflare.com → AI → AI Gateway\npi --provider cloudflare-ai-gateway --model \"claude-sonnet-4-5\"\n```\n\nRoutes vers OpenAI, Anthropic et Workers AI via Cloudflare AI Gateway. Workers AI utilise les identifiants de modèle unifiés API (`/compat`) et préfixés (`workers-ai/@cf/...`). OpenAI utilise la route passthrough OpenAI (`/openai`) avec des ID de modèle OpenAI natifs tels que `gpt-5.1`. Anthropic utilise la route passthrough Anthropic (`/anthropic`) avec des ID de modèle Anthropic natifs tels que `claude-sonnet-4-5`.\n\nL'authentification AI Gateway utilise `CLOUDFLARE_API_KEY` comme `cf-aig-authorization`. L'authentification en amont peut être l'une des suivantes:\n\n| Mode | Demander l'authentification | Authentification en amont |\n|------|--------------|---------------|\n| IA des travailleurs | Jeton Cloudflare uniquement | Natif de Cloudflare |\n| Facturation unifiée | Jeton Cloudflare uniquement | Cloudflare gère l'authentification en amont et déduit les crédits |\n| BYOK stocké | Jeton Cloudflare uniquement | Cloudflare injecte les clés du fournisseur stockées dans le tableau de bord AI Gateway |\n| BYOK en ligne | Jeton Cloudflare plus en-tête `Authorization` en amont | La requête fournit la clé du fournisseur en amont |\n\nPour une utilisation normale de Pi, préférez la facturation unifiée ou le BYOK stocké. Le BYOK en ligne nécessite la configuration d'un en-tête `Authorization` en amont supplémentaire pour le fournisseur Cloudflare AI Gateway, par exemple via un remplacement de fournisseur/modèle `models.json`.\n\n### IA des travailleurs Cloudflare\n\n`CLOUDFLARE_API_KEY` peut être réglé via `/login`. `CLOUDFLARE_ACCOUNT_ID` peut être défini comme variable d'environnement ou dans l'objet `env` de l'identifiant API key dans `auth.json`.\n\n```bash\nexport CLOUDFLARE_API_KEY=...           # or use /login\nexport CLOUDFLARE_ACCOUNT_ID=...\npi --provider cloudflare-workers-ai --model \"@cf/moonshotai/kimi-k2.6\"\n```\n\nPi définit automatiquement `x-session-affinity` pour [prefix caching](https://developers.cloudflare.com/workers-ai/features/prompt-caching/) remises.\n\n### Google Vertex AI\n\nUtilise les informations d'identification par défaut de l'application:\n\n```bash\ngcloud auth application-default login\nexport GOOGLE_CLOUD_PROJECT=your-project\nexport GOOGLE_CLOUD_LOCATION=us-central1\n```\n\nOu définissez `GOOGLE_APPLICATION_CREDENTIALS` sur un fichier de clé de compte de service.\n\n## llama.cpp\n\nPi prend en charge le serveur de routeur llama.cpp. Configurez-le avec `/login llama.cpp`, gérez les modèles chargés avec `/llama` et sélectionnez un modèle chargé avec `/model`.\n\nVoir [llama.cpp](llama-cpp.md) pour la configuration du serveur, la disposition du répertoire modèle, les variables d'environnement et l'utilisation des commandes.\n\n## Personnalisé Providers\n\n**Via models.json:** Ajoutez Ollama, LM Studio, vLLM ou tout fournisseur parlant un API pris en charge (achèvements OpenAI, réponses OpenAI, messages anthropiques, IA générative de Google). Voir [models.md](models.md).\n\n**Via des extensions:** Pour les fournisseurs qui ont besoin d'implémentations API personnalisées ou de flux OAuth personnalisés, créez une extension. Voir [custom-provider.md](custom-provider.md) et [examples/extensions/custom-provider-gitlab-duo](../examples/extensions/custom-provider-gitlab-duo/).\n\n## Ordonnance de résolution\n\nLors de la résolution des informations d'identification d'un fournisseur:\n\n1. CLI `--api-key` drapeau\n2. Entrée `auth.json` (jeton API key ou OAuth)\n3. Variable d'environnement\n4. Clés de fournisseur personnalisées à partir de `models.json`","sourceFile":"providers.md"},"quickstart":{"title":"Démarrage rapide","markdown":"Cette page vous amène de l'installation à une première session pi utile.\n\n## Installer\n\nPi est distribué sous forme de package npm:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` désactive les scripts de cycle de vie des dépendances pendant l'installation. Pi ne nécessite pas de scripts d'installation pour les installations npm normales.\n\n### Désinstaller\n\nUtilisez le gestionnaire de packages qui a installé pi. Le programme d'installation de curl utilise npm globalement, donc les installations curl et npm sont supprimées avec npm:\n\n```bash\n# curl installer or npm install -g\nnpm uninstall -g @earendil-works/pi-coding-agent\n\n# pnpm\npnpm remove -g @earendil-works/pi-coding-agent\n\n# Yarn\nyarn global remove @earendil-works/pi-coding-agent\n\n# Bun\nbun uninstall -g @earendil-works/pi-coding-agent\n```\n\nLa désinstallation de pi laisse les paramètres, les informations d'identification, les sessions et les packages pi installés dans `~/.pi/agent/`.\n\nEnsuite, démarrez pi dans le répertoire du projet sur lequel vous souhaitez qu'il fonctionne:\n\n```bash\ncd /path/to/project\npi\n```\n\n## Authentifier\n\nPi peut utiliser subscription providers à `/login`, ou API-fournisseurs de clés via des variables d'environnement ou le fichier d'authentification.\n\n### Option 1: connexion à l'abonnement\n\nDémarrez pi et exécutez:\n\n```text\n/login\n```\n\nSélectionnez ensuite un fournisseur. Les connexions d'abonnement intégrées incluent Claude Pro/Max, ChatGPT Plus/Pro (Codex) et GitHub Copilot.\n\n### Option 2: API key\n\nDéfinissez un API key avant de lancer pi:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\nVous pouvez également exécuter `/login` et sélectionner un fournisseur de clés API pour stocker la clé dans `~/.pi/agent/auth.json`.\n\nVoir [Providers](providers.md) pour tous les fournisseurs pris en charge, les variables d'environnement et la configuration du fournisseur de cloud.\n\n## Première séance\n\nUne fois pi démarré, tapez une requête et appuyez sur Entrée:\n\n```text\nSummarize this repository and tell me how to run its checks.\n```\n\nPar défaut, pi donne au modèle quatre outils:\n\n- `read` - lire les fichiers\n- `write` - créer ou écraser des fichiers\n- `edit` - fichiers de correctifs\n- `bash` - exécuter les commandes shell\n\nDes outils supplémentaires intégrés en lecture seule (`grep`, `find`, `ls`) sont disponibles via les options d'outils. Pi s'exécute dans votre répertoire de travail actuel et peut y modifier des fichiers. Utilisez git ou un autre workflow de point de contrôle si vous souhaitez une restauration facile.\n\n## Donner les instructions du projet Pi\n\nPi charge context files au démarrage. Ajoutez un fichier `AGENTS.md` pour lui indiquer comment travailler dans un projet:\n\n```markdown\n# Project Instructions\n\n- Run `npm run check` after code changes.\n- Do not run production migrations locally.\n- Keep responses concise.\n```\n\nPi charges:\n\n- `~/.pi/agent/AGENTS.md` pour les instructions globales\n- `AGENTS.md` ou `CLAUDE.md` des répertoires parents et du répertoire courant\n\nSi un répertoire contient `AGENTS.override.md`, Pi le charge au lieu de `AGENTS.md` ou `CLAUDE.md` à partir de ce répertoire.\n\nRedémarrez pi ou exécutez `/reload`, après avoir modifié context files.\n\n## Choses courantes à essayer\n\n### Fichiers de référence\n\nTapez `@` dans l'éditeur pour effectuer une recherche floue dans les fichiers ou transmettez les fichiers sur la ligne de commande:\n\n```bash\npi @README.md \"Summarize this\"\npi @src/app.ts @src/app.test.ts \"Review these together\"\n```\n\nLes images ou le texte peuvent être collés avec Ctrl+V (Alt+V sous Windows); les images peuvent également être glissées vers des terminaux pris en charge.\n\n### Exécuter des commandes shell\n\nEn mode interactif:\n\n```text\n!npm run lint\n```\n\nLe résultat de la commande est envoyé au modèle. Utilisez `!!command` pour exécuter une commande sans ajouter sa sortie au contexte du modèle.\n\n### Changer de modèle\n\nUtilisez `/model` ou Ctrl+L pour choisir un modèle. Utilisez Shift+Tab pour faire défiler le niveau de réflexion. Utilisez Ctrl+P / Shift+Ctrl+P pour parcourir les modèles étendus.\n\n### Continuer plus tard\n\nLes sessions sont enregistrées automatiquement:\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse previous sessions\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Open a specific session\n```\n\nDans pi, utilisez `/resume`, `/new`, `/tree`, `/fork` et `/clone` pour gérer les sessions.\n\n### Mode non interactif\n\nPour les invites ponctuelles:\n\n```bash\npi -p \"Summarize this codebase\"\ncat README.md | pi -p \"Summarize this text\"\npi -p @screenshot.png \"What's in this image?\"\n```\n\nUtilisez `--mode json` pour la sortie d'événement JSON ou `--mode rpc` pour l'intégration de processus.\n\n## Prochaines étapes\n\n- [Using Pi](usage.md) - mode interactif, slash commands, sessions, context files et CLI référence.\n- [Providers](providers.md) - authentification et configuration du modèle.\n- [Settings](settings.md) - configuration globale et projet.\n- [Keybindings](keybindings.md) - raccourcis et personnalisation.\n- [Pi Packages](packages.md) - installez des extensions, des compétences, des invites et des thèmes partagés.\n\nNotes de plateforme: [Windows](windows.md), [Termux](termux.md), [tmux](tmux.md), [Terminal setup](terminal-setup.md), [Shell aliases](shell-aliases.md).","sourceFile":"quickstart.md"},"rpc":{"title":"Mode RPC","markdown":"Le mode RPC permet un fonctionnement sans tête de l'agent de codage via un protocole JSON sur stdin/stdout. Ceci est utile pour intégrer l'agent dans d'autres applications, IDE ou interfaces utilisateur personnalisées.\n\n**Remarque pour les utilisateurs Node.js/TypeScript**: Si vous créez une application Node.js, envisagez d'utiliser `AgentSession` directement à partir de `@earendil-works/pi-coding-agent` au lieu de générer un sous-processus. Voir [`src/core/agent-session.ts`](../src/core/agent-session.ts) pour le API. Pour un client TypeScript basé sur des sous-processus, voir [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts).\n\n## Démarrage du mode RPC\n\n```bash\npi --mode rpc [options]\n```\n\nOptions courantes:\n- `--provider <name>`: Définissez le fournisseur LLM (anthropic, openai, google, etc.)\n- `--model <pattern>`: modèle ou ID de modèle (prend en charge `provider/id` et `:<thinking>` en option)\n- `--name <name>` / `-n <name>`: définissez le nom d'affichage de la session au démarrage\n- `--no-session`: Désactiver la persistance de la session\n- `--session-dir <path>`: répertoire de stockage de session personnalisé\n\n## Aperçu du protocole\n\n- **Commandes**: JSON objets envoyés à stdin, un par ligne\n- **Réponses**: JSON objets avec `type: \"response\"` indiquant le succès/l'échec de la commande\n- **Événements**: événements d'agent diffusés vers stdout sous forme de lignes JSON\n\nToutes les commandes prennent en charge un champ facultatif `id` pour la corrélation demande/réponse. Si elle est fournie, la réponse correspondante inclura le même `id`. Les événements `bash_execution_update` incluent également le `id` de leur commande `bash` d'origine.\n\n### Encadrement\n\nLe mode RPC utilise une sémantique JSONL stricte avec LF (`\\n`) comme seul délimiteur d'enregistrement.\n\nCeci est important pour les clients:\n- Fractionner les enregistrements sur `\\n` uniquement\n- Acceptez l'entrée facultative `\\r\\n` en supprimant un `\\r` final\n- N'utilisez pas de lecteurs de ligne génériques qui traitent les séparateurs Unicode comme des nouvelles lignes\n\nEn particulier, le nœud `readline` n'est pas conforme au protocole pour le mode RPC car il se divise également sur `U+2028` et `U+2029`, qui sont valides à l'intérieur des chaînes JSON.\n\n## Commandes\n\n### Invite\n\n#### rapide\n\nEnvoyez une invite utilisateur à l'agent. La réponse de la commande est émise une fois que l'invite est acceptée, mise en file d'attente ou gérée. Les événements continuent à être diffusés de manière asynchrone après acceptation.\n\n```json\n{\"id\": \"req-1\", \"type\": \"prompt\", \"message\": \"Hello, world!\"}\n```\n\nAvec des images:\n```json\n{\"type\": \"prompt\", \"message\": \"What's in this image?\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n**Pendant le streaming**: si l'agent diffuse déjà le message, vous devez spécifier `streamingBehavior` pour mettre le message en file d'attente:\n\n```json\n{\"type\": \"prompt\", \"message\": \"New instruction\", \"streamingBehavior\": \"steer\"}\n```\n\n- `\"steer\"`: mettez le message en file d'attente pendant l'exécution de l'agent. Il est délivré une fois que le tour d'assistant en cours a fini d'exécuter ses appels d'outil, avant le prochain appel LLM.\n- `\"followUp\"`: attendez que l'agent ait terminé. Le message est délivré uniquement lorsque l'agent s'arrête.\n\nSi l'agent diffuse et qu'aucun `streamingBehavior` n'est spécifié, la commande renvoie une erreur.\n\n**Commandes d'extension**: si le message est une commande d'extension (par exemple, `/mycommand`), il s'exécute immédiatement même pendant la diffusion. Les commandes d'extension gèrent leur propre interaction LLM via `pi.sendMessage()`.\n\n**Extension d'entrée**: les commandes de compétences (`/skill:name`) et prompt templates (`/template`) sont développées avant l'envoi/la mise en file d'attente.\n\nRéponse:\n```json\n{\"id\": \"req-1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true}\n```\n\n`success: true` signifie que l'invite a été acceptée, mise en file d'attente ou traitée immédiatement. `success: false` signifie que l'invite a été rejetée avant son acceptation. Les échecs après l'acceptation sont signalés via le flux normal d'événements et de messages, et non sous la forme d'un deuxième `response` pour le même identifiant de demande.\n\nLe champ `images` est facultatif. Chaque image utilise le format `ImageContent`: `{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}`.\n\n#### diriger\n\nMettez en file d'attente un message de pilotage pendant que l'agent est en cours d'exécution. Il est délivré une fois que le tour d'assistant en cours a fini d'exécuter ses appels d'outil, avant le prochain appel LLM. Les commandes de compétences et prompt templates sont développées. Les commandes d'extension ne sont pas autorisées (utilisez plutôt `prompt`).\n\n```json\n{\"type\": \"steer\", \"message\": \"Stop and do this instead\"}\n```\n\nAvec des images:\n```json\n{\"type\": \"steer\", \"message\": \"Look at this instead\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nLe champ `images` est facultatif. Chaque image utilise le format `ImageContent` (identique à `prompt`).\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"steer\", \"success\": true}\n```\n\nVoir [set_steering_mode](#set_steering_mode) pour contrôler la manière dont les messages de pilotage sont traités.\n\n#### suivi\n\nMettez en file d'attente un message de suivi à traiter une fois l'agent terminé. Distribué uniquement lorsque l'agent n'a plus d'appels d'outil ni de messages de pilotage. Les commandes de compétences et prompt templates sont développées. Les commandes d'extension ne sont pas autorisées (utilisez plutôt `prompt`).\n\n```json\n{\"type\": \"follow_up\", \"message\": \"After you're done, also do this\"}\n```\n\nAvec des images:\n```json\n{\"type\": \"follow_up\", \"message\": \"Also check this image\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nLe champ `images` est facultatif. Chaque image utilise le format `ImageContent` (identique à `prompt`).\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"follow_up\", \"success\": true}\n```\n\nVoir [set_follow_up_mode](#set_follow_up_mode) pour contrôler la façon dont les messages de suivi sont traités.\n\n#### avorter\n\nAbandonnez l’opération d’agent en cours.\n\n```json\n{\"type\": \"abort\"}\n```\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"abort\", \"success\": true}\n```\n\n#### nouvelle_session\n\nDémarrez une nouvelle session. Peut être annulé par un gestionnaire d'événements d'extension `session_before_switch`.\n\n```json\n{\"type\": \"new_session\"}\n```\n\nAvec suivi facultatif de la session parent:\n```json\n{\"type\": \"new_session\", \"parentSession\": \"/path/to/parent-session.jsonl\"}\n```\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nSi une prolongation est annulée:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n### État\n\n#### get_state\n\nObtenez l’état actuel de la session.\n\n```json\n{\"type\": \"get_state\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_state\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isStreaming\": false,\n    \"isCompacting\": false,\n    \"steeringMode\": \"all\",\n    \"followUpMode\": \"one-at-a-time\",\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"sessionName\": \"my-feature-work\",\n    \"autoCompactionEnabled\": true,\n    \"messageCount\": 5,\n    \"pendingMessageCount\": 0\n  }\n}\n```\n\nLe champ `model` est un objet [Model](#model) complet ou `null`. Le champ `sessionName` est le nom d'affichage défini via `set_session_name`, ou omis s'il n'est pas défini.\n\n#### get_messages\n\nRecevez tous les messages de la conversation.\n\n```json\n{\"type\": \"get_messages\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_messages\",\n  \"success\": true,\n  \"data\": {\"messages\": [...]}\n}\n```\n\nLes messages sont des objets `AgentMessage` (voir [Message Types](#message-types)).\n\n### Modèle\n\n#### set_model\n\nPassez à un modèle spécifique.\n\n```json\n{\"type\": \"set_model\", \"provider\": \"anthropic\", \"modelId\": \"claude-sonnet-4-20250514\"}\n```\n\nLa réponse contient l'objet [Model](#model) complet:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": true,\n  \"data\": {...}\n}\n```\n\n#### cycle_model\n\nPassez au prochain modèle disponible. Renvoie les données `null` si un seul modèle est disponible.\n\n```json\n{\"type\": \"cycle_model\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_model\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isScoped\": false\n  }\n}\n```\n\nLe champ `model` est un objet [Model](#model) complet.\n\n#### get_available_models\n\nRépertoriez tous les modèles configurés.\n\n```json\n{\"type\": \"get_available_models\"}\n```\n\nLa réponse contient un tableau d'objets [Model](#model) complets:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_models\",\n  \"success\": true,\n  \"data\": {\n    \"models\": [...]\n  }\n}\n```\n\n### Pensée\n\n#### set_thinking_level\n\nDéfinissez le niveau de raisonnement/réflexion pour les modèles qui le prennent en charge.\n\n```json\n{\"type\": \"set_thinking_level\", \"level\": \"high\"}\n```\n\nNiveaux: `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"`\n\n`\"xhigh\"` et `\"max\"` sont exposés uniquement lorsqu'ils sont pris en charge par le modèle sélectionné. Certains modèles, dont GPT-5.6, exposent les deux.\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"set_thinking_level\", \"success\": true}\n```\n\n#### cycle_thinking_level\n\nParcourez les niveaux de réflexion disponibles. Renvoie les données `null` si le modèle ne prend pas en charge la réflexion.\n\n```json\n{\"type\": \"cycle_thinking_level\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_thinking_level\",\n  \"success\": true,\n  \"data\": {\"level\": \"high\"}\n}\n```\n\n#### get_available_thinking_levels\n\nÉnumérez les niveaux de réflexion pris en charge par le modèle actuel. Renvoie `[\"off\"]` pour un modèle sans support de raisonnement.\n\n```json\n{\"type\": \"get_available_thinking_levels\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_thinking_levels\",\n  \"success\": true,\n  \"data\": {\n    \"levels\": [\"off\", \"minimal\", \"low\", \"medium\", \"high\"]\n  }\n}\n```\n\n### Modes de file d'attente\n\n#### set_steering_mode\n\nContrôlez la manière dont les messages de pilotage (à partir de `steer`) sont transmis.\n\n```json\n{\"type\": \"set_steering_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nModes:\n- `\"all\"`: délivre tous les messages de direction une fois que le tour d'assistant en cours a fini d'exécuter ses appels d'outil\n- `\"one-at-a-time\"`: délivre un message de direction par tour d'assistant terminé (par défaut)\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"set_steering_mode\", \"success\": true}\n```\n\n#### set_follow_up_mode\n\nContrôlez la manière dont les messages de suivi (à partir de `follow_up`) sont transmis.\n\n```json\n{\"type\": \"set_follow_up_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nModes:\n- `\"all\"`: Envoyez tous les messages de suivi lorsque l'agent a terminé\n- `\"one-at-a-time\"`: envoyer un message de suivi par achèvement d'agent (par défaut)\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"set_follow_up_mode\", \"success\": true}\n```\n\n### Compactage\n\n#### compact\n\nCompactez manuellement le contexte de conversation pour réduire l’utilisation des jetons.\n\n```json\n{\"type\": \"compact\"}\n```\n\nAvec instructions personnalisées:\n```json\n{\"type\": \"compact\", \"customInstructions\": \"Focus on code changes\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"compact\",\n  \"success\": true,\n  \"data\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  }\n}\n```\n\n`estimatedTokensAfter` est une estimation heuristique du contexte de message reconstruit immédiatement après le compactage, et non un nombre de jetons exact du fournisseur. `usage` signale le ou les appels LLM qui ont généré le résumé et peut être omis par les gestionnaires de compactage personnalisés.\n\n#### set_auto_compaction\n\nActivez ou désactivez le compactage automatique lorsque le contexte est presque plein.\n\n```json\n{\"type\": \"set_auto_compaction\", \"enabled\": true}\n```\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_compaction\", \"success\": true}\n```\n\n### Réessayer\n\n#### set_auto_retry\n\nActivez ou désactivez les nouvelles tentatives automatiques en cas d'erreurs transitoires (surcharge, limite de débit, 5xx).\n\n```json\n{\"type\": \"set_auto_retry\", \"enabled\": true}\n```\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_retry\", \"success\": true}\n```\n\n#### abort_retry\n\nAbandonnez une nouvelle tentative en cours (annulez le délai et arrêtez de réessayer).\n\n```json\n{\"type\": \"abort_retry\"}\n```\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"abort_retry\", \"success\": true}\n```\n\n### Frapper\n\n#### bash\n\nExécutez une commande shell et ajoutez une sortie au contexte de conversation. Flux de sortie sous forme d'événements `bash_execution_update` pendant l'exécution de la commande; la réponse contient le résultat final.\n\n```json\n{\"id\": \"req-1\", \"type\": \"bash\", \"command\": \"ls -la\"}\n```\n\nIncluez un `id` pour associer les événements `bash_execution_update` diffusés en streaming à cette commande.\n\nRéponse:\n```json\n{\n  \"id\": \"req-1\",\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"total 48\\ndrwxr-xr-x ...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": false\n  }\n}\n```\n\nSi la sortie a été tronquée, inclut `fullOutputPath`:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"truncated output...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": true,\n    \"fullOutputPath\": \"/tmp/pi-bash-abc123.log\"\n  }\n}\n```\n\n**Comment les résultats bash atteignent le LLM:**\n\nLa commande `bash` s'exécute immédiatement et renvoie un `BashResult`. En interne, un `BashExecutionMessage` est créé et stocké dans l'état de message de l'agent.\n\nLorsque la prochaine commande `prompt` est envoyée, tous les messages (y compris `BashExecutionMessage`) sont transformés avant d'être envoyés au LLM. Le `BashExecutionMessage` est converti en `UserMessage` avec ce format:\n\n````\nRan `ls -la`\n```\ntotal 48\ndessinxr-xr-x...\n```\n````\n\nCela signifie:\n1. La sortie Bash est incluse dans le contexte LLM à l'**invite suivante**, pas immédiatement\n2. Plusieurs commandes bash peuvent être exécutées avant une invite; toutes les sorties seront incluses\n\n#### abort_bash\n\nAbandonnez une commande bash en cours d'exécution.\n\n```json\n{\"type\": \"abort_bash\"}\n```\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"abort_bash\", \"success\": true}\n```\n\n### Session\n\n#### get_session_stats\n\nObtenez l'utilisation des jetons, les statistiques de coûts et l'utilisation actuelle de la fenêtre contextuelle.\n\n```json\n{\"type\": \"get_session_stats\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_session_stats\",\n  \"success\": true,\n  \"data\": {\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"userMessages\": 5,\n    \"assistantMessages\": 5,\n    \"toolCalls\": 12,\n    \"toolResults\": 12,\n    \"totalMessages\": 22,\n    \"tokens\": {\n      \"input\": 50000,\n      \"output\": 10000,\n      \"cacheRead\": 40000,\n      \"cacheWrite\": 5000,\n      \"total\": 105000\n    },\n    \"cost\": 0.45,\n    \"contextUsage\": {\n      \"tokens\": 60000,\n      \"contextWindow\": 200000,\n      \"percent\": 30\n    }\n  }\n}\n```\n\n`tokens` et `cost` incluent les messages de l'assistant, l'utilisation signalée par les outils et la génération de compactage/résumé de branche tout au long de la session complète. `contextUsage` contient l'estimation actuelle de la fenêtre contextuelle utilisée pour le compactage et l'affichage du pied de page.\n\n`contextUsage` est omis lorsqu'aucun modèle ou fenêtre contextuelle n'est disponible. `contextUsage.tokens` et `contextUsage.percent` sont `null` immédiatement après le compactage jusqu'à ce qu'une nouvelle réponse de l'assistant de post-compactage fournisse des données d'utilisation valides.\n\n#### export_html\n\nExportez la session vers un fichier HTML.\n\n```json\n{\"type\": \"export_html\"}\n```\n\nAvec chemin personnalisé:\n```json\n{\"type\": \"export_html\", \"outputPath\": \"/tmp/session.html\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"export_html\",\n  \"success\": true,\n  \"data\": {\"path\": \"/tmp/session.html\"}\n}\n```\n\n#### switch_session\n\nChargez un autre fichier de session. Peut être annulé par un gestionnaire d'événements d'extension `session_before_switch`.\n\n```json\n{\"type\": \"switch_session\", \"sessionPath\": \"/path/to/session.jsonl\"}\n```\n\nRéponse:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nSi un poste a annulé le changement:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n#### fourchette\n\nCréez un nouveau fork à partir d'un message utilisateur précédent sur la branche active. Peut être annulé par un gestionnaire d'événements d'extension `session_before_fork`. Renvoie le texte du message à partir duquel il est dérivé.\n\n```json\n{\"type\": \"fork\", \"entryId\": \"abc123\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": false}\n}\n```\n\nSi une extension annule le fork:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": true}\n}\n```\n\n#### cloner\n\nDupliquez la branche active actuelle dans une nouvelle session à la position actuelle. Peut être annulé par un gestionnaire d'événements d'extension `session_before_fork`.\n\n```json\n{\"type\": \"clone\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": false}\n}\n```\n\nSi une extension a annulé le clonage:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n#### get_fork_messages\n\nObtenez les messages utilisateur disponibles pour le forking.\n\n```json\n{\"type\": \"get_fork_messages\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_fork_messages\",\n  \"success\": true,\n  \"data\": {\n    \"messages\": [\n      {\"entryId\": \"abc123\", \"text\": \"First prompt...\"},\n      {\"entryId\": \"def456\", \"text\": \"Second prompt...\"}\n    ]\n  }\n}\n```\n\n#### get_entries\n\nObtenez toutes les entrées de session dans l’ordre d’ajout (à l’exclusion de l’en-tête de session). La session est une arborescence d'entrées en ajout uniquement avec des identifiants stables, donc un identifiant d'entrée fonctionne comme un curseur durable: transmettez le dernier identifiant d'entrée que vous avez vu comme `since` pour obtenir uniquement les entrées strictement après, même lors des redémarrages du client. Contrairement à `get_messages`, cela inclut l'historique de pré-compactage et les branches abandonnées.\n\n```json\n{\"type\": \"get_entries\"}\n```\n\nAvec un curseur:\n```json\n{\"type\": \"get_entries\", \"since\": \"abc123\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_entries\",\n  \"success\": true,\n  \"data\": {\n    \"entries\": [\n      {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"timestamp\": \"...\", \"message\": {\"role\": \"user\", \"...\": \"...\"}}\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n`leafId` est l'identifiant de l'entrée feuille actuelle (`null` pour une session vide), afin qu'un client puisse savoir en un aller-retour si la branche active a bougé. Si `since` ne correspond à aucun identifiant d'entrée, la réponse est `success: false`.\n\n#### get_tree\n\nObtenez la session sous forme d’arborescence d’entrées. Chaque nœud est `{entry, children, label?, labelTimestamp?}`. Une session bien formée a une seule racine; les entrées orphelines (chaîne parent brisée) apparaissent également comme racines.\n\n```json\n{\"type\": \"get_tree\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_tree\",\n  \"success\": true,\n  \"data\": {\n    \"tree\": [\n      {\n        \"entry\": {\"type\": \"message\", \"id\": \"abc123\", \"parentId\": null, \"...\": \"...\"},\n        \"children\": [\n          {\"entry\": {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"...\": \"...\"}, \"children\": []}\n        ]\n      }\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n#### get_last_assistant_text\n\nObtenez le contenu textuel du dernier message de l'assistant.\n\n```json\n{\"type\": \"get_last_assistant_text\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_last_assistant_text\",\n  \"success\": true,\n  \"data\": {\"text\": \"The assistant's response...\"}\n}\n```\n\nRenvoie `{\"text\": null}` si aucun message d'assistant n'existe.\n\n#### set_session_name\n\nDéfinissez un nom d'affichage pour la session en cours. Le nom apparaît dans les listes de sessions et permet d'identifier les sessions.\n\n```json\n{\"type\": \"set_session_name\", \"name\": \"my-feature-work\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_session_name\",\n  \"success\": true\n}\n```\n\nLe nom de la session actuelle est disponible via `get_state` dans le champ `sessionName`. Pour définir le nom initial lors du démarrage du mode RPC, passez `--name <name>` ou `-n <name>` au processus `pi --mode rpc`.\n\n### Commandes\n\n#### get_commands\n\nObtenez les commandes disponibles (commandes d'extension, prompt templates et compétences). Ceux-ci peuvent être invoqués via la commande `prompt` en préfixant `/`.\n\n```json\n{\"type\": \"get_commands\"}\n```\n\nRéponse:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_commands\",\n  \"success\": true,\n  \"data\": {\n    \"commands\": [\n      {\"name\": \"session-name\", \"description\": \"Set or clear session name\", \"source\": \"extension\", \"path\": \"/home/user/.pi/agent/extensions/session.ts\"},\n      {\"name\": \"fix-tests\", \"description\": \"Fix failing tests\", \"source\": \"prompt\", \"location\": \"project\", \"path\": \"/home/user/myproject/.pi/agent/prompts/fix-tests.md\"},\n      {\"name\": \"skill:brave-search\", \"description\": \"Web search via Brave API\", \"source\": \"skill\", \"location\": \"user\", \"path\": \"/home/user/.pi/agent/skills/brave-search/SKILL.md\"}\n    ]\n  }\n}\n```\n\nChaque commande possède:\n- `name`: nom de la commande (appelé avec `/name`)\n- `description`: description lisible par l'homme (facultatif pour les commandes d'extension)\n- `source`: Quel type de commande:\n  - `\"extension\"`: Enregistré via `pi.registerCommand()` dans une extension\n  - `\"prompt\"`: chargé à partir d'un fichier de modèle d'invite `.md`\n  - `\"skill\"`: Chargé à partir d'un répertoire de compétences (le nom est préfixé par `skill:`)\n- `location`: D'où il a été chargé (facultatif, non présent pour les extensions):\n  - `\"user\"`: niveau utilisateur (`~/.pi/agent/`)\n  - `\"project\"`: niveau du projet (`./.pi/agent/`)\n  - `\"path\"`: chemin explicite via CLI ou paramètres\n- `path`: chemin de fichier absolu vers la source de la commande (facultatif)\n\n**Remarque**: Les commandes TUI intégrées (`/settings`, `/hotkeys`, etc.) ne sont pas incluses. Ils sont gérés uniquement en mode interactif et ne s'exécuteront pas s'ils sont envoyés via `prompt`.\n\n## Événements\n\nLes événements sont diffusés vers stdout sous forme de lignes JSON pendant le fonctionnement de l'agent. Les événements n'incluent généralement pas de champ `id`; `bash_execution_update` inclut le `id` de sa commande `bash` d'origine lorsqu'elle est fournie.\n\n### Types d'événements\n\n| Événement | Description |\n|-------|-------------|\n| `agent_start` | L'agent commence le traitement |\n| `agent_end` | Une exécution d'agent de bas niveau est terminée (peut encore être suivie d'une nouvelle tentative, d'un compactage ou de continuations en file d'attente) |\n| `agent_settled` | L'exécution de l'agent est entièrement réglée; il ne reste aucune nouvelle tentative automatique, nouvelle tentative de compactage ou continuation en file d'attente |\n| `turn_start` | Un nouveau tour commence |\n| `turn_end` | Tour terminé (inclut le message de l'assistant et les résultats de l'outil) |\n| `message_start` | Le message commence |\n| `message_update` | Mise à jour en streaming (deltas texte/réflexion/appel d'outils) |\n| `message_end` | Message terminé |\n| `bash_execution_update` | Morceau de sortie de commande direct RPC bash |\n| `tool_execution_start` | L'outil commence son exécution |\n| `tool_execution_update` | Progression de l'exécution de l'outil (sortie en streaming) |\n| `tool_execution_end` | Outil terminé |\n| `queue_update` | Modification de la file d'attente de pilotage/suivi en attente |\n| `compaction_start` | Le compactage commence |\n| `compaction_end` | Le compactage est terminé |\n| `auto_retry_start` | La nouvelle tentative automatique commence (après une erreur passagère) |\n| `auto_retry_end` | La nouvelle tentative automatique est terminée (succès ou échec final) |\n| `summarization_retry_scheduled` | Nouvelle tentative planifiée pour une erreur de compactage transitoire ou de résumé de branchement |\n| `summarization_retry_attempt_start` | La demande de résumé réessayée démarre |\n| `summarization_retry_finished` | La boucle de nouvelle tentative de synthèse est terminée |\n| `extension_error` | L'extension a généré une erreur |\n\n### agent_start\n\nÉmis lorsque l'agent commence à traiter une invite.\n\n```json\n{\"type\": \"agent_start\"}\n```\n\n### fin_agent\n\nÉmis lorsqu’une exécution d’agent de bas niveau est terminée. Contient tous les messages générés lors de cette exécution. Si `willRetry` est vrai, une nouvelle tentative automatique suivra.\n\n```json\n{\n  \"type\": \"agent_end\",\n  \"messages\": [...],\n  \"willRetry\": false\n}\n```\n\n### agent_installé\n\nÉmis après le règlement de l’exécution complète au niveau de la session. À ce stade, Pi ne continuera pas automatiquement via une nouvelle tentative, une nouvelle tentative de compactage ou des messages de suivi en file d'attente.\n\n```json\n{\"type\": \"agent_settled\"}\n```\n\n### tour_début / tour_end\n\nUn tour se compose d’une réponse d’assistant ainsi que de tous les appels d’outils et résultats qui en résultent.\n\n```json\n{\"type\": \"turn_start\"}\n```\n\n```json\n{\n  \"type\": \"turn_end\",\n  \"message\": {...},\n  \"toolResults\": [...]\n}\n```\n\n### message_début / message_fin\n\nÉmis lorsqu'un message commence et se termine. Le champ `message` contient un `AgentMessage`.\n\n```json\n{\"type\": \"message_start\", \"message\": {...}}\n{\"type\": \"message_end\", \"message\": {...}}\n```\n\n### message_update (diffusion)\n\nÉmis lors du streaming des messages de l'assistant. Contient un événement delta sans instantané de message cumulatif.\n\n```json\n{\n  \"type\": \"message_update\",\n  \"assistantMessageEvent\": {\n    \"type\": \"text_delta\",\n    \"contentIndex\": 0,\n    \"delta\": \"Hello \"\n  }\n}\n```\n\nLe champ `assistantMessageEvent` contient l'un de ces types delta:\n\n| Taper | Description |\n|------|-------------|\n| `text_start` | Le bloc de contenu texte a démarré |\n| `text_delta` | Morceau de contenu textuel |\n| `text_end` | Le bloc de contenu texte est terminé |\n| `thinking_start` | Le bloc de réflexion a commencé |\n| `thinking_delta` | Contenu de réflexion |\n| `thinking_end` | Le bloc de réflexion est terminé |\n| `toolcall_start` | Appel d'outil lancé |\n| `toolcall_delta` | Morceau d'arguments d'appel d'outil |\n| `toolcall_end` | Appel d'outil terminé (inclut l'objet `toolCall` complet) |\n\nExemple de diffusion d'une réponse textuelle:\n```json\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_start\",\"contentIndex\":0}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\" world\"}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_end\",\"contentIndex\":0,\"content\":\"Hello world\"}}\n```\n\n`message_update` omet intentionnellement l'ancien champ cumulatif `message` et\n`assistantMessageEvent.partial`. Les clients qui ont besoin d'un message partiel en direct doivent l'assembler\nà partir de `message_start` et les événements suivants en utilisant `contentIndex`. Traiter `message_end.message`\ncomme faisant autorité. Pour les appels d'outils, tamponnez `toolcall_delta.delta`; `toolcall_end.toolCall`\ncontient l'appel terminé.\n\n### bash_execution_update\n\nÉmis une fois pour chaque morceau de sortie à partir d'une commande directe `bash`. `id` correspond au `id` de la commande, permettant aux clients d'associer la sortie à la commande correcte.\n\nLes événements diffusent toutes les sorties pendant l'exécution de la commande, même si le `output` de la réponse finale `bash` est tronqué.\n\n```json\n{\n  \"type\": \"bash_execution_update\",\n  \"id\": \"req-1\",\n  \"delta\": \"total 48\\n\"\n}\n```\n\n### tool_execution_start/tool_execution_update/tool_execution_end\n\nÉmis lorsqu'un outil démarre, diffuse la progression et termine l'exécution.\n\n```json\n{\n  \"type\": \"tool_execution_start\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"}\n}\n```\n\nPendant l'exécution, les événements `tool_execution_update` diffusent des résultats partiels (par exemple, la sortie bash à son arrivée):\n\n```json\n{\n  \"type\": \"tool_execution_update\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"},\n  \"partialResult\": {\n    \"content\": [{\"type\": \"text\", \"text\": \"partial output so far...\"}],\n    \"details\": {\"truncation\": null, \"fullOutputPath\": null}\n  }\n}\n```\n\nUne fois terminé:\n\n```json\n{\n  \"type\": \"tool_execution_end\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"result\": {\n    \"content\": [{\"type\": \"text\", \"text\": \"total 48\\n...\"}],\n    \"details\": {...}\n  },\n  \"isError\": false\n}\n```\n\nUtilisez `toolCallId` pour corréler les événements. Le `partialResult` dans `tool_execution_update` contient la sortie accumulée jusqu'à présent (pas seulement le delta), permettant aux clients de simplement remplacer leur affichage à chaque mise à jour.\n\n### mise à jour_file d'attente\n\nÉmis chaque fois que la file d'attente de pilotage ou de suivi en attente change.\n\n```json\n{\n  \"type\": \"queue_update\",\n  \"steering\": [\"Focus on error handling\"],\n  \"followUp\": [\"After that, summarize the result\"]\n}\n```\n\n### compaction_start / compaction_end\n\nÉmis lors du compactage, qu'il soit manuel ou automatique.\n\n```json\n{\"type\": \"compaction_start\", \"reason\": \"threshold\"}\n```\n\nLe champ `reason` est `\"manual\"`, `\"threshold\"` ou `\"overflow\"`.\n\n```json\n{\n  \"type\": \"compaction_end\",\n  \"reason\": \"threshold\",\n  \"result\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  },\n  \"aborted\": false,\n  \"willRetry\": false\n}\n```\n\nSi `reason` était `\"overflow\"` et que le compactage réussit, `willRetry` vaut `true` et l'agent réessayera automatiquement l'invite.\n\nSi le compactage a été interrompu, `result` est `null` et `aborted` est `true`.\n\nSi le compactage a échoué (par exemple, quota API dépassé), `result` est `null`, `aborted` est `false` et `errorMessage` contient la description de l'erreur.\n\n### auto_retry_start / auto_retry_end\n\nÉmis lorsqu'une nouvelle tentative automatique est déclenchée après une erreur transitoire (surcharge, limite de débit, 5xx).\n\n```json\n{\n  \"type\": \"auto_retry_start\",\n  \"attempt\": 1,\n  \"maxAttempts\": 3,\n  \"delayMs\": 2000,\n  \"errorMessage\": \"529 {\\\"type\\\":\\\"error\\\",\\\"error\\\":{\\\"type\\\":\\\"overloaded_error\\\",\\\"message\\\":\\\"Overloaded\\\"}}\"\n}\n```\n\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": true,\n  \"attempt\": 2\n}\n```\n\nEn cas d'échec final (nombre maximal de tentatives dépassé):\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": false,\n  \"attempt\": 3,\n  \"finalError\": \"529 overloaded_error: Overloaded\"\n}\n```\n\n### summarization_retry_scheduled / summarization_retry_attempt_start / summarization_retry_finished\n\nÉmis lors de nouvelles tentatives de compactage ou de résumé de branche après une erreur transitoire du fournisseur. Ces événements utilisent les mêmes paramètres de nouvelle tentative que les tentatives automatiques de tour d'assistant.\n\n```json\n{\n  \"type\": \"summarization_retry_scheduled\",\n  \"attempt\": 1,\n  \"maxAttempts\": 3,\n  \"delayMs\": 2000,\n  \"errorMessage\": \"terminated\"\n}\n```\n\n```json\n{\n  \"type\": \"summarization_retry_attempt_start\",\n  \"source\": \"compaction\",\n  \"reason\": \"threshold\"\n}\n```\n\nPour les résumés de branche, `source` est `\"branchSummary\"` et aucun `reason` n'est présent.\n\n```json\n{\n  \"type\": \"summarization_retry_finished\"\n}\n```\n\n### erreur_extension\n\nÉmis lorsqu'une extension génère une erreur.\n\n```json\n{\n  \"type\": \"extension_error\",\n  \"extensionPath\": \"/path/to/extension.ts\",\n  \"event\": \"tool_call\",\n  \"error\": \"Error message...\"\n}\n```\n\n## Protocole d'interface utilisateur d'extension\n\nExtensions peut demander une interaction de l'utilisateur via `ctx.ui.select()`, `ctx.ui.confirm()`, etc. En mode RPC, ceux-ci sont traduits en un sous-protocole de demande/réponse au-dessus du flux de commande/d'événement de base.\n\nIl existe deux catégories de méthodes d'extension de l'interface utilisateur:\n\n- **Méthodes de dialogue** (`select`, `confirm`, `input`, `editor`): émettent un `extension_ui_request` sur stdout et bloquent jusqu'à ce que le client renvoie un `extension_ui_response` sur stdin avec le `id` correspondant.\n- **Méthodes de tir et d'oubli** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): émet un `extension_ui_request` sur stdout mais n'attend pas de réponse. Le client peut afficher les informations ou les ignorer.\n\nSi une méthode de dialogue inclut un champ `timeout`, le côté agent se résoudra automatiquement avec une valeur par défaut à l'expiration du délai d'attente. Le client n'a pas besoin de suivre les délais d'attente.\n\nCertaines méthodes `ExtensionUIContext` ne sont pas prises en charge ou dégradées en mode RPC car elles nécessitent un accès direct TUI:\n- `custom()` renvoie `undefined`\n- `setWorkingMessage()`, `setWorkingIndicator()`, `setFooter()`, `setHeader()`, `setEditorComponent()`, `setToolsExpanded()` ne sont pas opérationnels\n- `getEditorText()` renvoie `\"\"`\n- `getToolsExpanded()` renvoie `false`\n- `pasteToEditor()` délégués à `setEditorText()` (pas de gestion du collage/réduction)\n- `getAllThemes()` renvoie `[]`\n- `getTheme()` renvoie `undefined`\n- `setTheme()` renvoie `{ success: false, error: \"...\" }`\n\nRemarque: `ctx.mode` est `\"rpc\"` et `ctx.hasUI` est `true` en mode RPC car les méthodes de dialogue et de déclenchement et d'oubli sont fonctionnelles via le sous-protocole d'extension de l'interface utilisateur. Utilisez `ctx.mode === \"tui\"` pour protéger les fonctionnalités spécifiques à TUI comme `custom()` qui nécessitent un vrai terminal.\n\n### Demandes d'extension de l'interface utilisateur (stdout)\n\nToutes les demandes ont un champ `type: \"extension_ui_request\"`, un champ `id` unique et un champ `method`.\n\n#### sélectionner\n\nInviter l'utilisateur à choisir dans une liste. Les méthodes de dialogue avec un champ `timeout` incluent le délai d'expiration en millisecondes; l'agent se résout automatiquement avec `undefined` si le client ne répond pas à temps.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-1\",\n  \"method\": \"select\",\n  \"title\": \"Allow dangerous command?\",\n  \"options\": [\"Allow\", \"Block\"],\n  \"timeout\": 10000\n}\n```\n\nRéponse attendue: `extension_ui_response` avec `value` (la chaîne d'option sélectionnée) ou `cancelled: true`.\n\n#### confirmer\n\nInviter l'utilisateur à confirmer oui/non.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-2\",\n  \"method\": \"confirm\",\n  \"title\": \"Clear session?\",\n  \"message\": \"All messages will be lost.\",\n  \"timeout\": 5000\n}\n```\n\nRéponse attendue: `extension_ui_response` avec `confirmed: true/false` ou `cancelled: true`.\n\n#### saisir\n\nInviter l'utilisateur à saisir un texte de forme libre.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-3\",\n  \"method\": \"input\",\n  \"title\": \"Enter a value\",\n  \"placeholder\": \"type something...\"\n}\n```\n\nRéponse attendue: `extension_ui_response` avec `value` (le texte saisi) ou `cancelled: true`.\n\n#### éditeur\n\nOuvrez un éditeur de texte multiligne avec du contenu prérempli facultatif.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-4\",\n  \"method\": \"editor\",\n  \"title\": \"Edit some text\",\n  \"prefill\": \"Line 1\\nLine 2\\nLine 3\"\n}\n```\n\nRéponse attendue: `extension_ui_response` avec `value` (le texte édité) ou `cancelled: true`.\n\n#### notifier\n\nAfficher une notification. Tirer et oublier, aucune réponse attendue.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-5\",\n  \"method\": \"notify\",\n  \"message\": \"Command blocked by user\",\n  \"notifyType\": \"warning\"\n}\n```\n\nLe champ `notifyType` est `\"info\"`, `\"warning\"` ou `\"error\"`. La valeur par défaut est `\"info\"` en cas d'omission.\n\n#### setStatus\n\nDéfinissez ou effacez une entrée d’état dans le pied de page/barre d’état. Tirez et oubliez.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-6\",\n  \"method\": \"setStatus\",\n  \"statusKey\": \"my-ext\",\n  \"statusText\": \"Turn 3 running...\"\n}\n```\n\nEnvoyez `statusText: undefined` (ou omettez-le) pour effacer l'entrée d'état de cette clé.\n\n#### définirWidget\n\nDéfinissez ou effacez un widget (bloc de lignes de texte) affiché au-dessus ou en dessous de l'éditeur. Tirez et oubliez.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-7\",\n  \"method\": \"setWidget\",\n  \"widgetKey\": \"my-ext\",\n  \"widgetLines\": [\"--- My Widget ---\", \"Line 1\", \"Line 2\"],\n  \"widgetPlacement\": \"aboveEditor\"\n}\n```\n\nEnvoyez `widgetLines: undefined` (ou omettez-le) pour effacer le widget. Le champ `widgetPlacement` est `\"aboveEditor\"` (par défaut) ou `\"belowEditor\"`. Seuls les tableaux de chaînes sont pris en charge en mode RPC; les usines de composants sont ignorées.\n\n#### définirTitre\n\nDéfinissez le titre de la fenêtre/de l'onglet du terminal. Tirez et oubliez.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-8\",\n  \"method\": \"setTitle\",\n  \"title\": \"pi - my project\"\n}\n```\n\n#### set_editor_text\n\nDéfinissez le texte dans l'éditeur de saisie. Tirez et oubliez.\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-9\",\n  \"method\": \"set_editor_text\",\n  \"text\": \"prefilled text for the user\"\n}\n```\n\n### Réponses de l'interface utilisateur de l'extension (stdin)\n\nLes réponses sont envoyées uniquement pour les méthodes de dialogue (`select`, `confirm`, `input`, `editor`). Le `id` doit correspondre à la demande.\n\n#### Réponse de valeur (sélection, saisie, éditeur)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-1\", \"value\": \"Allow\"}\n```\n\n#### Réponse de confirmation (confirmer)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-2\", \"confirmed\": true}\n```\n\n#### Réponse d'annulation (n'importe quelle boîte de dialogue)\n\nIgnorez toute méthode de dialogue. L'extension reçoit `undefined` (pour sélectionner/saisir/éditeur) ou `false` (pour confirmer).\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-3\", \"cancelled\": true}\n```\n\n## Gestion des erreurs\n\nLes commandes ayant échoué renvoient une réponse avec `success: false`:\n\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": false,\n  \"error\": \"Model not found: invalid/model\"\n}\n```\n\nErreurs d'analyse:\n\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"parse\",\n  \"success\": false,\n  \"error\": \"Failed to parse command: Unexpected token...\"\n}\n```\n\n## Espèces\n\nFichiers sources:\n- [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `Model`, `UserMessage`, `AssistantMessage`, `ToolResultMessage`\n- [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AgentMessage`, `AgentEvent`\n- [`src/core/messages.ts`](../src/core/messages.ts) - `BashExecutionMessage`\n- [`src/modes/json-event.ts`](../src/modes/json-event.ts) - `JsonAgentSessionEvent`\n- [`src/modes/rpc/rpc-types.ts`](../src/modes/rpc/rpc-types.ts) - RPC types de commande/réponse, types de demande/réponse de l'interface utilisateur d'extension\n\n### Modèle\n\n```json\n{\n  \"id\": \"claude-sonnet-4-20250514\",\n  \"name\": \"Claude Sonnet 4\",\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"baseUrl\": \"https://api.anthropic.com\",\n  \"reasoning\": true,\n  \"input\": [\"text\", \"image\"],\n  \"contextWindow\": 200000,\n  \"maxTokens\": 16384,\n  \"cost\": {\n    \"input\": 3.0,\n    \"output\": 15.0,\n    \"cacheRead\": 0.3,\n    \"cacheWrite\": 3.75\n  }\n}\n```\n\n### Message utilisateur\n\n```json\n{\n  \"role\": \"user\",\n  \"content\": \"Hello!\",\n  \"timestamp\": 1733234567890,\n  \"attachments\": []\n}\n```\n\nLe champ `content` peut être une chaîne ou un tableau de blocs `TextContent`/`ImageContent`.\n\n### AssistantMessage\n\n```json\n{\n  \"role\": \"assistant\",\n  \"content\": [\n    {\"type\": \"text\", \"text\": \"Hello! How can I help?\"},\n    {\"type\": \"thinking\", \"thinking\": \"User is greeting me...\"},\n    {\"type\": \"toolCall\", \"id\": \"call_123\", \"name\": \"bash\", \"arguments\": {\"command\": \"ls\"}}\n  ],\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"model\": \"claude-sonnet-4-20250514\",\n  \"usage\": {\n    \"input\": 100,\n    \"output\": 50,\n    \"cacheRead\": 0,\n    \"cacheWrite\": 0,\n    \"cost\": {\"input\": 0.0003, \"output\": 0.00075, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.00105}\n  },\n  \"stopReason\": \"stop\",\n  \"timestamp\": 1733234567890\n}\n```\n\nMotifs d'arrêt: `\"stop\"`, `\"length\"`, `\"toolUse\"`, `\"error\"`, `\"aborted\"`\n\n### Message de résultat de l'outil\n\n```json\n{\n  \"role\": \"toolResult\",\n  \"toolCallId\": \"call_123\",\n  \"toolName\": \"bash\",\n  \"content\": [{\"type\": \"text\", \"text\": \"total 48\\ndrwxr-xr-x ...\"}],\n  \"usage\": {\n    \"input\": 100,\n    \"output\": 50,\n    \"cacheRead\": 0,\n    \"cacheWrite\": 0,\n    \"totalTokens\": 150,\n    \"cost\": {\"input\": 0.0003, \"output\": 0.00075, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.00105}\n  },\n  \"isError\": false,\n  \"timestamp\": 1733234567890\n}\n```\n\n`usage` est facultatif et signale le travail LLM imbriqué effectué par l'outil. Lorsqu'il est présent, il contribue aux totaux des jetons de session et des coûts.\n\n### BashExecutionMessage\n\nCréé par la commande `bash` RPC (et non par les appels de l'outil LLM):\n\n```json\n{\n  \"role\": \"bashExecution\",\n  \"command\": \"ls -la\",\n  \"output\": \"total 48\\ndrwxr-xr-x ...\",\n  \"exitCode\": 0,\n  \"cancelled\": false,\n  \"truncated\": false,\n  \"fullOutputPath\": null,\n  \"timestamp\": 1733234567890\n}\n```\n\n### Pièce jointe\n\n```json\n{\n  \"id\": \"img1\",\n  \"type\": \"image\",\n  \"fileName\": \"photo.jpg\",\n  \"mimeType\": \"image/jpeg\",\n  \"size\": 102400,\n  \"content\": \"base64-encoded-data...\",\n  \"extractedText\": null,\n  \"preview\": null\n}\n```\n\n## Exemple: client de base (Python)\n\n```python\nimport subprocess\nimport json\n\nproc = subprocess.Popen(\n    [\"pi\", \"--mode\", \"rpc\", \"--no-session\"],\n    stdin=subprocess.PIPE,\n    stdout=subprocess.PIPE,\n    text=True\n)\n\ndef send(cmd):\n    proc.stdin.write(json.dumps(cmd) + \"\\n\")\n    proc.stdin.flush()\n\ndef read_events():\n    for line in proc.stdout:\n        yield json.loads(line)\n\n# Send prompt\nsend({\"type\": \"prompt\", \"message\": \"Hello!\"})\n\n# Process events\nfor event in read_events():\n    if event.get(\"type\") == \"message_update\":\n        delta = event.get(\"assistantMessageEvent\", {})\n        if delta.get(\"type\") == \"text_delta\":\n            print(delta[\"delta\"], end=\"\", flush=True)\n    \n    if event.get(\"type\") == \"agent_end\":\n        print()\n        break\n```\n\n## Exemple: Client interactif (Node.js)\n\nVoir [`test/rpc-example.ts`](../test/rpc-example.ts) pour un exemple interactif complet, ou [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts) pour une implémentation client typée.\n\nPour un exemple complet de gestion du protocole d'interface utilisateur de l'extension, voir [`examples/rpc-extension-ui.ts`](../examples/rpc-extension-ui.ts) qui s'associe à l'extension [`examples/extensions/rpc-demo.ts`](../examples/extensions/rpc-demo.ts).\n\n```javascript\nconst { spawn } = require(\"child_process\");\nconst { StringDecoder } = require(\"string_decoder\");\n\nconst agent = spawn(\"pi\", [\"--mode\", \"rpc\", \"--no-session\"]);\n\nfunction attachJsonlReader(stream, onLine) {\n    const decoder = new StringDecoder(\"utf8\");\n    let buffer = \"\";\n\n    stream.on(\"data\", (chunk) => {\n        buffer += typeof chunk === \"string\" ? chunk : decoder.write(chunk);\n\n        while (true) {\n            const newlineIndex = buffer.indexOf(\"\\n\");\n            if (newlineIndex === -1) break;\n\n            let line = buffer.slice(0, newlineIndex);\n            buffer = buffer.slice(newlineIndex + 1);\n            if (line.endsWith(\"\\r\")) line = line.slice(0, -1);\n            onLine(line);\n        }\n    });\n\n    stream.on(\"end\", () => {\n        buffer += decoder.end();\n        if (buffer.length > 0) {\n            onLine(buffer.endsWith(\"\\r\") ? buffer.slice(0, -1) : buffer);\n        }\n    });\n}\n\nattachJsonlReader(agent.stdout, (line) => {\n    const event = JSON.parse(line);\n\n    if (event.type === \"message_update\") {\n        const { assistantMessageEvent } = event;\n        if (assistantMessageEvent.type === \"text_delta\") {\n            process.stdout.write(assistantMessageEvent.delta);\n        }\n    }\n});\n\n// Send prompt\nagent.stdin.write(JSON.stringify({ type: \"prompt\", message: \"Hello\" }) + \"\\n\");\n\n// Abort on Ctrl+C\nprocess.on(\"SIGINT\", () => {\n    agent.stdin.write(JSON.stringify({ type: \"abort\" }) + \"\\n\");\n});\n```","sourceFile":"rpc.md"},"sdk":{"title":"SDK","markdown":"> pi peut vous aider à utiliser le SDK. Demandez-lui de créer une intégration pour votre cas d'utilisation.\n\n\nLe SDK fournit un accès programmatique aux capacités de l'agent de pi. Utilisez-le pour intégrer pi dans d'autres applications, créer des interfaces personnalisées ou intégrer des flux de travail automatisés.\n\n**Exemples de cas d'utilisation:**\n- Créez une interface utilisateur personnalisée (Web, ordinateur de bureau, mobile)\n- Intégrer les capacités des agents dans les applications existantes\n- Créez des pipelines automatisés avec le raisonnement des agents\n- Créez des outils personnalisés qui génèrent des sous-agents\n- Tester le comportement de l'agent par programmation\n\nVoir [examples/sdk/](../examples/sdk/) pour des exemples de travail allant du contrôle minimal au contrôle total.\n\n## Démarrage rapide\n\n```typescript\nimport { createAgentSession, ModelRuntime, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create();\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n  modelRuntime,\n});\n\nsession.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\nawait session.prompt(\"What files are in the current directory?\");\n```\n\n## Installation\n\n```bash\nnpm install @earendil-works/pi-coding-agent\n```\n\nLe SDK est inclus dans le package principal. Aucune installation séparée n'est nécessaire.\n\n## Concepts de base\n\n### créerAgentSession()\n\nLa fonction d'usine principale pour un seul `AgentSession`.\n\n`createAgentSession()` utilise un `ResourceLoader` pour fournir des extensions, des compétences, un prompt templates, des thèmes et un context files. Si vous n'en fournissez pas, il utilise `DefaultResourceLoader` avec la découverte standard.\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\n// Minimal: defaults with DefaultResourceLoader\nconst { session } = await createAgentSession();\n\n// Custom: override specific options\nconst { session } = await createAgentSession({\n  model: myModel,\n  tools: [\"read\", \"bash\"],\n  sessionManager: SessionManager.inMemory(),\n});\n```\n\n### SessionAgent\n\nLa session gère le cycle de vie des agents, l'historique des messages, l'état du modèle, le compactage et le streaming des événements.\n\n```typescript\ninterface AgentSession {\n  // Send a prompt and wait for completion\n  prompt(text: string, options?: PromptOptions): Promise<void>;\n\n  // Queue messages during streaming\n  steer(text: string): Promise<void>;\n  followUp(text: string): Promise<void>;\n\n  // Subscribe to events (returns unsubscribe function)\n  subscribe(listener: (event: AgentSessionEvent) => void): () => void;\n\n  // Session info\n  sessionFile: string | undefined;\n  sessionId: string;\n\n  // Model control\n  setModel(model: Model): Promise<void>;\n  setThinkingLevel(level: ThinkingLevel): void;\n  cycleModel(): Promise<ModelCycleResult | undefined>;\n  cycleThinkingLevel(): ThinkingLevel | undefined;\n\n  // State access\n  agent: Agent;\n  model: Model | undefined;\n  thinkingLevel: ThinkingLevel;\n  messages: AgentMessage[];\n  isStreaming: boolean;\n\n  // In-place tree navigation within the current session file\n  navigateTree(targetId: string, options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string }): Promise<{ editorText?: string; cancelled: boolean }>;\n\n  // Compaction\n  compact(customInstructions?: string): Promise<CompactionResult>;\n  abortCompaction(): void;\n\n  // Abort current operation\n  abort(): Promise<void>;\n\n  // Cleanup\n  dispose(): void;\n}\n```\n\nRemplacement de session API tels que nouvelle session, reprise, fork et importation en direct sur `AgentSessionRuntime`, pas sur `AgentSession`.\n\n### createAgentSessionRuntime() et AgentSessionRuntime\n\nUtilisez le runtime API lorsque vous devez remplacer la session active et reconstruire l'état d'exécution lié au cwd.\nIl s'agit du même calque utilisé par les modes interactif, d'impression et RPC intégrés.\n\n`createAgentSessionRuntime()` prend une usine d'exécution plus la cible initiale cwd/session. L'usine se ferme sur les entrées fixes globales du processus, recrée les services liés au cwd pour le cwd effectif, résout les options de session par rapport à ces services et renvoie un résultat d'exécution complet.\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({\n      services,\n      sessionManager,\n      sessionStartEvent,\n    })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\n\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n```\n\n`AgentSessionRuntime` possède le remplacement du runtime actif sur:\n\n- `newSession()`\n- `switchSession()`\n- `fork()`\n- le clonage circule via `fork(entryId, { position: \"at\" })`\n- `importFromJsonl()`\n\nComportement important:\n\n- `runtime.session` changements après ces opérations\n- les abonnements aux événements sont attachés à un `AgentSession` spécifique, alors réabonnez-vous après le remplacement\n- si vous utilisez des extensions, appelez à nouveau le `runtime.session.bindExtensions(...)` pour la nouvelle session\n- la création renvoie un diagnostic sur `runtime.diagnostics`\n- si la création ou le remplacement du runtime échoue, la méthode est lancée et l'appelant décide comment le gérer\n\n```typescript\nlet session = runtime.session;\nlet unsubscribe = session.subscribe(() => {});\n\nawait runtime.newSession();\n\nunsubscribe();\nsession = runtime.session;\nunsubscribe = session.subscribe(() => {});\n```\n\n### Invites et mise en file d'attente des messages\n\n`PromptOptions` contrôle l'expansion rapide, le comportement de la file d'attente pendant la diffusion et les notifications rapides de contrôle en amont:\n\n```typescript\ninterface PromptOptions {\n  expandPromptTemplates?: boolean;\n  images?: ImageContent[];\n  streamingBehavior?: \"steer\" | \"followUp\";\n  source?: InputSource;\n  preflightResult?: (success: boolean) => void;\n}\n```\n\n`preflightResult` est appelé une fois par invocation `prompt()`:\n\n- `true` lorsque l'invite a été acceptée, mise en file d'attente ou traitée immédiatement\n- `false` lorsque le contrôle en amont rapide est rejeté avant l'acceptation\n\nIl se déclenche avant que `prompt()` ne soit résolu. `prompt()` n'est toujours résolu qu'une fois l'exécution complète acceptée terminée, y compris les tentatives. Les échecs après acceptation sont signalés via le flux normal d'événements et de messages, et non via `preflightResult(false)`.\n\nLa méthode `prompt()` gère prompt templates, les commandes d'extension et l'envoi de messages:\n\n```typescript\n// Basic prompt (when not streaming)\nawait session.prompt(\"What files are here?\");\n\n// With images\nawait session.prompt(\"What's in this image?\", {\n  images: [{ type: \"image\", source: { type: \"base64\", mediaType: \"image/png\", data: \"...\" } }]\n});\n\n// During streaming: must specify how to queue the message\nawait session.prompt(\"Stop and do this instead\", { streamingBehavior: \"steer\" });\nawait session.prompt(\"After you're done, also check X\", { streamingBehavior: \"followUp\" });\n```\n\n**Comportement:**\n- **Commandes d'extension** (par exemple, `/mycommand`): exécutez-les immédiatement, même pendant la diffusion. Ils gèrent leur propre interaction LLM via `pi.sendMessage()`.\n- **Basé sur des fichiers prompt templates** (à partir de `.md` fichiers): étendu à leur contenu avant l'envoi ou la mise en file d'attente.\n- **Pendant la diffusion sans `streamingBehavior`**: génère une erreur. Utilisez `steer()` ou `followUp()` directement, ou spécifiez l'option.\n- **`preflightResult(true)`**: signifie que l'invite a été acceptée, mise en file d'attente ou traitée immédiatement.\n- **`preflightResult(false)`**: signifie que le contrôle en amont est rejeté avant l'acceptation.\n\nPour une mise en file d'attente explicite pendant le streaming:\n\n```typescript\n// Queue a steering message for delivery after the current assistant turn finishes its tool calls\nawait session.steer(\"New instruction\");\n\n// Wait for agent to finish (delivered only when agent stops)\nawait session.followUp(\"After you're done, also do this\");\n```\n\n`steer()` et `followUp()` développent tous deux prompt templates basé sur un fichier, mais erreur sur les commandes d'extension (les commandes d'extension ne peuvent pas être mises en file d'attente).\n\n### Agent et état de l'agent\n\nLa classe `Agent` (à partir de `@earendil-works/pi-agent-core`) gère l'interaction principale du LLM. Accédez-y via `session.agent`.\n\n```typescript\n// Access current state\nconst state = session.agent.state;\n\n// state.messages: AgentMessage[] - conversation history\n// state.model: Model - current model\n// state.thinkingLevel: ThinkingLevel - current thinking level\n// state.systemPrompt: string - system prompt\n// state.tools: AgentTool[] - available tools\n// state.streamingMessage?: AgentMessage - current partial assistant message\n// state.errorMessage?: string - latest assistant error\n\n// Replace messages (useful for branching or restoration)\nsession.agent.state.messages = messages; // copies the top-level array\n\n// Replace tools\nsession.agent.state.tools = tools; // copies the top-level array\n\n// Wait for agent to finish processing\nawait session.agent.waitForIdle();\n```\n\n### Événements\n\nAbonnez-vous aux événements pour recevoir des sorties en streaming et des notifications de cycle de vie.\n\n```typescript\nsession.subscribe((event) => {\n  switch (event.type) {\n    // Streaming text from assistant\n    case \"message_update\":\n      if (event.assistantMessageEvent.type === \"text_delta\") {\n        process.stdout.write(event.assistantMessageEvent.delta);\n      }\n      if (event.assistantMessageEvent.type === \"thinking_delta\") {\n        // Thinking output (if thinking enabled)\n      }\n      break;\n    \n    // Tool execution\n    case \"tool_execution_start\":\n      console.log(`Tool: ${event.toolName}`);\n      break;\n    case \"tool_execution_update\":\n      // Streaming tool output\n      break;\n    case \"tool_execution_end\":\n      console.log(`Result: ${event.isError ? \"error\" : \"success\"}`);\n      break;\n    \n    // Message lifecycle\n    case \"message_start\":\n      // New message starting\n      break;\n    case \"message_end\":\n      // Message complete\n      break;\n    \n    // Agent lifecycle\n    case \"agent_start\":\n      // Agent started processing prompt\n      break;\n    case \"agent_end\":\n      // Agent finished (event.messages contains new messages)\n      break;\n    \n    // Turn lifecycle (one LLM response + tool calls)\n    case \"turn_start\":\n      break;\n    case \"turn_end\":\n      // event.message: assistant response\n      // event.toolResults: tool results from this turn\n      break;\n    \n    // Session events (queue, compaction, retry)\n    case \"queue_update\":\n      console.log(event.steering, event.followUp);\n      break;\n    case \"compaction_start\":\n    case \"compaction_end\":\n    case \"auto_retry_start\":\n    case \"auto_retry_end\":\n    case \"summarization_retry_scheduled\":\n    case \"summarization_retry_attempt_start\":\n    case \"summarization_retry_finished\":\n      break;\n  }\n});\n```\n\n## Référence des options\n\n### Annuaires\n\n```typescript\nconst { session } = await createAgentSession({\n  // Working directory for DefaultResourceLoader discovery\n  cwd: process.cwd(), // default\n  \n  // Global config directory\n  agentDir: \"~/.pi/agent\", // default (expands ~)\n});\n```\n\n`cwd` est utilisé par `DefaultResourceLoader` pour:\n- Extensions de projet (`.pi/extensions/`)\n- Compétences projet:\n  - `.pi/skills/`\n  - `.agents/skills/` dans `cwd` et les répertoires ancêtres (jusqu'à la racine du dépôt git ou la racine du système de fichiers lorsqu'il n'est pas dans un dépôt)\n- Invites du projet (`.pi/prompts/`)\n- Fichiers de contexte (`AGENTS.md` en remontant de cwd)\n- Dénomination du répertoire de session\n\n`agentDir` est utilisé par `DefaultResourceLoader` pour:\n- Extensions globales (`extensions/`)\n- Compétences globales:\n  - `skills/` sous `agentDir` (par exemple `~/.pi/agent/skills/`)\n  - `~/.agents/skills/`\n- Invites globales (`prompts/`)\n- Fichier de contexte global (`AGENTS.md`)\n- Paramètres (`settings.json`)\n- Modèles personnalisés (`models.json`)\n- Identifiants (`auth.json`)\n- Séances (`sessions/`)\n\nLorsque vous transmettez un `ResourceLoader` personnalisé, `cwd` et `agentDir` ne contrôlent plus la découverte des ressources. Ils influencent toujours la dénomination des sessions et la résolution du chemin d'outil.\n\n### Modèle\n\n```typescript\nimport { getModel } from \"@earendil-works/pi-ai\";\nimport { ModelRuntime } from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create();\n\n// Find specific built-in model (doesn't check if API key exists)\nconst opus = getModel(\"anthropic\", \"claude-opus-4-5\");\nif (!opus) throw new Error(\"Model not found\");\n\n// Find any model by provider/id, including custom models from models.json\n// (doesn't check if API key exists)\nconst customModel = modelRuntime.getModel(\"my-provider\", \"my-model\");\n\n// Get only models that have valid authentication configured\nconst available = await modelRuntime.getAvailable();\n\nconst { session } = await createAgentSession({\n  model: opus,\n  thinkingLevel: \"medium\", // off, minimal, low, medium, high, xhigh, max\n  \n  // Models for cycling (Ctrl+P in interactive mode)\n  scopedModels: [\n    { model: opus, thinkingLevel: \"high\" },\n    { model: haiku, thinkingLevel: \"off\" },\n  ],\n  \n  modelRuntime,\n});\n```\n\nSi aucun modèle n'est fourni:\n1. Essaie de restaurer à partir de la session (si vous continuez)\n2. Utilise les paramètres par défaut\n3. Revient au premier modèle disponible\n\nPour faire correspondre l'analyse du modèle CLI, utilisez les assistants de résolution exportés:\n\n```typescript\nimport {\n  resolveCliModel,\n  resolveModelScopeWithDiagnostics,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst cliModel = resolveCliModel({\n  cliModel: \"anthropic/claude-opus-4-5:high\",\n  modelRuntime,\n});\nif (cliModel.error) throw new Error(cliModel.error);\nif (cliModel.warning) console.warn(cliModel.warning);\n\nconst { scopedModels, diagnostics } = await resolveModelScopeWithDiagnostics(\n  [\"anthropic/*:high\", \"gpt-5\"],\n  modelRuntime,\n);\nfor (const diagnostic of diagnostics) {\n  console.warn(diagnostic.message);\n}\n```\n\n`resolveCliModel()` utilise tous les modèles enregistrés, donc la première configuration de style `--api-key` peut résoudre un modèle avant que l'authentification stockée n'existe. `resolveModelScopeWithDiagnostics()` correspond à la sémantique `--models` et `enabledModels` tout en renvoyant les avertissements au lieu de les imprimer.\n\n> Voir [examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts)\n\n### Touches API et OAuth\n\nPriorité de résolution d'authentification (gérée par `ModelRuntime`):\n1. Remplacements d'exécution (via `setRuntimeApiKey`, non persistant)\n2. Informations d'identification stockées dans `auth.json` (API keys ou OAuth jetons)\n3. Variables d'environnement (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.)\n4. Résolveur de secours (pour les clés de fournisseur personnalisées à partir de `models.json`)\n\n```typescript\nimport { InMemoryCredentialStore } from \"@earendil-works/pi-ai\";\nimport { createAgentSession, ModelRuntime } from \"@earendil-works/pi-coding-agent\";\n\n// Default: uses ~/.pi/agent/auth.json and ~/.pi/agent/models.json\nconst modelRuntime = await ModelRuntime.create();\n\n// Provider-owned auth methods and current status\nfor (const provider of modelRuntime.getProviders()) {\n  const status = await modelRuntime.checkAuth(provider.id);\n  console.log(provider.name, provider.auth, status);\n}\n\n// Runtime API key override (not persisted to disk)\nawait modelRuntime.setRuntimeApiKey(\"anthropic\", \"sk-my-temp-key\");\n\n// Custom credential and model locations\nconst customRuntime = await ModelRuntime.create({\n  authPath: \"/my/app/auth.json\",\n  modelsPath: \"/my/app/models.json\",\n});\n\n// Or inject any pi-ai CredentialStore\nconst credentials = new InMemoryCredentialStore();\nconst inMemoryRuntime = await ModelRuntime.create({ credentials });\n\nconst { session } = await createAgentSession({\n  modelRuntime: customRuntime,\n});\n```\n\n`login()`, `logout()`, `setRuntimeApiKey()` et `removeRuntimeApiKey()` sont résolus une fois que le catalogue mis en cache/intégré, la composition et l'instantané de disponibilité du fournisseur concerné sont cohérents localement. Ils n’attendent pas la fraîcheur du catalogue distant. Si les informations d'identification ont été validées mais que la synchronisation locale échoue, elles sont rejetées avec le `CredentialSynchronizationError` exporté; inspectez ses champs `providerId`, `operation`, `credential` et `cause` au lieu de réessayer aveuglément la mutation des informations d'identification.\n\nLes opérations publiques de modèle/d'authentification et `ModelRuntime.create({ signal })` acceptent les signaux d'abandon facultatifs et sont illimitées lorsqu'elles sont omises. SDK les applications ont leur propre politique de délai pour la fraîcheur du catalogue à distance:\n\n```typescript\nconst signal = AbortSignal.timeout(15_000);\nconst result = await modelRuntime.refresh({\n  providers: [\"anthropic\"],\n  signal,\n});\nif (result.aborted) console.warn(\"Catalog refresh timed out; using cached models\");\nfor (const [providerId, error] of result.errors) {\n  console.warn(`Could not refresh ${providerId}:`, error);\n}\n```\n\nUn échec ou un délai d'actualisation du réseau n'annule pas une opération d'identification réussie. `refresh()` démarre une nouvelle génération de fournisseur, il n'attend donc pas une ancienne actualisation bloquée et les générations obsolètes ne peuvent pas publier par la suite.\n\n> Voir [examples/sdk/09-api-keys-and-oauth.ts](../examples/sdk/09-api-keys-and-oauth.ts)\n\n### Invite système\n\nUtilisez un `ResourceLoader` pour remplacer l'invite du système:\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  systemPromptOverride: () => \"You are a helpful assistant.\",\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> Voir [examples/sdk/03-custom-prompt.ts](../examples/sdk/03-custom-prompt.ts)\n\n### Outils\n\nSpécifiez les outils intégrés à activer:\n\n- Noms des outils intégrés: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`\n- Intégrés par défaut: `read`, `bash`, `edit`, `write`\n- `noTools: \"all\"` désactive tous les outils\n- `noTools: \"builtin\"` désactive les éléments intégrés par défaut tout en gardant les extensions et les outils personnalisés activés\n- `excludeTools` désactive les noms spécifiques d'outils intégrés, d'extension ou personnalisés après l'application d'une liste autorisée `tools`\n\nL'outil `edit` renvoie `details.diff` pour l'affichage TUI de Pi et `details.patch` en tant que correctif unifié standard pour les consommateurs SDK.\n\n```typescript\nimport { createAgentSession } from \"@earendil-works/pi-coding-agent\";\n\n// Read-only mode\nconst { session } = await createAgentSession({\n  tools: [\"read\", \"grep\", \"find\", \"ls\"],\n});\n\n// Pick specific tools\nconst { session } = await createAgentSession({\n  tools: [\"read\", \"bash\", \"grep\"],\n});\n\n// Disable one tool while keeping the rest available\nconst { session } = await createAgentSession({\n  excludeTools: [\"ask_question\"],\n});\n```\n\n#### Outils avec cwd personnalisé\n\nLorsque vous transmettez un `cwd` personnalisé, `createAgentSession()` crée les outils intégrés sélectionnés pour ce cwd.\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst cwd = \"/path/to/project\";\n\n// Use default tools for custom cwd\nconst { session } = await createAgentSession({\n  cwd,\n  sessionManager: SessionManager.inMemory(cwd),\n});\n\n// Or pick specific tools for custom cwd\nconst { session } = await createAgentSession({\n  cwd,\n  tools: [\"read\", \"bash\", \"grep\"],\n  sessionManager: SessionManager.inMemory(cwd),\n});\n```\n\n> Voir [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### Outils personnalisés\n\n```typescript\nimport { Type } from \"typebox\";\nimport { createAgentSession, defineTool } from \"@earendil-works/pi-coding-agent\";\n\n// Inline custom tool\nconst myTool = defineTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"Does something useful\",\n  parameters: Type.Object({\n    input: Type.String({ description: \"Input value\" }),\n  }),\n  execute: async (_toolCallId, params) => ({\n    content: [{ type: \"text\", text: `Result: ${params.input}` }],\n    details: {},\n  }),\n});\n\n// Pass custom tools directly\nconst { session } = await createAgentSession({\n  customTools: [myTool],\n});\n```\n\nUtilisez `defineTool()` pour les définitions autonomes et les tableaux comme `customTools: [myTool]`. Inline `pi.registerTool({... })` déduit déjà correctement les types de paramètres.\n\nLes outils personnalisés transmis via `customTools` sont combinés avec des outils enregistrés par extension. Extensions chargé par le ResourceLoader peut également enregistrer des outils via `pi.registerTool()`.\n\nSi vous transmettez `tools`, incluez chaque nom d'outil personnalisé ou d'extension que vous souhaitez activer, par exemple `tools: [\"read\", \"bash\", \"my_tool\"]`.\n\n> Voir [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### Extensions\n\nLes Extensions sont chargés par les `ResourceLoader`. `DefaultResourceLoader` découvre les extensions des sources d'extension `~/.pi/agent/extensions/`, `.pi/extensions/` et settings.json.\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  additionalExtensionPaths: [\"/path/to/my-extension.ts\"],\n  extensionFactories: [\n    (pi) => {\n      pi.on(\"agent_start\", () => {\n        console.log(\"[Inline Extension] Agent starting\");\n      });\n    },\n  ],\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\nExtensions peut enregistrer des outils, s'abonner à des événements, ajouter des commandes, etc. Voir [extensions.md](extensions.md) pour le API complet.\n\n**Extensions en ligne nommées:** Par défaut, les usines en ligne s'affichent sous la forme `<inline:1>`, `<inline:2>`, etc. dans la liste de démarrage Extensions. Pour afficher un nom descriptif à la place, enveloppez la fabrique:\n\n```typescript\nimport type { InlineExtension } from \"@earendil-works/pi-coding-agent\";\n\nconst myProvider: InlineExtension = {\n  name: \"my-provider\",\n  factory: (pi) => {\n    pi.on(\"agent_start\", () => {\n      console.log(\"[my-provider] Agent starting\");\n    });\n  },\n};\n\nconst loader = new DefaultResourceLoader({\n  extensionFactories: [myProvider],\n});\n```\n\nCela s'affiche sous la forme `<inline:my-provider>` au lieu de `<inline:1>`. Les fonctions d'usine nues sont toujours acceptées pour des raisons de compatibilité ascendante.\n\n**Event Bus:** Extensions peut communiquer via `pi.events`. Passez un `eventBus` partagé à un `DefaultResourceLoader` si vous avez besoin d'émettre ou d'écouter de l'extérieur:\n\n```typescript\nimport { createEventBus, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst eventBus = createEventBus();\nconst loader = new DefaultResourceLoader({\n  eventBus,\n});\nawait loader.reload();\n\neventBus.on(\"my-extension:status\", (data) => console.log(data));\n```\n\n> Voir [examples/sdk/06-extensions.ts](../examples/sdk/06-extensions.ts) et [docs/extensions.md](extensions.md)\n\n### Skills\n\n```typescript\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  type Skill,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst customSkill: Skill = {\n  name: \"my-skill\",\n  description: \"Custom instructions\",\n  filePath: \"/path/to/SKILL.md\",\n  baseDir: \"/path/to\",\n  source: \"custom\",\n};\n\nconst loader = new DefaultResourceLoader({\n  skillsOverride: (current) => ({\n    skills: [...current.skills, customSkill],\n    diagnostics: current.diagnostics,\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> Voir [examples/sdk/04-skills.ts](../examples/sdk/04-skills.ts)\n\n### Fichiers contextuels\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  agentsFilesOverride: (current) => ({\n    agentsFiles: [\n      ...current.agentsFiles,\n      { path: \"/virtual/AGENTS.md\", content: \"# Guidelines\\n\\n- Be concise\" },\n    ],\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> Voir [examples/sdk/07-context-files.ts](../examples/sdk/07-context-files.ts)\n\n### Commandes barre oblique\n\n```typescript\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  type PromptTemplate,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst customCommand: PromptTemplate = {\n  name: \"deploy\",\n  description: \"Deploy the application\",\n  source: \"(custom)\",\n  content: \"# Deploy\\n\\n1. Build\\n2. Test\\n3. Deploy\",\n};\n\nconst loader = new DefaultResourceLoader({\n  promptsOverride: (current) => ({\n    prompts: [...current.prompts, customCommand],\n    diagnostics: current.diagnostics,\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> Voir [examples/sdk/08-prompt-templates.ts](../examples/sdk/08-prompt-templates.ts)\n\n### Gestion des sessions\n\nLes sessions utilisent une structure arborescente avec des liens `id`/`parentId`, permettant un branchement sur place.\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSession,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\n// In-memory (no persistence)\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n});\n\n// New persistent session\nconst { session: persisted } = await createAgentSession({\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\n// Continue most recent\nconst { session: continued, modelFallbackMessage } = await createAgentSession({\n  sessionManager: SessionManager.continueRecent(process.cwd()),\n});\nif (modelFallbackMessage) {\n  console.log(\"Note:\", modelFallbackMessage);\n}\n\n// Open specific file\nconst { session: opened } = await createAgentSession({\n  sessionManager: SessionManager.open(\"/path/to/session.jsonl\"),\n});\n\n// List sessions\nconst currentProjectSessions = await SessionManager.list(process.cwd());\nconst allSessions = await SessionManager.listAll(process.cwd());\n\n// Session replacement API for /new, /resume, /fork, /clone, and import flows.\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({\n      services,\n      sessionManager,\n      sessionStartEvent,\n    })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\n\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\n// Replace the active session with a fresh one\nawait runtime.newSession();\n\n// Replace the active session with another saved session\nawait runtime.switchSession(\"/path/to/session.jsonl\");\n\n// Replace the active session with a fork from a specific user entry\nawait runtime.fork(\"entry-id\");\n\n// Clone the active path through a specific entry\nawait runtime.fork(\"entry-id\", { position: \"at\" });\n```\n\n**Arborescence SessionManager API:**\n\n```typescript\nconst sm = SessionManager.open(\"/path/to/session.jsonl\");\n\n// Session listing\nconst currentProjectSessions = await SessionManager.list(process.cwd());\nconst allSessions = await SessionManager.listAll(process.cwd());\n\n// Tree traversal\nconst entries = sm.getEntries();        // All entries (excludes header)\nconst tree = sm.getTree();              // Full tree structure\nconst path = sm.getPath();              // Path from root to current leaf\nconst leaf = sm.getLeafEntry();         // Current leaf entry\nconst entry = sm.getEntry(id);          // Get entry by ID\nconst children = sm.getChildren(id);    // Direct children of entry\n\n// Labels\nconst label = sm.getLabel(id);          // Get label for entry\nsm.appendLabelChange(id, \"checkpoint\"); // Set label\n\n// Branching\nsm.branch(entryId);                     // Move leaf to earlier entry\nsm.branchWithSummary(id, \"Summary...\");  // Branch with context summary\nsm.createBranchedSession(leafId);       // Extract path to new file\n```\n\n> Voir [examples/sdk/11-sessions.ts](../examples/sdk/11-sessions.ts) et [Session Format](session-format.md)\n\n### Gestion des paramètres\n\n```typescript\nimport { createAgentSession, SettingsManager, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\n// Default: loads from files (global + project merged)\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.create(),\n});\n\n// With overrides\nconst settingsManager = SettingsManager.create();\nsettingsManager.applyOverrides({\n  compaction: { enabled: false },\n  retry: { enabled: true, maxRetries: 5 },\n});\nconst { session } = await createAgentSession({ settingsManager });\n\n// In-memory (no file I/O, for testing)\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),\n  sessionManager: SessionManager.inMemory(),\n});\n\n// Custom directories\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.create(\"/custom/cwd\", \"/custom/agent\"),\n});\n```\n\n**Usines statiques:**\n- `SettingsManager.create(cwd?, agentDir?)` - Charger à partir de fichiers\n- `SettingsManager.inMemory(settings?)` - Aucune E/S de fichier\n\n**Paramètres spécifiques au projet:**\n\nLes paramètres se chargent à partir de deux emplacements et fusionnent:\n1. Mondial: `~/.pi/agent/settings.json`\n2. Projet: `<cwd>/.pi/settings.json`\n\nLe projet remplace le global. Les objets imbriqués fusionnent les clés. Les setters modifient les paramètres globaux par défaut.\n\n**Sémantique de persistance et de gestion des erreurs:**\n\n- Les getters/setters de paramètres sont synchrones pour l’état en mémoire.\n- Les setters mettent en file d'attente les écritures persistantes de manière asynchrone.\n- Appelez `await settingsManager.flush()` lorsque vous avez besoin d'une limite de durabilité (par exemple, avant la sortie du processus ou avant d'affirmer le contenu du fichier dans les tests).\n- `SettingsManager` n'imprime pas les erreurs d'E/S des paramètres. Utilisez `settingsManager.drainErrors()` et signalez-les dans votre couche d'application.\n\n> Voir [examples/sdk/10-settings.ts](../examples/sdk/10-settings.ts)\n\n## Chargeur de ressources\n\nUtilisez `DefaultResourceLoader` pour découvrir des extensions, des compétences, des invites, des thèmes et context files.\n\n```typescript\nimport {\n  DefaultResourceLoader,\n  getAgentDir,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  cwd,\n  agentDir: getAgentDir(),\n});\nawait loader.reload();\n\nconst extensions = loader.getExtensions();\nconst skills = loader.getSkills();\nconst prompts = loader.getPrompts();\nconst themes = loader.getThemes();\nconst contextFiles = loader.getAgentsFiles().agentsFiles;\n```\n\n## Valeur de retour\n\n`createAgentSession()` renvoie:\n\n```typescript\ninterface CreateAgentSessionResult {\n  // The session\n  session: AgentSession;\n  \n  // Extensions result (for runner setup)\n  extensionsResult: LoadExtensionsResult;\n  \n  // Warning if session model couldn't be restored\n  modelFallbackMessage?: string;\n}\n\ninterface LoadExtensionsResult {\n  extensions: Extension[];\n  errors: Array<{ path: string; error: string }>;\n  runtime: ExtensionRuntime;\n}\n```\n\n## Exemple complet\n\n```typescript\nimport { getModel } from \"@earendil-works/pi-ai\";\nimport { Type } from \"typebox\";\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  defineTool,\n  ModelRuntime,\n  SessionManager,\n  SettingsManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create({\n  authPath: \"/custom/agent/auth.json\",\n  modelsPath: \"/custom/agent/models.json\",\n});\nif (process.env.MY_KEY) {\n  await modelRuntime.setRuntimeApiKey(\"anthropic\", process.env.MY_KEY);\n}\n\n// Inline tool\nconst statusTool = defineTool({\n  name: \"status\",\n  label: \"Status\",\n  description: \"Get system status\",\n  parameters: Type.Object({}),\n  execute: async () => ({\n    content: [{ type: \"text\", text: `Uptime: ${process.uptime()}s` }],\n    details: {},\n  }),\n});\n\nconst model = getModel(\"anthropic\", \"claude-opus-4-5\");\nif (!model) throw new Error(\"Model not found\");\n\n// In-memory settings with overrides\nconst settingsManager = SettingsManager.inMemory({\n  compaction: { enabled: false },\n  retry: { enabled: true, maxRetries: 2 },\n});\n\nconst loader = new DefaultResourceLoader({\n  cwd: process.cwd(),\n  agentDir: \"/custom/agent\",\n  settingsManager,\n  systemPromptOverride: () => \"You are a minimal assistant. Be concise.\",\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({\n  cwd: process.cwd(),\n  agentDir: \"/custom/agent\",\n\n  model,\n  thinkingLevel: \"off\",\n  modelRuntime,\n\n  tools: [\"read\", \"bash\", \"status\"],\n  customTools: [statusTool],\n  resourceLoader: loader,\n\n  sessionManager: SessionManager.inMemory(),\n  settingsManager,\n});\n\nsession.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\nawait session.prompt(\"Get status and list files.\");\n```\n\n## Modes d'exécution\n\nLe SDK exporte les utilitaires en mode exécution pour créer des interfaces personnalisées au-dessus de `createAgentSession()`:\n\n### Mode interactif\n\nMode interactif complet TUI avec éditeur, historique des discussions et toutes les commandes intégrées:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  InteractiveMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nconst mode = new InteractiveMode(runtime, {\n  migratedProviders: [],\n  modelFallbackMessage: undefined,\n  initialMessage: \"Hello\",\n  initialImages: [],\n  initialMessages: [],\n});\n\nawait mode.run();\n```\n\n### ModeImpression\n\nMode mono-coup: envoyer des invites, afficher le résultat, quitter:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  runPrintMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nawait runPrintMode(runtime, {\n  mode: \"text\",\n  initialMessage: \"Hello\",\n  initialImages: [],\n  messages: [\"Follow up\"],\n});\n```\n\n### exécuterRpcMode\n\nMode JSON-RPC pour l'intégration des sous-processus:\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  runRpcMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nawait runRpcMode(runtime);\n```\n\nVoir [RPC documentation](rpc.md) pour le protocole JSON.\n\n## RPC Mode alternatif\n\nPour une intégration basée sur des sous-processus sans construire avec le SDK, utilisez directement le CLI:\n\n```bash\npi --mode rpc --no-session\n```\n\nVoir [RPC documentation](rpc.md) pour le protocole JSON.\n\nLe SDK est préféré lorsque:\n- Vous voulez la sécurité du type\n- Vous êtes dans le même processus Node.js\n- Vous avez besoin d'un accès direct à l'état de l'agent\n- Vous souhaitez personnaliser les outils/extensions par programme\n\nLe mode RPC est préféré lorsque:\n- Vous intégrez depuis une autre langue\n- Vous souhaitez une isolation des processus\n- Vous créez un client indépendant de la langue\n\n## Exportations\n\nLe principal point d’entrée exporte:\n\n```typescript\n// Factory\ncreateAgentSession\ncreateAgentSessionRuntime\nAgentSessionRuntime\n\n// Auth and Models\nModelRuntime // implements pi-ai Models and owns credential storage\nModelRegistry // synchronous extension compatibility facade\nCredentialSynchronizationError\nresolveCliModel\nresolveModelScopeWithDiagnostics\n\n// Resource loading\nDefaultResourceLoader\ntype ResourceLoader\ncreateEventBus\n\n// Constants and helpers\nCONFIG_DIR_NAME\ndefineTool\ngetAgentDir\ngetPackageDir\ngetReadmePath\ngetDocsPath\ngetExamplesPath\n\n// Session management\nSessionManager\nSettingsManager\n\n// Tool factories\ncreateCodingTools\ncreateReadOnlyTools\ncreateReadTool, createBashTool, createEditTool, createWriteTool\ncreateGrepTool, createFindTool, createLsTool\n\n// Types\ntype CreateAgentSessionOptions\ntype CreateAgentSessionResult\ntype ExtensionFactory\ntype InlineExtension\ntype ExtensionAPI\ntype ToolDefinition\ntype Skill\ntype PromptTemplate\ntype Tool\n```\n\nPour les types d'extensions, voir [extensions.md](extensions.md) pour le API complet.","sourceFile":"sdk.md"},"security":{"title":"Sécurité","markdown":"Pi est un agent de codage local. Il s'exécute avec les autorisations du compte d'utilisateur qui le démarre et traite les fichiers accessibles en écriture par cet utilisateur comme à l'intérieur de la même limite de confiance locale.\n\n## Fiducie du projet\n\nLa confiance du projet contrôle si pi charge les paramètres, ressources, packages et extensions locaux du projet. Ce n'est pas un sandbox et cela ne limite pas ce que le modèle peut demander aux outils de faire après avoir commencé à travailler dans un répertoire.\n\nPi considère qu'un projet dispose de ressources qui nécessitent de la confiance lorsqu'il en trouve dans le répertoire de travail actuel:\n\n- `.pi/settings.json`\n- `.pi/extensions`, `.pi/skills`, `.pi/prompts` ou `.pi/themes`\n- `.pi/SYSTEM.md` ou `.pi/APPEND_SYSTEM.md`\n- projet `.agents/skills` dans le répertoire courant ou un répertoire ancêtre\n\nUn simple répertoire `.pi` ne compte pas comme une ressource de projet nécessitant une confiance.\n\nLorsqu'une session interactive démarre dans un projet avec des ressources qui nécessitent de la confiance et aucune décision enregistrée pour le répertoire actuel ou un répertoire parent, pi suit `defaultProjectTrust` des paramètres globaux. La valeur par défaut est `\"ask\"`, qui demande s'il faut faire confiance au projet lorsque l'interface utilisateur est disponible. Les décisions enregistrées sont stockées par répertoire canonique dans `~/.pi/agent/trust.json`, et la décision enregistrée la plus proche sur le chemin actuel ou parent s'applique avant la décision globale par défaut.\n\nFaire confiance à un projet permet à pi de charger les ressources du projet qui nécessitent une confiance, notamment:\n\n- `.pi/settings.json`\n- `.pi` ressources telles que les extensions, les compétences, prompt templates, les thèmes et les fichiers d'invite système\n- packages de projet manquants configurés via les paramètres du projet\n- extensions locales du projet et extensions gérées par les packages de projet\n\nLe déclin de la confiance ignore les ressources protégées. Les fichiers de contexte tels que `AGENTS.override.md`, `AGENTS.md` et `CLAUDE.md` sont chargés quelle que soit l'approbation du projet, sauf si le chargement du contexte est désactivé. Avant que la confiance ne soit résolue, pi ne charge que les extensions context files, utilisateur/globales et CLI `-e`. Les extensions utilisateur/global et CLI peuvent gérer l'événement `project_trust`; la première extension qui renvoie une décision oui/non est propriétaire de la décision.\n\nLes modes non interactifs (`-p`, `--mode json` et `--mode rpc`) n'affichent pas d'invite de confiance. Sans décision de confiance enregistrée applicable, `defaultProjectTrust: \"ask\"` et `\"never\"` ignorent ces ressources, tandis que `\"always\"` leur fait confiance. Utilisez `--approve`/`-a` ou `--no-approve`/`-na` pour remplacer la confiance du projet pour une exécution.\n\n## Pas de bac à sable intégré\n\nPi n'inclut pas de sandbox intégré. Les outils intégrés peuvent lire des fichiers, écrire des fichiers, modifier des fichiers et exécuter des commandes shell avec les autorisations du processus pi. Extensions sont des modules TypeScript qui s'exécutent avec les mêmes autorisations. Les installations de packages, les commandes shell, les serveurs de langage, les commandes de test et autres outils de développement se comportent comme des processus locaux ordinaires.\n\nC'est intentionnel. Pi est conçu pour fonctionner sur des arborescences sources locales, invoquer des chaînes d'outils de projet et s'intégrer à l'environnement de développement existant de l'utilisateur. Un sandbox partiel en cours serait facile à comprendre comme une limite de sécurité tout en dépendant du shell hôte, du système de fichiers, des gestionnaires de packages, des informations d'identification et du code d'extension. La véritable isolation doit provenir du système d’exploitation ou d’une frontière virtualisation/conteneur.\n\nLa confiance dans le projet n'est qu'une protection contre le chargement des entrées. Cela empêche un référentiel de modifier silencieusement les paramètres ou les extensions de pi avant que vous ne l'approuviez. Il ne sécurise pas le code non fiable, les invites non fiables ou la sortie de modèle non fiable. L'injection rapide à partir des fichiers du référentiel, des commentaires, de la documentation, de context files ou de la sortie de build est un risque attendu pour l'agent local et ne peut pas être évitée de manière fiable par pi.\n\n## Exécution de travaux non fiables ou non surveillés\n\nPour les référentiels non fiables, le code généré que vous n'avez pas l'intention de surveiller de près ou l'automatisation sans surveillance, exécutez pi dans un environnement confiné. Utilisez un conteneur, une VM, une micro-VM, un sandbox distant ou un sandbox contrôlé par une stratégie avec uniquement les fichiers et les informations d'identification requis pour la tâche.\n\nLes modèles courants sont documentés dans [Containerization](containerization.md):\n\n- exécuter l'ensemble du processus `pi` dans un conteneur/sandbox\n- exécutez l'hôte pi tout en acheminant l'exécution de l'outil intégré dans une micro-VM Gondolin\n- monter uniquement les chemins d'espace de travail auxquels l'agent doit accéder\n- évitez de monter l'hôte `~/.pi/agent` à moins que le conteneur ne doive accéder aux sessions, aux paramètres et aux informations d'identification de l'hôte\n- passez le minimum de API key requis ou utilisez des informations d'identification de courte durée\n- restreindre l'accès au réseau lorsque la tâche n'en a pas besoin\n- examiner les différences et les sorties avant de recopier les résultats vers des systèmes fiables\n\nSi vous montez en liaison un espace de travail hôte en lecture/écriture, les écritures depuis l'intérieur du conteneur ou de la VM peuvent toujours modifier les fichiers hôte. Utilisez des montages en lecture seule ou copiez des fichiers vers et depuis le sandbox lorsque vous avez besoin d'une protection plus renforcée contre les écritures involontaires.\n\n## Signaler des problèmes de sécurité\n\nPour signaler un problème de sécurité, suivez le référentiel [Security Policy](https://github.com/earendil-works/pi-mono/blob/main/SECURITY.md). N'ouvrez pas de problème public pour les rapports sensibles en matière de sécurité.\n\nLe comportement attendu de l'agent local, l'absence de sandbox intégré, l'injection rapide de contenu non fiable et le comportement des extensions ou compétences installées par l'utilisateur sont généralement en dehors des limites de sécurité, à moins que le rapport ne démontre un véritable contournement des limites de privilèges ou montre comment pi accorde un accès que l'utilisateur local n'avait pas déjà.","sourceFile":"security.md"},"session-format":{"title":"Format de fichier de session","markdown":"Les sessions sont stockées sous forme de fichiers JSONL (JSON Lines). Chaque ligne est un objet JSON avec un champ `type`. Les entrées de session forment une arborescence via les champs `id`/`parentId`, permettant un branchement sur place sans créer de nouveaux fichiers.\n\n## Emplacement du fichier\n\n```\n~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl\n```\n\nOù `<path>` est le répertoire de travail avec `/` remplacé par `-`.\n\n## Suppression de sessions\n\nLes sessions peuvent être supprimées en supprimant leurs fichiers `.jsonl` sous `~/.pi/agent/sessions/`.\n\nPi prend également en charge la suppression interactive de sessions à partir de `/resume` (sélectionnez une session et appuyez sur `Ctrl+D`, puis confirmez). Lorsqu'il est disponible, pi utilise le `trash` CLI pour éviter une suppression permanente.\n\n## Version de la séance\n\nLes sessions ont un champ de version dans l'en-tête:\n\n- **Version 1**: séquence d'entrée linéaire (héritée, migrée automatiquement au chargement)\n- **Version 2**: Arborescence avec liaison `id`/`parentId`\n- **Version 3**: rôle `hookMessage` renommé en `custom` (unification des extensions)\n\nLes sessions existantes sont automatiquement migrées vers la version actuelle (v3) une fois chargées.\n\n## Fichiers sources\n\nSource sur GitHub ([pi-mono](https://github.com/earendil-works/pi-mono)):\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) - Types d'entrée de session et SessionManager\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts) - Types de messages étendus (BashExecutionMessage, CustomMessage, etc.)\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts) - Types de messages de base (UserMessage, AssistantMessage, ToolResultMessage)\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts) - Type d'union AgentMessage\n\nPour les définitions TypeScript de votre projet, inspectez `node_modules/@earendil-works/pi-coding-agent/dist/` et `node_modules/@earendil-works/pi-ai/dist/`.\n\n## Types de messages\n\nLes entrées de session contiennent `AgentMessage` objets. Comprendre ces types est essentiel pour analyser les sessions et écrire des extensions.\n\n### Blocs de contenu\n\nLes messages contiennent des tableaux de blocs de contenu typés:\n\n```typescript\ninterface TextContent {\n  type: \"text\";\n  text: string;\n}\n\ninterface ImageContent {\n  type: \"image\";\n  data: string;      // base64 encoded\n  mimeType: string;  // e.g., \"image/jpeg\", \"image/png\"\n}\n\ninterface ThinkingContent {\n  type: \"thinking\";\n  thinking: string;\n}\n\ninterface ToolCall {\n  type: \"toolCall\";\n  id: string;\n  name: string;\n  arguments: Record<string, any>;\n}\n```\n\n### Types de messages de base (de pi-ai)\n\n```typescript\ninterface UserMessage {\n  role: \"user\";\n  content: string | (TextContent | ImageContent)[];\n  timestamp: number;  // Unix ms\n}\n\ninterface AssistantMessage {\n  role: \"assistant\";\n  content: (TextContent | ThinkingContent | ToolCall)[];\n  api: string;\n  provider: string;\n  model: string;\n  usage: Usage;\n  stopReason: \"stop\" | \"length\" | \"toolUse\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n  timestamp: number;\n}\n\ninterface ToolResultMessage {\n  role: \"toolResult\";\n  toolCallId: string;\n  toolName: string;\n  content: (TextContent | ImageContent)[];\n  details?: any;      // Tool-specific metadata\n  usage?: Usage;      // Nested LLM work performed by the tool\n  isError: boolean;\n  timestamp: number;\n}\n\ninterface Usage {\n  input: number;\n  output: number;\n  cacheRead: number;\n  cacheWrite: number;\n  totalTokens: number;\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n    total: number;\n  };\n}\n```\n\nLe type pi-ai `StopReason` exporté inclut également `\"pending\"`, mais cette valeur est réservée aux messages partiels dans les événements de streaming. Les messages du terminal `done`/`error` le remplacent par une raison d'achèvement avant que pi ne persiste le message de l'assistant, donc `\"pending\"` ne devrait jamais apparaître dans la session JSONL.\n\n### Types de messages étendus (de pi-coding-agent)\n\n```typescript\ninterface BashExecutionMessage {\n  role: \"bashExecution\";\n  command: string;\n  output: string;\n  exitCode: number | undefined;\n  cancelled: boolean;\n  truncated: boolean;\n  fullOutputPath?: string;\n  excludeFromContext?: boolean;  // true for !! prefix commands\n  timestamp: number;\n}\n\ninterface CustomMessage {\n  role: \"custom\";\n  customType: string;            // Extension identifier\n  content: string | (TextContent | ImageContent)[];\n  display: boolean;              // Show in TUI\n  details?: any;                 // Extension-specific metadata\n  timestamp: number;\n}\n\ninterface BranchSummaryMessage {\n  role: \"branchSummary\";\n  summary: string;\n  fromId: string;                // Entry we branched from\n  timestamp: number;\n}\n\ninterface CompactionSummaryMessage {\n  role: \"compactionSummary\";\n  summary: string;\n  tokensBefore: number;\n  timestamp: number;\n}\n```\n\n### AgentMessage Union\n\n```typescript\ntype AgentMessage =\n  | UserMessage\n  | AssistantMessage\n  | ToolResultMessage\n  | BashExecutionMessage\n  | CustomMessage\n  | BranchSummaryMessage\n  | CompactionSummaryMessage;\n```\n\n## Base d'entrée\n\nToutes les entrées (sauf `SessionHeader`) étendent `SessionEntryBase`:\n\n```typescript\ninterface SessionEntryBase {\n  type: string;\n  id: string;           // 8-char hex ID\n  parentId: string | null;  // Parent entry ID (null for first entry)\n  timestamp: string;    // ISO timestamp\n}\n```\n\n## Types d'entrée\n\n### En-tête de session\n\nPremière ligne du fichier. Métadonnées uniquement, ne faisant pas partie de l'arborescence (pas de `id`/`parentId`).\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\"}\n```\n\nPour les sessions avec un parent (créées via `/fork`, `/clone` ou `newSession({ parentSession })`):\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\",\"parentSession\":\"/path/to/original/session.jsonl\"}\n```\n\n### Entrée de message de session\n\nUn message dans la conversation. Le champ `message` contient un `AgentMessage`.\n\n```json\n{\"type\":\"message\",\"id\":\"a1b2c3d4\",\"parentId\":\"prev1234\",\"timestamp\":\"2024-12-03T14:00:01.000Z\",\"message\":{\"role\":\"user\",\"content\":\"Hello\"}}\n{\"type\":\"message\",\"id\":\"b2c3d4e5\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:00:02.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Hi!\"}],\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\"}}\n{\"type\":\"message\",\"id\":\"c3d4e5f6\",\"parentId\":\"b2c3d4e5\",\"timestamp\":\"2024-12-03T14:00:03.000Z\",\"message\":{\"role\":\"toolResult\",\"toolCallId\":\"call_123\",\"toolName\":\"bash\",\"content\":[{\"type\":\"text\",\"text\":\"output\"}],\"isError\":false}}\n```\n\n### ModèleChangeEntry\n\nÉmis lorsque l'utilisateur change de modèle en cours de session.\n\n```json\n{\"type\":\"model_change\",\"id\":\"d4e5f6g7\",\"parentId\":\"c3d4e5f6\",\"timestamp\":\"2024-12-03T14:05:00.000Z\",\"provider\":\"openai\",\"modelId\":\"gpt-4o\"}\n```\n\n### RéflexionNiveauChangeEntrée\n\nÉmis lorsque l'utilisateur change le niveau de réflexion/raisonnement.\n\n```json\n{\"type\":\"thinking_level_change\",\"id\":\"e5f6g7h8\",\"parentId\":\"d4e5f6g7\",\"timestamp\":\"2024-12-03T14:06:00.000Z\",\"thinkingLevel\":\"high\"}\n```\n\n### Entrée de compactage\n\nCréé lorsque le contexte est compacté. Stocke un résumé des messages précédents.\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"firstKeptEntryId\":\"c3d4e5f6\",\"tokensBefore\":50000}\n```\n\nLes compactages plus récents générés par le faisceau intègrent le contexte post-compactage conservé directement dans l'entrée, au lieu de `firstKeptEntryId`:\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"tokensBefore\":50000,\"retainedTail\":[{\"role\":\"user\",\"content\":\"latest request\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"latest reply\"}],\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\"}]}\n```\n\nChamps facultatifs:\n- `usage`: utilisation du LLM à partir de la génération du résumé; inclus dans le jeton de session et les totaux des coûts\n- `retainedTail`: `AgentMessage[]` matérialisé conservé après compactage. Ceci est facultatif uniquement pour des raisons de compatibilité descendante avec les anciennes sessions. Les compactages plus récents générés par le faisceau l'incluent afin que nous puissions reconstruire le contexte à partir de ce point de contrôle sans parcourir les anciennes entrées avant l'entrée de compactage.\n- `details`: données spécifiques à l'implémentation (par exemple, `{ readFiles: string[], modifiedFiles: string[] }` pour les données par défaut ou personnalisées pour les extensions)\n- `fromHook`: `true` si généré par une extension, `false`/`undefined` si généré par pi (nom de champ hérité)\n- `firstKeptEntryId`: pour la compatibilité avec l'ancien format de saisie.\n\n### BranchSummaryEntrée\n\nCréé lors du changement de branche via `/tree` avec un résumé généré par LLM de la branche gauche jusqu'à l'ancêtre commun. Capture le contexte du chemin abandonné.\n\n```json\n{\"type\":\"branch_summary\",\"id\":\"g7h8i9j0\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:15:00.000Z\",\"fromId\":\"f6g7h8i9\",\"summary\":\"Branch explored approach A...\"}\n```\n\nChamps facultatifs:\n- `usage`: utilisation du LLM à partir de la génération du résumé; inclus dans le jeton de session et les totaux des coûts\n- `details`: données de suivi de fichiers (`{ readFiles: string[], modifiedFiles: string[] }`) pour les données par défaut ou personnalisées pour les extensions\n- `fromHook`: `true` si généré par une extension, `false`/`undefined` si généré par pi (nom de champ hérité)\n\n### Entrée personnalisée\n\nPersistance de l’état d’extension. Ne participe PAS au contexte LLM.\n\n```json\n{\"type\":\"custom\",\"id\":\"h8i9j0k1\",\"parentId\":\"g7h8i9j0\",\"timestamp\":\"2024-12-03T14:20:00.000Z\",\"customType\":\"my-extension\",\"data\":{\"count\":42}}\n```\n\nUtilisez `customType` pour identifier les entrées de votre extension lors du rechargement. Le mode interactif peut restituer les entrées personnalisées via `pi.registerEntryRenderer(customType, renderer)`, mais elles ne participent toujours pas au contexte LLM.\n\n### Entrée de message personnalisé\n\nMessages injectés par extension qui participent au contexte LLM.\n\n```json\n{\"type\":\"custom_message\",\"id\":\"i9j0k1l2\",\"parentId\":\"h8i9j0k1\",\"timestamp\":\"2024-12-03T14:25:00.000Z\",\"customType\":\"my-extension\",\"content\":\"Injected context...\",\"display\":true}\n```\n\nChamps:\n- `content`: chaîne ou `(TextContent | ImageContent)[]` (identique à UserMessage)\n- `display`: `true` = afficher dans TUI avec un style distinct, `false` = masqué\n- `details`: métadonnées facultatives spécifiques à l'extension (non envoyées à LLM)\n\n### EntréeÉtiquette\n\nSignet/marqueur défini par l'utilisateur sur une entrée.\n\n```json\n{\"type\":\"label\",\"id\":\"j0k1l2m3\",\"parentId\":\"i9j0k1l2\",\"timestamp\":\"2024-12-03T14:30:00.000Z\",\"targetId\":\"a1b2c3d4\",\"label\":\"checkpoint-1\"}\n```\n\nRéglez `label` sur `undefined` pour effacer une étiquette.\n\n### EntréeInfoSession\n\nMétadonnées de session (par exemple, nom d'affichage défini par l'utilisateur). Définissez via `/name`, `--name` / `-n` ou `pi.setSessionName()` dans les extensions.\n\n```json\n{\"type\":\"session_info\",\"id\":\"k1l2m3n4\",\"parentId\":\"j0k1l2m3\",\"timestamp\":\"2024-12-03T14:35:00.000Z\",\"name\":\"Refactor auth module\"}\n```\n\nLe nom de la session est affiché dans le sélecteur de session (`/resume`) au lieu du premier message lorsqu'il est défini.\n\n## Structure arborescente\n\nLes entrées forment un arbre:\n- La première entrée a `parentId: null`\n- Chaque entrée suivante pointe vers son parent via `parentId`\n- Le branchement crée de nouveaux enfants à partir d'une entrée antérieure\n- La \"feuille\" est la position actuelle dans l'arborescence\n\n```\n[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf\n                                                            │\n                                                            └─ [branch_summary] ─── [user msg] ← alternate branch\n```\n\n## Création de contexte\n\n`buildContextEntries()` marche de la feuille actuelle à la racine, produisant la liste des entrées actives tout en respectant le compactage:\n\n1. Collecte toutes les entrées sur le chemin\n2. Si un `CompactionEntry` est sur le chemin:\n   - Inclut d'abord l'entrée de compactage\n   - Si `retainedTail` est présent, il agit comme un point de contrôle autonome et les entrées après le compactage sont incluses\n   - Sinon les entrées de `firstKeptEntryId` au compactage sont incluses\n   - Ensuite, les entrées après compactage sont incluses\n3. Préserve les entrées sans message dans la plage sélectionnée afin que le mode interactif puisse les restituer\n\n`buildSessionContext()` s'appuie sur cette liste d'entrées pour produire la liste de messages pour le LLM:\n\n1. Extrait les paramètres actuels du modèle et du niveau de réflexion du chemin complet\n2. Convertit les entrées sélectionnées en messages:\n   - `message` -> stocké `AgentMessage`\n   - `compaction` -> `compactionSummary` plus `retainedTail` lorsqu'il est présent\n   - `branch_summary` -> `branchSummary`\n   - `custom_message` -> `CustomMessage`\n   - `custom` -> pas de message contextuel\n\nCela fait que les compactages les plus récents agissent comme des points de contrôle autonomes. `retainedTail` est facultatif uniquement, donc les anciennes sessions qui stockent uniquement `firstKeptEntryId` continuent de se charger correctement.\n\n## Exemple d'analyse\n\n```typescript\nimport { readFileSync } from \"fs\";\n\nconst lines = readFileSync(\"session.jsonl\", \"utf8\").trim().split(\"\\n\");\n\nfor (const line of lines) {\n  const entry = JSON.parse(line);\n\n  switch (entry.type) {\n    case \"session\":\n      console.log(`Session v${entry.version ?? 1}: ${entry.id}`);\n      break;\n    case \"message\":\n      console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);\n      break;\n    case \"compaction\":\n      console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);\n      break;\n    case \"branch_summary\":\n      console.log(`[${entry.id}] Branch from ${entry.fromId}`);\n      break;\n    case \"custom\":\n      console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);\n      break;\n    case \"custom_message\":\n      console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);\n      break;\n    case \"label\":\n      console.log(`[${entry.id}] Label \"${entry.label}\" on ${entry.targetId}`);\n      break;\n    case \"model_change\":\n      console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);\n      break;\n    case \"thinking_level_change\":\n      console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);\n      break;\n  }\n}\n```\n\n## Gestionnaire de sessions API\n\nMéthodes clés pour travailler avec des sessions par programmation.\n\n### Méthodes de création statique\n- `SessionManager.create(cwd, sessionDir?)` - Nouvelle séance\n- `SessionManager.open(path, sessionDir?)` - Ouvrir le fichier de session existant\n- `SessionManager.continueRecent(cwd, sessionDir?)` - Continuer le plus récent ou créer un nouveau\n- `SessionManager.inMemory(cwd?)` - Aucune persistance du fichier\n- `SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)` - Session Fork d'un autre projet\n\n### Méthodes de liste statique\n- `SessionManager.list(cwd, sessionDir?, onProgress?)` - Liste des sessions pour un répertoire\n- `SessionManager.listAll(onProgress?)` - Répertorier toutes les sessions de tous les projets\n\n### Méthodes d'instance - Gestion de session\n- `newSession(options?)` - Démarrer une nouvelle session (options: `{ parentSession?: string }`)\n- `setSessionFile(path)` - Passer à un autre fichier de session\n- `createBranchedSession(leafId)` - Extraire la branche vers un nouveau fichier de session\n\n### Méthodes d'instance - Ajout (tous les ID d'entrée de retour)\n- `appendMessage(message)` - Ajouter un message\n- `appendThinkingLevelChange(level)` – Enregistrer le changement de pensée\n- `appendModelChange(provider, modelId)` - Enregistrer le changement de modèle\n- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Ajouter un compactage\n- `appendCustomEntry(customType, data?)` - État de l'extension (pas dans son contexte)\n- `appendSessionInfo(name)` - Définir le nom d'affichage de la session\n- `appendCustomMessageEntry(customType, content, display, details?)` - Message d'extension (en contexte)\n- `appendLabelChange(targetId, label)` - Définir/effacer l'étiquette\n\n### Méthodes d'instance - Navigation dans l'arborescence\n- `getLeafId()` - Position actuelle\n- `getLeafEntry()` - Obtenir l'entrée de feuille actuelle\n- `getEntry(id)` - Obtenez une entrée par pièce d'identité\n- `getBranch(fromId?)` - Marcher de l'entrée à la racine\n- `getTree()` - Obtenez l'arborescence complète\n- `getChildren(parentId)` - Obtenez des enfants directs\n- `getLabel(id)` - Obtenir l'étiquette pour l'entrée\n- `branch(entryId)` - Déplacer la feuille vers l'entrée précédente\n- `resetLeaf()` - Réinitialiser la feuille à null (avant toute entrée)\n- `branchWithSummary(entryId, summary, details?, fromHook?)` - Branche avec résumé du contexte\n\n### Méthodes d'instance - Contexte et informations\n- `buildContextEntries()` - Obtenez les entrées de branche actives avec le compactage appliqué\n- `buildSessionContext()` - Obtenez des messages, un niveau de réflexion et un modèle pour le LLM\n- `getEntries()` - Toutes les entrées (hors en-tête)\n- `getHeader()` - Métadonnées d'en-tête de session\n- `getSessionName()` - Obtenez le nom d'affichage de la dernière entrée session_info\n- `getCwd()` - Répertoire de travail\n- `getSessionDir()` - Répertoire de stockage de session\n- `getSessionId()` - UUID de session\n- `getSessionFile()` - Chemin du fichier de session (non défini pour la mémoire)\n- `isPersisted()` - Indique si la session est enregistrée sur le disque","sourceFile":"session-format.md"},"sessions":{"title":"Séances","markdown":"Pi enregistre les conversations sous forme de sessions afin que vous puissiez continuer à travailler, repartir des tours précédents et revisiter les chemins précédents.\n\n## Stockage de sessions\n\nLes sessions sont automatiquement enregistrées dans `~/.pi/agent/sessions/`, organisées par répertoire de travail. Chaque session est un fichier JSONL avec une arborescence.\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse and select from past sessions\npi --no-session        # Ephemeral mode; do not save\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Use a specific session file or partial session ID\npi --fork <path|id>    # Fork a session file or partial session ID into a new session\n```\n\nUtilisez `/session` en mode interactif pour voir le fichier de session en cours, l'ID de session, le nombre de messages, les jetons et le coût.\n\nPour le format de fichier JSONL et SessionManager API, voir [Session Format](session-format.md).\n\n## Commandes de session\n\n| Commande | Description |\n|---------|-------------|\n| `/resume` | Parcourir et sélectionner les sessions précédentes |\n| `/new` | Démarrer une nouvelle session |\n| `/name <name>` | Définir le nom d'affichage de la session en cours |\n| `/session` | Afficher les informations sur la session |\n| `/tree` | Naviguez dans le session tree actuel |\n| `/fork` | Créer une nouvelle session à partir d'un message utilisateur précédent |\n| `/clone` | Dupliquer la branche active actuelle dans une nouvelle session |\n| `/compact [prompt]` | Résumer le contexte plus ancien; voir [Compaction](compaction.md) |\n| `/export [file]` | Exporter la session au format HTML |\n| `/share` | Télécharger en tant qu'essentiel GitHub privé avec un lien HTML partageable |\n\n## Reprise et suppression de sessions\n\n`/resume` ouvre un sélecteur de session interactif pour le projet en cours. `pi -r` ouvre le même sélecteur au démarrage.\n\nDans le sélecteur, vous pouvez:\n\n- rechercher en tapant\n- basculer l'affichage du chemin avec Ctrl+P\n- basculer le mode de tri avec Ctrl+S\n- filtrer les sessions nommées avec Ctrl+N\n- renommer avec Ctrl+R\n- supprimer avec Ctrl+D, puis valider\n\nLorsqu'il est disponible, pi utilise le `trash` CLI pour la suppression au lieu de supprimer définitivement les fichiers.\n\n## Nommer les sessions\n\nUtilisez `/name <name>` pour définir un nom de session lisible par l'homme:\n\n```text\n/name Refactor auth module\n```\n\nDéfinissez le nom au démarrage avec `--name` ou `-n`:\n\n```bash\npi --name \"Refactor auth module\"\npi --name \"CI audit\" -p \"Review this build failure\"\n```\n\nLes sessions nommées sont plus faciles à trouver dans `/resume` et `pi -r`.\n\n## Branchement avec `/tree`\n\nLes sessions sont stockées sous forme d'arborescences. Chaque entrée a un `id` et un `parentId`, et la position actuelle est la feuille active. `/tree` vous permet de passer à n'importe quel point précédent et de continuer à partir de là sans créer de nouveau fichier.\n\n<p align=\"center\"><img src=\"images/tree-view.png\" alt=\"Tree View\" width=\"600\"></p>\n\nExemple de forme:\n\n```text\n├─ user: \"Hello, can you help...\"\n│  └─ assistant: \"Of course! I can...\"\n│     ├─ user: \"Let's try approach A...\"\n│     │  └─ assistant: \"For approach A...\"\n│     │     └─ user: \"That worked...\"  ← active\n│     └─ user: \"Actually, approach B...\"\n│        └─ assistant: \"For approach B...\"\n```\n\n### Contrôles de l'arborescence\n\n| Clé | Action |\n|-----|--------|\n| ↑/↓ | Naviguer dans les entrées visibles |\n| ←/→ | Page précédente/suivante |\n| Ctrl+←/Ctrl+→ ou Alt+←/Alt+→ | Pliez/dépliez ou sautez entre les segments de branche |\n| Maj+L | Définir ou effacer une étiquette sur l'entrée sélectionnée |\n| Maj+T | Basculer les horodatages des étiquettes |\n| Entrer | Sélectionner une entrée |\n| Échap/Ctrl+C | Annuler |\n| Ctrl+O | Mode de filtrage cyclique |\n\nLes modes de filtrage sont: par défaut, sans outils, utilisateur uniquement, étiqueté uniquement et tous. Configurez la valeur par défaut avec `treeFilterMode` dans [Settings](settings.md).\n\n### Comportement de sélection\n\nSélection d'un utilisateur ou d'un message personnalisé:\n\n1. Déplace la feuille vers le parent du message sélectionné.\n2. Place le texte du message sélectionné dans l'éditeur.\n3. Vous permet de modifier et de soumettre à nouveau, en créant une nouvelle branche.\n\nSélection d'un assistant, d'un outil, d'un compactage ou d'une autre entrée non utilisateur:\n\n1. Déplace la feuille vers cette entrée.\n2. Laisse l'éditeur vide.\n3. Vous permet de continuer à partir de ce point.\n\nLa sélection du message de l'utilisateur root réinitialise la feuille vers une conversation vide et place l'invite d'origine dans l'éditeur.\n\n## `/tree`, `/fork` et `/clone`\n\n| Fonctionnalité | `/tree` | `/fork` | `/clone` |\n|---------|---------|---------|----------|\n| Sortir | Même fichier de session | Nouveau fichier de session | Nouveau fichier de session |\n| Voir | Arbre complet | Sélecteur de message utilisateur | Branche active actuelle |\n| Utilisation typique | Explorer les alternatives en place | Démarrer une nouvelle session à partir d'une invite précédente | Dupliquer le travail en cours avant de continuer |\n| Résumé | Résumé de branche facultatif | Aucun | Aucun |\n\nUtilisez `/tree` lorsque vous souhaitez conserver les alternatives ensemble. Utilisez `/fork` ou `/clone` lorsque vous souhaitez un fichier de session séparé.\n\n## Sommaires des succursales\n\nLorsque `/tree` passe d'une branche à une autre, pi peut résumer la branche abandonnée et joindre ce résumé à la nouvelle position. Cela préserve le contexte important du chemin que vous avez quitté sans rejouer la branche entière.\n\nLorsque vous y êtes invité, choisissez l'une des options suivantes:\n\n1. pas de résumé\n2. résumer avec l'invite par défaut\n3. résumer avec des instructions de mise au point personnalisées\n\nVoir [Compaction](compaction.md) pour les composants internes et les crochets d'extension branch summarization.\n\n## Format de la séance\n\nLes fichiers de session sont JSONL et contiennent des entrées de message, des modifications de modèle, des changements de niveau de réflexion, des étiquettes, des compactages, des résumés de branche et des entrées d'extension.\n\nPour les analyseurs, les extensions, l'utilisation de SDK et le SessionManager complet API, voir [Session Format](session-format.md).","sourceFile":"sessions.md"},"settings":{"title":"Paramètres","markdown":"Pi utilise les fichiers de paramètres JSON avec les paramètres du projet remplaçant les paramètres globaux.\n\n| Emplacement | Portée |\n|----------|-------|\n| `~/.pi/agent/settings.json` | Global (tous les projets) |\n| `.pi/settings.json` | Projet (répertoire actuel) |\n\nModifiez directement ou utilisez `/settings` pour les options courantes.\n\n## Fiducie du projet\n\nAu démarrage interactif, pi demande avant de faire confiance à un dossier de projet qui contient des paramètres locaux du projet, des ressources ou un projet `.agents/skills` et n'a aucune décision enregistrée pour le dossier ou un dossier parent dans `~/.pi/agent/trust.json`. Faire confiance à un projet permet à pi de charger les ressources `.pi/settings.json` et `.pi`, d'installer les packages de projet manquants et d'exécuter des extensions de projet.\n\nLes modes non interactifs (`-p`, `--mode json` et `--mode rpc`) n'affichent pas d'invite de confiance. Sans décision de confiance enregistrée applicable, ils utilisent `defaultProjectTrust` à partir des paramètres globaux: `ask` (par défaut) et `never` ignorent ces ressources du projet, tandis que `always` leur fait confiance. Passez `--approve`/`-a` ou `--no-approve`/`-na` pour remplacer la confiance du projet pour une exécution.\n\nSi aucune extension ou décision enregistrée ne s'applique, `defaultProjectTrust` contrôle le comportement de repli. Réglez-le sur `\"ask\"`, `\"always\"` ou `\"never\"` dans `~/.pi/agent/settings.json`, ou modifiez-le avec `/settings`.\n\nLes commandes `pi config` et package utilisent le même flux de confiance du projet, sauf que `pi update` ne vous invite jamais. Passez `--approve` pour faire confiance aux paramètres locaux du projet pour une commande ou `--no-approve` pour les ignorer.\n\nUtilisez `/trust` en mode interactif pour enregistrer une décision d'approbation de projet pour les sessions futures, y compris l'approbation pour le dossier parent immédiat. Il écrit `~/.pi/agent/trust.json` uniquement; la session en cours n'est pas rechargée, alors redémarrez pi pour que les modifications prennent effet.\n\n## Tous les paramètres\n\n### Modèle et réflexion\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `defaultProvider` | chaîne | - | Fournisseur par défaut (par exemple, `\"anthropic\"`, `\"openai\"`) |\n| `defaultModel` | chaîne | - | ID de modèle par défaut |\n| `defaultThinkingLevel` | chaîne | - | `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"` |\n| `hideThinkingBlock` | booléen | `false` | Masquer les blocs de réflexion dans la sortie |\n| `showCacheMissNotices` | booléen | `false` | Afficher les notifications de transcription en cas d'échecs importants du cache d'invite |\n| `thinkingBudgets` | objet | - | Budgets de jetons personnalisés par niveau de réflexion |\n\n#### penserBudgets\n\n```json\n{\n  \"thinkingBudgets\": {\n    \"minimal\": 1024,\n    \"low\": 4096,\n    \"medium\": 10240,\n    \"high\": 32768\n  }\n}\n```\n\n### Interface utilisateur et affichage\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `theme` | chaîne | `\"dark\"` | Nom du thème (`\"dark\"`, `\"light\"` ou personnalisé) |\n| `externalEditor` | chaîne | `$VISUAL`, puis `$EDITOR`, puis Bloc-notes sous Windows ou `nano` ailleurs | Commande pour l'éditeur externe Ctrl+G; a priorité sur les variables d'environnement |\n| `quietStartup` | booléen | `false` | Masquer l'en-tête de démarrage |\n| `defaultProjectTrust` | chaîne | `\"ask\"` | Comportement de confiance du projet de secours: `\"ask\"`, `\"always\"` ou `\"never\"`. Paramètre global uniquement |\n| `collapseChangelog` | booléen | `false` | Afficher le journal des modifications condensé après les mises à jour |\n| `enableInstallTelemetry` | booléen | `true` | Envoyez un ping anonyme de version d'installation/mise à jour après la première installation ou les mises à jour détectées par le journal des modifications. Cela ne contrôle pas les vérifications de mise à jour |\n| `enableAnalytics` | booléen | `false` | Partage de données analytiques opt-in. Actuellement demandé uniquement lors de la première configuration expérimentale (`PI_EXPERIMENTAL=1`) |\n| `trackingId` | chaîne | - | Identifiant de suivi Analytics, généré lorsque `enableAnalytics` est activé |\n| `doubleEscapeAction` | chaîne | `\"tree\"` | Action pour la double évasion: `\"tree\"`, `\"fork\"` ou `\"none\"` |\n| `treeFilterMode` | chaîne | `\"default\"` | Filtre par défaut pour `/tree`: `\"default\"`, `\"no-tools\"`, `\"user-only\"`, `\"labeled-only\"`, `\"all\"` |\n| `editorPaddingX` | nombre | `0` | Remplissage horizontal pour l'éditeur d'entrée (0-3) |\n| `outputPad` | nombre | `1` | Remplissage horizontal pour les messages utilisateur, les messages de l'assistant et la réflexion (0 ou 1) |\n| `autocompleteMaxVisible` | nombre | `5` | Nombre maximum d'éléments visibles dans la liste déroulante de saisie semi-automatique (3-20) |\n| `showHardwareCursor` | booléen | `false` | Afficher le curseur du terminal pendant que TUI le positionne pour la prise en charge IME |\n| `tuiMode` | chaîne | `\"regular\"` | Mode interactif TUI: `\"regular\"` ou expérimental `\"fullscreen\"`. Les modifications de `/settings` s'appliquent immédiatement; `--tui-mode` remplace ce paramètre au démarrage |\n| `fullscreenExitOutput` | chaîne | `\"transcript\"` | Sortie de sortie plein écran: `\"transcript\"` imprime la transcription finale et l'indice de reprise, tandis que `\"resume-hint\"` restaure l'écran précédent et imprime uniquement l'indice de reprise. N'a aucun effet en mode normal TUI |\n| `fullscreenScrollbar` | chaîne | `\"auto\"` | Barre de défilement de transcription plein écran: `\"auto\"` l'affiche temporairement pendant le défilement, `\"always\"` réserve la colonne la plus à droite et la garde visible, et `\"hidden\"` la cache. N'a aucun effet en mode normal TUI |\n\nPour VS Code, incluez `--wait` pour que pi reprenne après la fermeture de l'éditeur:\n\n```json\n{\n  \"externalEditor\": \"code --wait\"\n}\n```\n\n### Contrôles de télémétrie et de mise à jour\n\n`enableInstallTelemetry` contrôle uniquement le ping anonyme d'installation/mise à jour vers `https://pi.dev/api/report-install`. La désactivation de la télémétrie ne désactive pas les vérifications de mise à jour; Pi peut toujours récupérer `https://pi.dev/api/latest-version` pour rechercher la dernière version.\n\nDéfinissez `PI_SKIP_VERSION_CHECK=1` pour désactiver la vérification de mise à jour de version Pi. Utilisez `--offline` ou `PI_OFFLINE=1` pour désactiver toutes les opérations réseau de démarrage décrites ici, y compris les vérifications de mise à jour, les vérifications de mise à jour des packages et la télémétrie d'installation/mise à jour.\n\n### Réseau\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `httpProxy` | chaîne | - | URL du proxy HTTP appliquée comme `HTTP_PROXY` et `HTTPS_PROXY`. Paramètre global uniquement. |\n\n```json\n{\n  \"httpProxy\": \"http://127.0.0.1:7890\"\n}\n```\n\n### Avertissements\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `warnings.anthropicExtraUsage` | booléen | `true` | Afficher un avertissement lorsque l'authentification de l'abonnement Anthropic peut utiliser une utilisation supplémentaire payante |\n\n```json\n{\n  \"warnings\": {\n    \"anthropicExtraUsage\": false\n  }\n}\n```\n\n### Compactage\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `compaction.enabled` | booléen | `true` | Activer le compactage automatique |\n| `compaction.reserveTokens` | nombre | `16384` | Jetons réservés à la réponse LLM |\n| `compaction.keepRecentTokens` | nombre | `20000` | Jetons récents à conserver (non résumés) |\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n### Résumé de la succursale\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `branchSummary.reserveTokens` | nombre | `16384` | Jetons réservés au branch summarization |\n| `branchSummary.skipPrompt` | booléen | `false` | Ignorer « Résumer la branche? » invite sur la navigation `/tree` (par défaut, aucun résumé) |\n\n### Réessayer\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `retry.enabled` | booléen | `true` | Activer les nouvelles tentatives automatiques au niveau de l'agent en cas d'erreurs passagères |\n| `retry.maxRetries` | nombre | `3` | Nombre maximal de nouvelles tentatives au niveau de l'agent |\n| `retry.baseDelayMs` | nombre | `2000` | Délai de base pour l'intervalle exponentiel au niveau de l'agent (2 s, 4 s, 8 s) |\n| `retry.provider.timeoutMs` | nombre | SDK par défaut | Fournisseur/SDK délai d'expiration de la demande en millisecondes |\n| `retry.provider.maxRetries` | nombre | `0` | Fournisseur/SDK nouvelle tentative |\n| `retry.provider.maxRetryDelayMs` | nombre | `60000` | Délai maximum demandé par le serveur avant échec (60 s) |\n\nLorsqu'un fournisseur demande un délai de nouvelle tentative supérieur à `retry.provider.maxRetryDelayMs`, la demande échoue immédiatement avec une erreur informative au lieu d'attendre silencieusement. Réglez-le sur `0` pour désactiver la limite.\n\nConservez `retry.provider.maxRetries` à `0` à moins que de nouvelles tentatives au niveau du fournisseur ne soient explicitement nécessaires. Le définir au-dessus de `0` peut faire en sorte que SDK/les tentatives du fournisseur traitent les erreurs hors limite d'utilisation avant que Pi ne les voient, ce qui peut bloquer l'agent jusqu'à ce que le quota du fournisseur soit réinitialisé dans certaines circonstances.\n\n```json\n{\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3,\n    \"baseDelayMs\": 2000,\n    \"provider\": {\n      \"timeoutMs\": 3600000,\n      \"maxRetries\": 0,\n      \"maxRetryDelayMs\": 60000\n    }\n  }\n}\n```\n\n### Livraison des messages\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `steeringMode` | chaîne | `\"one-at-a-time\"` | Comment les messages de pilotage sont envoyés: `\"all\"` ou `\"one-at-a-time\"` |\n| `followUpMode` | chaîne | `\"one-at-a-time\"` | Comment les messages de suivi sont envoyés: `\"all\"` ou `\"one-at-a-time\"` |\n| `transport` | chaîne | `\"auto\"` | Transport préféré pour les fournisseurs prenant en charge plusieurs transports: `\"sse\"`, `\"websocket\"`, `\"websocket-cached\"` ou `\"auto\"` |\n| `httpIdleTimeoutMs` | nombre | `300000` | Délai d'inactivité de l'en-tête/corps HTTP en millisecondes, également utilisé par les fournisseurs avec des délais d'inactivité de flux explicites. Réglez sur `0` pour désactiver. |\n| `websocketConnectTimeoutMs` | nombre | `15000` | Délai d'expiration de la connexion/ouverture de la liaison WebSocket en millisecondes pour les fournisseurs prenant en charge les transports WebSocket. Réglez sur `0` pour désactiver. |\n\n### Terminal et images\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `terminal.showImages` | booléen | `true` | Afficher les images dans le terminal (si pris en charge) |\n| `terminal.imageWidthCells` | nombre | `60` | Largeur d'image en ligne préférée dans les cellules terminales |\n| `terminal.clearOnShrink` | booléen | `false` | Effacer les lignes vides lorsque le contenu est réduit (peut provoquer un scintillement) |\n| `images.autoResize` | booléen | `true` | Redimensionnez les images à 2000x2000 maximum. S'applique aux pièces jointes `@file`, `read` et aux images renvoyées par les outils |\n| `images.blockImages` | booléen | `false` | Bloquer l'envoi de toutes les images à LLM |\n\n### Coquille\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `shellPath` | chaîne | - | Chemin d'accès au shell personnalisé (par exemple, pour Cygwin sous Windows); prend en charge un `~` de début pour le répertoire personnel |\n| `shellCommandPrefix` | chaîne | - | Préfixe pour chaque commande bash (par exemple, `\"shopt -s expand_aliases\"`) |\n| `npmCommand` | chaîne[] | - | Commande argv utilisée pour les opérations de recherche/installation de package npm (par exemple, `[\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]`) |\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n`npmCommand` est utilisé pour toutes les npm opérations du gestionnaire de packages, y compris les installations, les désinstallations et les installations de dépendances à l'intérieur des packages git. Les packages npm à l'échelle de l'utilisateur sont installés sous `~/.pi/agent/npm/`; Les packages npm à l'échelle du projet sont installés sous `.pi/npm/`. Utilisez les entrées de style argv exactement comme le processus doit être lancé. Lorsque `npmCommand` est configuré, les installations de dépendances de packages git utilisent plain `install` pour éviter les indicateurs spécifiques à npm dans les wrappers ou les gestionnaires de packages alternatifs.\n\n### Séances\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `sessionDir` | chaîne | - | Répertoire où sont stockés les fichiers de session. Accepte les chemins absolus ou relatifs, plus `~`. |\n\n```json\n{ \"sessionDir\": \".pi/sessions\" }\n```\n\nLorsque plusieurs sources spécifient un répertoire de session, la priorité est `--session-dir`, `PI_CODING_AGENT_SESSION_DIR`, puis `sessionDir` dans settings.json.\n\n### Modèle de cyclisme\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `enabledModels` | chaîne[] | - | Modèles de modèle pour le cyclisme Ctrl+P (même format que le drapeau `--models` CLI) |\n\n```json\n{\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\", \"gemini-2*\"]\n}\n```\n\n### Markdown\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `markdown.codeBlockIndent` | chaîne | `\"  \"` | Indentation pour les blocs de code |\n| `markdown.mermaid` | chaîne | `\"streaming\"` | Mode de rendu sirène: `\"off\"`, `\"final\"` ou `\"streaming\"` |\n\n### Ressources\n\nCes paramètres définissent où charger les extensions, les compétences, les invites et les thèmes.\n\nLes chemins en `~/.pi/agent/settings.json` se résolvent par rapport à `~/.pi/agent`. Les chemins dans `.pi/settings.json` se résolvent par rapport à `.pi`. Les chemins absolus et `~` sont pris en charge.\n\n| Paramètre | Taper | Défaut | Description |\n|---------|------|---------|-------------|\n| `packages` | tableau | `[]` | npm/git packages à partir desquels charger les ressources |\n| `extensions` | chaîne[] | `[]` | Chemins ou répertoires de fichiers d'extension locaux |\n| `skills` | chaîne[] | `[]` | Chemins ou répertoires de fichiers de compétences locaux |\n| `prompts` | chaîne[] | `[]` | Chemins ou répertoires de modèles d'invite locaux |\n| `themes` | chaîne[] | `[]` | Chemins ou répertoires de fichiers de thème locaux |\n| `enableSkillCommands` | booléen | `true` | Enregistrez les compétences sous forme de commandes `/skill:name` |\n\nLes tableaux prennent en charge les modèles globaux et les exclusions. Utilisez `!pattern` pour exclure. Utilisez `+path` pour forcer l'inclusion d'un chemin exact et `-path` pour forcer l'exclusion d'un chemin exact.\n\n#### forfaits\n\nLe formulaire de chaîne charge toutes les ressources d'un package:\n\n```json\n{\n  \"packages\": [\"pi-skills\", \"@org/my-extension\"]\n}\n```\n\nLe formulaire d'objet filtre les ressources à charger:\n\n```json\n{\n  \"packages\": [\n    {\n      \"source\": \"pi-skills\",\n      \"skills\": [\"brave-search\", \"transcribe\"],\n      \"extensions\": []\n    }\n  ]\n}\n```\n\nVoir [packages.md](packages.md) pour les détails de gestion des packages.\n\n## Exemple\n\n```json\n{\n  \"defaultProvider\": \"anthropic\",\n  \"defaultModel\": \"claude-sonnet-4-20250514\",\n  \"defaultThinkingLevel\": \"medium\",\n  \"theme\": \"dark\",\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  },\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3\n  },\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\"],\n  \"warnings\": {\n    \"anthropicExtraUsage\": true\n  },\n  \"packages\": [\"pi-skills\"]\n}\n```\n\n## Remplacements de projet\n\nLes paramètres du projet (`.pi/settings.json`) remplacent les paramètres globaux. Les objets imbriqués sont fusionnés:\n\n```json\n// ~/.pi/agent/settings.json (global)\n{\n  \"theme\": \"dark\",\n  \"compaction\": { \"enabled\": true, \"reserveTokens\": 16384 }\n}\n\n// .pi/settings.json (project)\n{\n  \"compaction\": { \"reserveTokens\": 8192 }\n}\n\n// Result\n{\n  \"theme\": \"dark\",\n  \"compaction\": { \"enabled\": true, \"reserveTokens\": 8192 }\n}\n```","sourceFile":"settings.md"},"shell-aliases":{"title":"Alias ​​de shell","markdown":"Pi exécute bash en mode non interactif (`bash -c`), qui ne développe pas les alias par défaut.\n\nPour activer vos alias shell, ajoutez à `~/.pi/agent/settings.json`:\n\n```json\n{\n  \"shellCommandPrefix\": \"shopt -s expand_aliases\\neval \\\"$(grep '^alias ' ~/.zshrc)\\\"\"\n}\n```\n\nAjustez le chemin (`~/.zshrc`, `~/.bashrc`, etc.) pour qu'il corresponde à la configuration de votre shell.","sourceFile":"shell-aliases.md"},"skills":{"title":"Skills","markdown":"> pi peut créer des compétences. Demandez-lui d'en créer un pour votre cas d'utilisation.\n\n\nSkills sont des packages de fonctionnalités autonomes que l'agent charge à la demande. Une compétence fournit des flux de travail spécialisés, des instructions de configuration, des scripts d'assistance et une documentation de référence pour des tâches spécifiques.\n\nPi implémente le [Agent Skills standard](https://agentskills.io/specification), avertissant de la plupart des violations mais restant indulgent. Pi permet aux noms de compétences de différer de leur répertoire parent même si la norme l'interdit; cette règle n'est pas optimale pour les répertoires de compétences partagés utilisés dans plusieurs harnais d'agents.\n\n## Table des matières\n\n- [Locations](#locations)\n- [How Skills Work](#how-skills-work)\n- [Skill Commands](#skill-commands)\n- [Skill Structure](#skill-structure)\n- [Frontmatter](#frontmatter)\n- [Validation](#validation)\n- [Example](#example)\n- [Skill Repositories](#skill-repositories)\n\n## Emplacements\n\n> **Sécurité:** Skills peut demander au modèle d'effectuer n'importe quelle action et peut inclure du code exécutable invoqué par le modèle. Examinez le contenu des compétences avant utilisation.\n\nPi charge les compétences de:\n\n- Mondial:\n  - `~/.pi/agent/skills/`\n  - `~/.agents/skills/`\n- Projet (uniquement une fois que le projet est approuvé):\n  - `.pi/skills/`\n  - `.agents/skills/` dans `cwd` et les répertoires ancêtres (jusqu'à la racine du dépôt git ou la racine du système de fichiers lorsqu'il n'est pas dans un dépôt)\n- Forfaits: `skills/` répertoires ou `pi.skills` entrées dans `package.json`\n- Paramètres: `skills` tableau avec des fichiers ou des répertoires\n- CLI: `--skill <path>` (répétable, additif même avec `--no-skills`)\n\nRègles de découverte:\n- Dans `~/.pi/agent/skills/` et `.pi/skills/`, les fichiers racine directe `.md` sont découverts en tant que compétences individuelles\n- Dans tous les emplacements de compétences, les répertoires contenant `SKILL.md` sont découverts de manière récursive\n- Dans `~/.agents/skills/` et le projet `.agents/skills/`, les fichiers racine `.md` sont ignorés\n\nDésactivez la découverte avec `--no-skills` (les chemins explicites `--skill` sont toujours chargés).\n\n### Utilisation de Skills à partir d'autres harnais\n\nPour utiliser les compétences de Claude Code ou OpenAI Codex, ajoutez leurs répertoires aux paramètres:\n\n```json\n{\n  \"skills\": [\n    \"~/.claude/skills\",\n    \"~/.codex/skills\"\n  ]\n}\n```\n\nPour les compétences Claude Code au niveau du projet, ajoutez à `.pi/settings.json`:\n\n```json\n{\n  \"skills\": [\"../.claude/skills\"]\n}\n```\n\n## Comment fonctionne Skills\n\n1. Au démarrage, pi analyse les emplacements des compétences et en extrait les noms et les descriptions\n2. L'invite système inclut les compétences disponibles au format XML selon le [specification](https://agentskills.io/integrate-skills)\n3. Lorsqu'une tâche correspond, l'agent utilise `read` pour charger le SKILL.md complet (les modèles ne le font pas toujours; utilisez l'invite ou `/skill:name` pour le forcer)\n4. L'agent suit les instructions, en utilisant des chemins relatifs pour référencer les scripts et les ressources.\n\nIl s'agit d'une divulgation progressive: seules les descriptions sont toujours contextuelles, les instructions complètes sont chargées à la demande.\n\n## Commandes de compétences\n\nSkills s'inscrire en tant que commandes `/skill:name`:\n\n```bash\n/skill:brave-search           # Load and execute the skill\n/skill:pdf-tools extract      # Load skill with arguments\n```\n\nLes arguments après la commande sont ajoutés au contenu de la compétence sous la forme `User: <args>`.\n\nBasculez les commandes de compétences via `/settings` en mode interactif ou en `settings.json`:\n\n```json\n{\n  \"enableSkillCommands\": true\n}\n```\n\n## Structure des compétences\n\nUne compétence est un répertoire avec un fichier `SKILL.md`. Tout le reste est de forme libre.\n\n```\nmy-skill/\n├── SKILL.md              # Required: frontmatter + instructions\n├── scripts/              # Helper scripts\n│   └── process.sh\n├── references/           # Detailed docs loaded on-demand\n│   └── api-reference.md\n└── assets/\n    └── template.json\n```\n\n### Format SKILL.md\n\n````markdown\n---\nname: my-skill\ndescription: What this skill does and when to use it. Be specific.\n---\n\n# My Skill\n\n## Setup\n\nRun once before first use:\n```bash\ncd /chemin/vers/compétence && npm installer\n```\n\n## Usage\n\n```bash\n./scripts/process.sh <input>\n```\n````\n\nUtilisez les chemins relatifs du répertoire de compétences:\n\n```markdown\nSee [the reference guide](references/REFERENCE.md) for details.\n```\n\n## Frontière\n\nPar le [Agent Skills specification](https://agentskills.io/specification#frontmatter-required):\n\n| Champ | Requis | Description |\n|-------|----------|-------------|\n| `name` | Oui | Max 64 caractères. Minuscules a-z, 0-9, tirets. Contrairement à la norme, Pi n'exige pas que cela corresponde au répertoire parent car cette exigence standard n'est pas optimale pour les répertoires de compétences partagés. |\n| `description` | Oui | Maximum 1024 caractères. À quoi sert la compétence et quand l’utiliser. |\n| `license` | Non | Nom de la licence ou référence au fichier groupé. |\n| `compatibility` | Non | Max 500 caractères. Exigences environnementales. |\n| `metadata` | Non | Mappage clé-valeur arbitraire. |\n| `allowed-tools` | Non | Liste délimitée par des espaces d'outils pré-approuvés (expérimentaux). |\n| `disable-model-invocation` | Non | Lorsque `true`, la compétence est masquée de l'invite du système. Les utilisateurs doivent utiliser `/skill:name`. |\n\n### Règles de nom\n\n- 1 à 64 caractères\n- Lettres minuscules, chiffres et traits d'union uniquement\n- Pas de tirets de début/fin\n- Pas de tirets consécutifs\nPi ne nécessite pas que le nom corresponde au répertoire parent. La norme Agent Skills le fait, mais cette exigence n'est pas optimale pour les répertoires de compétences partagés utilisés par plusieurs outils.\n\nValide: `pdf-processing`, `data-analysis`, `code-review`\nInvalide: `PDF-Processing`, `-pdf`, `pdf--processing`\n\n### Description Meilleures pratiques\n\nLa description détermine le moment où l'agent charge la compétence. Soyez précis.\n\nBien:\n```yaml\ndescription: Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs. Use when working with PDF documents.\n```\n\nPauvre:\n```yaml\ndescription: Helps with PDFs.\n```\n\n## Validation\n\nPi valide les compétences par rapport à la norme Agent Skills. La plupart des problèmes génèrent des avertissements mais chargent quand même la compétence:\n\n- Le nom dépasse 64 caractères ou contient des caractères non valides\n- Le nom commence/se termine par un trait d'union ou comporte des traits d'union consécutifs\n- La description dépasse 1 024 caractères\n\nLes champs de contenu inconnus sont ignorés.\n\n**Exception:** Skills avec une description manquante ne sont pas chargés.\n\nLes collisions de noms (même nom à différents endroits) avertissent et conservent la première compétence trouvée.\n\n## Exemple\n\n```\nbrave-search/\n├── SKILL.md\n├── search.js\n└── content.js\n```\n\n**COMPÉTENCE.md:**\n````markdown\n---\nname: brave-search\ndescription: Web search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content.\n---\n\n# Brave Search\n\n## Setup\n\n```bash\ncd /chemin/vers/brave-search && npm installer\n```\n\n## Search\n\n```bash\n./search.js \"query\" # Recherche de base\n./search.js \"query\" --content # Inclure le contenu de la page\n```\n\n## Extract Page Content\n\n```bash\n./content.js https://exemple.com\n```\n````\n\n## Référentiels de compétences\n\n- [Anthropic Skills](https://github.com/anthropics/skills) - Traitement de documents (docx, pdf, pptx, xlsx), développement web\n- [Pi Skills](https://github.com/badlogic/pi-skills) - Recherche sur le Web, automatisation du navigateur, Google APIs, transcription","sourceFile":"skills.md"},"terminal-setup":{"title":"Configuration du terminal","markdown":"Pi utilise le [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) pour une détection fiable des touches de modification. La plupart des terminaux modernes prennent en charge ce protocole, mais certains nécessitent une configuration.\n\n## Chaton, iTerm2\n\nTravaillez hors des sentiers battus.\n\n## Terminal Apple\n\nPi permet des rapports clés améliorés lorsqu'ils sont disponibles. Si Terminal.app envoie toujours un retour simple pour `Shift+Enter`, pi utilise un modificateur macOS local de secours pour traiter ce retour comme `Shift+Enter`.\n\nCette solution de secours ne fonctionne que lorsque pi s'exécute sur le même Mac que Terminal.app. Il ne peut pas détecter le clavier local sur le SSH distant.\n\n## Fantôme\n\nAjoutez à votre configuration Ghostty (`~/Library/Application Support/com.mitchellh.ghostty/config` sur macOS, `~/.config/ghostty/config` sur Linux):\n\n```\nkeybind = alt+backspace=text:\\x1b\\x7f\n```\n\nLes anciennes versions de Claude Code peuvent avoir ajouté ce mappage Ghostty:\n\n```\nkeybind = shift+enter=text:\\n\n```\n\nCe mappage envoie un octet de saut de ligne brut. À l'intérieur de pi, cela ne se distingue pas de `Ctrl+J`, donc tmux et pi ne voient plus un véritable événement clé `shift+enter`.\n\nSi Claude Code 2.x ou plus récent est la seule raison pour laquelle vous avez ajouté ce mappage, vous pouvez le supprimer, sauf si vous souhaitez utiliser Claude Code dans tmux, où il nécessite toujours ce mappage Ghostty.\n\nPi lie `Ctrl+J` comme alias de nouvelle ligne par défaut, donc `Shift+Enter` continue de fonctionner dans tmux via ce remappage sans configuration pi supplémentaire.\n\n## WezTerm\n\nWezTerm fonctionne généralement immédiatement pour `Shift+Enter` via xterm modifierOtherKeys. Pour utiliser explicitement le protocole du clavier Kitty, créez `~/.wezterm.lua`:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.enable_kitty_keyboard = true\nreturn config\n```\n\nSur macOS, WezTerm lie `Option+Enter` au plein écran par défaut. Pour utiliser `Option+Enter` pour la file d'attente de suivi pi, ajoutez ce remplacement de clé:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.keys = {\n  {\n    key = 'Enter',\n    mods = 'ALT',\n    action = wezterm.action.SendString('\\x1b[13;3u'),\n  },\n}\nreturn config\n```\n\nSi vous disposez déjà d'une table `config.keys`, ajoutez-y l'entrée.\n\nSur WSL, WezTerm peut nécessiter un curseur matériel visible pour le positionnement de la fenêtre candidate IME. Si les candidats CJK IME ne suivent pas le curseur de texte, définissez `PI_HARDWARE_CURSOR=1` avant d'exécuter pi ou définissez `showHardwareCursor` sur `true` dans les paramètres.\n\n## Empressement\n\nAlacritty fonctionne généralement immédiatement pour `Shift+Enter`. Sur macOS, `Option+Enter` peut apparaître sous la forme simple `Enter`. Pour utiliser `Option+Enter` pour la file d'attente de suivi pi, ajoutez à `~/.config/alacritty/alacritty.toml`:\n\n```toml\n[[keyboard.bindings]]\nkey = \"Enter\"\nmods = \"Alt\"\nchars = \"\\u001b[13;3u\"\n```\n\nRedémarrez Alacritty après avoir modifié la configuration.\n\n## VS Code (terminal intégré)\n\nVS Code 1.109.5 et les versions plus récentes activent par défaut le protocole de clavier Kitty dans le terminal intégré, donc `Shift+Enter` devrait fonctionner immédiatement.\n\nLes versions de VS Code antérieures à 1.109.5 nécessitent une liaison de touche de terminal explicite pour `Shift+Enter`.\n\n`keybindings.json` emplacements:\n- macOS: `~/Library/Application Support/Code/User/keybindings.json`\n- Linux: `~/.config/Code/User/keybindings.json`\n- Windows: `%APPDATA%\\\\Code\\\\User\\\\keybindings.json`\n\nAjouter à `keybindings.json`:\n\n```json\n{\n  \"key\": \"shift+enter\",\n  \"command\": \"workbench.action.terminal.sendSequence\",\n  \"args\": { \"text\": \"\\u001b[13;2u\" },\n  \"when\": \"terminalFocus\"\n}\n```\n\n## Terminal Windows\n\nAjoutez à `settings.json` (Ctrl+Shift+ ou Paramètres → Ouvrir le fichier JSON) pour transférer les touches Entrée modifiées que pi utilise:\n\n```json\n{\n  \"actions\": [\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;2u\" },\n      \"keys\": \"shift+enter\"\n    },\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;3u\" },\n      \"keys\": \"alt+enter\"\n    }\n  ]\n}\n```\n\n- `Shift+Enter` insère une nouvelle ligne.\n- Le terminal Windows lie `Alt+Enter` au plein écran par défaut. Cela empêche pi de recevoir `Alt+Enter` pour la file d'attente de suivi.\n- Le remappage de `Alt+Enter` en `sendInput` transmet le véritable accord clé à pi à la place.\n\nSi vous disposez déjà d'un tableau `actions`, ajoutez-y les objets. Si l'ancien comportement en plein écran persiste, fermez complètement et rouvrez le terminal Windows.\n\n## xfce4-terminal, terminateur\n\nCes terminaux ont une prise en charge limitée des séquences d'échappement. Les touches Entrée modifiées comme `Ctrl+Enter` et `Shift+Enter` ne peuvent pas être distinguées du simple `Enter`, empêchant les raccourcis clavier personnalisés tels que `submit: [\"ctrl+enter\"]` de fonctionner.\n\nPour une expérience optimale, utilisez un terminal prenant en charge le protocole du clavier Kitty:\n- [Kitty](https://sw.kovidgoyal.net/kitty/)\n- [Ghostty](https://ghostty.org/)\n- [WezTerm](https://wezfurlong.org/wezterm/)\n- [iTerm2](https://iterm2.com/)\n- [Alacritty](https://github.com/alacritty/alacritty) (nécessite une compilation avec la prise en charge du protocole Kitty)\n\n## IntelliJ IDEA (terminal intégré)\n\nLe terminal intégré a une prise en charge limitée des séquences d'échappement. Shift+Enter ne peut pas être distingué de Enter dans le terminal d'IntelliJ.\n\nSi vous souhaitez que le curseur matériel soit visible, définissez `PI_HARDWARE_CURSOR=1` avant d'exécuter pi (désactivé par défaut pour des raisons de compatibilité).\n\nPensez à utiliser un émulateur de terminal dédié pour la meilleure expérience.","sourceFile":"terminal-setup.md"},"termux":{"title":"Configuration Termux (Android)","markdown":"Pi fonctionne sur Android via [Termux](https://termux.dev/), un émulateur de terminal et un environnement Linux pour Android.\n\n## Conditions préalables\n\n1. Installez [Termux](https://github.com/termux/termux-app#installation) depuis GitHub ou F-Droid (pas Google Play, cette version est obsolète)\n2. Installez [Termux:API](https://github.com/termux/termux-api#installation) depuis GitHub ou F-Droid pour le presse-papiers et d'autres intégrations de périphériques\n\n## Installation\n\n```bash\n# Update packages\npkg update && pkg upgrade\n\n# Install dependencies\npkg install nodejs termux-api git\n\n# Install pi\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\n# Create config directory\nmkdir -p ~/.pi/agent\n\n# Run pi\npi\n```\n\n## Prise en charge du presse-papiers\n\nLes opérations du Presse-papiers utilisent `termux-clipboard-set` et `termux-clipboard-get` lorsqu'elles sont exécutées dans Termux. L'application Termux:API doit être installée pour que celles-ci fonctionnent.\n\nLe presse-papiers d'images n'est pas pris en charge sur Termux (la fonction de collage d'image `ctrl+v` ne fonctionnera pas).\n\n## Exemple AGENTS.md pour Termux\n\nCréez `~/.pi/agent/AGENTS.md` pour aider l'agent à comprendre l'environnement Termux:\n\n````markdown\n# Agent Environment: Termux on Android\n\n## Location\n- **OS**: Android (Termux terminal emulator)\n- **Home**: `/data/data/com.termux/files/home`\n- **Prefix**: `/data/data/com.termux/files/usr`\n- **Shared storage**: `/storage/emulated/0` (Downloads, Documents, etc.)\n\n## Opening URLs\n```bash\ntermux-open-url \"https://exemple.com\"\n```\n\n## Opening Files\n```bash\ntermux-open file.pdf # S'ouvre avec l'application par défaut\ntermux-open --chooser image.jpg # Choisir l'application\n```\n\n## Clipboard\n```bash\ntermux-clipboard-set \"texte\" # Copier\ntermux-clipboard-get # Coller\n```\n\n## Notifications\n```bash\ntermux-notification -t \"Titre\" -c \"Contenu\"\n```\n\n## Device Info\n```bash\ntermux-battery-status # Informations sur la batterie\ntermux-wifi-connectioninfo # Informations WiFi\ntermux-telephony-deviceinfo # Informations sur l'appareil\n```\n\n## Sharing\n```bash\ntermux-share -a send file.txt # Partager le fichier\n```\n\n## Other Useful Commands\n```bash\ntermux-toast \"message\" # Popup rapide de toast\ntermux-vibrate # Appareil vibrant\ntermux-tts-speak \"bonjour\" # Synthèse vocale\ntermux-camera-photo out.jpg # Prendre une photo\n```\n\n## Notes\n- Termux:API app must be installed for `termux-*` commands\n- Use `pkg install termux-api` for the command-line tools\n- Storage permission needed for `/storage/emulated/0` access\n````\n\n## Limites\n\n- **Pas de presse-papiers d'images**: Termux le presse-papiers API ne prend en charge que le texte\n- **Aucun binaire natif**: certaines dépendances natives facultatives (comme le module presse-papiers) ne sont pas disponibles sur Android ARM64 et sont ignorées lors de l'installation.\n- **Accès au stockage**: pour accéder aux fichiers dans `/storage/emulated/0` (téléchargements, etc.), exécutez `termux-setup-storage` une fois pour accorder les autorisations\n\n## Dépannage\n\n### Le presse-papier ne fonctionne pas\n\nAssurez-vous que les deux applications sont installées:\n1. Termux (depuis GitHub ou F-Droid)\n2. Termux: API (à partir de GitHub ou F-Droid)\n\nInstallez ensuite les outils CLI:\n```bash\npkg install termux-api\n```\n\n### Autorisation refusée pour le stockage partagé\n\nExécutez une fois pour accorder les autorisations de stockage:\n```bash\ntermux-setup-storage\n```\n\n### Node.js problèmes d'installation\n\nSi npm échoue, essayez de vider le cache:\n```bash\nnpm cache clean --force\n```","sourceFile":"termux.md"},"themes":{"title":"Thèmes","markdown":"> pi peut créer des thèmes. Demandez-lui d'en créer un pour votre configuration.\n\n\nLes thèmes sont des fichiers JSON qui définissent les couleurs du TUI.\n\n## Table des matières\n\n- [Locations](#locations)\n- [Selecting a Theme](#selecting-a-theme)\n- [Creating a Custom Theme](#creating-a-custom-theme)\n- [Theme Format](#theme-format)\n- [Color Tokens](#color-tokens)\n- [Color Values](#color-values)\n- [Tips](#tips)\n\n## Emplacements\n\nPi charge les thèmes de:\n\n- Intégré: `dark`, `light`\n- Mondial: `~/.pi/agent/themes/*.json`\n- Projet: `.pi/themes/*.json` (uniquement une fois le projet approuvé)\n- Forfaits: `themes/` répertoires ou `pi.themes` entrées dans `package.json`\n- Paramètres: `themes` tableau avec des fichiers ou des répertoires\n- CLI: `--theme <path>` (répétable)\n\nDésactivez la découverte avec `--no-themes`.\n\n## Sélection d'un thème\n\nSélectionnez un thème via `/settings` ou en `settings.json`:\n\n```json\n{\n  \"theme\": \"my-theme\"\n}\n```\n\nLors de la première exécution, pi détecte l'arrière-plan de votre terminal et prend par défaut la valeur `dark` ou `light`.\n\n## Création d'un thème personnalisé\n\n1. Créez un fichier de thème:\n\n```bash\nmkdir -p ~/.pi/agent/themes\nvim ~/.pi/agent/themes/my-theme.json\n```\n\n2. Définissez le thème avec toutes les couleurs requises (voir [Color Tokens](#color-tokens)):\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json\",\n  \"name\": \"my-theme\",\n  \"vars\": {\n    \"primary\": \"#00aaff\",\n    \"secondary\": 242\n  },\n  \"colors\": {\n    \"accent\": \"primary\",\n    \"border\": \"primary\",\n    \"borderAccent\": \"#00ffff\",\n    \"borderMuted\": \"secondary\",\n    \"success\": \"#00ff00\",\n    \"error\": \"#ff0000\",\n    \"warning\": \"#ffff00\",\n    \"muted\": \"secondary\",\n    \"dim\": 240,\n    \"text\": \"\",\n    \"thinkingText\": \"secondary\",\n    \"selectedBg\": \"#2d2d30\",\n    \"scrollbarThumb\": \"#555566\",\n    \"userMessageBg\": \"#2d2d30\",\n    \"userMessageText\": \"\",\n    \"customMessageBg\": \"#2d2d30\",\n    \"customMessageText\": \"\",\n    \"customMessageLabel\": \"primary\",\n    \"toolPendingBg\": \"#1e1e2e\",\n    \"toolSuccessBg\": \"#1e2e1e\",\n    \"toolErrorBg\": \"#2e1e1e\",\n    \"toolTitle\": \"primary\",\n    \"toolOutput\": \"\",\n    \"mdHeading\": \"#ffaa00\",\n    \"mdLink\": \"primary\",\n    \"mdLinkUrl\": \"secondary\",\n    \"mdCode\": \"#00ffff\",\n    \"mdCodeBlock\": \"\",\n    \"mdCodeBlockBorder\": \"secondary\",\n    \"mdQuote\": \"secondary\",\n    \"mdQuoteBorder\": \"secondary\",\n    \"mdHr\": \"secondary\",\n    \"mdListBullet\": \"#00ffff\",\n    \"toolDiffAdded\": \"#00ff00\",\n    \"toolDiffRemoved\": \"#ff0000\",\n    \"toolDiffContext\": \"secondary\",\n    \"syntaxComment\": \"secondary\",\n    \"syntaxKeyword\": \"primary\",\n    \"syntaxFunction\": \"#00aaff\",\n    \"syntaxVariable\": \"#ffaa00\",\n    \"syntaxString\": \"#00ff00\",\n    \"syntaxNumber\": \"#ff00ff\",\n    \"syntaxType\": \"#00aaff\",\n    \"syntaxOperator\": \"primary\",\n    \"syntaxPunctuation\": \"secondary\",\n    \"thinkingOff\": \"secondary\",\n    \"thinkingMinimal\": \"primary\",\n    \"thinkingLow\": \"#00aaff\",\n    \"thinkingMedium\": \"#00ffff\",\n    \"thinkingHigh\": \"#ff00ff\",\n    \"thinkingXhigh\": \"#ff0000\",\n    \"thinkingMax\": \"#ff0088\",\n    \"bashMode\": \"#ffaa00\"\n  }\n}\n```\n\n3. Sélectionnez le thème via `/settings`.\n\n**Rechargement à chaud:** Lorsque vous modifiez le fichier de thème personnalisé actuellement actif, pi le recharge automatiquement pour un retour visuel immédiat.\n\n## Format du thème\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json\",\n  \"name\": \"my-theme\",\n  \"vars\": {\n    \"blue\": \"#0066cc\",\n    \"gray\": 242\n  },\n  \"colors\": {\n    \"accent\": \"blue\",\n    \"muted\": \"gray\",\n    \"text\": \"\",\n    ...\n  }\n}\n```\n\n- `name` est obligatoire, doit être unique et ne doit pas contenir `/`.\n- `vars` est facultatif. Définissez ici les couleurs réutilisables, puis référencez-les dans `colors`.\n- `colors` doit définir les 51 jetons requis. `thinkingMax` est facultatif et revient à `thinkingXhigh`; `scrollbarThumb` est facultatif et revient à `selectedBg`.\n\nLe champ `$schema` permet la saisie semi-automatique et la validation de l'éditeur.\n\n## Jetons de couleur\n\nChaque thème doit définir les 51 jetons de couleur requis. `thinkingMax` et `scrollbarThumb` sont facultatifs pour la compatibilité avec les thèmes existants; lorsqu'ils sont omis, ils utilisent respectivement `thinkingXhigh` et `selectedBg`.\n\n### Interface utilisateur principale (11 couleurs)\n\n| Jeton | But |\n|-------|---------|\n| `accent` | Accent principal (logo, éléments sélectionnés, curseur) |\n| `border` | Frontières normales |\n| `borderAccent` | Bordures en surbrillance |\n| `borderMuted` | Bordures subtiles (éditeur) |\n| `success` | États de réussite |\n| `error` | États d'erreur |\n| `warning` | États d'avertissement |\n| `muted` | Texte secondaire |\n| `dim` | Texte tertiaire |\n| `text` | Texte par défaut (généralement `\"\"`) |\n| `thinkingText` | Texte du bloc de réflexion |\n\n### Arrière-plans et contenu (11 requis, 1 facultatif)\n\n| Jeton | But |\n|-------|---------|\n| `selectedBg` | Arrière-plan de la ligne sélectionnée |\n| `scrollbarThumb` | Arrière-plan du pouce de la barre de défilement plein écran; facultatif, revient à `selectedBg` |\n| `userMessageBg` | Contexte du message utilisateur |\n| `userMessageText` | Texte du message utilisateur |\n| `customMessageBg` | Arrière-plan du message d'extension |\n| `customMessageText` | Texte du message d'extension |\n| `customMessageLabel` | Libellé du message d'extension |\n| `toolPendingBg` | Boîte à outils (en attente) |\n| `toolSuccessBg` | Boîte à outils (succès) |\n| `toolErrorBg` | Boîte à outils (erreur) |\n| `toolTitle` | Titre de l'outil |\n| `toolOutput` | Texte de sortie de l'outil |\n\n### Markdown (10 couleurs)\n\n| Jeton | But |\n|-------|---------|\n| `mdHeading` | Rubriques |\n| `mdLink` | Texte du lien |\n| `mdLinkUrl` | URL du lien |\n| `mdCode` | Code en ligne |\n| `mdCodeBlock` | Contenu du bloc de code |\n| `mdCodeBlockBorder` | Clôtures de blocs de code |\n| `mdQuote` | Texte de citation |\n| `mdQuoteBorder` | Bordure de citation de bloc |\n| `mdHr` | Règle horizontale |\n| `mdListBullet` | Liste des puces |\n\n### Différents d'outils (3 couleurs)\n\n| Jeton | But |\n|-------|---------|\n| `toolDiffAdded` | Lignes ajoutées |\n| `toolDiffRemoved` | Lignes supprimées |\n| `toolDiffContext` | Lignes de contexte |\n\n### Mise en évidence de la syntaxe (9 couleurs)\n\n| Jeton | But |\n|-------|---------|\n| `syntaxComment` | Commentaires |\n| `syntaxKeyword` | Mots-clés |\n| `syntaxFunction` | Noms des fonctions |\n| `syntaxVariable` | Variables |\n| `syntaxString` | Cordes |\n| `syntaxNumber` | Nombres |\n| `syntaxType` | Espèces |\n| `syntaxOperator` | Opérateurs |\n| `syntaxPunctuation` | Ponctuation |\n\n### Bordures du niveau de réflexion (6 obligatoires, 1 facultative)\n\nCouleurs de bordure de l'éditeur indiquant le niveau de réflexion (hiérarchie visuelle de subtil à proéminent):\n\n| Jeton | But |\n|-------|---------|\n| `thinkingOff` | Penser |\n| `thinkingMinimal` | Pensée minimale |\n| `thinkingLow` | Faible réflexion |\n| `thinkingMedium` | Pensée moyenne |\n| `thinkingHigh` | Haute réflexion |\n| `thinkingXhigh` | Réflexion très élevée |\n| `thinkingMax` | Réflexion maximale; facultatif, revient à `thinkingXhigh` |\n\n### Mode Bash (1 couleur)\n\n| Jeton | But |\n|-------|---------|\n| `bashMode` | Bordure de l'éditeur en mode bash (préfixe `!`) |\n\n### Exportation HTML (facultatif)\n\nLa section `export` contrôle les couleurs pour la sortie HTML `/export`. En cas d'omission, les couleurs sont dérivées de `userMessageBg`.\n\n```json\n{\n  \"export\": {\n    \"pageBg\": \"#18181e\",\n    \"cardBg\": \"#1e1e24\",\n    \"infoBg\": \"#3c3728\"\n  }\n}\n```\n\n## Valeurs de couleur\n\nQuatre formats sont pris en charge:\n\n| Format | Exemple | Description |\n|--------|---------|-------------|\n| Hex | `\"#ff0000\"` | RVB hexadécimal à 6 chiffres |\n| 256 couleurs | `39` | Index de palette xterm de 256 couleurs (0-255) |\n| Variable | `\"primary\"` | Référence à une entrée `vars` |\n| Défaut | `\"\"` | Couleur par défaut du terminal |\n\n### Palette de 256 couleurs\n\n- `0-15`: couleurs ANSI de base (en fonction du terminal)\n- `16-231`: cube RVB 6×6×6 (`16 + 36×R + 6×G + B` où R, V, B sont 0-5)\n- `232-255`: rampe en niveaux de gris\n\n### Compatibilité des terminaux\n\nPi utilise des couleurs RVB 24 bits. La plupart des terminaux modernes le prennent en charge (iTerm2, Kitty, WezTerm, Windows Terminal, VS Code). Pour les terminaux plus anciens ne prenant en charge que 256 couleurs, pi revient à l'approximation la plus proche.\n\nVérifiez la prise en charge des couleurs vraies:\n\n```bash\necho $COLORTERM  # Should output \"truecolor\" or \"24bit\"\n```\n\n## Conseils\n\n**Terminaux sombres:** Utilisez des couleurs vives et saturées avec un contraste plus élevé.\n\n**Bornes claires:** Utilisez des couleurs plus sombres et atténuées avec un contraste plus faible.\n\n**Harmonie des couleurs:** Commencez par une palette de base (Nord, Gruvbox, Tokyo Night), définissez-la en `vars` et référencez-la de manière cohérente.\n\n**Test:** Vérifiez votre thème avec différents types de messages, états d'outils, contenu de démarque et texte long renvoyé à la ligne.\n\n**Code VS:** Réglez `terminal.integrated.minimumContrastRatio` sur `1` pour des couleurs précises.\n\n## Exemples\n\nVoir les thèmes intégrés:\n- [dark.json](../src/modes/interactive/theme/dark.json)\n- [light.json](../src/modes/interactive/theme/light.json)","sourceFile":"themes.md"},"tmux":{"title":"tmux Configuration","markdown":"Pi fonctionne à l'intérieur de tmux, mais tmux supprime par défaut les informations de modificateur de certaines touches. Sans configuration, `Shift+Enter` et `Ctrl+Enter` sont généralement impossibles à distinguer du simple `Enter`.\n\n## Configuration recommandée\n\nAjouter à `~/.tmux.conf`:\n\n```tmux\nset -g extended-keys on\nset -g extended-keys-format csi-u\n```\n\nPuis redémarrez complètement tmux:\n\n```bash\ntmux kill-server\ntmux\n```\n\nPi demande automatiquement des rapports de touches étendus lorsque le protocole du clavier Kitty n'est pas disponible. Avec `extended-keys-format csi-u`, tmux transmet les clés modifiées au format CSI-u, qui est la configuration la plus fiable. L'option `extended-keys-format` nécessite tmux 3.5 ou version ultérieure.\n\n## Pourquoi `csi-u` est recommandé\n\nAvec seulement:\n\n```tmux\nset -g extended-keys on\n```\n\ntmux est par défaut `extended-keys-format xterm`. Lorsqu'une application demande un rapport de clé étendu, les clés modifiées sont transmises au format xterm `modifyOtherKeys` tel que:\n\n- `Ctrl+C` → `\\x1b[27;5;99~`\n- `Ctrl+D` → `\\x1b[27;5;100~`\n- `Ctrl+Enter` → `\\x1b[27;5;13~`\n\nAvec `extended-keys-format csi-u`, les mêmes clés sont transmises comme:\n\n- `Ctrl+C` → `\\x1b[99;5u`\n- `Ctrl+D` → `\\x1b[100;5u`\n- `Ctrl+Enter` → `\\x1b[13;5u`\n\nPi prend en charge les deux formats, mais `csi-u` est la configuration tmux recommandée.\n\n## Ce que cela corrige\n\nSans les touches étendues tmux, les touches Entrée modifiées se réduisent aux séquences héritées:\n\n| Clé | Sans clés externes | Avec `csi-u` |\n|-----|-----------------|--------------|\n| Entrer | `\\r` | `\\r` |\n| Maj+Entrée | `\\r` | `\\x1b[13;2u` |\n| Ctrl+Entrée | `\\r` | `\\x1b[13;5u` |\n| Alt/Option+Entrée | `\\x1b\\r` | `\\x1b[13;3u` |\n\nCela affecte les raccourcis clavier par défaut (`Enter` pour soumettre, `Shift+Enter` pour une nouvelle ligne) et tous les raccourcis clavier personnalisés utilisant Entrée modifiée.\n\n## Exigences\n\n- tmux 3.5 ou version ultérieure pour `extended-keys-format csi-u` (exécutez `tmux -V` pour vérifier)\n- Un émulateur de terminal prenant en charge les clés étendues (Ghostty, Kitty, iTerm2, WezTerm, Windows Terminal)\n\nAvec tmux 3.2 à 3.4, omettez `extended-keys-format csi-u`; Pi prend toujours en charge le format xterm `modifyOtherKeys` par défaut de tmux.","sourceFile":"tmux.md"},"tui":{"title":"TUI Composants","markdown":"> pi peut créer TUI composants. Demandez-lui d'en créer un pour votre cas d'utilisation.\n\n\nExtensions et les outils personnalisés peuvent restituer des composants TUI personnalisés pour les interfaces utilisateur interactives. Cette page couvre le système de composants et les blocs de construction disponibles.\n\n**Source:** [`@earendil-works/pi-tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui)\n\n## Interface des composants\n\nTous les composants implémentent:\n\n```typescript\ninterface Component {\n  render(width: number): string[];\n  handleInput?(data: string): void;\n  wantsKeyRelease?: boolean;\n  invalidate(): void;\n}\n```\n\n| Méthode | Description |\n|--------|-------------|\n| `render(width)` | Renvoie un tableau de chaînes (une par ligne). Chaque ligne **ne doit pas dépasser `width`**. |\n| `handleInput?(data)` | Recevoir une entrée au clavier lorsque le composant a le focus. |\n| `wantsKeyRelease?` | Si c'est vrai, le composant reçoit les événements de version de clé (protocole Kitty). Par défaut: faux. |\n| `invalidate()` | Effacer l'état de rendu mis en cache. Appelé aux changements de thème. |\n\nLe TUI ajoute une réinitialisation complète SGR et une réinitialisation OSC 8 à la fin de chaque ligne rendue. Les styles ne s’étendent pas sur les lignes. Si vous émettez du texte multiligne avec style, réappliquez les styles par ligne ou utilisez `wrapTextWithAnsi()` afin que les styles soient préservés pour chaque ligne renvoyée à la ligne.\n\n## Interface focalisable (prise en charge IME)\n\nLes composants qui affichent un curseur de texte et nécessitent la prise en charge IME (Input Method Editor) doivent implémenter l'interface `Focusable`:\n\n```typescript\nimport { CURSOR_MARKER, type Component, type Focusable } from \"@earendil-works/pi-tui\";\n\nclass MyInput implements Component, Focusable {\n  focused: boolean = false;  // Set by TUI when focus changes\n  \n  render(width: number): string[] {\n    const marker = this.focused ? CURSOR_MARKER : \"\";\n    // Emit marker right before the fake cursor\n    return [`> ${beforeCursor}${marker}\\x1b[7m${atCursor}\\x1b[27m${afterCursor}`];\n  }\n}\n```\n\nLorsqu'un composant `Focusable` a le focus, TUI:\n1. Définit `focused = true` sur le composant\n2. Analyse la sortie rendue pour `CURSOR_MARKER` (une séquence d'échappement APC de largeur nulle)\n3. Positionne le curseur du terminal matériel à cet emplacement\n4. Affiche le curseur matériel uniquement lorsque `showHardwareCursor` est activé\n\nLe curseur reste masqué par défaut. Cela conserve le faux rendu du curseur, tout en positionnant le curseur matériel pour les terminaux qui suivent les fenêtres candidates IME avec des curseurs cachés. Certains terminaux nécessitent un curseur matériel visible pour le positionnement IME; activez-le avec `showHardwareCursor`, `setShowHardwareCursor(true)` ou `PI_HARDWARE_CURSOR=1`. Les composants intégrés `Editor` et `Input` implémentent déjà cette interface.\n\n### Composants de conteneur avec entrées intégrées\n\nLorsqu'un composant conteneur (boîte de dialogue, sélecteur, etc.) contient un enfant `Input` ou `Editor`, le conteneur doit implémenter `Focusable` et propager l'état de focus à l'enfant. Sinon, le curseur matériel ne sera pas positionné correctement pour la saisie IME.\n\n```typescript\nimport { Container, type Focusable, Input } from \"@earendil-works/pi-tui\";\n\nclass SearchDialog extends Container implements Focusable {\n  private searchInput: Input;\n\n  // Focusable implementation - propagate to child input for IME cursor positioning\n  private _focused = false;\n  get focused(): boolean {\n    return this._focused;\n  }\n  set focused(value: boolean) {\n    this._focused = value;\n    this.searchInput.focused = value;\n  }\n\n  constructor() {\n    super();\n    this.searchInput = new Input();\n    this.addChild(this.searchInput);\n  }\n}\n```\n\nSans cette propagation, taper avec un IME (chinois, japonais, coréen, etc.) affichera la fenêtre candidat dans la mauvaise position à l'écran.\n\n## Utilisation de composants\n\n**Dans les extensions** via `ctx.ui.custom()`:\n\n```typescript\npi.on(\"session_start\", async (_event, ctx) => {\n  const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>\n    new MyComponent({\n      theme,\n      keybindings,\n      onChange: () => tui.requestRender(),\n      onSelect: (value) => done(value),\n      onCancel: () => done(null),\n    })\n  );\n});\n```\n\n**Dans les outils personnalisés** via `ctx.ui.custom()`:\n\n```typescript\nasync execute(toolCallId, params, signal, onUpdate, ctx) {\n  const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>\n    new MyComponent({\n      theme,\n      keybindings,\n      onChange: () => tui.requestRender(),\n      onSelect: (value) => done(value),\n      onCancel: () => done(null),\n    })\n  );\n  // Use result...\n}\n```\n\n## Superpositions\n\nLes superpositions affichent les composants au-dessus du contenu existant sans effacer l'écran. Passez `{ overlay: true }` à `ctx.ui.custom()`:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyDialog({ onClose: done }),\n  { overlay: true }\n);\n```\n\nPour le positionnement et le dimensionnement, utilisez `overlayOptions`:\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new SidePanel({ onClose: done }),\n  {\n    overlay: true,\n    overlayOptions: {\n      // Size: number or percentage string\n      width: \"50%\",          // 50% of terminal width\n      minWidth: 40,          // minimum 40 columns\n      maxHeight: \"80%\",      // max 80% of terminal height\n\n      // Position: anchor-based (default: \"center\")\n      anchor: \"right-center\", // 9 positions: center, top-left, top-center, etc.\n      offsetX: -2,            // offset from anchor\n      offsetY: 0,\n\n      // Or percentage/absolute positioning\n      row: \"25%\",            // 25% from top\n      col: 10,               // column 10\n\n      // Margins\n      margin: 2,             // all sides, or { top, right, bottom, left }\n\n      // Responsive: hide on narrow terminals\n      visible: (termWidth, termHeight) => termWidth >= 80,\n    },\n    // Get handle for programmatic focus and visibility control\n    onHandle: (handle) => {\n      // handle.focus() - focus this overlay and bring it to the visual front\n      // handle.unfocus() - release input to normal fallback\n      // handle.unfocus({ target }) - release input to a specific component or null\n      // handle.setHidden(true/false) - toggle visibility\n      // handle.hide() - permanently remove\n    },\n  }\n);\n```\n\n### Mise au point de superposition\n\nUne superposition visible et ciblée conserve la propriété des entrées dans l'interface utilisateur temporaire sans superposition. Si une superposition ouvre un autre composant `ctx.ui.custom()` sans `{ overlay: true }`, cette interface utilisateur de remplacement reçoit une entrée pendant qu'elle est active; lorsqu'elle se ferme, la superposition ciblée peut récupérer l'entrée.\n\nUtilisez `handle.unfocus()` lorsqu'une superposition visible ne doit plus posséder d'entrée et laissez TUI revenir à une autre superposition de capture visible ou à la cible de focus précédente. Utilisez `handle.unfocus({ target })` lorsqu'un composant spécifique doit recevoir une entrée tandis que la superposition reste visible. Passer `{ target: null }` intentionnellement ne laisse aucun composant focalisé jusqu'à ce que le focus soit à nouveau défini.\n\n### Cycle de vie de la superposition\n\nLes composants de superposition sont éliminés une fois fermés. Ne réutilisez pas les références - créez de nouvelles instances:\n\n```typescript\n// Wrong - stale reference\nlet menu: MenuComponent;\nawait ctx.ui.custom((_, __, ___, done) => {\n  menu = new MenuComponent(done);\n  return menu;\n}, { overlay: true });\nsetActiveComponent(menu);  // Disposed\n\n// Correct - re-call to re-show\nconst showMenu = () => ctx.ui.custom((_, __, ___, done) => \n  new MenuComponent(done), { overlay: true });\n\nawait showMenu();  // First show\nawait showMenu();  // \"Back\" = just call again\n```\n\nVoir [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) pour des exemples complets couvrant les ancres, les marges, l'empilement, la visibilité réactive et l'animation.\n\n## Composants intégrés\n\nImporter depuis `@earendil-works/pi-tui`:\n\n```typescript\nimport { Text, Box, Container, Spacer, Markdown } from \"@earendil-works/pi-tui\";\n```\n\n### Texte\n\nTexte multiligne avec retour à la ligne.\n\n```typescript\nconst text = new Text(\n  \"Hello World\",    // content\n  1,                // paddingX (default: 1)\n  1,                // paddingY (default: 1)\n  (s) => bgGray(s)  // optional background function\n);\ntext.setText(\"Updated\");\n```\n\n### Boîte\n\nConteneur avec rembourrage et couleur d'arrière-plan.\n\n```typescript\nconst box = new Box(\n  1,                // paddingX\n  1,                // paddingY\n  (s) => bgGray(s)  // background function\n);\nbox.addChild(new Text(\"Content\", 0, 0));\nbox.setBgFn((s) => bgBlue(s));\n```\n\n### Récipient\n\nRegroupe les composants enfants verticalement.\n\n```typescript\nconst container = new Container();\ncontainer.addChild(component1);\ncontainer.addChild(component2);\ncontainer.removeChild(component1);\n```\n\n### Entretoise\n\nEspace vertical vide.\n\n```typescript\nconst spacer = new Spacer(2);  // 2 empty lines\n```\n\n### Markdown\n\nRend le démarque avec la coloration syntaxique.\n\n```typescript\nconst md = new Markdown(\n  \"# Title\\n\\nSome **bold** text\",\n  1,        // paddingX\n  1,        // paddingY\n  theme     // MarkdownTheme (see below)\n);\nmd.setText(\"Updated markdown\");\n```\n\n### Image\n\nRend les images dans les terminaux pris en charge (Kitty, iTerm2, Ghostty, WezTerm, Warp).\n\n```typescript\nconst image = new Image(\n  base64Data,   // base64-encoded image\n  \"image/png\",  // MIME type\n  theme,        // ImageTheme\n  { maxWidthCells: 80, maxHeightCells: 24 }\n);\n```\n\n## Entrée au clavier\n\nUtilisez `matchesKey()` pour la détection de clé:\n\n```typescript\nimport { matchesKey, Key } from \"@earendil-works/pi-tui\";\n\nhandleInput(data: string) {\n  if (matchesKey(data, Key.up)) {\n    this.selectedIndex--;\n  } else if (matchesKey(data, Key.enter)) {\n    this.onSelect?.(this.selectedIndex);\n  } else if (matchesKey(data, Key.escape)) {\n    this.onCancel?.();\n  } else if (matchesKey(data, Key.ctrl(\"c\"))) {\n    // Ctrl+C\n  }\n}\n```\n\n**Identifiants de clé** (utilisez `Key.*` pour la saisie semi-automatique ou les chaînes littérales):\n- Touches de base: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`\n- Touches fléchées: `Key.up`, `Key.down`, `Key.left`, `Key.right`\n- Avec modificateurs: `Key.ctrl(\"c\")`, `Key.shift(\"tab\")`, `Key.alt(\"left\")`, `Key.ctrlShift(\"p\")`\n- Le format de chaîne fonctionne également: `\"enter\"`, `\"ctrl+c\"`, `\"shift+tab\"`, `\"ctrl+shift+p\"`\n\n## Largeur de ligne\n\n**Critique:** Chaque ligne de `render()` ne doit pas dépasser le paramètre `width`.\n\n```typescript\nimport { visibleWidth, truncateToWidth } from \"@earendil-works/pi-tui\";\n\nrender(width: number): string[] {\n  // Truncate long lines\n  return [truncateToWidth(this.text, width)];\n}\n```\n\nUtilitaires:\n- `visibleWidth(str)` - Obtenir la largeur d'affichage (ignore les codes ANSI)\n- `truncateToWidth(str, width, ellipsis?)` - Tronquer avec des points de suspension facultatifs\n- `wrapTextWithAnsi(str, width)` - Retour à la ligne préservant les codes ANSI\n\n## Création de composants personnalisés\n\nExemple: sélecteur interactif\n\n```typescript\nimport {\n  matchesKey, Key,\n  truncateToWidth, visibleWidth\n} from \"@earendil-works/pi-tui\";\n\nclass MySelector {\n  private items: string[];\n  private selected = 0;\n  private cachedWidth?: number;\n  private cachedLines?: string[];\n  \n  public onSelect?: (item: string) => void;\n  public onCancel?: () => void;\n\n  constructor(items: string[]) {\n    this.items = items;\n  }\n\n  handleInput(data: string): void {\n    if (matchesKey(data, Key.up) && this.selected > 0) {\n      this.selected--;\n      this.invalidate();\n    } else if (matchesKey(data, Key.down) && this.selected < this.items.length - 1) {\n      this.selected++;\n      this.invalidate();\n    } else if (matchesKey(data, Key.enter)) {\n      this.onSelect?.(this.items[this.selected]);\n    } else if (matchesKey(data, Key.escape)) {\n      this.onCancel?.();\n    }\n  }\n\n  render(width: number): string[] {\n    if (this.cachedLines && this.cachedWidth === width) {\n      return this.cachedLines;\n    }\n\n    this.cachedLines = this.items.map((item, i) => {\n      const prefix = i === this.selected ? \"> \" : \"  \";\n      return truncateToWidth(prefix + item, width);\n    });\n    this.cachedWidth = width;\n    return this.cachedLines;\n  }\n\n  invalidate(): void {\n    this.cachedWidth = undefined;\n    this.cachedLines = undefined;\n  }\n}\n```\n\nUtilisation dans une extension:\n\n```typescript\npi.registerCommand(\"pick\", {\n  description: \"Pick an item\",\n  handler: async (_args, ctx) => {\n    const items = [\"Option A\", \"Option B\", \"Option C\"];\n    const selected = await ctx.ui.custom<string | null>((tui, _theme, _keybindings, done) => {\n      const selector = new MySelector(items);\n      selector.onSelect = done;\n      selector.onCancel = () => done(null);\n\n      return {\n        render: (width) => selector.render(width),\n        handleInput: (data) => {\n          selector.handleInput(data);\n          tui.requestRender();\n        },\n        invalidate: () => selector.invalidate(),\n      };\n    });\n\n    if (selected !== null) {\n      ctx.ui.notify(`Selected: ${selected}`, \"info\");\n    }\n  }\n});\n```\n\n## Thématisation\n\nLes composants acceptent les objets de thème pour le style.\n\n**Dans `renderCall`/`renderResult`**, utilisez le paramètre `theme`:\n\n```typescript\nrenderResult(result, options, theme, context) {\n  // Use theme.fg() for foreground colors\n  return new Text(theme.fg(\"success\", \"Done!\"), 0, 0);\n  \n  // Use theme.bg() for background colors\n  const styled = theme.bg(\"toolPendingBg\", theme.fg(\"accent\", \"text\"));\n}\n```\n\n**Couleurs de premier plan** (`theme.fg(color, text)`):\n\n| Catégorie | Couleurs |\n|----------|--------|\n| Général | `text`, `accent`, `muted`, `dim` |\n| Statut | `success`, `error`, `warning` |\n| Frontières | `border`, `borderAccent`, `borderMuted` |\n| Messages | `userMessageText`, `customMessageText`, `customMessageLabel` |\n| Outils | `toolTitle`, `toolOutput` |\n| Différences | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |\n| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |\n| Syntaxe | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |\n| Pensée | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `thinkingMax` |\n| Modes | `bashMode` |\n\n**Couleurs de fond** (`theme.bg(color, text)`):\n\n`selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`\n\n**Pour Markdown**, utilisez `getMarkdownTheme()`:\n\n```typescript\nimport { getMarkdownTheme } from \"@earendil-works/pi-coding-agent\";\nimport { Markdown } from \"@earendil-works/pi-tui\";\n\nrenderResult(result, options, theme, context) {\n  const mdTheme = getMarkdownTheme();\n  return new Markdown(result.details.markdown, 0, 0, mdTheme);\n}\n```\n\n**Pour les composants personnalisés**, définissez votre propre interface de thème:\n\n```typescript\ninterface MyTheme {\n  selected: (s: string) => string;\n  normal: (s: string) => string;\n}\n```\n\n## Journalisation du débogage\n\nDéfinissez `PI_TUI_WRITE_LOG` pour capturer le flux ANSI brut écrit sur stdout.\n\n```bash\nPI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts\n```\n\n## Performance\n\nCacher la sortie rendue lorsque cela est possible:\n\n```typescript\nclass CachedComponent {\n  private cachedWidth?: number;\n  private cachedLines?: string[];\n\n  render(width: number): string[] {\n    if (this.cachedLines && this.cachedWidth === width) {\n      return this.cachedLines;\n    }\n    // ... compute lines ...\n    this.cachedWidth = width;\n    this.cachedLines = lines;\n    return lines;\n  }\n\n  invalidate(): void {\n    this.cachedWidth = undefined;\n    this.cachedLines = undefined;\n  }\n}\n```\n\nAppelez `invalidate()` lorsque l'état change, puis utilisez le `tui.requestRender()` injecté pour déclencher un nouveau rendu.\n\n## Invalidation et changements de thème\n\nLorsque le thème change, le TUI appelle `invalidate()` sur tous les composants pour vider leurs caches. Les composants doivent implémenter correctement `invalidate()` pour garantir que les changements de thème prennent effet.\n\n### Le problème\n\nSi un composant pré-prépare les couleurs du thème dans des chaînes (via `theme.fg()`, `theme.bg()`, etc.) et les met en cache, les chaînes mises en cache contiennent les codes d'échappement ANSI de l'ancien thème. Il ne suffit pas de vider le cache de rendu si le composant stocke le contenu thématique séparément.\n\n**Mauvaise approche** (les couleurs du thème ne seront pas mises à jour):\n\n```typescript\nclass BadComponent extends Container {\n  private content: Text;\n\n  constructor(message: string, theme: Theme) {\n    super();\n    // Pre-baked theme colors stored in Text component\n    this.content = new Text(theme.fg(\"accent\", message), 1, 0);\n    this.addChild(this.content);\n  }\n  // No invalidate override - parent's invalidate only clears\n  // child render caches, not the pre-baked content\n}\n```\n\n### La solution\n\nLes composants qui créent du contenu avec des couleurs de thème doivent reconstruire ce contenu lorsque `invalidate()` est appelé:\n\n```typescript\nclass GoodComponent extends Container {\n  private message: string;\n  private content: Text;\n\n  constructor(message: string) {\n    super();\n    this.message = message;\n    this.content = new Text(\"\", 1, 0);\n    this.addChild(this.content);\n    this.updateDisplay();\n  }\n\n  private updateDisplay(): void {\n    // Rebuild content with current theme\n    this.content.setText(theme.fg(\"accent\", this.message));\n  }\n\n  override invalidate(): void {\n    super.invalidate();  // Clear child caches\n    this.updateDisplay(); // Rebuild with new theme\n  }\n}\n```\n\n### Modèle: Reconstruire en cas d'invalidation\n\nPour les composants au contenu complexe:\n\n```typescript\nclass ComplexComponent extends Container {\n  private data: SomeData;\n\n  constructor(data: SomeData) {\n    super();\n    this.data = data;\n    this.rebuild();\n  }\n\n  private rebuild(): void {\n    this.clear();  // Remove all children\n\n    // Build UI with current theme\n    this.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Title\")), 1, 0));\n    this.addChild(new Spacer(1));\n\n    for (const item of this.data.items) {\n      const color = item.active ? \"success\" : \"muted\";\n      this.addChild(new Text(theme.fg(color, item.label), 1, 0));\n    }\n  }\n\n  override invalidate(): void {\n    super.invalidate();\n    this.rebuild();\n  }\n}\n```\n\n### Quand c'est important\n\nCe modèle est nécessaire lorsque:\n\n1. **Couleurs du thème de pré-cuisson** - Utilisation de `theme.fg()` ou `theme.bg()` pour créer des chaînes stylisées stockées dans les composants enfants\n2. **Mise en évidence de la syntaxe** - Utilisation de `highlightCode()` qui applique des couleurs de syntaxe basées sur un thème\n3. **Mises en page complexes** - Création d'arborescences de composants enfants qui intègrent des couleurs de thème\n\nCe modèle n'est PAS nécessaire lorsque:\n\n1. **Utilisation des rappels de thème** - Passage de fonctions telles que `(text) => theme.fg(\"accent\", text)` qui sont appelées lors du rendu\n2. **Conteneurs simples** - Regrouper simplement d'autres composants sans ajouter de contenu thématique\n3. **Rendu sans état** - Sortie thématique informatique à chaque appel `render()` (pas de mise en cache)\n\n## Modèles courants\n\nCes modèles couvrent les besoins d’interface utilisateur les plus courants dans les extensions. **Copiez ces modèles au lieu de créer à partir de zéro.**\n\n### Modèle 1: boîte de dialogue de sélection (SelectList)\n\nPour permettre aux utilisateurs de choisir parmi une liste d'options. Utilisez `SelectList` de `@earendil-works/pi-tui` avec `DynamicBorder` pour le cadrage.\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { DynamicBorder } from \"@earendil-works/pi-coding-agent\";\nimport { Container, type SelectItem, SelectList, Text } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"pick\", {\n  handler: async (_args, ctx) => {\n    const items: SelectItem[] = [\n      { value: \"opt1\", label: \"Option 1\", description: \"First option\" },\n      { value: \"opt2\", label: \"Option 2\", description: \"Second option\" },\n      { value: \"opt3\", label: \"Option 3\" },  // description is optional\n    ];\n\n    const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {\n      const container = new Container();\n\n      // Top border\n      container.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n      // Title\n      container.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Pick an Option\")), 1, 0));\n\n      // SelectList with theme\n      const selectList = new SelectList(items, Math.min(items.length, 10), {\n        selectedPrefix: (t) => theme.fg(\"accent\", t),\n        selectedText: (t) => theme.fg(\"accent\", t),\n        description: (t) => theme.fg(\"muted\", t),\n        scrollInfo: (t) => theme.fg(\"dim\", t),\n        noMatch: (t) => theme.fg(\"warning\", t),\n      });\n      selectList.onSelect = (item) => done(item.value);\n      selectList.onCancel = () => done(null);\n      container.addChild(selectList);\n\n      // Help text\n      container.addChild(new Text(theme.fg(\"dim\", \"↑↓ navigate • enter select • esc cancel\"), 1, 0));\n\n      // Bottom border\n      container.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n      return {\n        render: (w) => container.render(w),\n        invalidate: () => container.invalidate(),\n        handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },\n      };\n    });\n\n    if (result) {\n      ctx.ui.notify(`Selected: ${result}`, \"info\");\n    }\n  },\n});\n```\n\n**Exemples:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)\n\n### Modèle 2: opération asynchrone avec annulation (BorderedLoader)\n\nPour les opérations qui prennent du temps et doivent être annulables. `BorderedLoader` affiche une roulette et gère l'échappement pour annuler.\n\n```typescript\nimport { BorderedLoader } from \"@earendil-works/pi-coding-agent\";\n\npi.registerCommand(\"fetch\", {\n  handler: async (_args, ctx) => {\n    const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {\n      const loader = new BorderedLoader(tui, theme, \"Fetching data...\");\n      loader.onAbort = () => done(null);\n\n      // Do async work\n      fetchData(loader.signal)\n        .then((data) => done(data))\n        .catch(() => done(null));\n\n      return loader;\n    });\n\n    if (result === null) {\n      ctx.ui.notify(\"Cancelled\", \"info\");\n    } else {\n      ctx.ui.setEditorText(result);\n    }\n  },\n});\n```\n\n**Exemples:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)\n\n### Modèle 3: Paramètres/Bascules (Liste des paramètres)\n\nPour basculer plusieurs paramètres. Utilisez `SettingsList` de `@earendil-works/pi-tui` avec `getSettingsListTheme()`.\n\n```typescript\nimport { getSettingsListTheme } from \"@earendil-works/pi-coding-agent\";\nimport { Container, type SettingItem, SettingsList, Text } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"settings\", {\n  handler: async (_args, ctx) => {\n    const items: SettingItem[] = [\n      { id: \"verbose\", label: \"Verbose mode\", currentValue: \"off\", values: [\"on\", \"off\"] },\n      { id: \"color\", label: \"Color output\", currentValue: \"on\", values: [\"on\", \"off\"] },\n    ];\n\n    await ctx.ui.custom((_tui, theme, _kb, done) => {\n      const container = new Container();\n      container.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Settings\")), 1, 1));\n\n      const settingsList = new SettingsList(\n        items,\n        Math.min(items.length + 2, 15),\n        getSettingsListTheme(),\n        (id, newValue) => {\n          // Handle value change\n          ctx.ui.notify(`${id} = ${newValue}`, \"info\");\n        },\n        () => done(undefined),  // On close\n        { enableSearch: true }, // Optional: enable fuzzy search by label\n      );\n      container.addChild(settingsList);\n\n      return {\n        render: (w) => container.render(w),\n        invalidate: () => container.invalidate(),\n        handleInput: (data) => settingsList.handleInput?.(data),\n      };\n    });\n  },\n});\n```\n\n**Exemples:** [tools.ts](../examples/extensions/tools.ts)\n\n### Modèle 4: indicateur d'état persistant\n\nAfficher l'état dans le pied de page qui persiste dans les rendus. Bon pour les indicateurs de mode.\n\n```typescript\n// Set status (shown in footer)\nctx.ui.setStatus(\"my-ext\", ctx.ui.theme.fg(\"accent\", \"● active\"));\n\n// Clear status\nctx.ui.setStatus(\"my-ext\", undefined);\n```\n\n**Exemples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)\n\n### Modèle 4b: personnalisation des indicateurs de travail\n\nPersonnalisez l'indicateur de travail en ligne affiché pendant que pi diffuse une réponse.\n\n```typescript\n// Static indicator\nctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg(\"accent\", \"●\")] });\n\n// Custom animated indicator\nctx.ui.setWorkingIndicator({\n  frames: [\n    ctx.ui.theme.fg(\"dim\", \"·\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n    ctx.ui.theme.fg(\"accent\", \"●\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n  ],\n  intervalMs: 120,\n});\n\n// Hide the indicator entirely\nctx.ui.setWorkingIndicator({ frames: [] });\n\n// Restore pi's default spinner\nctx.ui.setWorkingIndicator();\n```\n\nCela n’affecte que l’indicateur de fonctionnement normal du streaming. Les chargeurs de compactage et de nouvelle tentative conservent leur style intégré. Les cadres personnalisés sont rendus textuellement, les extensions doivent donc ajouter leurs propres couleurs si nécessaire.\n\n**Exemples:** [working-indicator.ts](../examples/extensions/working-indicator.ts)\n\n### Modèle 5: Éditeur de widgets au-dessus/en dessous\n\nAfficher le contenu persistant au-dessus ou en dessous de l'éditeur d'entrée. Bon pour les listes de tâches, les progrès.\n\n```typescript\n// Simple string array (above editor by default)\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"]);\n\n// Render below the editor\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"], { placement: \"belowEditor\" });\n\n// Or with theme\nctx.ui.setWidget(\"my-widget\", (_tui, theme) => {\n  const lines = items.map((item, i) =>\n    item.done\n      ? theme.fg(\"success\", \"✓ \") + theme.fg(\"muted\", item.text)\n      : theme.fg(\"dim\", \"○ \") + item.text\n  );\n  return {\n    render: () => lines,\n    invalidate: () => {},\n  };\n});\n\n// Clear\nctx.ui.setWidget(\"my-widget\", undefined);\n```\n\n**Exemples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)\n\n### Modèle 6: pied de page personnalisé\n\nRemplacez le pied de page. `footerData` expose des données qui ne seraient autrement pas accessibles aux extensions.\n\n```typescript\nctx.ui.setFooter((tui, theme, footerData) => ({\n  invalidate() {},\n  render(width: number): string[] {\n    // footerData.getGitBranch(): string | null\n    // footerData.getExtensionStatuses(): ReadonlyMap<string, string>\n    return [`${ctx.model?.id} (${footerData.getGitBranch() || \"no git\"})`];\n  },\n  dispose: footerData.onBranchChange(() => tui.requestRender()), // reactive\n}));\n\nctx.ui.setFooter(undefined); // restore default\n```\n\nStatistiques des jetons disponibles via `ctx.sessionManager.getBranch()` et `ctx.model`.\n\n**Exemples:** [custom-footer.ts](../examples/extensions/custom-footer.ts)\n\n### Modèle 7: Éditeur personnalisé (mode vim, etc.)\n\nRemplacez l'éditeur d'entrée principal par une implémentation personnalisée. Utile pour l'édition modale (vim), différentes combinaisons de touches (emacs) ou la gestion des entrées spécialisées.\n\n```typescript\nimport { CustomEditor, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { matchesKey, truncateToWidth } from \"@earendil-works/pi-tui\";\n\ntype Mode = \"normal\" | \"insert\";\n\nclass VimEditor extends CustomEditor {\n  private mode: Mode = \"insert\";\n\n  handleInput(data: string): void {\n    // Escape: switch to normal mode, or pass through for app handling\n    if (matchesKey(data, \"escape\")) {\n      if (this.mode === \"insert\") {\n        this.mode = \"normal\";\n        return;\n      }\n      // In normal mode, escape aborts agent (handled by CustomEditor)\n      super.handleInput(data);\n      return;\n    }\n\n    // Insert mode: pass everything to CustomEditor\n    if (this.mode === \"insert\") {\n      super.handleInput(data);\n      return;\n    }\n\n    // Normal mode: vim-style navigation\n    switch (data) {\n      case \"i\": this.mode = \"insert\"; return;\n      case \"h\": super.handleInput(\"\\x1b[D\"); return; // Left\n      case \"j\": super.handleInput(\"\\x1b[B\"); return; // Down\n      case \"k\": super.handleInput(\"\\x1b[A\"); return; // Up\n      case \"l\": super.handleInput(\"\\x1b[C\"); return; // Right\n    }\n    // Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars\n    if (data.length === 1 && data.charCodeAt(0) >= 32) return;\n    super.handleInput(data);\n  }\n\n  render(width: number): string[] {\n    const lines = super.render(width);\n    // Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)\n    if (lines.length > 0) {\n      const label = this.mode === \"normal\" ? \" NORMAL \" : \" INSERT \";\n      const lastLine = lines[lines.length - 1]!;\n      // Pass \"\" as ellipsis to avoid adding \"...\" when truncating\n      lines[lines.length - 1] = truncateToWidth(lastLine, width - label.length, \"\") + label;\n    }\n    return lines;\n  }\n}\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    // Factory receives the TUI, theme, and keybindings from the app\n    ctx.ui.setEditorComponent((tui, theme, keybindings) =>\n      new VimEditor(tui, theme, keybindings)\n    );\n  });\n}\n```\n\n**Points clés:**\n\n- **Étendez `CustomEditor`** (pas la base `Editor`) pour obtenir les raccourcis clavier de l'application (échappement pour abandonner, ctrl+d pour quitter, changement de modèle, etc.)\n- **Appelez le `super.handleInput(data)`** pour les clés que vous ne gérez pas\n- **Modèle d'usine**: `setEditorComponent` reçoit une fonction d'usine qui obtient `tui`, `theme` et `keybindings`\n- **Passez `undefined`** pour restaurer l'éditeur par défaut: `ctx.ui.setEditorComponent(undefined)`\n\n**Exemples:** [modal-editor.ts](../examples/extensions/modal-editor.ts)\n\n## Règles clés\n\n1. **Toujours utiliser le thème du rappel** - N'importez pas le thème directement. Utilisez `theme` à partir du rappel `ctx.ui.custom((tui, theme, keybindings, done) =>...)`.\n\n2. **Toujours taper le paramètre de couleur DynamicBorder** - Écrivez `(s: string) => theme.fg(\"accent\", s)`, pas `(s) => theme.fg(\"accent\", s)`.\n\n3. **Appelez tui.requestRender() après un changement d'état** - Dans `handleInput`, appelez `tui.requestRender()` après la mise à jour de l'état.\n\n4. **Renvoyer l'objet à trois méthodes** - Les composants personnalisés ont besoin de `{ render, invalidate, handleInput }`.\n\n5. **Utiliser les composants existants** - `SelectList`, `SettingsList`, `BorderedLoader` couvrent 90 % des cas. Ne les reconstruisez pas.\n\n## Exemples\n\n- **UI de sélection**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList avec cadrage DynamicBorder\n- **Async avec annulation**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader pour les appels LLM\n- **Les paramètres basculent**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - Liste des paramètres pour l'activation/la désactivation de l'outil\n- **Indicateurs d'état**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus et setWidget\n- **Indicateur de fonctionnement**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator\n- **Pied de page personnalisé**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter avec statistiques\n- **Éditeur personnalisé**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Édition modale de type Vim\n- **Jeu de serpent**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Jeu complet avec saisie au clavier, boucle de jeu\n- **Rendu d'outil personnalisé**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall et renderResult","sourceFile":"tui.md"},"usage":{"title":"Utiliser Pi","markdown":"Cette page collecte des détails d'utilisation quotidienne qui ne tiennent pas sur la page de démarrage rapide.\n\n## Mode interactif\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Interactive Mode\" width=\"600\"></p>\n\nL'interface comporte quatre zones principales:\n\n- **En-tête de démarrage** - raccourcis, chargés context files, prompt templates, compétences et extensions\n- **Messages**: messages utilisateur, réponses de l'assistant, appels d'outils, résultats d'outils, notifications, erreurs et interface utilisateur d'extension.\n- **Éditeur** - où vous tapez; la couleur de la bordure indique le niveau de réflexion actuel\n- **Pied de page**: répertoire de travail, nom de session, utilisation du jeton/cache, coût, utilisation du contexte et modèle actuel. Les totaux incluent les réponses de l'assistant, l'utilisation signalée par les outils et la génération de résumés.\n\nL'éditeur peut être remplacé temporairement par une interface utilisateur intégrée telle que `/settings` ou par une interface utilisateur d'extension personnalisée.\n\n### Fonctionnalités de l'éditeur\n\n| Fonctionnalité | Comment |\n|---------|-----|\n| Référence du fichier | Tapez `@` pour effectuer une recherche floue dans les fichiers de projet |\n| Achèvement du chemin | Appuyez sur Tab pour compléter les chemins |\n| Entrée multiligne | Maj+Entrée ou Ctrl+Entrée sur le terminal Windows |\n| Copier la réponse | Ctrl+X copie le dernier message de l'assistant; en `/tree`, il copie le message sélectionné |\n| Images | Collez avec Ctrl+V, Alt+V sous Windows ou faites glisser dans le terminal |\n| Commande Shell | `!command` exécute et envoie la sortie au modèle |\n| Commande shell cachée | `!!command` s'exécute sans envoyer de sortie au modèle |\n| Éditeur externe | Ctrl+G ouvre `externalEditor`, `$VISUAL`, `$EDITOR`, le Bloc-notes sous Windows ou `nano` ailleurs |\n\nVoir [Keybindings](keybindings.md) pour tous les raccourcis et personnalisations.\n\n## Commandes barre oblique\n\nTapez `/` dans l'éditeur pour ouvrir la complétion de la commande. Extensions peut enregistrer des commandes personnalisées, les compétences sont disponibles sous la forme `/skill:name` et prompt templates se développent via `/templatename`.\n\n| Commande | Description |\n|---------|-------------|\n| `/login`, `/logout` | Gérer les identifiants de clé OAuth ou API |\n| [`/llama`](llama-cpp.md) | Téléchargez, chargez et déchargez les modèles de routeurs llama.cpp |\n| `/model` | Changer de modèle |\n| `/scoped-models` | Activer/désactiver les modèles pour le cycle Ctrl+P |\n| `/settings` | Niveau de réflexion, thème, transmission du message, transport |\n| `/resume` | Pick des sessions précédentes |\n| `/new` | Démarrer une nouvelle session |\n| `/name <name>` | Définir le nom d'affichage de la session |\n| `/session` | Afficher le fichier de session, l'ID, les messages, les jetons et le coût |\n| `/tree` | Accédez à n’importe quel moment de la session et continuez à partir de là |\n| `/trust` | Enregistrer la décision d'approbation du projet pour les sessions futures |\n| `/fork` | Créer une nouvelle session à partir d'un message utilisateur précédent |\n| `/clone` | Dupliquer la branche active actuelle dans une nouvelle session |\n| `/compact [prompt]` | Contexte compacté manuellement, éventuellement avec des instructions personnalisées |\n| `/copy` | Copier le dernier message de l'assistant dans le presse-papiers |\n| `/export [file]` | Exporter la session au format HTML ou JSONL |\n| `/import <file>` | Importer et reprendre une session à partir d'un fichier JSONL |\n| `/share` | Télécharger en tant qu'essentiel GitHub privé avec un lien HTML partageable |\n| `/reload` | Rechargez les raccourcis clavier, les extensions, les compétences, les invites, les thèmes et context files |\n| `/hotkeys` | Afficher tous les raccourcis clavier |\n| `/changelog` | Afficher l'historique des versions |\n| `/quit` | Quitter pi |\n\n## File d'attente des messages\n\nVous pouvez envoyer des messages pendant que l'agent travaille encore:\n\n- **Entrée** met en file d'attente un message de direction, délivré une fois que le tour d'assistant en cours a fini d'exécuter ses appels d'outil.\n- **Alt+Entrée** met en file d'attente un message de suivi, délivré une fois que l'agent a terminé tout son travail.\n- **Escape** abandonne et restaure les messages en file d'attente dans l'éditeur.\n- **Alt+Up** récupère les messages en file d'attente vers l'éditeur.\n\nSur le terminal Windows, Alt+Entrée est en plein écran par défaut. Remappez-le comme décrit dans [Terminal setup](terminal-setup.md) si vous souhaitez que pi reçoive le raccourci.\n\nConfigurez la livraison en [Settings](settings.md) avec `steeringMode` et `followUpMode`.\n\n## Séances\n\nLes sessions sont automatiquement enregistrées dans `~/.pi/agent/sessions/`, organisées par répertoire de travail.\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse and select a session\npi --no-session        # Ephemeral mode; do not save\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Use a specific session file or session ID\npi --fork <path|id>    # Fork a session into a new session file\n```\n\nCommandes de session utiles:\n\n- `/session` affiche le fichier et l'ID de la session en cours.\n- `/tree` parcourt le fichier session tree et peut résumer les branches abandonnées.\n- `/fork` crée une nouvelle session à partir d'un message utilisateur précédent.\n- `/clone` duplique la branche active actuelle dans un nouveau fichier de session.\n- `/compact` résume les messages plus anciens dans un contexte libre.\n\nVoir [Sessions](sessions.md) et [Compaction](compaction.md) pour plus de détails.\n\n## Fichiers contextuels\n\nPi charge `AGENTS.md` ou `CLAUDE.md` au démarrage depuis:\n\n- `~/.pi/agent/AGENTS.md` pour les instructions globales\n- répertoires parents, en remontant du répertoire de travail actuel\n- le répertoire actuel\n\nSi un répertoire contient `AGENTS.override.md`, Pi le charge au lieu de `AGENTS.md` ou `CLAUDE.md` à partir de ce répertoire. Les fichiers de contexte provenant d'autres répertoires se superposent toujours normalement.\n\nUtilisez context files pour les conventions, commandes, règles de sécurité et préférences du projet. Désactivez le chargement avec `--no-context-files` ou `-nc`.\n\n### Fichiers d'invite système\n\nRemplacez l'invite système par défaut par:\n\n- `.pi/SYSTEM.md` pour un projet\n- `~/.pi/agent/SYSTEM.md` dans le monde\n\nAjoutez à l'invite par défaut sans la remplacer par `APPEND_SYSTEM.md` à aucun des emplacements.\n\n### Fiducie du projet\n\nAu démarrage interactif, pi demande avant de faire confiance à un dossier de projet qui contient des paramètres locaux du projet, des ressources ou un projet `.agents/skills` et n'a aucune décision enregistrée pour le dossier ou un dossier parent dans `~/.pi/agent/trust.json`. Faire confiance à un projet permet à pi de charger les ressources `.pi/settings.json` et `.pi`, d'installer les packages de projet manquants et d'exécuter des extensions de projet.\n\nAvant la décision de confiance, pi charge uniquement les extensions context files, utilisateur/global et CLI `-e` afin qu'ils puissent gérer l'événement `project_trust`. Les extensions locales du projet, les extensions gérées par les packages de projet et les paramètres du projet ne sont chargés qu'une fois le projet approuvé. Cette répartition s'applique également lors du passage à une session à partir d'un autre cwd dont la confiance n'a pas été résolue dans le processus en cours.\n\nLes modes non interactifs (`-p`, `--mode json` et `--mode rpc`) n'affichent pas d'invite de confiance. Sans décision de confiance enregistrée applicable, ils utilisent `defaultProjectTrust` à partir des paramètres globaux: `ask` (par défaut) et `never` ignorent ces ressources du projet, tandis que `always` leur fait confiance. Passez `--approve`/`-a` ou `--no-approve`/`-na` pour remplacer la confiance du projet pour une exécution.\n\nSi aucune extension ou décision enregistrée ne s'applique, `defaultProjectTrust` contrôle le comportement de repli. Réglez-le sur `\"ask\"`, `\"always\"` ou `\"never\"` dans `~/.pi/agent/settings.json`, ou modifiez-le avec `/settings`.\n\nLes commandes `pi config` et package utilisent le même flux de confiance du projet, sauf que `pi update` ne vous invite jamais. Passez `--approve` pour faire confiance aux paramètres locaux du projet pour une commande ou `--no-approve` pour les ignorer.\n\nUtilisez `/trust` en mode interactif pour enregistrer une décision d'approbation de projet pour les sessions futures, y compris l'approbation pour le dossier parent immédiat. Il écrit `~/.pi/agent/trust.json` uniquement; la session en cours n'est pas rechargée, alors redémarrez pi pour que les modifications prennent effet.\n\n\n## Exportation et partage de sessions\n\nUtilisez `/export [file]` pour écrire une session au format HTML.\n\nUtilisez `/share` pour télécharger un résumé privé GitHub avec un lien HTML partageable.\n\nSi vous utilisez pi pour un travail open source et que vous souhaitez publier des sessions pour la recherche de modèles, d'invites, d'outils et d'évaluation, voir [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Il publie des sessions sur Hugging Face ensembles de données.\n\n## CLI Référence\n\n```bash\npi [options] [@files...] [messages...]\n```\n\n### Commandes du package\n\n```bash\npi install <source> [-l]     # Install package, -l for project-local\npi remove <source> [-l]      # Remove package\npi uninstall <source> [-l]   # Alias for remove\npi update [source|self|pi]   # Update pi only, or one package source\npi update --all              # Update pi and packages; reconcile pinned git refs\npi update --extensions       # Update packages only; reconcile pinned git refs\npi update --models           # Refresh model catalogs only\npi update --self             # Update pi only\npi update --extension <src>  # Update one package\npi list                      # List installed packages\npi config                    # Enable/disable package resources\n```\n\nCes commandes gèrent les packages pi et `pi update` peuvent mettre à jour l'installation de pi CLI. Pour désinstaller pi lui-même, voir [Quickstart](quickstart.md#uninstall). `pi config` et les commandes du package de projet acceptent `--approve`/`--no-approve` pour faire confiance ou ignorer les paramètres locaux du projet pour une commande. `pi update` ne demande jamais d'approbation du projet.\n\nVoir [Pi Packages](packages.md) pour les sources des packages et les notes de sécurité.\n\n### Modes\n\n| Drapeau | Description |\n|------|-------------|\n| défaut | Mode interactif |\n| `-p`, `--print` | Imprimer la réponse et quitter |\n| `--mode json` | Affichez tous les événements sous forme de lignes JSON; voir [JSON mode](json.md) |\n| `--mode rpc` | Mode RPC sur stdin/stdout; voir [RPC mode](rpc.md) |\n| `--export <in> [out]` | Exporter une session au format HTML |\n\nEn mode impression, pi lit également le canal stdin et le fusionne dans l'invite initiale:\n\n```bash\ncat README.md | pi -p \"Summarize this text\"\n```\n\n### Options de modèle\n\n| Option | Description |\n|--------|-------------|\n| `--provider <name>` | Fournisseur, tel que `anthropic`, `openai` ou `google` |\n| `--model <pattern>` | Modèle ou identifiant du modèle; prend en charge `provider/id` et facultatif `:<thinking>` |\n| `--api-key <key>` | API key, remplacement des variables d'environnement |\n| `--thinking <level>` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` |\n| `--models <patterns>` | Modèles séparés par des virgules pour le cycle Ctrl+P |\n| `--list-models [search]` | Liste des modèles disponibles |\n\n### Options de séance\n\n| Option | Description |\n|--------|-------------|\n| `-c`, `--continue` | Continuer la session la plus récente |\n| `-r`, `--resume` | Parcourir et sélectionner une session |\n| `--session <chemin\\ | identifiant>` | Utiliser un fichier de session spécifique ou un UUID partiel |\n| `--fork <chemin\\ | identifiant>` | Forkez un fichier de session ou un UUID partiel dans une nouvelle session |\n| `--session-dir <dir>` | Répertoire de stockage de session personnalisé |\n| `--no-session` | Mode éphémère; ne sauvegarde pas |\n| `--name <name>`, `-n <name>` | Définir le nom d'affichage de la session au démarrage |\n\n### Options des outils\n\n| Option | Description |\n|--------|-------------|\n| `--tools <list>`, `-t <list>` | Liste d'autorisation d'outils intégrés, d'extension et personnalisés spécifiques |\n| `--exclude-tools <list>`, `-xt <list>` | Désactiver les outils spécifiques intégrés, d'extension et personnalisés |\n| `--no-builtin-tools`, `-nbt` | Désactivez les outils intégrés mais gardez les outils d'extension/personnalisés activés |\n| `--no-tools`, `-nt` | Désactivez tous les outils |\n\nOutils intégrés: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`.\n\n### Options de ressources\n\n| Option | Description |\n|--------|-------------|\n| `-e`, `--extension <source>` | Chargez une extension à partir du chemin, npm ou git; répétable |\n| `--no-extensions` | Désactiver la découverte d'extensions |\n| `--skill <path>` | Charger une compétence; répétable |\n| `--no-skills` | Désactiver la découverte de compétences |\n| `--prompt-template <path>` | Chargez un modèle d'invite; répétable |\n| `--no-prompt-templates` | Désactiver la découverte de modèles d'invite |\n| `--theme <path>` | Charger un thème; répétable |\n| `--no-themes` | Désactiver la découverte de thèmes |\n| `--no-context-files`, `-nc` | Désactivez la découverte `AGENTS.md` et `CLAUDE.md` |\n\nCombinez `--no-*` avec des indicateurs explicites pour charger exactement ce dont vous avez besoin, en ignorant les paramètres. Exemple:\n\n```bash\npi --no-extensions -e ./my-extension.ts\n```\n\n### Autres options\n\n| Option | Description |\n|--------|-------------|\n| `--system-prompt <text>` | Remplacer l'invite par défaut; context files et les compétences sont toujours ajoutées |\n| `--append-system-prompt <text>` | Ajouter à l'invite du système |\n| `--tui-mode <mode>` | Mode TUI: `regular` (par défaut) ou expérimental `fullscreen` |\n| `--verbose` | Forcer le démarrage verbeux |\n| `-a`, `--approve` | Faire confiance aux fichiers locaux du projet pour cette exécution |\n| `-na`, `--no-approve` | Ignorer les fichiers locaux du projet pour cette exécution |\n| `-h`, `--help` | Afficher l'aide |\n| `-v`, `--version` | Afficher la version |\n\nEn mode `fullscreen`, la transcription défile dans la fenêtre d'affichage du terminal tandis que les messages en file d'attente, l'état de fonctionnement, les widgets d'extension, l'éditeur et le pied de page restent fixes en bas. L'entrée de la souris/du trackpad fait défiler la région sous le pointeur; les actions de la fenêtre d'affichage du clavier restent toujours disponibles. Les images en ligne fonctionnent sur les terminaux prenant en charge le protocole graphique Kitty, notamment Kitty et Ghostty. Dans iTerm2, ils s'affichent sous forme d'espaces réservés de texte, car son protocole d'image en ligne ne peut pas supprimer ou recadrer les emplacements pendant le défilement appartenant à l'application. En mode `regular`, pi utilise l'écran principal et le défilement appartenant au terminal, et les images en ligne iTerm2 continuent de s'afficher normalement.\n\nDéfinissez le mode **TUI** dans `/settings` pour basculer immédiatement entre `regular` et `fullscreen` et choisissez la valeur par défaut pour les sessions futures. **Sortie de sortie plein écran** contrôle si la sortie du plein écran imprime la transcription finale ou restaure l'écran précédent et imprime uniquement l'indice de reprise de session.\n\n### Arguments du fichier\n\nPréfixez les fichiers avec `@` pour les inclure dans le message:\n\n```bash\npi @prompt.md \"Answer this\"\npi -p @screenshot.png \"What's in this image?\"\npi @code.ts @test.ts \"Review these files\"\n```\n\n### Exemples\n\n```bash\n# Interactive with initial prompt\npi \"List all .ts files in src/\"\n\n# Non-interactive\npi -p \"Summarize this codebase\"\n\n# Non-interactive with piped stdin\ncat README.md | pi -p \"Summarize this text\"\n\n# Named one-shot session\npi --name \"release audit\" -p \"Audit this repository\"\n\n# Different model\npi --provider openai --model gpt-4o \"Help me refactor\"\n\n# Model with provider prefix\npi --model openai/gpt-4o \"Help me refactor\"\n\n# Model with thinking level shorthand\npi --model sonnet:high \"Solve this complex problem\"\n\n# Limit model cycling\npi --models \"claude-*,gpt-4o\"\n\n# Read-only mode\npi --tools read,grep,find,ls -p \"Review the code\"\n\n# Disable one extension or built-in tool while keeping the rest available\npi --exclude-tools ask_question\n```\n\n## Principes de conception\n\nPi garde le noyau petit et pousse le comportement spécifique au flux de travail dans les extensions, les compétences, prompt templates et les packages.\n\nIl n'inclut intentionnellement pas le MCP intégré, les sous-agents, les fenêtres contextuelles d'autorisation, le mode plan, les tâches ou l'arrière-plan bash. Vous pouvez créer ou installer ces flux de travail sous forme d'extensions ou de packages, ou utiliser des outils externes tels que des conteneurs et tmux.\n\nPour la justification complète, lisez le [blog post](https://mariozechner.at/posts/2025-11-30-pi-coding-agent/).","sourceFile":"usage.md"},"windows":{"title":"Installation de Windows","markdown":"Pi nécessite un shell bash sous Windows. Emplacements vérifiés (dans l'ordre):\n\n1. Chemin personnalisé à partir de `~/.pi/agent/settings.json`\n2. Git Coup (`C:\\Program Files\\Git\\bin\\bash.exe`)\n3. `bash.exe` sur PATH (Cygwin, MSYS2, WSL)\n\nPour la plupart des utilisateurs, [Git for Windows](https://git-scm.com/download/win) est suffisant.\n\n## Chemin d'accès au shell personnalisé\n\n```json\n{\n  \"shellPath\": \"C:\\\\cygwin64\\\\bin\\\\bash.exe\"\n}\n```","sourceFile":"windows.md"}}},"navigation":{"fr":[{"title":"Commencer ici","items":[{"title":"Pi Documentation","path":"/docs/latest","slug":"index"},{"title":"Démarrage rapide","path":"/docs/latest/quickstart","slug":"quickstart"},{"title":"Utiliser Pi","path":"/docs/latest/usage","slug":"usage"},{"title":"Providers","path":"/docs/latest/providers","slug":"providers"},{"title":"Sécurité","path":"/docs/latest/security","slug":"security"},{"title":"Conteneurisation","path":"/docs/latest/containerization","slug":"containerization"},{"title":"Paramètres","path":"/docs/latest/settings","slug":"settings"},{"title":"Raccourcis clavier","path":"/docs/latest/keybindings","slug":"keybindings"},{"title":"Séances","path":"/docs/latest/sessions","slug":"sessions"},{"title":"Compactage et résumé des branches","path":"/docs/latest/compaction","slug":"compaction"}]},{"title":"Personnalisation","items":[{"title":"Extensions","path":"/docs/latest/extensions","slug":"extensions"},{"title":"Skills","path":"/docs/latest/skills","slug":"skills"},{"title":"Modèles d'invite","path":"/docs/latest/prompt-templates","slug":"prompt-templates"},{"title":"Thèmes","path":"/docs/latest/themes","slug":"themes"},{"title":"Pi Packages","path":"/docs/latest/packages","slug":"packages"},{"title":"Personnalisé Models","path":"/docs/latest/models","slug":"models"},{"title":"Personnalisé Providers","path":"/docs/latest/custom-provider","slug":"custom-provider"}]},{"title":"Référence","items":[{"title":"Format de fichier de session","path":"/docs/latest/session-format","slug":"session-format"}]},{"title":"Utilisation programmatique","items":[{"title":"SDK","path":"/docs/latest/sdk","slug":"sdk"},{"title":"Mode RPC","path":"/docs/latest/rpc","slug":"rpc"},{"title":"JSON Mode flux d'événements","path":"/docs/latest/json","slug":"json"},{"title":"TUI Composants","path":"/docs/latest/tui","slug":"tui"}]},{"title":"Configuration de plateforme","items":[{"title":"Installation de Windows","path":"/docs/latest/windows","slug":"windows"},{"title":"Configuration Termux (Android)","path":"/docs/latest/termux","slug":"termux"},{"title":"tmux Configuration","path":"/docs/latest/tmux","slug":"tmux"},{"title":"Configuration du terminal","path":"/docs/latest/terminal-setup","slug":"terminal-setup"},{"title":"Alias ​​de shell","path":"/docs/latest/shell-aliases","slug":"shell-aliases"}]},{"title":"Développement","items":[{"title":"Développement","path":"/docs/latest/development","slug":"development"}]}]}}
