mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 10:01:30 -10:00
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:
1 parent
eb59a59f46
commit
d5cec111e6
26 files changed
+447
-234
No files matched your search
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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]]",
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(®istry));
|
||||
let ctx = ToolContext::new(root.path())
|
||||
.with_plugin_registry(Arc::clone(®istry))
|
||||
.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(®istry))
|
||||
.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(®istry))
|
||||
.with_state_namespace("request-plugin-install-once-b");
|
||||
assert!(
|
||||
RequestPluginInstallTool
|
||||
.execute(input, &other)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user