fix(plugins): one surface, one switch, one budget for plugin offers (P3, P4, P6)

0.10.1 plugin offering policy ("helpful, not pushy"), rules 1, 3, 4, 8, 9,
10 and 11.

P3 (PLG-3, PLG-4, PLG-12): the live as-you-type composer bar is gone. It
re-matched every 200 ms, reloaded the marketplace store each time, and
ignored tips and budget. The send-time toast is the only unprompted
surface. The one-line row now appears only for a model-requested review,
and:
- obeys contextual_tips and draws from the shared per-session guidance
  budget; turning tips off hides it;
- names the true next step: Install, Review trust, or Enable;
- only the labelled button acts, and it opens `/plugin show <name>`,
  never install/trust/enable; a click elsewhere on the row does nothing;
- Esc clears a non-empty draft first, then hides the row for this session
  only and persists nothing; "Don't suggest again" is the explicit,
  persisted dismissal.

P4 (PLG-5): request_plugin_install is registered only when the engine runs
with terminal chrome (the interactive TUI; exec, ACP and runtime-API hosts
set it false) and contextual_tips is on. One successful call per session
(keyed by the session's tool state namespace); a second call errors. The
description is narrowed and shorter (full tool-schema bytes -17).

P6 (PLG-9, PLG-10, PLG-11): PLUGINS.md is rewritten once around the policy
and gains "Browser: pick one" (chrome-devtools MCP, Playwright MCP,
Computer Use browser_*, Chromewhale developer preview). The /mcp
recommendations heading reads "Suggested MCP servers (nothing installs
automatically)".

Localization: new PluginCtaInstall, PluginCtaReviewTrust, PluginCtaEnable;
PluginCtaInstallPrompt, PluginCtaDismiss and McpRecommendationsHeading
changed. All 15 packs updated.

Checks (targeted, per the lane rules):
- cargo test -p codewhale-tui --lib -- plugin mcp escape tool_setup:
  776 passed, 1 failed (OS fixture in plugins::recommend, fixed in the
  previous commit and rerun green: 9 passed, 0 failed)
- cargo test -p codewhale-localization: 50 passed, 0 failed
- python3 scripts/check-runtime-contract-budget.py: PASS, 55 ceilings
  respected (6 can tighten; not tightened here)
- cargo clippy -p codewhale-tui --lib --tests: no findings in touched
  files (existing findings elsewhere, e.g. lib.rs, pricing.rs)

Refs: 0.10.1 addendum P3, P4, P6 (codewhale-ops/releases/0.10.1/ADDENDUM-EXPERIENCE.md)

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-09-22 15:05:37 -07:00
1 parent eb59a59f46
commit d5cec111e6
26 files changed
+447 -234

No files matched your search

+6 -3
View File
@@ -449,7 +449,7 @@
"CmdMcpDescription": "Obre o gestiona servidors MCP — el subcomandament init afegeix un servidor i doctor el comprova",
"McpReloadAlreadyRunning": "La recàrrega MCP ja s'està executant; la barra d'estat en fa el seguiment.",
"McpRecommendedUnknownId": "ID MCP recomanat desconegut. Executa {recommendations_command} per revisar la llista seleccionada.",
"McpRecommendationsHeading": "Plugins suggerits de Codewhale (components MCP; no s’instal·la res automàticament)",
"McpRecommendationsHeading": "Servidors MCP suggerits (no s’instal·la res automàticament)",
"McpRecommendationsSafety": "Veure aquesta llista no afegeix ni activa res. Un afegit explícit només escriu la configuració; revisa-la abans que {restart_command} connecti el servidor.",
"McpRecommendationGithub": "• github — punt final MCP remot oficial de GitHub\n punt final: {endpoint}\n l’autenticació va a part: usa {login_command} només si el servidor anuncia OAuth;\n si no, configura fora de l’historial un PAT amb privilegis mínims. Els permisos\n concedits poden escriure o suprimir dades del repositori; comença en només lectura si és possible.\n afegeix explícitament: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP oficial de Chrome DevTools mitjançant un paquet npm fixat\n paquet: {package} ({launcher})\n pot inspeccionar/controlar Chrome i llegir pàgines autenticades. Tanca les pestanyes\n sensibles i verifica el paquet abans d’afegir-lo; {restart_command} pot baixar-lo i executar-lo.\n afegeix explícitament: {add_command}",
@@ -500,9 +500,12 @@
"PluginPromptSuggestTrust": "Això sembla feina de {name}. Revisa-ho amb /plugin trust {name} abans d’activar-ho.",
"PluginPromptSuggestEnable": "Això sembla feina de {name}. Activa-ho amb /plugin enable {name}.",
"PluginPromptSuggestMarketplace": "Això sembla feina de {name}. Instal·la-ho del catàleg `{catalog}` amb /plugin marketplace install {catalog} {name}.",
"PluginCtaInstallPrompt": "Instal·lar el connector {name}?",
"PluginCtaInstallPrompt": "Connector suggerit: {name}",
"PluginCtaReview": "Revisa",
"PluginCtaDismiss": "Descarta",
"PluginCtaInstall": "Instal·la",
"PluginCtaReviewTrust": "Revisa la confiança",
"PluginCtaEnable": "Activa",
"PluginCtaDismiss": "No ho tornis a suggerir",
"PluginCtaDismissSaveFailed": "Ocult durant aquesta sessió; no s'ha pogut desar la preferència del connector.",
"PluginSuggestionReason": "Coincideix amb «{trigger}»",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\nVersió: {version}\nFont: {origin} ({scope})\nEstat: {state}\nConfiança: {trust}\nComponents: {inventory}\nPermisos sol·licitats: {permissions}\nServidors MCP: {mcp}\nNo compatible/inactiu: {unsupported}\nHash de contingut: {content_hash}\nHash de capacitats: {capability_hash}\nRuta: {path}",
+6 -3
View File
@@ -449,7 +449,7 @@
"CmdMcpDescription": "MCP-Server öffnen oder verwalten — der Unterbefehl init fügt einen Server hinzu und doctor prüft ihn",
"McpReloadAlreadyRunning": "Ein MCP-Neuladen läuft bereits; die Statusleiste verfolgt es.",
"McpRecommendedUnknownId": "Unbekannte empfohlene MCP-ID. Mit {recommendations_command} kann die kuratierte Liste geprüft werden.",
"McpRecommendationsHeading": "Vorgeschlagene Codewhale-Plugins (MCP-Komponenten; nichts wird automatisch installiert)",
"McpRecommendationsHeading": "Vorgeschlagene MCP-Server (nichts wird automatisch installiert)",
"McpRecommendationsSafety": "Diese Liste fügt nichts hinzu und aktiviert nichts. Explizites Hinzufügen schreibt nur die Konfiguration; prüfe sie, bevor {restart_command} den Server verbindet.",
"McpRecommendationGithub": "• github — offizieller Remote-MCP-Endpunkt von GitHub\n Endpunkt: {endpoint}\n Authentifizierung erfolgt getrennt: {login_command} nur verwenden, wenn der Server OAuth anbietet;\n andernfalls außerhalb des Befehlsverlaufs ein PAT mit minimalen Rechten konfigurieren. Erteilte\n Rechte können Repository-Daten schreiben oder löschen; möglichst schreibgeschützt beginnen.\n explizit hinzufügen: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — offizielles Chrome-DevTools-MCP über ein fest versioniertes npm-Paket\n Paket: {package} ({launcher})\n es kann Chrome untersuchen/steuern und authentifizierte Seiten lesen. Vertrauliche Tabs\n schließen und das Paket vor dem Hinzufügen prüfen; {restart_command} kann es laden und ausführen.\n explizit hinzufügen: {add_command}",
@@ -500,9 +500,12 @@
"PluginPromptSuggestTrust": "Das sieht nach {name}-Arbeit aus. Vor dem Aktivieren mit /plugin trust {name} prüfen.",
"PluginPromptSuggestEnable": "Das sieht nach {name}-Arbeit aus. Mit /plugin enable {name} aktivieren.",
"PluginPromptSuggestMarketplace": "Das sieht nach {name}-Arbeit aus. Aus Katalog `{catalog}` installieren: /plugin marketplace install {catalog} {name}.",
"PluginCtaInstallPrompt": "{name}-Plugin installieren?",
"PluginCtaInstallPrompt": "Vorgeschlagenes Plugin: {name}",
"PluginCtaReview": "Prüfen",
"PluginCtaDismiss": "Verwerfen",
"PluginCtaInstall": "Installieren",
"PluginCtaReviewTrust": "Vertrauen prüfen",
"PluginCtaEnable": "Aktivieren",
"PluginCtaDismiss": "Nicht mehr vorschlagen",
"PluginCtaDismissSaveFailed": "Für diese Sitzung ausgeblendet; die Plugin-Einstellung konnte nicht gespeichert werden.",
"PluginSuggestionReason": "Treffer für „{trigger}“",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\nVersion: {version}\nQuelle: {origin} ({scope})\nStatus: {state}\nVertrauen: {trust}\nKomponenten: {inventory}\nAngeforderte Berechtigungen: {permissions}\nMCP-Server: {mcp}\nNicht unterstützt/inaktiv: {unsupported}\nInhalts-Hash: {content_hash}\nFähigkeits-Hash: {capability_hash}\nPfad: {path}",
+6 -3
View File
@@ -452,7 +452,7 @@
"CmdMcpDescription": "Open or manage MCP servers — the init subcommand adds a server and doctor checks it",
"McpReloadAlreadyRunning": "MCP reload is already running; the status bar tracks it.",
"McpRecommendedUnknownId": "Unknown MCP suggestion. Run {recommendations_command} to see the list.",
"McpRecommendationsHeading": "Suggested Codewhale plugins (MCP components; nothing installs automatically)",
"McpRecommendationsHeading": "Suggested MCP servers (nothing installs automatically)",
"McpRecommendationsSafety": "Looking adds nothing. Adding writes config only — review it before {restart_command} connects anything.",
"McpRecommendationGithub": "• github — GitHub's official remote MCP endpoint\n endpoint: {endpoint}\n auth is separate: {login_command} only for advertised OAuth;\n otherwise set a least-privilege PAT outside history. Scopes\n can write or delete repo data, so start read-only.\n add explicitly: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — official Chrome DevTools MCP (pinned npm package)\n package: {package} ({launcher})\n it can drive Chrome and read signed-in pages. Close sensitive\n tabs and verify the package first; {restart_command} may download and run it.\n add explicitly: {add_command}",
@@ -503,9 +503,12 @@
"PluginPromptSuggestTrust": "This looks like {name} work — run /plugin trust {name} before enabling.",
"PluginPromptSuggestEnable": "This looks like {name} work — enable it with /plugin enable {name}.",
"PluginPromptSuggestMarketplace": "This looks like {name} work. Install it from catalog `{catalog}` with /plugin marketplace install {catalog} {name}.",
"PluginCtaInstallPrompt": "Install {name} plugin?",
"PluginCtaInstallPrompt": "Suggested plugin: {name}",
"PluginCtaReview": "Review",
"PluginCtaDismiss": "Dismiss",
"PluginCtaInstall": "Install",
"PluginCtaReviewTrust": "Review trust",
"PluginCtaEnable": "Enable",
"PluginCtaDismiss": "Don't suggest again",
"PluginCtaDismissSaveFailed": "Hidden for this session; could not save the plugin preference.",
"PluginSuggestionReason": "Matched “{trigger}”",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\nVersion: {version}\nSource: {origin} ({scope})\nState: {state}\nTrust: {trust}\nComponents: {inventory}\nRequested permissions: {permissions}\nMCP servers: {mcp}\nUnsupported/inactive: {unsupported}\nContent hash: {content_hash}\nCapability hash: {capability_hash}\nPath: {path}",
+6 -3
View File
@@ -452,7 +452,7 @@
"CmdMcpDescription": "Abrir o gestionar servidores MCP — el subcomando init agrega un servidor y doctor lo revisa",
"McpReloadAlreadyRunning": "La recarga de MCP ya está en curso; la barra de estado la sigue.",
"McpRecommendedUnknownId": "ID de MCP recomendado desconocido. Ejecuta {recommendations_command} para revisar la lista seleccionada.",
"McpRecommendationsHeading": "Plugins sugeridos de Codewhale (componentes MCP; nada se instala automáticamente)",
"McpRecommendationsHeading": "Servidores MCP sugeridos (nada se instala automáticamente)",
"McpRecommendationsSafety": "Ver esta lista no agrega ni habilita nada. Agregar algo explícitamente solo escribe la configuración; revísala antes de que {restart_command} conecte el servidor.",
"McpRecommendationGithub": "• github — endpoint MCP remoto oficial de GitHub\n endpoint: {endpoint}\n la autenticación es aparte: usa {login_command} solo si el servidor anuncia OAuth;\n de lo contrario, configura un PAT con privilegios mínimos fuera del historial de comandos. Los\n permisos concedidos pueden escribir o borrar datos del repositorio; empieza en modo de solo lectura cuando sea posible.\n agregar explícitamente: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP oficial de Chrome DevTools mediante un paquete npm fijado\n paquete: {package} ({launcher})\n puede inspeccionar o controlar Chrome y leer páginas autenticadas. Cierra las pestañas\n sensibles y verifica el paquete antes de agregarlo; {restart_command} puede descargarlo y ejecutarlo.\n agregar explícitamente: {add_command}",
@@ -503,9 +503,12 @@
"PluginPromptSuggestTrust": "Esto parece trabajo de {name}. Revísalo con /plugin trust {name} antes de habilitarlo.",
"PluginPromptSuggestEnable": "Esto parece trabajo de {name}. Habilítalo con /plugin enable {name}.",
"PluginPromptSuggestMarketplace": "Esto parece trabajo de {name}. Instálalo del catálogo `{catalog}` con /plugin marketplace install {catalog} {name}.",
"PluginCtaInstallPrompt": "¿Instalar el plugin {name}?",
"PluginCtaInstallPrompt": "Plugin sugerido: {name}",
"PluginCtaReview": "Revisar",
"PluginCtaDismiss": "Descartar",
"PluginCtaInstall": "Instalar",
"PluginCtaReviewTrust": "Revisar confianza",
"PluginCtaEnable": "Habilitar",
"PluginCtaDismiss": "No volver a sugerir",
"PluginCtaDismissSaveFailed": "Oculto durante esta sesión; no se pudo guardar la preferencia del complemento.",
"PluginSuggestionReason": "Coincide con «{trigger}»",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\nVersión: {version}\nFuente: {origin} ({scope})\nEstado: {state}\nConfianza: {trust}\nComponentes: {inventory}\nPermisos solicitados: {permissions}\nServidores MCP: {mcp}\nNo compatible/inactivo: {unsupported}\nHash de contenido: {content_hash}\nHash de capacidades: {capability_hash}\nRuta: {path}",
+6 -3
View File
@@ -449,7 +449,7 @@
"CmdMcpDescription": "Ouvrir ou gérer les serveurs MCP — la sous-commande init ajoute un serveur et doctor le vérifie",
"McpReloadAlreadyRunning": "Un rechargement MCP est déjà en cours ; la barre d'état le suit.",
"McpRecommendedUnknownId": "ID MCP recommandé inconnu. Exécutez {recommendations_command} pour consulter la liste sélectionnée.",
"McpRecommendationsHeading": "Plugins Codewhale suggérés (composants MCP ; aucune installation automatique)",
"McpRecommendationsHeading": "Serveurs MCP suggérés (aucune installation automatique)",
"McpRecommendationsSafety": "Consulter cette liste n’ajoute ni n’active rien. Un ajout explicite écrit seulement la configuration ; vérifiez-la avant que {restart_command} connecte le serveur.",
"McpRecommendationGithub": "• github — point de terminaison MCP distant officiel de GitHub\n point de terminaison : {endpoint}\n l’authentification est séparée : utilisez {login_command} uniquement si le serveur annonce OAuth ;\n sinon, configurez hors de l’historique un PAT aux privilèges minimaux. Les autorisations\n accordées peuvent écrire ou supprimer des données du dépôt ; commencez en lecture seule si possible.\n ajouter explicitement : {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP Chrome DevTools officiel via un paquet npm à version fixe\n paquet : {package} ({launcher})\n il peut inspecter/contrôler Chrome et lire des pages authentifiées. Fermez les onglets\n sensibles et vérifiez le paquet avant l’ajout ; {restart_command} peut le télécharger et l’exécuter.\n ajouter explicitement : {add_command}",
@@ -500,9 +500,12 @@
"PluginPromptSuggestTrust": "Cela ressemble à un travail {name}. Vérifiez-le avec /plugin trust {name} avant de l’activer.",
"PluginPromptSuggestEnable": "Cela ressemble à un travail {name}. Activez-le avec /plugin enable {name}.",
"PluginPromptSuggestMarketplace": "Cela ressemble à un travail {name}. Installez-le depuis le catalogue `{catalog}` avec /plugin marketplace install {catalog} {name}.",
"PluginCtaInstallPrompt": "Installer le plugin {name} ?",
"PluginCtaInstallPrompt": "Plugin suggéré : {name}",
"PluginCtaReview": "Examiner",
"PluginCtaDismiss": "Ignorer",
"PluginCtaInstall": "Installer",
"PluginCtaReviewTrust": "Examiner la confiance",
"PluginCtaEnable": "Activer",
"PluginCtaDismiss": "Ne plus suggérer",
"PluginCtaDismissSaveFailed": "Masqué pour cette session ; impossible d’enregistrer la préférence du plugin.",
"PluginSuggestionReason": "Correspond à « {trigger} »",
"CmdPluginBundleDetail": "{name}\n========================================\nID : {id}\nVersion : {version}\nSource : {origin} ({scope})\nÉtat : {state}\nConfiance : {trust}\nComposants : {inventory}\nPermissions demandées : {permissions}\nServeurs MCP : {mcp}\nNon pris en charge/inactif : {unsupported}\nHash du contenu : {content_hash}\nHash des capacités : {capability_hash}\nChemin : {path}",
+6 -3
View File
@@ -449,7 +449,7 @@
"CmdMcpDescription": "MCP सर्वर खोलें या प्रबंधित करें — init उपकमांड सर्वर जोड़ता है और doctor उसकी जाँच करता है",
"McpReloadAlreadyRunning": "MCP पुनः लोड पहले से चल रहा है; स्थिति पट्टी इसे ट्रैक करती है।",
"McpRecommendedUnknownId": "अज्ञात सुझाई गई MCP ID। चुनी हुई सूची देखने के लिए {recommendations_command} चलाएँ।",
"McpRecommendationsHeading": "सुझाए गए Codewhale प्लगइन (MCP घटक; कुछ भी अपने आप इंस्टॉल नहीं होता)",
"McpRecommendationsHeading": "सुझाए गए MCP सर्वर (कुछ भी अपने आप इंस्टॉल नहीं होता)",
"McpRecommendationsSafety": "इस सूची को देखने से कुछ भी जुड़ता या चालू नहीं होता। स्पष्ट रूप से जोड़ने पर केवल कॉन्फ़िगरेशन लिखा जाता है; {restart_command} से सर्वर जुड़ने से पहले इसकी जाँच करें।",
"McpRecommendationGithub": "• github — GitHub का आधिकारिक रिमोट MCP एंडपॉइंट\n एंडपॉइंट: {endpoint}\n प्रमाणीकरण अलग है: {login_command} केवल तभी चलाएँ जब सर्वर OAuth उपलब्ध बताए;\n अन्यथा कम-से-कम अधिकार वाला PAT कमांड इतिहास से बाहर कॉन्फ़िगर करें। दिए गए\n स्कोप रिपॉज़िटरी डेटा लिख या मिटा सकते हैं; जहाँ संभव हो केवल-पढ़ने से शुरू करें।\n स्पष्ट रूप से जोड़ें: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — निश्चित संस्करण वाले npm पैकेज से आधिकारिक Chrome DevTools MCP\n पैकेज: {package} ({launcher})\n यह Chrome की जाँच/नियंत्रण और प्रमाणित पेज पढ़ सकता है। संवेदनशील टैब बंद करें\n और जोड़ने से पहले पैकेज जाँचें; {restart_command} इसे डाउनलोड करके चला सकता है।\n स्पष्ट रूप से जोड़ें: {add_command}",
@@ -500,9 +500,12 @@
"PluginPromptSuggestTrust": "यह {name} वाला काम लगता है। सक्षम करने से पहले /plugin trust {name} से समीक्षा करें।",
"PluginPromptSuggestEnable": "यह {name} वाला काम लगता है। /plugin enable {name} से सक्षम करें।",
"PluginPromptSuggestMarketplace": "यह {name} वाला काम लगता है। कैटलॉग `{catalog}` से /plugin marketplace install {catalog} {name} से इंस्टॉल करें।",
"PluginCtaInstallPrompt": "{name} प्लगइन इंस्टॉल करें?",
"PluginCtaInstallPrompt": "सुझाया गया प्लगइन: {name}",
"PluginCtaReview": "समीक्षा",
"PluginCtaDismiss": "बंद करें",
"PluginCtaInstall": "इंस्टॉल करें",
"PluginCtaReviewTrust": "भरोसे की समीक्षा करें",
"PluginCtaEnable": "सक्षम करें",
"PluginCtaDismiss": "फिर से सुझाव न दें",
"PluginCtaDismissSaveFailed": "इस सत्र के लिए छिपाया गया; प्लगइन की प्राथमिकता सहेजी नहीं जा सकी।",
"PluginSuggestionReason": "“{trigger}” से मेल खाता है",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\nसंस्करण: {version}\nस्रोत: {origin} ({scope})\nस्थिति: {state}\nट्रस्ट: {trust}\nघटक: {inventory}\nअनुरोधित अनुमतियाँ: {permissions}\nMCP सर्वर: {mcp}\nअसमर्थित/निष्क्रिय: {unsupported}\nकंटेंट हैश: {content_hash}\nक्षमता हैश: {capability_hash}\nपथ: {path}",
+6 -3
View File
@@ -449,7 +449,7 @@
"CmdMcpDescription": "Buka atau kelola server MCP — subperintah init menambah server dan doctor memeriksanya",
"McpReloadAlreadyRunning": "Pemuatan ulang MCP sedang berjalan; bilah status melacaknya.",
"McpRecommendedUnknownId": "ID MCP rekomendasi tidak dikenal. Jalankan {recommendations_command} untuk memeriksa daftar pilihan.",
"McpRecommendationsHeading": "Plugin Codewhale yang disarankan (komponen MCP; tidak ada pemasangan otomatis)",
"McpRecommendationsHeading": "Server MCP yang disarankan (tidak ada pemasangan otomatis)",
"McpRecommendationsSafety": "Melihat daftar ini tidak menambah atau mengaktifkan apa pun. Penambahan eksplisit hanya menulis konfigurasi; periksa sebelum {restart_command} menghubungkan server.",
"McpRecommendationGithub": "• github — endpoint MCP jarak jauh resmi GitHub\n endpoint: {endpoint}\n autentikasi terpisah: gunakan {login_command} hanya jika server menawarkan OAuth;\n jika tidak, atur PAT dengan hak minimum di luar riwayat perintah. Cakupan yang\n diberikan dapat menulis atau menghapus data repositori; mulai dengan akses hanya-baca jika memungkinkan.\n tambahkan secara eksplisit: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP Chrome DevTools resmi lewat paket npm dengan versi terkunci\n paket: {package} ({launcher})\n ini dapat memeriksa/mengontrol Chrome dan membaca halaman terautentikasi. Tutup tab\n sensitif dan verifikasi paket sebelum menambahkannya; {restart_command} dapat mengunduh dan menjalankannya.\n tambahkan secara eksplisit: {add_command}",
@@ -500,9 +500,12 @@
"PluginPromptSuggestTrust": "Ini tampak seperti pekerjaan {name}. Tinjau dengan /plugin trust {name} sebelum mengaktifkan.",
"PluginPromptSuggestEnable": "Ini tampak seperti pekerjaan {name}. Aktifkan dengan /plugin enable {name}.",
"PluginPromptSuggestMarketplace": "Ini tampak seperti pekerjaan {name}. Pasang dari katalog `{catalog}` dengan /plugin marketplace install {catalog} {name}.",
"PluginCtaInstallPrompt": "Pasang plugin {name}?",
"PluginCtaInstallPrompt": "Plugin yang disarankan: {name}",
"PluginCtaReview": "Tinjau",
"PluginCtaDismiss": "Tutup",
"PluginCtaInstall": "Pasang",
"PluginCtaReviewTrust": "Tinjau kepercayaan",
"PluginCtaEnable": "Aktifkan",
"PluginCtaDismiss": "Jangan sarankan lagi",
"PluginCtaDismissSaveFailed": "Disembunyikan untuk sesi ini; preferensi plugin tidak dapat disimpan.",
"PluginSuggestionReason": "Cocok dengan “{trigger}”",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\nVersi: {version}\nSumber: {origin} ({scope})\nStatus: {state}\nKepercayaan: {trust}\nKomponen: {inventory}\nIzin yang diminta: {permissions}\nServer MCP: {mcp}\nTidak didukung/nonaktif: {unsupported}\nHash konten: {content_hash}\nHash kapabilitas: {capability_hash}\nJalur: {path}",
+6 -3
View File
@@ -452,7 +452,7 @@
"CmdMcpDescription": "MCP サーバを開く・管理する — init サブコマンドでサーバを追加し doctor で点検する",
"McpReloadAlreadyRunning": "MCP の再読み込みは実行中です。ステータスバーが進行状況を示します。",
"McpRecommendedUnknownId": "推奨 MCP ID が不明です。{recommendations_command} で精選リストを確認してください。",
"McpRecommendationsHeading": "Codewhale の推奨プラグイン(MCP コンポーネント。自動インストールなし)",
"McpRecommendationsHeading": "推奨 MCP サーバー(自動インストールなし)",
"McpRecommendationsSafety": "この一覧を見ても、何も追加・有効化されません。明示的な追加は設定を書き込むだけです。{restart_command} がサーバーへ接続する前に確認してください。",
"McpRecommendationGithub": "• github — GitHub 公式のリモート MCP エンドポイント\n エンドポイント: {endpoint}\n 認証は別です。サーバーが OAuth を提供する場合だけ {login_command} を使用してください。\n それ以外は、コマンド履歴の外で最小権限の PAT を設定してください。付与した\n スコープはリポジトリデータを書き込み・削除できるため、可能なら読み取り専用で始めてください。\n 明示的に追加: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — バージョン固定 npm パッケージによる公式 Chrome DevTools MCP\n パッケージ: {package} ({launcher})\n Chrome の調査・操作や認証済みページの読み取りが可能です。機密タブを\n 閉じ、追加前にパッケージを確認してください。{restart_command} はダウンロードして実行する場合があります。\n 明示的に追加: {add_command}",
@@ -503,9 +503,12 @@
"PluginPromptSuggestTrust": "これは {name} 向けの作業に見えます。有効化の前に /plugin trust {name} で確認してください。",
"PluginPromptSuggestEnable": "これは {name} 向けの作業に見えます。/plugin enable {name} で有効化できます。",
"PluginPromptSuggestMarketplace": "これは {name} 向けの作業に見えます。カタログ `{catalog}` から /plugin marketplace install {catalog} {name} でインストールできます。",
"PluginCtaInstallPrompt": "{name} プラグインをインストールしますか?",
"PluginCtaInstallPrompt": "おすすめのプラグイン: {name}",
"PluginCtaReview": "確認",
"PluginCtaDismiss": "閉じる",
"PluginCtaInstall": "インストール",
"PluginCtaReviewTrust": "信頼を確認",
"PluginCtaEnable": "有効化",
"PluginCtaDismiss": "今後提案しない",
"PluginCtaDismissSaveFailed": "このセッションでは非表示にしました。プラグインの設定を保存できませんでした。",
"PluginSuggestionReason": "「{trigger}」に一致",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\nバージョン: {version}\n出所: {origin} ({scope})\n状態: {state}\n信頼: {trust}\nコンポーネント: {inventory}\n要求権限: {permissions}\nMCP サーバー: {mcp}\n未対応/無効: {unsupported}\nコンテンツハッシュ: {content_hash}\n権限ハッシュ: {capability_hash}\nパス: {path}",
+6 -3
View File
@@ -452,7 +452,7 @@
"CmdMcpDescription": "MCP 서버를 열거나 관리합니다 — init 하위 명령은 서버를 추가하고 doctor 는 상태를 점검합니다",
"McpReloadAlreadyRunning": "MCP 다시 불러오기가 이미 실행 중입니다. 상태 표시줄에서 확인하세요.",
"McpRecommendedUnknownId": "알 수 없는 권장 MCP ID입니다. {recommendations_command} 명령으로 선별 목록을 확인하세요.",
"McpRecommendationsHeading": "추천 Codewhale 플러그인(MCP 구성 요소, 자동 설치 없음)",
"McpRecommendationsHeading": "추천 MCP 서버(자동 설치 없음)",
"McpRecommendationsSafety": "이 목록을 보는 것만으로는 아무것도 추가하거나 활성화하지 않습니다. 명시적 추가는 설정만 기록합니다. {restart_command} 명령이 서버를 연결하기 전에 검토하세요.",
"McpRecommendationGithub": "• github — GitHub 공식 원격 MCP 엔드포인트\n 엔드포인트: {endpoint}\n 인증은 별도입니다. 서버가 OAuth를 제공할 때만 {login_command} 명령을 사용하세요.\n 그 외에는 명령 기록 밖에서 최소 권한 PAT를 설정하세요. 부여한 범위는\n 저장소 데이터를 쓰거나 삭제할 수 있으므로 가능하면 읽기 전용으로 시작하세요.\n 명시적으로 추가: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — 버전을 고정한 npm 패키지 기반 공식 Chrome DevTools MCP\n 패키지: {package} ({launcher})\n Chrome을 검사/제어하고 인증된 페이지를 읽을 수 있습니다. 민감한 탭을\n 닫고 추가 전에 패키지를 확인하세요. {restart_command} 명령이 다운로드하여 실행할 수 있습니다.\n 명시적으로 추가: {add_command}",
@@ -503,9 +503,12 @@
"PluginPromptSuggestTrust": "이 작업은 {name}와(과) 관련이 있어 보입니다. 활성화하기 전에 /plugin trust {name}으로 검토하세요.",
"PluginPromptSuggestEnable": "이 작업은 {name}와(과) 관련이 있어 보입니다. /plugin enable {name}으로 활성화하세요.",
"PluginPromptSuggestMarketplace": "이 작업은 {name}와(과) 관련이 있어 보입니다. 카탈로그 `{catalog}`에서 /plugin marketplace install {catalog} {name}으로 설치하세요.",
"PluginCtaInstallPrompt": "{name} 플러그인을 설치할까요?",
"PluginCtaInstallPrompt": "추천 플러그인: {name}",
"PluginCtaReview": "검토",
"PluginCtaDismiss": "닫기",
"PluginCtaInstall": "설치",
"PluginCtaReviewTrust": "신뢰 검토",
"PluginCtaEnable": "활성화",
"PluginCtaDismiss": "다시 제안하지 않기",
"PluginCtaDismissSaveFailed": "이 세션에서는 숨겼습니다. 플러그인 설정을 저장하지 못했습니다.",
"PluginSuggestionReason": "“{trigger}”와 일치",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\n버전: {version}\n출처: {origin} ({scope})\n상태: {state}\n신뢰: {trust}\n구성 요소: {inventory}\n요청 권한: {permissions}\nMCP 서버: {mcp}\n미지원/비활성: {unsupported}\n콘텐츠 해시: {content_hash}\n기능 해시: {capability_hash}\n경로: {path}",
+6 -3
View File
@@ -452,7 +452,7 @@
"CmdMcpDescription": "Abrir ou gerenciar servidores MCP — o subcomando init adiciona um servidor e doctor o verifica",
"McpReloadAlreadyRunning": "A recarga do MCP já está em andamento; a barra de status a acompanha.",
"McpRecommendedUnknownId": "ID de MCP recomendado desconhecido. Execute {recommendations_command} para conferir a lista selecionada.",
"McpRecommendationsHeading": "Plugins sugeridos do Codewhale (componentes MCP; nada é instalado automaticamente)",
"McpRecommendationsHeading": "Servidores MCP sugeridos (nada é instalado automaticamente)",
"McpRecommendationsSafety": "Ver esta lista não adiciona nem ativa nada. Uma adição explícita só grava a configuração; revise-a antes que {restart_command} conecte o servidor.",
"McpRecommendationGithub": "• github — endpoint MCP remoto oficial do GitHub\n endpoint: {endpoint}\n a autenticação é separada: use {login_command} somente se o servidor anunciar OAuth;\n caso contrário, configure um PAT com privilégios mínimos fora do histórico de comandos. Os\n escopos concedidos podem gravar ou excluir dados do repositório; comece em modo somente leitura quando possível.\n adicionar explicitamente: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP oficial do Chrome DevTools via pacote npm com versão fixada\n pacote: {package} ({launcher})\n ele pode inspecionar/controlar o Chrome e ler páginas autenticadas. Feche abas\n confidenciais e verifique o pacote antes de adicioná-lo; {restart_command} pode baixá-lo e executá-lo.\n adicionar explicitamente: {add_command}",
@@ -503,9 +503,12 @@
"PluginPromptSuggestTrust": "Isso parece um trabalho de {name}. Revise com /plugin trust {name} antes de ativar.",
"PluginPromptSuggestEnable": "Isso parece um trabalho de {name}. Ative com /plugin enable {name}.",
"PluginPromptSuggestMarketplace": "Isso parece um trabalho de {name}. Instale do catálogo `{catalog}` com /plugin marketplace install {catalog} {name}.",
"PluginCtaInstallPrompt": "Instalar o plugin {name}?",
"PluginCtaInstallPrompt": "Plugin sugerido: {name}",
"PluginCtaReview": "Revisar",
"PluginCtaDismiss": "Dispensar",
"PluginCtaInstall": "Instalar",
"PluginCtaReviewTrust": "Revisar confiança",
"PluginCtaEnable": "Ativar",
"PluginCtaDismiss": "Não sugerir novamente",
"PluginCtaDismissSaveFailed": "Oculto nesta sessão; não foi possível salvar a preferência do plugin.",
"PluginSuggestionReason": "Corresponde a “{trigger}”",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\nVersão: {version}\nOrigem: {origin} ({scope})\nEstado: {state}\nConfiança: {trust}\nComponentes: {inventory}\nPermissões solicitadas: {permissions}\nServidores MCP: {mcp}\nNão suportado/inativo: {unsupported}\nHash do conteúdo: {content_hash}\nHash de capacidades: {capability_hash}\nCaminho: {path}",
+6 -3
View File
@@ -449,7 +449,7 @@
"CmdMcpDescription": "Открыть список серверов MCP или управлять ими — подкоманда init добавляет сервер а doctor его проверяет",
"McpReloadAlreadyRunning": "Перезагрузка MCP уже выполняется; строка состояния показывает прогресс.",
"McpRecommendedUnknownId": "Неизвестный идентификатор рекомендованного MCP. Выполните {recommendations_command}, чтобы просмотреть отобранный список.",
"McpRecommendationsHeading": "Рекомендуемые плагины Codewhale (компоненты MCP; автоматической установки нет)",
"McpRecommendationsHeading": "Рекомендуемые серверы MCP (автоматической установки нет)",
"McpRecommendationsSafety": "Просмотр списка ничего не добавляет и не включает. Явное добавление только записывает конфигурацию; проверьте её до подключения сервера командой {restart_command}.",
"McpRecommendationGithub": "• github — официальный удалённый MCP-адрес GitHub\n адрес: {endpoint}\n аутентификация выполняется отдельно: используйте {login_command}, только если сервер заявляет OAuth;\n иначе настройте PAT с минимальными правами вне истории команд. Выданные\n области доступа могут изменять или удалять данные репозитория; по возможности начните с чтения.\n добавить явно: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — официальный Chrome DevTools MCP через npm-пакет закреплённой версии\n пакет: {package} ({launcher})\n он может исследовать/управлять Chrome и читать страницы с авторизацией. Закройте\n конфиденциальные вкладки и проверьте пакет до добавления; {restart_command} может скачать и запустить его.\n добавить явно: {add_command}",
@@ -500,9 +500,12 @@
"PluginPromptSuggestTrust": "Похоже на работу с {name}. Перед включением проверьте: /plugin trust {name}",
"PluginPromptSuggestEnable": "Похоже на работу с {name}. Включите: /plugin enable {name}",
"PluginPromptSuggestMarketplace": "Похоже на работу с {name}. Установите из каталога `{catalog}`: /plugin marketplace install {catalog} {name}",
"PluginCtaInstallPrompt": "Установить плагин {name}?",
"PluginCtaInstallPrompt": "Рекомендуемый плагин: {name}",
"PluginCtaReview": "Проверить",
"PluginCtaDismiss": "Скрыть",
"PluginCtaInstall": "Установить",
"PluginCtaReviewTrust": "Проверить доверие",
"PluginCtaEnable": "Включить",
"PluginCtaDismiss": "Больше не предлагать",
"PluginCtaDismissSaveFailed": "Скрыто на время сеанса; не удалось сохранить настройку плагина.",
"PluginSuggestionReason": "Совпадение с «{trigger}»",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\nВерсия: {version}\nИсточник: {origin} ({scope})\nСостояние: {state}\nДоверие: {trust}\nКомпоненты: {inventory}\nЗапрошенные разрешения: {permissions}\nСерверы MCP: {mcp}\nНеподдерживаемые/неактивные: {unsupported}\nХэш содержимого: {content_hash}\nХэш возможностей: {capability_hash}\nПуть: {path}",
+6 -3
View File
@@ -449,7 +449,7 @@
"CmdMcpDescription": "Відкрити або керувати серверами MCP — підкоманда init додає сервер а doctor його перевіряє",
"McpReloadAlreadyRunning": "Перезавантаження MCP уже триває; рядок стану показує поступ.",
"McpRecommendedUnknownId": "Невідомий ідентифікатор рекомендованого MCP. Виконайте {recommendations_command}, щоб переглянути відібраний список.",
"McpRecommendationsHeading": "Рекомендовані плагіни Codewhale (компоненти MCP; автоматичного встановлення немає)",
"McpRecommendationsHeading": "Рекомендовані сервери MCP (автоматичного встановлення немає)",
"McpRecommendationsSafety": "Перегляд списку нічого не додає й не вмикає. Явне додавання лише записує конфігурацію; перевірте її до підключення сервера командою {restart_command}.",
"McpRecommendationGithub": "• github — офіційна віддалена MCP-адреса GitHub\n адреса: {endpoint}\n автентифікація виконується окремо: використовуйте {login_command}, лише якщо сервер заявляє OAuth;\n інакше налаштуйте PAT із мінімальними правами поза історією команд. Надані\n області доступу можуть змінювати або видаляти дані репозиторію; за можливості почніть із читання.\n додати явно: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — офіційний Chrome DevTools MCP через npm-пакет закріпленої версії\n пакет: {package} ({launcher})\n він може досліджувати/керувати Chrome і читати сторінки з авторизацією. Закрийте\n конфіденційні вкладки й перевірте пакет до додавання; {restart_command} може завантажити та запустити його.\n додати явно: {add_command}",
@@ -500,9 +500,12 @@
"PluginPromptSuggestTrust": "Схоже на роботу з {name}. Перед увімкненням перевірте: /plugin trust {name}",
"PluginPromptSuggestEnable": "Схоже на роботу з {name}. Увімкніть: /plugin enable {name}",
"PluginPromptSuggestMarketplace": "Схоже на роботу з {name}. Встановіть із каталогу `{catalog}`: /plugin marketplace install {catalog} {name}",
"PluginCtaInstallPrompt": "Встановити плагін {name}?",
"PluginCtaInstallPrompt": "Рекомендований плагін: {name}",
"PluginCtaReview": "Перевірити",
"PluginCtaDismiss": "Сховати",
"PluginCtaInstall": "Встановити",
"PluginCtaReviewTrust": "Перевірити довіру",
"PluginCtaEnable": "Увімкнути",
"PluginCtaDismiss": "Більше не пропонувати",
"PluginCtaDismissSaveFailed": "Приховано на час сеансу; не вдалося зберегти налаштування плагіна.",
"PluginSuggestionReason": "Збіг із «{trigger}»",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\nВерсія: {version}\nДжерело: {origin} ({scope})\nСтан: {state}\nДовіра: {trust}\nКомпоненти: {inventory}\nЗапитувані дозволи: {permissions}\nСервери MCP: {mcp}\nНепідтримувані/неактивні: {unsupported}\nХеш вмісту: {content_hash}\nХеш можливостей: {capability_hash}\nШлях: {path}",
+6 -3
View File
@@ -452,7 +452,7 @@
"CmdMcpDescription": "Mở hoặc quản lý các máy chủ MCP — lệnh con init thêm máy chủ và doctor kiểm tra nó",
"McpReloadAlreadyRunning": "MCP đang tải lại; thanh trạng thái theo dõi tiến trình.",
"McpRecommendedUnknownId": "ID MCP được đề xuất không xác định. Chạy {recommendations_command} để xem danh sách tuyển chọn.",
"McpRecommendationsHeading": "Plugin Codewhale được đề xuất (thành phần MCP; không tự động cài đặt)",
"McpRecommendationsHeading": "Máy chủ MCP được đề xuất (không tự động cài đặt)",
"McpRecommendationsSafety": "Xem danh sách này không thêm hoặc bật gì cả. Thao tác thêm rõ ràng chỉ ghi cấu hình; hãy kiểm tra trước khi {restart_command} kết nối máy chủ.",
"McpRecommendationGithub": "• github — điểm cuối MCP từ xa chính thức của GitHub\n điểm cuối: {endpoint}\n xác thực là bước riêng: chỉ dùng {login_command} khi máy chủ công bố OAuth;\n nếu không, hãy cấu hình PAT có quyền tối thiểu ngoài lịch sử lệnh. Phạm vi được\n cấp có thể ghi hoặc xóa dữ liệu kho mã; nên bắt đầu ở chế độ chỉ đọc khi có thể.\n thêm rõ ràng: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP Chrome DevTools chính thức qua gói npm đã ghim phiên bản\n gói: {package} ({launcher})\n có thể kiểm tra/điều khiển Chrome và đọc trang đã xác thực. Đóng các thẻ\n nhạy cảm và xác minh gói trước khi thêm; {restart_command} có thể tải xuống và chạy gói.\n thêm rõ ràng: {add_command}",
@@ -503,9 +503,12 @@
"PluginPromptSuggestTrust": "Có vẻ đây là công việc {name}. Xem xét bằng /plugin trust {name} trước khi bật.",
"PluginPromptSuggestEnable": "Có vẻ đây là công việc {name}. Bật bằng /plugin enable {name}.",
"PluginPromptSuggestMarketplace": "Có vẻ đây là công việc {name}. Cài từ catalog `{catalog}` bằng /plugin marketplace install {catalog} {name}.",
"PluginCtaInstallPrompt": "Cài plugin {name}?",
"PluginCtaInstallPrompt": "Plugin gợi ý: {name}",
"PluginCtaReview": "Xem lại",
"PluginCtaDismiss": "Bỏ",
"PluginCtaInstall": "Cài",
"PluginCtaReviewTrust": "Xem lại tin cậy",
"PluginCtaEnable": "Bật",
"PluginCtaDismiss": "Không gợi ý nữa",
"PluginCtaDismissSaveFailed": "Đã ẩn trong phiên này; không thể lưu tùy chọn plugin.",
"PluginSuggestionReason": "Khớp với “{trigger}”",
"CmdPluginBundleDetail": "{name}\n========================================\nID: {id}\nPhiên bản: {version}\nNguồn: {origin} ({scope})\nTrạng thái: {state}\nTin cậy: {trust}\nThành phần: {inventory}\nQuyền được yêu cầu: {permissions}\nMáy chủ MCP: {mcp}\nKhông hỗ trợ/chưa hoạt động: {unsupported}\nMã băm nội dung: {content_hash}\nMã băm khả năng: {capability_hash}\nĐường dẫn: {path}",
+6 -3
View File
@@ -452,7 +452,7 @@
"CmdMcpDescription": "打开或管理 MCP 服务器 — init 子命令添加服务器 doctor 子命令检查服务器",
"McpReloadAlreadyRunning": "MCP 重新加载正在进行中,状态栏会显示进度。",
"McpRecommendedUnknownId": "未知的推荐 MCP ID。运行 {recommendations_command} 查看精选列表。",
"McpRecommendationsHeading": "Codewhale 推荐插件(MCP 组件;不会自动安装)",
"McpRecommendationsHeading": "推荐的 MCP 服务器(不会自动安装)",
"McpRecommendationsSafety": "查看此列表不会添加或启用任何内容。显式添加只会写入配置;请在 {restart_command} 连接服务器前检查配置。",
"McpRecommendationGithub": "• github — GitHub 官方远程 MCP 端点\n 端点:{endpoint}\n 身份验证独立进行:仅在服务器声明支持 OAuth 时使用 {login_command};\n 否则请在命令历史之外配置最小权限 PAT。授予的范围可能写入或删除\n 仓库数据,因此请尽可能从只读权限开始。\n 显式添加:{add_command}",
"McpRecommendationChrome": "• chrome-devtools — 通过锁定版本的 npm 包提供的官方 Chrome DevTools MCP\n 包:{package}({launcher})\n 它可以检查或控制 Chrome,并读取已认证页面。请关闭敏感标签页并在\n 添加前验证软件包;{restart_command} 可能会下载并运行它。\n 显式添加:{add_command}",
@@ -503,9 +503,12 @@
"PluginPromptSuggestTrust": "这看起来像 {name} 相关工作。启用前请先审查:/plugin trust {name}",
"PluginPromptSuggestEnable": "这看起来像 {name} 相关工作。启用它:/plugin enable {name}",
"PluginPromptSuggestMarketplace": "这看起来像 {name} 相关工作。从目录 `{catalog}` 安装:/plugin marketplace install {catalog} {name}",
"PluginCtaInstallPrompt": "安装 {name} 插件?",
"PluginCtaInstallPrompt": "建议的插件:{name}",
"PluginCtaReview": "审查",
"PluginCtaDismiss": "关闭",
"PluginCtaInstall": "安装",
"PluginCtaReviewTrust": "审查信任",
"PluginCtaEnable": "启用",
"PluginCtaDismiss": "不再建议",
"PluginCtaDismissSaveFailed": "已在本次会话中隐藏,但无法保存插件偏好设置。",
"PluginSuggestionReason": "匹配“{trigger}”",
"CmdPluginBundleDetail": "{name}\n========================================\nID:{id}\n版本:{version}\n来源:{origin}({scope})\n状态:{state}\n信任:{trust}\n组件:{inventory}\n请求的权限:{permissions}\nMCP 服务器:{mcp}\n不支持/未启用:{unsupported}\n内容哈希:{content_hash}\n能力哈希:{capability_hash}\n路径:{path}",
+6 -3
View File
@@ -345,7 +345,7 @@
"CmdMcpDescription": "開啟或管理 MCP 伺服器 — init 子命令新增伺服器 doctor 子命令檢查伺服器",
"McpReloadAlreadyRunning": "MCP 重新載入正在進行中,狀態列會顯示進度。",
"McpRecommendedUnknownId": "未知的建議 MCP ID。執行 {recommendations_command} 查看精選清單。",
"McpRecommendationsHeading": "Codewhale 建議外掛(MCP 元件;不會自動安裝)",
"McpRecommendationsHeading": "建議的 MCP 伺服器(不會自動安裝)",
"McpRecommendationsSafety": "查看此清單不會新增或啟用任何內容。明確新增只會寫入設定;請在 {restart_command} 連線伺服器前檢查設定。",
"McpRecommendationGithub": "• github — GitHub 官方遠端 MCP 端點\n 端點:{endpoint}\n 驗證會另外進行:只有伺服器宣告支援 OAuth 時才使用 {login_command};\n 否則請在指令歷程之外設定最小權限 PAT。授予的範圍可能寫入或刪除\n 儲存庫資料,因此請盡可能從唯讀權限開始。\n 明確新增:{add_command}",
"McpRecommendationChrome": "• chrome-devtools — 透過鎖定版本 npm 套件提供的官方 Chrome DevTools MCP\n 套件:{package}({launcher})\n 它可檢查或控制 Chrome,並讀取已驗證頁面。請關閉敏感分頁並在\n 新增前驗證套件;{restart_command} 可能會下載並執行它。\n 明確新增:{add_command}",
@@ -413,9 +413,12 @@
"PluginPromptSuggestTrust": "這看起來像 {name} 相關工作。啟用前請先審查:/plugin trust {name}",
"PluginPromptSuggestEnable": "這看起來像 {name} 相關工作。啟用它:/plugin enable {name}",
"PluginPromptSuggestMarketplace": "這看起來像 {name} 相關工作。從目錄 `{catalog}` 安裝:/plugin marketplace install {catalog} {name}",
"PluginCtaInstallPrompt": "安裝 {name} 插件?",
"PluginCtaInstallPrompt": "建議的插件:{name}",
"PluginCtaReview": "審查",
"PluginCtaDismiss": "關閉",
"PluginCtaInstall": "安裝",
"PluginCtaReviewTrust": "審查信任",
"PluginCtaEnable": "啟用",
"PluginCtaDismiss": "不再建議",
"PluginCtaDismissSaveFailed": "已在本次工作階段中隱藏,但無法儲存外掛偏好設定。",
"PluginSuggestionReason": "符合「{trigger}」",
"CmdPluginBundleUsage": "用法:/plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
+6
View File
@@ -689,6 +689,9 @@ pub enum MessageId {
PluginPromptSuggestMarketplace,
PluginCtaInstallPrompt,
PluginCtaReview,
PluginCtaInstall,
PluginCtaReviewTrust,
PluginCtaEnable,
PluginCtaDismiss,
PluginCtaDismissSaveFailed,
PluginSuggestionReason,
@@ -3029,6 +3032,9 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::PluginPromptSuggestMarketplace,
MessageId::PluginCtaInstallPrompt,
MessageId::PluginCtaReview,
MessageId::PluginCtaInstall,
MessageId::PluginCtaReviewTrust,
MessageId::PluginCtaEnable,
MessageId::PluginCtaDismiss,
MessageId::PluginCtaDismissSaveFailed,
MessageId::PluginSuggestionReason,
@@ -249,10 +249,7 @@ fn mcp_unknown_id(presentation: &mut dyn CommandPresentationContext) -> String {
fn recommended_mcp_text(presentation: &mut dyn CommandPresentationContext) -> String {
let heading = presentation
.translate("mcp_recommendations_heading", &[])
.unwrap_or_else(|_| {
"Suggested Codewhale plugins (MCP components; nothing installs automatically)"
.to_string()
});
.unwrap_or_else(|_| "Suggested MCP servers (nothing installs automatically)".to_string());
let safety = presentation
.translate(
"mcp_recommendations_safety",
@@ -387,8 +384,7 @@ mod tests {
"Unknown MCP suggestion. Run {recommendations_command} to see the list.".to_string()
}
"mcp_recommendations_heading" => {
"Suggested Codewhale plugins (MCP components; nothing installs automatically)"
.to_string()
"Suggested MCP servers (nothing installs automatically)".to_string()
}
"mcp_recommendations_safety" => {
"Looking adds nothing. Adding writes config only — review it before {restart_command} connects anything."
@@ -600,7 +596,7 @@ mod tests {
fn recommendations_state_execution_and_install_boundaries() {
let text = recommended_mcp_text(&mut FakePresentation);
assert!(text.contains("nothing installs automatically"));
assert!(text.contains("Suggested Codewhale plugins"));
assert!(text.contains("Suggested MCP servers"));
assert!(text.contains("never downloads or"));
assert!(text.contains("installs this binary"));
assert!(text.contains("experimental"));
+13 -3
View File
@@ -137,9 +137,19 @@ impl Engine {
// headless entry points install the merged notification policy before
// tool setup, including method=off, quiet/category, and attention.
// The tool returns a truthful suppressed/delivered receipt.
builder = builder
.with_notify_tool()
.with_request_plugin_install_tool();
builder = builder.with_notify_tool();
// `request_plugin_install` returns a TUI slash command and is a
// proactive offer, so it exists only in the interactive TUI with
// contextual tips on (0.10.1 plugin offering policy, rules 3 and 11).
// Exec, ACP, and runtime-API hosts run with terminal chrome off.
if self.config.terminal_chrome_enabled
&& crate::settings::Settings::load_read_only()
.map(|settings| settings.contextual_tips)
.unwrap_or(true)
{
builder = builder.with_request_plugin_install_tool();
}
// Register the `registry_sync` tool for fetching and caching
// MCP Registry server metadata. Rides on `Feature::Mcp` — the same
+84 -11
View File
@@ -1,4 +1,11 @@
//! Model-callable plugin review request. Never installs, trusts, or enables.
//!
//! 0.10.1 plugin offering policy: the tool is registered only in the
//! interactive TUI with contextual tips on (it returns a TUI slash command),
//! and a session gets one review request. A second call errors.
use std::collections::HashSet;
use std::sync::{LazyLock, Mutex};
use async_trait::async_trait;
use serde_json::{Value, json};
@@ -12,6 +19,26 @@ pub const REQUEST_PLUGIN_INSTALL_TOOL_NAME: &str = "request_plugin_install";
pub struct RequestPluginInstallTool;
/// Sessions (by `ToolContext::state_namespace`, the session id) that already
/// surfaced a review request. The registry is rebuilt every turn, so the
/// once-per-session budget cannot live on the tool value.
static REQUESTED_SESSIONS: LazyLock<Mutex<HashSet<String>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));
fn session_already_requested(namespace: &str) -> bool {
REQUESTED_SESSIONS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.contains(namespace)
}
fn record_session_request(namespace: &str) {
REQUESTED_SESSIONS
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(namespace.to_string());
}
#[async_trait]
impl ToolSpec for RequestPluginInstallTool {
fn name(&self) -> &'static str {
@@ -19,10 +46,10 @@ impl ToolSpec for RequestPluginInstallTool {
}
fn description(&self) -> &'static str {
"Ask the human to review installing or trusting a plugin that is \
already installed-but-idle or listed in a marketplace catalog they \
added. Does not install, trust, or enable anything. Fails if the \
plugin name is unknown. Pass `name` and a short `reason`."
"Ask the human to review a plugin the current task clearly needs \
(installed-but-idle, or in a catalog they added). Never to advertise. \
Once per session; a second call fails. Installs, trusts, and enables \
nothing. Pass `name` and a short `reason`."
}
fn input_schema(&self) -> Value {
@@ -63,6 +90,11 @@ impl ToolSpec for RequestPluginInstallTool {
"request_plugin_install: reason must not be empty",
));
}
if session_already_requested(&ctx.state_namespace) {
return Err(ToolError::not_available(
"request_plugin_install: already used this session; a plugin review may be requested once per session",
));
}
let Some(registry) = ctx.plugin_registry.as_ref() else {
return Err(ToolError::not_available(
"request_plugin_install: plugin registry is not available",
@@ -74,6 +106,7 @@ impl ToolSpec for RequestPluginInstallTool {
"request_plugin_install: unknown plugin `{name}`"
)));
};
record_session_request(&ctx.state_namespace);
let command = matched.command();
let payload = json!({
"completed": false,
@@ -122,7 +155,19 @@ mod tests {
.registry_for_workspace(root.path());
let bundle = root.path().join(".codewhale/plugins/supabase/plugin.toml");
let before = fs::read(&bundle).unwrap();
let ctx = ToolContext::new(root.path()).with_plugin_registry(Arc::clone(&registry));
let ctx = ToolContext::new(root.path())
.with_plugin_registry(Arc::clone(&registry))
.with_state_namespace("request-plugin-install-disk-test");
let err = RequestPluginInstallTool
.execute(
json!({"name": "not-a-real-plugin", "reason": "guess"}),
&ctx,
)
.await
.unwrap_err();
assert!(err.to_string().to_lowercase().contains("unknown"), "{err}");
assert_eq!(fs::read(&bundle).unwrap(), before);
let result = RequestPluginInstallTool
.execute(
@@ -137,15 +182,43 @@ mod tests {
assert_eq!(meta["installed"], json!(false));
assert_eq!(meta["command"], json!("/plugin trust supabase"));
assert_eq!(fs::read(&bundle).unwrap(), before);
}
/// Policy rule 4: one review request per session; a second call errors,
/// and another session keeps its own budget.
#[tokio::test]
async fn request_plugin_install_is_once_per_session() {
let _lock = lock_test_env();
let root = TempDir::new().unwrap();
let _home = EnvVarGuard::set("CODEWHALE_HOME", root.path().join("home"));
write_keyword_bundle(root.path(), "supabase");
let registry = crate::plugins::PluginDiscoveryContext::capture_pre_dotenv()
.registry_for_workspace(root.path());
let ctx = ToolContext::new(root.path())
.with_plugin_registry(Arc::clone(&registry))
.with_state_namespace("request-plugin-install-once-a");
let input = json!({"name": "supabase", "reason": "needs hosted auth"});
assert!(
RequestPluginInstallTool
.execute(input.clone(), &ctx)
.await
.is_ok()
);
let err = RequestPluginInstallTool
.execute(
json!({"name": "not-a-real-plugin", "reason": "guess"}),
&ctx,
)
.execute(input.clone(), &ctx)
.await
.unwrap_err();
assert!(err.to_string().to_lowercase().contains("unknown"), "{err}");
assert_eq!(fs::read(&bundle).unwrap(), before);
assert!(err.to_string().contains("once per session"), "{err}");
let other = ToolContext::new(root.path())
.with_plugin_registry(Arc::clone(&registry))
.with_state_namespace("request-plugin-install-once-b");
assert!(
RequestPluginInstallTool
.execute(input, &other)
.await
.is_ok()
);
}
}
+2
View File
@@ -138,6 +138,8 @@ impl App {
StatusToastKind::BehavioralTip(_) | StatusToastKind::PluginSuggestion
)
});
// One switch governs every plugin offer, the review row included.
self.plugin_cta.phase = crate::tui::plugin_suggestions::PluginCtaPhase::Hidden;
}
self.needs_redraw = true;
}
+4 -2
View File
@@ -37,10 +37,12 @@ pub(crate) fn next_escape_action(app: &App, slash_menu_open: bool) -> EscapeActi
|| matches!(app.runtime_turn_status.as_deref(), Some("in_progress"))
{
EscapeAction::CancelRequest
} else if !app.input.is_empty() {
// A draft is the person's work: Esc clears it (recoverably) before it
// dismisses a plugin offer (0.10.1 plugin offering policy, rule 9).
EscapeAction::ClearInput
} else if app.plugin_cta.phase.is_visible() {
EscapeAction::DismissPluginCta
} else if !app.input.is_empty() {
EscapeAction::ClearInput
} else {
EscapeAction::Noop
}
+7 -3
View File
@@ -325,12 +325,16 @@ fn handle_plugin_cta_mouse(app: &mut App, mouse: MouseEvent) -> Option<Vec<ViewE
return None;
}
if mouse_hits_rect(mouse, app.viewport.last_plugin_cta_dismiss_area) {
// "Don't suggest again": the explicit, persisted dismissal.
let _ = app.dismiss_plugin_cta();
return Some(Vec::new());
}
// Review button, or the rest of the CTA line, runs the existing review
// command. Never auto-installs: the slash command is the human path.
if let Some(command) = app.accept_plugin_cta_command() {
// Only the labelled button acts, and it opens `/plugin show <name>`;
// install, trust, and enable stay the person's own next command. A click
// elsewhere on the row is consumed and does nothing.
if mouse_hits_rect(mouse, app.viewport.last_plugin_cta_review_area)
&& let Some(command) = app.accept_plugin_cta_command()
{
return Some(apply_sidebar_row_action(
app,
crate::tui::app::SidebarRowAction::Command(command),
+186 -148
View File
@@ -1,5 +1,17 @@
//! In-context plugin reminders: prompt matching, live composer CTA, and idle
//! catalog polling.
//! In-context plugin reminders: the send-time toast, the model-requested
//! review row, and idle catalog polling.
//!
//! 0.10.1 plugin offering policy ("helpful, not pushy"):
//! - The only unprompted surface is the send-time toast. There is no live
//! as-you-type matching.
//! - The review row appears only when the model calls `request_plugin_install`
//! (once per session), and only while contextual tips are on and the shared
//! per-session guidance budget has room.
//! - The row names the true next step (Install, Review trust, Enable). Only
//! that button acts, and it opens `/plugin show <name>`; it never runs
//! install, trust, or enable directly.
//! - Esc hides the row for this session only. "Don't suggest again" is the
//! explicit, persisted dismissal.
use std::collections::BTreeSet;
use std::time::{Duration, Instant};
@@ -18,12 +30,40 @@ use crate::tui::app::{App, StatusToast, StatusToastKind, StatusToastLevel};
use codewhale_localization::{MessageId, tr};
const CATALOG_POLL_INTERVAL: Duration = Duration::from_secs(2);
const CTA_DEBOUNCE: Duration = Duration::from_millis(200);
/// The step a plugin actually needs next, as named on the review button.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginCtaStep {
Install,
ReviewTrust,
Enable,
}
impl PluginCtaStep {
/// Derive the step from the review command the tool returned. Anything
/// that is not trust or enable is an install path.
fn from_command(command: &str) -> Self {
let mut words = command.split_whitespace();
match (words.next(), words.next()) {
(Some("/plugin"), Some("trust")) => Self::ReviewTrust,
(Some("/plugin"), Some("enable")) => Self::Enable,
_ => Self::Install,
}
}
fn label(self) -> MessageId {
match self {
Self::Install => MessageId::PluginCtaInstall,
Self::ReviewTrust => MessageId::PluginCtaReviewTrust,
Self::Enable => MessageId::PluginCtaEnable,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PluginCtaPhase {
Hidden,
Matched { name: String, command: String },
Matched { name: String, step: PluginCtaStep },
}
impl PluginCtaPhase {
@@ -44,10 +84,9 @@ impl PluginCtaPhase {
#[derive(Debug, Clone)]
pub struct PluginCtaState {
pub phase: PluginCtaPhase,
/// Lowercased names hidden from every proactive path: persisted "Don't
/// suggest again" choices plus this session's Esc dismissals.
pub dismissed: BTreeSet<String>,
matched_term: Option<String>,
debounce_at: Option<Instant>,
last_draft: String,
}
impl Default for PluginCtaState {
@@ -55,9 +94,6 @@ impl Default for PluginCtaState {
Self {
phase: PluginCtaPhase::Hidden,
dismissed: BTreeSet::new(),
matched_term: None,
debounce_at: None,
last_draft: String::new(),
}
}
}
@@ -137,67 +173,30 @@ impl App {
}
}
/// Arm a short debounce whenever the composer draft changes.
pub fn notify_plugin_cta_text_changed(&mut self) {
if self.input == self.plugin_cta.last_draft {
return;
}
self.plugin_cta.last_draft = self.input.clone();
self.plugin_cta.debounce_at = Some(Instant::now() + CTA_DEBOUNCE);
}
/// Recompute the live CTA after the debounce window. One match at a
/// time; already-active plugins stay hidden; a dismissed name stays
/// dismissed across sessions. Never auto-installs.
pub fn handle_plugin_cta_debounce_expired(&mut self) {
self.plugin_cta.debounce_at = None;
self.plugin_cta.last_draft = self.input.clone();
let marketplace = load_marketplace_candidates(self.plugin_registry.state_path());
let Some(matched) = match_plugin_for_draft(
&self.input,
self.plugin_registry.as_ref(),
&marketplace,
&self.plugin_cta.dismissed,
) else {
if self.plugin_cta.phase.is_visible() {
self.plugin_cta.phase = PluginCtaPhase::Hidden;
self.needs_redraw = true;
}
return;
};
let command = matched.command();
let new_phase = PluginCtaPhase::Matched {
name: matched.name,
command,
};
if self.plugin_cta.phase != new_phase
|| self.plugin_cta.matched_term != matched.matched_term
{
self.plugin_cta.matched_term = matched.matched_term;
self.plugin_cta.phase = new_phase;
self.needs_redraw = true;
}
}
/// Poll draft changes and fire the CTA debounce without a dedicated timer
/// task. The event loop already ticks this often.
pub fn maybe_poll_plugin_cta(&mut self) {
self.notify_plugin_cta_text_changed();
let Some(at) = self.plugin_cta.debounce_at else {
return;
};
if Instant::now() < at {
return;
}
self.handle_plugin_cta_debounce_expired();
}
#[must_use]
pub fn plugin_cta_row_height(&self) -> u16 {
u16::from(self.plugin_cta.phase.is_visible())
}
/// Persist an explicit dismissal while hiding it immediately this session.
/// Esc: hide the row and skip this plugin for the rest of the session.
/// Persists nothing, so the next session may offer it again.
pub fn dismiss_plugin_cta_for_session(&mut self) -> bool {
let Some(name) = self
.plugin_cta
.phase
.matched_name()
.map(str::to_ascii_lowercase)
else {
return false;
};
self.plugin_cta.dismissed.insert(name);
self.plugin_cta.phase = PluginCtaPhase::Hidden;
self.needs_redraw = true;
true
}
/// "Don't suggest again": the explicit, persisted dismissal. Also hides
/// the row immediately for this session, even if saving fails.
pub fn dismiss_plugin_cta(&mut self) -> bool {
let Some(name) = self.plugin_cta.phase.matched_name().map(str::to_string) else {
return false;
@@ -222,26 +221,29 @@ impl App {
true
}
/// Human-initiated review: return the slash command so the TUI can run
/// the existing `/plugin trust` / marketplace-install / `/plugin install`
/// path. Never runs it here.
/// Human-initiated review from the labelled button: open the plugin's
/// detail page, where install, trust, or enable is the person's own next
/// command. Never runs install, trust, or enable directly.
#[must_use]
pub fn accept_plugin_cta_command(&mut self) -> Option<String> {
let (command, name) = match &self.plugin_cta.phase {
PluginCtaPhase::Matched { command, name } => (command.clone(), name.clone()),
let name = match &self.plugin_cta.phase {
PluginCtaPhase::Matched { name, .. } => name.clone(),
PluginCtaPhase::Hidden => return None,
};
self.plugin_cta.dismissed.insert(name.to_ascii_lowercase());
self.plugin_cta.phase = PluginCtaPhase::Hidden;
self.needs_redraw = true;
Some(command)
Some(format!("/plugin show {name}"))
}
/// Model-requested review: show the live CTA and a toast. Does not run
/// the command, so nothing is installed, trusted, or enabled.
/// Model-requested review: show the review row and a toast naming the
/// command. Does not run it, so nothing is installed, trusted, or
/// enabled. Obeys the tips switch and draws from the shared per-session
/// guidance budget like every other proactive offer.
pub fn surface_plugin_review_request(&mut self, name: &str, command: &str) {
if name.trim().is_empty()
|| command.trim().is_empty()
|| !self.behavioral_tips.guidance_available()
|| self
.plugin_cta
.dismissed
@@ -249,36 +251,32 @@ impl App {
{
return;
}
self.plugin_cta.matched_term = None;
self.behavioral_tips.record_guidance_impression();
self.plugin_cta.phase = PluginCtaPhase::Matched {
name: name.to_string(),
command: command.to_string(),
step: PluginCtaStep::from_command(command),
};
self.push_status_toast(command.to_string(), StatusToastLevel::Info, Some(8_000));
let mut toast = StatusToast::new(command.to_string(), StatusToastLevel::Info, Some(8_000));
toast.kind = StatusToastKind::PluginSuggestion;
self.push_status_toast_record(toast);
self.needs_redraw = true;
}
}
/// Draw the one-line live CTA above the composer. No-op when hidden.
/// Draw the one-line review row above the composer. No-op when hidden.
pub fn draw_plugin_cta(app: &mut App, area: Rect, buf: &mut Buffer) {
app.viewport.last_plugin_cta_area = None;
app.viewport.last_plugin_cta_review_area = None;
app.viewport.last_plugin_cta_dismiss_area = None;
let PluginCtaPhase::Matched { name, .. } = &app.plugin_cta.phase else {
let PluginCtaPhase::Matched { name, step } = &app.plugin_cta.phase else {
return;
};
let name = name.clone();
let (name, step) = (name.clone(), *step);
if area.height == 0 || area.width == 0 {
return;
}
let mut prompt = tr(app.ui_locale, MessageId::PluginCtaInstallPrompt).replace("{name}", &name);
if let Some(term) = &app.plugin_cta.matched_term {
prompt.push_str(" · ");
prompt.push_str(
&tr(app.ui_locale, MessageId::PluginSuggestionReason).replace("{trigger}", term),
);
}
let review = tr(app.ui_locale, MessageId::PluginCtaReview);
let prompt = tr(app.ui_locale, MessageId::PluginCtaInstallPrompt).replace("{name}", &name);
let review = tr(app.ui_locale, step.label());
let dismiss = tr(app.ui_locale, MessageId::PluginCtaDismiss);
let review_label = format!("[{review}]");
let dismiss_label = format!("[{dismiss}]");
@@ -392,31 +390,37 @@ mod tests {
}
#[test]
fn tips_off_removes_plugin_guidance_but_preserves_required_notices_and_explicit_review() {
fn tips_off_removes_every_plugin_offer_but_preserves_required_notices() {
let _lock = crate::test_support::lock_test_env();
let (mut app, _root, _home) = app_with_supabase_plugin();
app.set_contextual_tips_enabled(false);
assert!(!app.maybe_nudge_plugin_for_prompt("add supabase auth"));
app.surface_plugin_review_request("supabase", "/plugin trust supabase");
assert!(
!app.plugin_cta.phase.is_visible(),
"tips off: no review row, even when the model asks"
);
assert_eq!(app.plugin_cta_row_height(), 0);
assert!(app.status_toasts.is_empty());
app.set_contextual_tips_enabled(true);
assert!(app.maybe_nudge_plugin_for_prompt("add supabase auth"));
app.surface_plugin_review_request("supabase", "/plugin trust supabase");
assert!(app.plugin_cta.phase.is_visible());
app.push_status_toast_record(
StatusToast::new("Review required", StatusToastLevel::Warning, None).for_action("a"),
);
app.push_status_toast("Keep this error", StatusToastLevel::Error, None);
app.set_contextual_tips_enabled(false);
assert!(
!app.plugin_cta.phase.is_visible(),
"turning tips off hides the row"
);
assert_eq!(app.status_toasts.len(), 2);
assert!(
app.status_toasts
.iter()
.all(|toast| toast.kind != StatusToastKind::PluginSuggestion)
);
app.surface_plugin_review_request("supabase", "/plugin trust supabase");
assert!(app.plugin_cta.phase.is_visible());
assert_eq!(
app.status_toasts.len(),
3,
"explicit review is not unsolicited guidance"
);
app.set_contextual_tips_enabled(true);
assert!(
!app.maybe_nudge_plugin_for_prompt("add supabase auth"),
@@ -425,60 +429,101 @@ mod tests {
}
#[test]
fn live_cta_shows_for_a_matching_idle_plugin() {
fn model_requested_review_draws_from_the_shared_budget() {
let _lock = crate::test_support::lock_test_env();
let (mut app, _root, _home) = app_with_supabase_plugin();
app.input = "add supabase auth to login".to_string();
app.handle_plugin_cta_debounce_expired();
assert_eq!(
app.plugin_cta.phase.matched_name(),
Some("supabase"),
"{:?}",
app.plugin_cta.phase
);
assert_eq!(app.plugin_cta_row_height(), 1);
assert_eq!(app.plugin_cta.matched_term.as_deref(), Some("supabase"));
let area = Rect::new(0, 0, 140, 1);
let mut buffer = Buffer::empty(area);
draw_plugin_cta(&mut app, area, &mut buffer);
let row = buffer
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(row.contains("Matched “supabase”"), "{row}");
}
#[test]
fn live_cta_hides_when_the_plugin_is_already_active() {
let _lock = crate::test_support::lock_test_env();
let (mut app, _root, _home) = app_with_supabase_plugin();
let registry = std::sync::Arc::make_mut(&mut app.plugin_registry);
registry.trust("supabase").unwrap();
registry.enable("supabase").unwrap();
app.input = "add supabase auth to login".to_string();
app.handle_plugin_cta_debounce_expired();
assert!(app.maybe_nudge_plugin_for_prompt("add supabase auth"));
app.surface_plugin_review_request("supabase", "/plugin trust supabase");
assert!(
!app.plugin_cta.phase.is_visible(),
"{:?}",
app.plugin_cta.phase
"the send-time toast already spent this session's budget"
);
}
#[test]
fn live_cta_dismiss_stays_dismissed_for_that_name_this_session() {
fn typing_a_matching_draft_never_shows_a_row() {
let _lock = crate::test_support::lock_test_env();
let (mut app, _root, _home) = app_with_supabase_plugin();
app.input = "add supabase auth to login".to_string();
app.handle_plugin_cta_debounce_expired();
assert!(app.dismiss_plugin_cta());
app.maybe_poll_plugin_catalog_idle();
assert!(!app.plugin_cta.phase.is_visible());
app.handle_plugin_cta_debounce_expired();
assert!(
!app.plugin_cta.phase.is_visible(),
"dismissed names must not reappear this session: {:?}",
app.plugin_cta.phase
assert_eq!(app.plugin_cta_row_height(), 0);
}
#[test]
fn review_row_names_the_true_next_step_and_only_opens_plugin_show() {
let _lock = crate::test_support::lock_test_env();
for (command, step, label) in [
(
"/plugin trust supabase",
PluginCtaStep::ReviewTrust,
"[Review trust]",
),
("/plugin enable supabase", PluginCtaStep::Enable, "[Enable]"),
(
"/plugin marketplace install official supabase",
PluginCtaStep::Install,
"[Install]",
),
] {
let (mut app, _root, _home) = app_with_supabase_plugin();
app.surface_plugin_review_request("supabase", command);
assert_eq!(
app.plugin_cta.phase,
PluginCtaPhase::Matched {
name: "supabase".into(),
step
}
);
let area = Rect::new(0, 0, 140, 1);
let mut buffer = Buffer::empty(area);
draw_plugin_cta(&mut app, area, &mut buffer);
let row = buffer
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(row.contains("supabase"), "{row}");
assert!(row.contains(label), "{row}");
assert!(row.contains("[Don't suggest again]"), "{row}");
assert_eq!(
app.accept_plugin_cta_command().as_deref(),
Some("/plugin show supabase"),
"accepting opens the detail page, never {command}"
);
assert!(!app.plugin_cta.phase.is_visible());
}
}
#[test]
fn esc_clears_a_draft_first_then_dismisses_for_the_session_only() {
use crate::settings::Settings;
use crate::tui::composer_ui::{EscapeAction, next_escape_action};
let _lock = crate::test_support::lock_test_env();
let (mut app, root, _home) = app_with_supabase_plugin();
app.surface_plugin_review_request("supabase", "/plugin trust supabase");
app.input = "half-written draft".into();
assert_eq!(next_escape_action(&app, false), EscapeAction::ClearInput);
app.input.clear();
assert_eq!(
next_escape_action(&app, false),
EscapeAction::DismissPluginCta
);
assert!(app.dismiss_plugin_cta_for_session());
assert!(!app.plugin_cta.phase.is_visible());
assert!(!app.maybe_nudge_plugin_for_prompt("add supabase auth"));
let saved = Settings::load_read_only().unwrap_or_default();
assert!(
saved.dismissed_plugin_suggestions.is_empty(),
"Esc persists nothing"
);
let restarted = App::new_with_plugin_registry(
crate::test_support::test_tui_options(root.path()),
&Config::default(),
app.plugin_registry.clone(),
);
assert!(!restarted.plugin_cta.dismissed.contains("supabase"));
}
#[test]
@@ -487,8 +532,7 @@ mod tests {
let _lock = crate::test_support::lock_test_env();
let (mut app, root, _home) = app_with_supabase_plugin();
Settings::transact(|settings| settings.set("max_history", "321")).unwrap();
app.input = "add supabase auth to login".into();
app.handle_plugin_cta_debounce_expired();
app.surface_plugin_review_request("supabase", "/plugin trust supabase");
assert!(app.dismiss_plugin_cta());
let saved = Settings::load_read_only().unwrap();
assert_eq!(saved.max_input_history, 321);
@@ -499,10 +543,7 @@ mod tests {
&Config::default(),
app.plugin_registry.clone(),
);
restarted.input = app.input.clone();
restarted.handle_plugin_cta_debounce_expired();
assert!(!restarted.plugin_cta.phase.is_visible());
assert!(!restarted.maybe_nudge_plugin_for_prompt(&app.input));
assert!(!restarted.maybe_nudge_plugin_for_prompt("add supabase auth to login"));
restarted.surface_plugin_review_request("supabase", "/plugin trust supabase");
assert!(!restarted.plugin_cta.phase.is_visible());
assert!(
@@ -520,8 +561,7 @@ mod tests {
fn failed_dismissal_save_preserves_malformed_preferences_and_hides_this_session() {
let _lock = crate::test_support::lock_test_env();
let (mut app, root, _home) = app_with_supabase_plugin();
app.input = "add supabase auth".into();
app.handle_plugin_cta_debounce_expired();
app.surface_plugin_review_request("supabase", "/plugin trust supabase");
let path = crate::settings::Settings::path().unwrap();
assert!(path.starts_with(root.path()));
fs::create_dir_all(path.parent().unwrap()).unwrap();
@@ -529,9 +569,7 @@ mod tests {
fs::write(&path, malformed).unwrap();
assert!(app.dismiss_plugin_cta());
assert_eq!(fs::read_to_string(&path).unwrap(), malformed);
app.handle_plugin_cta_debounce_expired();
assert!(!app.plugin_cta.phase.is_visible());
assert!(!app.maybe_nudge_plugin_for_prompt("add supabase auth"));
let toast = app.status_toasts.back().expect("save failure receipt");
assert!(toast.text.contains("could not save"));
assert!(!toast.text.contains("private_fixture_payload"));
+1 -2
View File
@@ -1902,7 +1902,6 @@ pub(crate) async fn run_event_loop(
// potentially long engine batch so composer/modal input stays live.
collect_pending_terminal_events(&terminal_input, &mut pending_terminal_events)?;
app.maybe_poll_plugin_catalog_idle();
app.maybe_poll_plugin_cta();
if drain_remote_control_events(app, config, &engine_handle).await? {
app.needs_redraw = true;
@@ -6233,7 +6232,7 @@ pub(crate) async fn run_event_loop(
}
EscapeAction::DismissPluginCta => {
app.backtrack.reset();
let _ = app.dismiss_plugin_cta();
let _ = app.dismiss_plugin_cta_for_session();
}
EscapeAction::ClearInput => {
app.backtrack.reset();
+49 -12
View File
@@ -16,18 +16,55 @@ hosts) and any marketplace catalogs you have added with `/plugin marketplace add
It explains the match and gives the next review, enable, or catalog-install
step, but never installs, trusts, or enables a bundle on its own.
Sending a task also surfaces one quiet toast when the prompt strongly matches
an installed-but-idle plugin or a catalog candidate you do not have yet — for
example a prompt about Supabase suggesting `/plugin trust supabase` or
`/plugin marketplace install <catalog> supabase`. Description-only matches do
not toast. While you type, a one-line composer CTA (`Install {name} plugin?`)
offers the same review command after a short debounce; it never auto-installs,
hides when the plugin is already active, and stays dismissed for that name
this session. Nothing is appended to your message to advertise plugins;
the model can call `request_plugin_install` to surface review for the human
without changing disk. Codewhale does not invent a remote plugin URL; missing
plugins are suggested only from catalogs you added. On-disk bundle changes
still toast `/plugin reload` on send and between turns.
## How Codewhale offers plugins
Codewhale is helpful about plugins, not pushy. The rules:
- **One proactive surface.** Sending a task can show one quiet toast when the
prompt matches an installed-but-idle plugin or a catalog candidate you do
not have yet, for example `/plugin trust supabase` or
`/plugin marketplace install <catalog> supabase`. Nothing appears while you
type, and nothing is appended to your message to advertise plugins.
- **One switch.** With `contextual_tips` off, no plugin guidance appears
anywhere. Required notices and your own `/plugin` commands still work.
- **One budget.** Plugin offers share the per-session guidance budget with
other tips. In the interactive TUI the model can call
`request_plugin_install` once per session to ask you to review a plugin the
task needs; a second call fails. Exec, ACP, and runtime-API sessions do not
get the tool.
- **Built-ins are never advertised.** Bundled plugins such as Computer Use
appear only in `/plugin list` and Extensions.
- **Specific terms only.** Generic words (accessibility, browser, chrome,
docs, screenshot, web, wiki, …) never trigger an offer. The matcher and the
marketplace's `check-marketplace.mjs` share one stoplist.
- **Only what runs here.** Plugins whose `when.os` excludes this OS are not
offered.
- **The true next step.** A model-requested review row says Install, Review
trust, or Enable to match what the plugin needs. Only that button acts, and
it opens `/plugin show <name>`; it never installs, trusts, or enables.
- **Reversible dismissal.** Esc clears a non-empty draft first, then hides the
row for this session only. "Don't suggest again" is the explicit,
persisted choice.
- **Discovery is passive.** Find new plugins in these docs,
`/plugin marketplace list`, Extensions, and the browser guide below.
Codewhale does not invent a remote plugin URL; missing plugins are suggested
only from catalogs you added. On-disk bundle changes still toast
`/plugin reload` on send and between turns.
## Browser: pick one
Several options can drive a browser. They differ in whose browser it is and
what it can see.
| Option | Whose browser | Good for |
| --- | --- | --- |
| `chrome-devtools` MCP (`/mcp recommendations`) | A Chrome it drives, which can include signed-in pages | DevTools-level inspection and performance work |
| Playwright MCP (`/mcp recommendations`) | A fresh, isolated profile with `--isolated` | Scripted flows and testing without your identity |
| Computer Use `browser_*` tools (bundled, off until reviewed) | One it launches, in a profile of its own | Browser steps inside a wider desktop task |
| Chromewhale (developer preview, `Hmbown/codewhale-plugin-marketplace`) | Yours, already open, in your own Chrome profile; load unpacked | Reading or acting on the tab you are looking at, one granted site at a time |
None of these is offered to you proactively. Add the one that fits the job.
## Sources
+2 -1
View File
@@ -284,7 +284,8 @@ bits themselves and always drop into this same review — see
[PLUGINS.md](PLUGINS.md). `/plugin suggest` ranks installed bundles and
any locally added marketplace catalogs; sending a matching task can toast the
same next step without installing anything. Nothing is written into the
model's request to advertise plugins.)
model's request to advertise plugins; the full offering policy is in
[PLUGINS.md](PLUGINS.md#how-codewhale-offers-plugins).)
Trust, enable, disable, revoke, and reload rebuild the current workspace's
Skills, MCP, Commands, Agent profiles, and Hooks immediately. Each persisted