mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
feat(tui): make settings MCP recovery first-class and clickable (#5655)
* feat(tui): make settings MCP recovery first-class and clickable Settings Advanced now exposes Connect/Reconnect/Diagnose and plugin rows instead of a path-only MCP section, and the category tabs are mouse-clickable. The extensions MCP tab, plugin Problems group, and /mcp manager name recovery commands that exist (/mcp login, /mcp reload, /mcp validate, /plugin validate). Stale OAuth copy no longer points at the non-existent /mcp auth. OpenRouter Hunyuan aliases (hy3, hunyuan) resolve to tencent/hy3-preview; hy4 is not added. Signed-off-by: CodeWhale Bot <[email protected]> * docs: add TUI settings/MCP recovery lane handoff Record the isolated worktree SHA, journeys, tests actually run, PR #5643 overlap, and what remains unverified. Signed-off-by: CodeWhale Bot <[email protected]> * test(tui): bump Extensions locale census for MCP recovery actions Connect/Reconnect/Re-auth/Diagnose add four ExtensionsAction keys. The complete-set census is 86, not 82. Signed-off-by: CodeWhale Bot <[email protected]> --------- Signed-off-by: CodeWhale Bot <[email protected]> Co-authored-by: CodeWhale Bot <[email protected]>
This commit is contained in:
1 parent
de63df50e2
commit
71253d5656
28 files changed
+855
-37
No files matched your search
@@ -0,0 +1,124 @@
|
||||
# TUI settings / MCP recovery lane — 2026-08-27
|
||||
|
||||
## Branch and SHA
|
||||
|
||||
- Worktree: `/Volumes/VIXinSSD/CW/worktrees/cw-v0912-tui-settings-mcp-20260827`
|
||||
- Branch: `grok/v0912-tui-settings-mcp-20260827`
|
||||
- Base: freshly fetched `origin/main` at `a96ea6cb09cc464ea2e88f251c538c239d1fe9ad`
|
||||
- Product commit: `8e15e51d43433f5e2e924611aed21ba4fa5b6709`
|
||||
`feat(tui): make settings MCP recovery first-class and clickable`
|
||||
|
||||
No push, PR, merge, release, or deploy was performed. The machine `codewhale`
|
||||
binary was not installed or overwritten. `/Volumes/VIXinSSD/CW/cwc` was not
|
||||
edited. Occupied worktrees were not reused.
|
||||
|
||||
`docs/CURRENT.md` is not present on this `origin/main`. `AGENTS.md` and
|
||||
`docs/SETTINGS_PICKER_FRAMEWORK.md` / `docs/MCP.md` were the live contracts.
|
||||
|
||||
## PR #5643 overlap
|
||||
|
||||
Open PR #5643 (`codex/v0912-tui-polish-20260827`, `92ef9aa4d`) already contains
|
||||
the MCP login copy fix in `streamable_http.rs` **and** the welcome-shine timing
|
||||
fix. It is **not** in `origin/main`. This lane built on `origin/main` and did
|
||||
**not** copy the welcome-shine / composer / launch-menu work.
|
||||
|
||||
The `/mcp auth` recovery sentence still exists on current `main`. This slice
|
||||
fixes that string (required: recovery copy must name commands that exist) and
|
||||
then **deepens settings/plugin management** rather than duplicating 5643's
|
||||
animation work.
|
||||
|
||||
If both land, the `streamable_http.rs` hint change will overlap with 5643 and
|
||||
should be a trivial conflict: both replace `/mcp auth` with `/mcp login`.
|
||||
|
||||
## User journeys that changed
|
||||
|
||||
1. **Settings (`F2` / `/settings`) is clickable.** Category tabs
|
||||
(General / Models / Permissions / Display / Advanced) have mouse hitboxes.
|
||||
One click switches the tab. Rows keep the existing select-then-activate
|
||||
pattern.
|
||||
|
||||
2. **Settings → Advanced → MCP is no longer a path-only wall.** Calm action
|
||||
rows, each with an obvious next command:
|
||||
- MCP manager → `/mcp`
|
||||
- Reconnect MCP → `/mcp reload`
|
||||
- Diagnose MCP → `/mcp validate`
|
||||
- Plugins → `/plugin`
|
||||
Enter (or the second click on a selected action row) runs that command.
|
||||
|
||||
3. **Extensions → MCP recovery is first-class.** Each server row's action is
|
||||
Connect / Reconnect / Re-auth / Diagnose / Enable, mapped to real commands:
|
||||
- disabled → `/mcp enable <name>`
|
||||
- not inspected → `/mcp reload` (Connect)
|
||||
- disconnected → `/mcp reload` (Reconnect)
|
||||
- 401 / unauthorized / oauth-capable stale session → `/mcp login <name>`
|
||||
- other errors or healthy connected → `/mcp validate` (Diagnose)
|
||||
There is no `/mcp auth`.
|
||||
|
||||
4. **Plugin invalid manifests and duplicates are diagnosable.** Registry
|
||||
diagnostics (including `manifest-invalid`, `duplicate-root`, `name-conflict`)
|
||||
appear in a Problems group. Error-bearing plugins run `/plugin validate
|
||||
<name>` instead of a mute Inspect.
|
||||
|
||||
5. **`/mcp` manager pager names a next action per server** (`next: Re-auth
|
||||
/mcp login github`) instead of a command-prose wall. Footer is
|
||||
`Next: Connect /mcp reload · Diagnose /mcp validate`.
|
||||
|
||||
6. **Stale Streamable HTTP OAuth recovery** now says `/mcp login <name>`
|
||||
(CLI remains `codewhale mcp login`). Bearer-only sessions still point at
|
||||
the configured token, not a login command.
|
||||
|
||||
7. **Hunyuan / Hy in model selection.** There is no native Tencent Hunyuan
|
||||
`ApiProvider` and no models.dev bundled row. The public OpenRouter model
|
||||
`tencent/hy3-preview` already exists. Search/canonical aliases now include
|
||||
`hy3`, `hunyuan`, `tencent-hunyuan`, `hunyuan-hy3`. **hy4 is not invented.**
|
||||
|
||||
## Files changed
|
||||
|
||||
- `crates/tui/src/tui/views/mod.rs` — clickable tabs; MCP/plugin action rows
|
||||
- `crates/tui/src/tui/views/extensions.rs` — MCP/plugin recovery actions + Problems
|
||||
- `crates/tui/src/tui/mcp_routing.rs` — per-server next actions in `/mcp` pager
|
||||
- `crates/tui/src/mcp.rs` — `McpRecoveryKind` + `mcp_recovery_kind`
|
||||
- `crates/tui/src/mcp/oauth.rs` — `tui_reauth_hint` / text classifier
|
||||
- `crates/tui/src/mcp/streamable_http.rs` — `/mcp login` recovery copy
|
||||
- `crates/tui/src/localization.rs` + 15 locale packs
|
||||
- `crates/tui/src/config.rs`, `crates/config/src/lib.rs`, `crates/agent/src/lib.rs`
|
||||
— Hunyuan aliases for hy3 only
|
||||
- tests next to those modules
|
||||
|
||||
## Tests actually run
|
||||
|
||||
Isolated build-dir via `scripts/dev-cache.sh`. Command:
|
||||
|
||||
```
|
||||
cargo nextest run -p codewhale-tui --lib --locked -E 'test(/mcp_recovery_kind|tui_reauth_hints|manager_text_names_login|config_view_tabs_are_clickable|config_view_mcp_action_rows|openrouter_hunyuan|mcp_item_action_for|message_id_list_english|shipped_complete_packs_have_raw|settings_registry_types_every|config_view_includes_expected_editable|config_view_can_edit_filtered|config_view_mouse_click_selects|auth_required_login_hint_names|manager_text_shows_failed/)'
|
||||
```
|
||||
|
||||
Result: **16 passed**, 11198 skipped. Also `cargo fmt --all -- --check` and
|
||||
`git diff --check`.
|
||||
|
||||
Not run: full `codewhale-tui --lib` suite, clippy `-D warnings`, live TUI/PTY
|
||||
at multiple widths, live MCP OAuth, live Hunyuan/OpenRouter calls (no provider
|
||||
money spent).
|
||||
|
||||
## What remains
|
||||
|
||||
- Welcome shine vs launch-menu regression is still on `main`; 5643 owns that
|
||||
fix. This lane did not duplicate it.
|
||||
- `/mcp` manager is still a pager, not a clickable modal. Recovery is named
|
||||
on each row; clicking the pager text does not run the command.
|
||||
- Settings action rows still use select-then-activate (same as provider/model),
|
||||
not single-click-to-run. Tabs are single-click.
|
||||
- No native Hunyuan provider adapter. If Tencent ships a public API distinct
|
||||
from OpenRouter `tencent/hy3-preview`, that is a later provider lane. Do not
|
||||
add hy4 until a public id exists.
|
||||
- Plugin Problems rows always run `/plugin validate` (global); they do not yet
|
||||
deep-link a specific diagnostic path into a dedicated inspector.
|
||||
- Locale packs have the new keys; only English was used in the focused tests
|
||||
besides the completeness parity gate.
|
||||
|
||||
## Safest next integration step
|
||||
|
||||
Review `8e15e51d4` on this branch. Do not merge over 5643's welcome-shine
|
||||
files. If 5643 merges first, rebase this branch; expect a small
|
||||
`streamable_http.rs` overlap on the login hint only. Then PTY-check Settings
|
||||
tabs and an MCP 401 row at ~80 and ~40 columns.
|
||||
@@ -575,7 +575,14 @@ impl Default for ModelRegistry {
|
||||
ModelInfo {
|
||||
id: "tencent/hy3-preview".to_string(),
|
||||
provider: ProviderKind::Openrouter,
|
||||
aliases: vec!["hy3-preview".to_string(), "tencent-hy3-preview".to_string()],
|
||||
aliases: vec![
|
||||
"hy3-preview".to_string(),
|
||||
"tencent-hy3-preview".to_string(),
|
||||
"hy3".to_string(),
|
||||
"hunyuan".to_string(),
|
||||
"tencent-hunyuan".to_string(),
|
||||
"hunyuan-hy3".to_string(),
|
||||
],
|
||||
supports_tools: true,
|
||||
supports_reasoning: true,
|
||||
},
|
||||
|
||||
@@ -4442,9 +4442,13 @@ fn canonical_openrouter_recent_model_id(model: &str) -> Option<&'static str> {
|
||||
OPENROUTER_QWEN_3_8_FLASH_MODEL | "qwen3.8-flash" | "qwen-3.8-flash" => {
|
||||
Some(OPENROUTER_QWEN_3_8_FLASH_MODEL)
|
||||
}
|
||||
OPENROUTER_TENCENT_HY3_PREVIEW_MODEL | "hy3-preview" | "tencent-hy3-preview" => {
|
||||
Some(OPENROUTER_TENCENT_HY3_PREVIEW_MODEL)
|
||||
}
|
||||
OPENROUTER_TENCENT_HY3_PREVIEW_MODEL
|
||||
| "hy3-preview"
|
||||
| "tencent-hy3-preview"
|
||||
| "hy3"
|
||||
| "hunyuan"
|
||||
| "tencent-hunyuan"
|
||||
| "hunyuan-hy3" => Some(OPENROUTER_TENCENT_HY3_PREVIEW_MODEL),
|
||||
OPENROUTER_XIAOMI_MIMO_V2_5_PRO_MODEL
|
||||
| "mimo-v2.5-pro"
|
||||
| "mimo-v2-5-pro"
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "afegeix",
|
||||
"ExtensionsActionEnable": "activa",
|
||||
"ExtensionsActionReload": "torna a carregar",
|
||||
"ExtensionsActionConnect": "connecta",
|
||||
"ExtensionsActionReconnect": "torna a connectar",
|
||||
"ExtensionsActionReauth": "reautentica",
|
||||
"ExtensionsActionDiagnose": "diagnostica",
|
||||
"ExtensionsActionFocus": "focus",
|
||||
"ExtensionsActionFold": "plega",
|
||||
"ExtensionsActionTabs": "pestanyes",
|
||||
@@ -291,6 +295,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "Llindar de compactació",
|
||||
"ConfigLabelMaxHistory": "Historial d'entrada",
|
||||
"ConfigLabelMcpConfigPath": "Ruta de configuració MCP",
|
||||
"ConfigLabelMcpOpen": "Gestor MCP",
|
||||
"ConfigLabelMcpReconnect": "Torna a connectar MCP",
|
||||
"ConfigLabelMcpDiagnose": "Diagnostica MCP",
|
||||
"ConfigLabelPluginsOpen": "Extensions",
|
||||
"ConfigLabelFleetSpawnDepth": "Profunditat recursiva de Fleet",
|
||||
"ConfigLabelGoalCommand": "Comanda d'objectiu",
|
||||
"ConfigLabelWorkflow": "Flux de treball",
|
||||
@@ -300,6 +308,10 @@
|
||||
"ConfigColumnScope": "Abast",
|
||||
"ConfigActionOpenProvider": "Enter obre el selector de proveïdor",
|
||||
"ConfigActionOpenModel": "Enter obre el selector de model",
|
||||
"ConfigActionOpenMcp": "Enter obre el gestor MCP",
|
||||
"ConfigActionMcpReconnect": "Enter torna a connectar MCP",
|
||||
"ConfigActionMcpDiagnose": "Enter diagnostica MCP",
|
||||
"ConfigActionOpenPlugins": "Enter obre les extensions",
|
||||
"ConfigActionToggle": "Enter/Space commuta",
|
||||
"ConfigActionChoose": "Enter obre les opcions",
|
||||
"ConfigActionEdit": "Enter edita",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "hinzufügen",
|
||||
"ExtensionsActionEnable": "aktivieren",
|
||||
"ExtensionsActionReload": "neu laden",
|
||||
"ExtensionsActionConnect": "verbinden",
|
||||
"ExtensionsActionReconnect": "neu verbinden",
|
||||
"ExtensionsActionReauth": "neu anmelden",
|
||||
"ExtensionsActionDiagnose": "diagnostizieren",
|
||||
"ExtensionsActionFocus": "Fokus",
|
||||
"ExtensionsActionFold": "einklappen",
|
||||
"ExtensionsActionTabs": "Tabs",
|
||||
@@ -291,6 +295,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "Komprimierungs-Schwelle",
|
||||
"ConfigLabelMaxHistory": "Eingabeverlauf",
|
||||
"ConfigLabelMcpConfigPath": "MCP-Konfigpfad",
|
||||
"ConfigLabelMcpOpen": "MCP-Manager",
|
||||
"ConfigLabelMcpReconnect": "MCP neu verbinden",
|
||||
"ConfigLabelMcpDiagnose": "MCP diagnostizieren",
|
||||
"ConfigLabelPluginsOpen": "Plugins",
|
||||
"ConfigLabelFleetSpawnDepth": "Fleet-Rekursionstiefe",
|
||||
"ConfigLabelGoalCommand": "Goal-Befehl",
|
||||
"ConfigLabelWorkflow": "Workflow",
|
||||
@@ -300,6 +308,10 @@
|
||||
"ConfigColumnScope": "Bereich",
|
||||
"ConfigActionOpenProvider": "Enter öffnet Provider-Auswahl",
|
||||
"ConfigActionOpenModel": "Enter öffnet Modellauswahl",
|
||||
"ConfigActionOpenMcp": "Enter öffnet den MCP-Manager",
|
||||
"ConfigActionMcpReconnect": "Enter verbindet MCP neu",
|
||||
"ConfigActionMcpDiagnose": "Enter diagnostiziert MCP",
|
||||
"ConfigActionOpenPlugins": "Enter öffnet Plugins",
|
||||
"ConfigActionToggle": "Enter/Space schaltet um",
|
||||
"ConfigActionChoose": "Enter öffnet Auswahl",
|
||||
"ConfigActionEdit": "Enter bearbeitet",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "add",
|
||||
"ExtensionsActionEnable": "enable",
|
||||
"ExtensionsActionReload": "reload",
|
||||
"ExtensionsActionConnect": "connect",
|
||||
"ExtensionsActionReconnect": "reconnect",
|
||||
"ExtensionsActionReauth": "re-auth",
|
||||
"ExtensionsActionDiagnose": "diagnose",
|
||||
"ExtensionsActionFocus": "focus",
|
||||
"ExtensionsActionFold": "fold",
|
||||
"ExtensionsActionTabs": "tabs",
|
||||
@@ -293,6 +297,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "Compact threshold",
|
||||
"ConfigLabelMaxHistory": "Input history",
|
||||
"ConfigLabelMcpConfigPath": "MCP config path",
|
||||
"ConfigLabelMcpOpen": "MCP manager",
|
||||
"ConfigLabelMcpReconnect": "Reconnect MCP",
|
||||
"ConfigLabelMcpDiagnose": "Diagnose MCP",
|
||||
"ConfigLabelPluginsOpen": "Plugins",
|
||||
"ConfigLabelFleetSpawnDepth": "Fleet recursion depth",
|
||||
"ConfigLabelGoalCommand": "Goal command",
|
||||
"ConfigLabelWorkflow": "Workflow",
|
||||
@@ -302,6 +310,10 @@
|
||||
"ConfigColumnScope": "Scope",
|
||||
"ConfigActionOpenProvider": "Enter opens provider picker",
|
||||
"ConfigActionOpenModel": "Enter opens model picker",
|
||||
"ConfigActionOpenMcp": "Enter opens MCP manager",
|
||||
"ConfigActionMcpReconnect": "Enter reconnects MCP",
|
||||
"ConfigActionMcpDiagnose": "Enter diagnoses MCP",
|
||||
"ConfigActionOpenPlugins": "Enter opens plugins",
|
||||
"ConfigActionToggle": "Enter/Space toggles",
|
||||
"ConfigActionChoose": "Enter opens choices",
|
||||
"ConfigActionEdit": "Enter edits",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "agregar",
|
||||
"ExtensionsActionEnable": "habilitar",
|
||||
"ExtensionsActionReload": "recargar",
|
||||
"ExtensionsActionConnect": "conectar",
|
||||
"ExtensionsActionReconnect": "reconectar",
|
||||
"ExtensionsActionReauth": "reautenticar",
|
||||
"ExtensionsActionDiagnose": "diagnosticar",
|
||||
"ExtensionsActionFocus": "foco",
|
||||
"ExtensionsActionFold": "plegar",
|
||||
"ExtensionsActionTabs": "pestañas",
|
||||
@@ -293,6 +297,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "Umbral de compactación",
|
||||
"ConfigLabelMaxHistory": "Historial de entrada",
|
||||
"ConfigLabelMcpConfigPath": "Ruta de configuración MCP",
|
||||
"ConfigLabelMcpOpen": "Administrador MCP",
|
||||
"ConfigLabelMcpReconnect": "Reconectar MCP",
|
||||
"ConfigLabelMcpDiagnose": "Diagnosticar MCP",
|
||||
"ConfigLabelPluginsOpen": "Complementos",
|
||||
"ConfigLabelFleetSpawnDepth": "Profundidad recursiva de Fleet",
|
||||
"ConfigLabelGoalCommand": "Comando de objetivo",
|
||||
"ConfigLabelWorkflow": "Flujo de trabajo",
|
||||
@@ -302,6 +310,10 @@
|
||||
"ConfigColumnScope": "Alcance",
|
||||
"ConfigActionOpenProvider": "Enter abre el selector de proveedor",
|
||||
"ConfigActionOpenModel": "Enter abre el selector de modelo",
|
||||
"ConfigActionOpenMcp": "Enter abre el administrador MCP",
|
||||
"ConfigActionMcpReconnect": "Enter reconecta MCP",
|
||||
"ConfigActionMcpDiagnose": "Enter diagnostica MCP",
|
||||
"ConfigActionOpenPlugins": "Enter abre complementos",
|
||||
"ConfigActionToggle": "Enter/Espacio alterna",
|
||||
"ConfigActionChoose": "Enter abre las opciones",
|
||||
"ConfigActionEdit": "Enter edita",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "ajouter",
|
||||
"ExtensionsActionEnable": "activer",
|
||||
"ExtensionsActionReload": "recharger",
|
||||
"ExtensionsActionConnect": "connecter",
|
||||
"ExtensionsActionReconnect": "reconnecter",
|
||||
"ExtensionsActionReauth": "réauthentifier",
|
||||
"ExtensionsActionDiagnose": "diagnostiquer",
|
||||
"ExtensionsActionFocus": "focus",
|
||||
"ExtensionsActionFold": "replier",
|
||||
"ExtensionsActionTabs": "onglets",
|
||||
@@ -291,6 +295,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "Seuil de compaction",
|
||||
"ConfigLabelMaxHistory": "Historique de saisie",
|
||||
"ConfigLabelMcpConfigPath": "Chemin de la config MCP",
|
||||
"ConfigLabelMcpOpen": "Gestionnaire MCP",
|
||||
"ConfigLabelMcpReconnect": "Reconnecter MCP",
|
||||
"ConfigLabelMcpDiagnose": "Diagnostiquer MCP",
|
||||
"ConfigLabelPluginsOpen": "Extensions",
|
||||
"ConfigLabelFleetSpawnDepth": "Profondeur de récursion Fleet",
|
||||
"ConfigLabelGoalCommand": "Commande d'objectif",
|
||||
"ConfigLabelWorkflow": "Workflow",
|
||||
@@ -300,6 +308,10 @@
|
||||
"ConfigColumnScope": "Portée",
|
||||
"ConfigActionOpenProvider": "Enter ouvre le sélecteur de fournisseur",
|
||||
"ConfigActionOpenModel": "Enter ouvre le sélecteur de modèle",
|
||||
"ConfigActionOpenMcp": "Entrée ouvre le gestionnaire MCP",
|
||||
"ConfigActionMcpReconnect": "Entrée reconnecte MCP",
|
||||
"ConfigActionMcpDiagnose": "Entrée diagnostique MCP",
|
||||
"ConfigActionOpenPlugins": "Entrée ouvre les extensions",
|
||||
"ConfigActionToggle": "Enter/Espace bascule",
|
||||
"ConfigActionChoose": "Enter ouvre les choix",
|
||||
"ConfigActionEdit": "Enter modifie",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "जोड़ें",
|
||||
"ExtensionsActionEnable": "सक्षम करें",
|
||||
"ExtensionsActionReload": "पुनः लोड करें",
|
||||
"ExtensionsActionConnect": "कनेक्ट करें",
|
||||
"ExtensionsActionReconnect": "फिर कनेक्ट करें",
|
||||
"ExtensionsActionReauth": "फिर प्रमाणित करें",
|
||||
"ExtensionsActionDiagnose": "निदान करें",
|
||||
"ExtensionsActionFocus": "फ़ोकस",
|
||||
"ExtensionsActionFold": "समेटें",
|
||||
"ExtensionsActionTabs": "टैब",
|
||||
@@ -291,6 +295,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "कॉम्पैक्ट सीमा",
|
||||
"ConfigLabelMaxHistory": "इनपुट इतिहास",
|
||||
"ConfigLabelMcpConfigPath": "MCP कॉन्फ़िग पथ",
|
||||
"ConfigLabelMcpOpen": "MCP प्रबंधक",
|
||||
"ConfigLabelMcpReconnect": "MCP फिर कनेक्ट करें",
|
||||
"ConfigLabelMcpDiagnose": "MCP निदान",
|
||||
"ConfigLabelPluginsOpen": "प्लगइन",
|
||||
"ConfigLabelFleetSpawnDepth": "Fleet पुनरावृत्ति गहराई",
|
||||
"ConfigLabelGoalCommand": "गोल कमांड",
|
||||
"ConfigLabelWorkflow": "वर्कफ़्लो",
|
||||
@@ -300,6 +308,10 @@
|
||||
"ConfigColumnScope": "दायरा",
|
||||
"ConfigActionOpenProvider": "Enter प्रोवाइडर चयनकर्ता खोलता है",
|
||||
"ConfigActionOpenModel": "Enter मॉडल चयनकर्ता खोलता है",
|
||||
"ConfigActionOpenMcp": "Enter MCP प्रबंधक खोलता है",
|
||||
"ConfigActionMcpReconnect": "Enter MCP फिर कनेक्ट करता है",
|
||||
"ConfigActionMcpDiagnose": "Enter MCP का निदान करता है",
|
||||
"ConfigActionOpenPlugins": "Enter प्लगइन खोलता है",
|
||||
"ConfigActionToggle": "Enter/Space टॉगल करता है",
|
||||
"ConfigActionChoose": "Enter विकल्प खोलता है",
|
||||
"ConfigActionEdit": "Enter संपादित करता है",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "tambahkan",
|
||||
"ExtensionsActionEnable": "aktifkan",
|
||||
"ExtensionsActionReload": "muat ulang",
|
||||
"ExtensionsActionConnect": "hubungkan",
|
||||
"ExtensionsActionReconnect": "hubungkan ulang",
|
||||
"ExtensionsActionReauth": "otentikasi ulang",
|
||||
"ExtensionsActionDiagnose": "diagnosa",
|
||||
"ExtensionsActionFocus": "fokus",
|
||||
"ExtensionsActionFold": "lipat",
|
||||
"ExtensionsActionTabs": "tab",
|
||||
@@ -291,6 +295,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "Ambang pemadatan",
|
||||
"ConfigLabelMaxHistory": "Riwayat input",
|
||||
"ConfigLabelMcpConfigPath": "Jalur konfig MCP",
|
||||
"ConfigLabelMcpOpen": "Pengelola MCP",
|
||||
"ConfigLabelMcpReconnect": "Hubungkan ulang MCP",
|
||||
"ConfigLabelMcpDiagnose": "Diagnosa MCP",
|
||||
"ConfigLabelPluginsOpen": "Plugin",
|
||||
"ConfigLabelFleetSpawnDepth": "Kedalaman rekursi Fleet",
|
||||
"ConfigLabelGoalCommand": "Perintah tujuan",
|
||||
"ConfigLabelWorkflow": "Alur kerja",
|
||||
@@ -300,6 +308,10 @@
|
||||
"ConfigColumnScope": "Cakupan",
|
||||
"ConfigActionOpenProvider": "Enter membuka pemilih penyedia",
|
||||
"ConfigActionOpenModel": "Enter membuka pemilih model",
|
||||
"ConfigActionOpenMcp": "Enter membuka pengelola MCP",
|
||||
"ConfigActionMcpReconnect": "Enter menghubungkan ulang MCP",
|
||||
"ConfigActionMcpDiagnose": "Enter mendiagnosa MCP",
|
||||
"ConfigActionOpenPlugins": "Enter membuka plugin",
|
||||
"ConfigActionToggle": "Enter/Space mengalihkan",
|
||||
"ConfigActionChoose": "Enter membuka pilihan",
|
||||
"ConfigActionEdit": "Enter mengedit",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "追加",
|
||||
"ExtensionsActionEnable": "有効化",
|
||||
"ExtensionsActionReload": "再読み込み",
|
||||
"ExtensionsActionConnect": "接続",
|
||||
"ExtensionsActionReconnect": "再接続",
|
||||
"ExtensionsActionReauth": "再認証",
|
||||
"ExtensionsActionDiagnose": "診断",
|
||||
"ExtensionsActionFocus": "フォーカス",
|
||||
"ExtensionsActionFold": "折りたたむ",
|
||||
"ExtensionsActionTabs": "タブ",
|
||||
@@ -293,6 +297,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "コンパクトしきい値",
|
||||
"ConfigLabelMaxHistory": "入力履歴",
|
||||
"ConfigLabelMcpConfigPath": "MCP 設定パス",
|
||||
"ConfigLabelMcpOpen": "MCP マネージャ",
|
||||
"ConfigLabelMcpReconnect": "MCP を再接続",
|
||||
"ConfigLabelMcpDiagnose": "MCP を診断",
|
||||
"ConfigLabelPluginsOpen": "プラグイン",
|
||||
"ConfigLabelFleetSpawnDepth": "Fleet 再帰深度",
|
||||
"ConfigLabelGoalCommand": "目標コマンド",
|
||||
"ConfigLabelWorkflow": "ワークフロー",
|
||||
@@ -302,6 +310,10 @@
|
||||
"ConfigColumnScope": "範囲",
|
||||
"ConfigActionOpenProvider": "Enter でプロバイダ選択を開く",
|
||||
"ConfigActionOpenModel": "Enter でモデル選択を開く",
|
||||
"ConfigActionOpenMcp": "Enter で MCP マネージャを開く",
|
||||
"ConfigActionMcpReconnect": "Enter で MCP を再接続",
|
||||
"ConfigActionMcpDiagnose": "Enter で MCP を診断",
|
||||
"ConfigActionOpenPlugins": "Enter でプラグインを開く",
|
||||
"ConfigActionToggle": "Enter/Space で切り替え",
|
||||
"ConfigActionChoose": "Enter で選択肢を開く",
|
||||
"ConfigActionEdit": "Enter で編集",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "추가",
|
||||
"ExtensionsActionEnable": "활성화",
|
||||
"ExtensionsActionReload": "다시 로드",
|
||||
"ExtensionsActionConnect": "연결",
|
||||
"ExtensionsActionReconnect": "다시 연결",
|
||||
"ExtensionsActionReauth": "재인증",
|
||||
"ExtensionsActionDiagnose": "진단",
|
||||
"ExtensionsActionFocus": "포커스",
|
||||
"ExtensionsActionFold": "접기",
|
||||
"ExtensionsActionTabs": "탭",
|
||||
@@ -293,6 +297,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "압축 임계값",
|
||||
"ConfigLabelMaxHistory": "입력 기록",
|
||||
"ConfigLabelMcpConfigPath": "MCP 설정 경로",
|
||||
"ConfigLabelMcpOpen": "MCP 관리자",
|
||||
"ConfigLabelMcpReconnect": "MCP 다시 연결",
|
||||
"ConfigLabelMcpDiagnose": "MCP 진단",
|
||||
"ConfigLabelPluginsOpen": "플러그인",
|
||||
"ConfigLabelFleetSpawnDepth": "Fleet 재귀 깊이",
|
||||
"ConfigLabelGoalCommand": "목표 명령",
|
||||
"ConfigLabelWorkflow": "워크플로",
|
||||
@@ -302,6 +310,10 @@
|
||||
"ConfigColumnScope": "범위",
|
||||
"ConfigActionOpenProvider": "Enter로 프로바이더 선택기 열기",
|
||||
"ConfigActionOpenModel": "Enter로 모델 선택기 열기",
|
||||
"ConfigActionOpenMcp": "Enter로 MCP 관리자 열기",
|
||||
"ConfigActionMcpReconnect": "Enter로 MCP 다시 연결",
|
||||
"ConfigActionMcpDiagnose": "Enter로 MCP 진단",
|
||||
"ConfigActionOpenPlugins": "Enter로 플러그인 열기",
|
||||
"ConfigActionToggle": "Enter/Space로 전환",
|
||||
"ConfigActionChoose": "Enter로 선택 항목 열기",
|
||||
"ConfigActionEdit": "Enter로 편집",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "adicionar",
|
||||
"ExtensionsActionEnable": "ativar",
|
||||
"ExtensionsActionReload": "recarregar",
|
||||
"ExtensionsActionConnect": "conectar",
|
||||
"ExtensionsActionReconnect": "reconectar",
|
||||
"ExtensionsActionReauth": "reautenticar",
|
||||
"ExtensionsActionDiagnose": "diagnosticar",
|
||||
"ExtensionsActionFocus": "foco",
|
||||
"ExtensionsActionFold": "recolher",
|
||||
"ExtensionsActionTabs": "abas",
|
||||
@@ -293,6 +297,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "Limite de compactação",
|
||||
"ConfigLabelMaxHistory": "Histórico de entrada",
|
||||
"ConfigLabelMcpConfigPath": "Caminho da configuração MCP",
|
||||
"ConfigLabelMcpOpen": "Gerenciador MCP",
|
||||
"ConfigLabelMcpReconnect": "Reconectar MCP",
|
||||
"ConfigLabelMcpDiagnose": "Diagnosticar MCP",
|
||||
"ConfigLabelPluginsOpen": "Plugins",
|
||||
"ConfigLabelFleetSpawnDepth": "Profundidade recursiva da Fleet",
|
||||
"ConfigLabelGoalCommand": "Comando de objetivo",
|
||||
"ConfigLabelWorkflow": "Fluxo de trabalho",
|
||||
@@ -302,6 +310,10 @@
|
||||
"ConfigColumnScope": "Escopo",
|
||||
"ConfigActionOpenProvider": "Enter abre o seletor de provedor",
|
||||
"ConfigActionOpenModel": "Enter abre o seletor de modelo",
|
||||
"ConfigActionOpenMcp": "Enter abre o gerenciador MCP",
|
||||
"ConfigActionMcpReconnect": "Enter reconecta o MCP",
|
||||
"ConfigActionMcpDiagnose": "Enter diagnostica o MCP",
|
||||
"ConfigActionOpenPlugins": "Enter abre os plugins",
|
||||
"ConfigActionToggle": "Enter/Espaço alterna",
|
||||
"ConfigActionChoose": "Enter abre as opções",
|
||||
"ConfigActionEdit": "Enter edita",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "добавить",
|
||||
"ExtensionsActionEnable": "включить",
|
||||
"ExtensionsActionReload": "перезагрузить",
|
||||
"ExtensionsActionConnect": "подключить",
|
||||
"ExtensionsActionReconnect": "переподключить",
|
||||
"ExtensionsActionReauth": "повторно войти",
|
||||
"ExtensionsActionDiagnose": "диагностика",
|
||||
"ExtensionsActionFocus": "фокус",
|
||||
"ExtensionsActionFold": "свернуть",
|
||||
"ExtensionsActionTabs": "вкладки",
|
||||
@@ -291,6 +295,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "Порог сжатия",
|
||||
"ConfigLabelMaxHistory": "История ввода",
|
||||
"ConfigLabelMcpConfigPath": "Путь к конфигурации MCP",
|
||||
"ConfigLabelMcpOpen": "Менеджер MCP",
|
||||
"ConfigLabelMcpReconnect": "Переподключить MCP",
|
||||
"ConfigLabelMcpDiagnose": "Диагностика MCP",
|
||||
"ConfigLabelPluginsOpen": "Плагины",
|
||||
"ConfigLabelFleetSpawnDepth": "Глубина рекурсии Fleet",
|
||||
"ConfigLabelGoalCommand": "Команда цели",
|
||||
"ConfigLabelWorkflow": "Воркфлоу",
|
||||
@@ -300,6 +308,10 @@
|
||||
"ConfigColumnScope": "Область",
|
||||
"ConfigActionOpenProvider": "Enter — выбор провайдера",
|
||||
"ConfigActionOpenModel": "Enter — выбор модели",
|
||||
"ConfigActionOpenMcp": "Enter открывает менеджер MCP",
|
||||
"ConfigActionMcpReconnect": "Enter переподключает MCP",
|
||||
"ConfigActionMcpDiagnose": "Enter диагностирует MCP",
|
||||
"ConfigActionOpenPlugins": "Enter открывает плагины",
|
||||
"ConfigActionToggle": "Enter/Space — переключить",
|
||||
"ConfigActionChoose": "Enter — открыть варианты",
|
||||
"ConfigActionEdit": "Enter — изменить",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "додати",
|
||||
"ExtensionsActionEnable": "увімкнути",
|
||||
"ExtensionsActionReload": "перезавантажити",
|
||||
"ExtensionsActionConnect": "підключити",
|
||||
"ExtensionsActionReconnect": "перепідключити",
|
||||
"ExtensionsActionReauth": "повторно увійти",
|
||||
"ExtensionsActionDiagnose": "діагностика",
|
||||
"ExtensionsActionFocus": "фокус",
|
||||
"ExtensionsActionFold": "згорнути",
|
||||
"ExtensionsActionTabs": "вкладки",
|
||||
@@ -291,6 +295,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "Поріг стиснення",
|
||||
"ConfigLabelMaxHistory": "Історія вводу",
|
||||
"ConfigLabelMcpConfigPath": "Шлях конфігурації MCP",
|
||||
"ConfigLabelMcpOpen": "Менеджер MCP",
|
||||
"ConfigLabelMcpReconnect": "Перепідключити MCP",
|
||||
"ConfigLabelMcpDiagnose": "Діагностика MCP",
|
||||
"ConfigLabelPluginsOpen": "Плагіни",
|
||||
"ConfigLabelFleetSpawnDepth": "Глибина рекурсії Fleet",
|
||||
"ConfigLabelGoalCommand": "Команда цілі",
|
||||
"ConfigLabelWorkflow": "Робочий процес",
|
||||
@@ -300,6 +308,10 @@
|
||||
"ConfigColumnScope": "Область",
|
||||
"ConfigActionOpenProvider": "Enter відкриває вибір провайдера",
|
||||
"ConfigActionOpenModel": "Enter відкриває вибір моделі",
|
||||
"ConfigActionOpenMcp": "Enter відкриває менеджер MCP",
|
||||
"ConfigActionMcpReconnect": "Enter перепідключає MCP",
|
||||
"ConfigActionMcpDiagnose": "Enter діагностує MCP",
|
||||
"ConfigActionOpenPlugins": "Enter відкриває плагіни",
|
||||
"ConfigActionToggle": "Enter/Space перемикає",
|
||||
"ConfigActionChoose": "Enter відкриває варіанти",
|
||||
"ConfigActionEdit": "Enter редагує",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "thêm",
|
||||
"ExtensionsActionEnable": "bật",
|
||||
"ExtensionsActionReload": "tải lại",
|
||||
"ExtensionsActionConnect": "kết nối",
|
||||
"ExtensionsActionReconnect": "kết nối lại",
|
||||
"ExtensionsActionReauth": "xác thực lại",
|
||||
"ExtensionsActionDiagnose": "chẩn đoán",
|
||||
"ExtensionsActionFocus": "tiêu điểm",
|
||||
"ExtensionsActionFold": "thu gọn",
|
||||
"ExtensionsActionTabs": "thẻ",
|
||||
@@ -293,6 +297,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "Ngưỡng thu gọn",
|
||||
"ConfigLabelMaxHistory": "Lịch sử nhập",
|
||||
"ConfigLabelMcpConfigPath": "Đường dẫn cấu hình MCP",
|
||||
"ConfigLabelMcpOpen": "Trình quản lý MCP",
|
||||
"ConfigLabelMcpReconnect": "Kết nối lại MCP",
|
||||
"ConfigLabelMcpDiagnose": "Chẩn đoán MCP",
|
||||
"ConfigLabelPluginsOpen": "Plugin",
|
||||
"ConfigLabelFleetSpawnDepth": "Độ sâu đệ quy Fleet",
|
||||
"ConfigLabelGoalCommand": "Lệnh mục tiêu",
|
||||
"ConfigLabelWorkflow": "Quy trình làm việc",
|
||||
@@ -302,6 +310,10 @@
|
||||
"ConfigColumnScope": "Phạm vi",
|
||||
"ConfigActionOpenProvider": "Enter mở bộ chọn nhà cung cấp",
|
||||
"ConfigActionOpenModel": "Enter mở bộ chọn mô hình",
|
||||
"ConfigActionOpenMcp": "Enter mở trình quản lý MCP",
|
||||
"ConfigActionMcpReconnect": "Enter kết nối lại MCP",
|
||||
"ConfigActionMcpDiagnose": "Enter chẩn đoán MCP",
|
||||
"ConfigActionOpenPlugins": "Enter mở plugin",
|
||||
"ConfigActionToggle": "Enter/Dấu cách để bật tắt",
|
||||
"ConfigActionChoose": "Enter mở các lựa chọn",
|
||||
"ConfigActionEdit": "Enter để sửa",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "添加",
|
||||
"ExtensionsActionEnable": "启用",
|
||||
"ExtensionsActionReload": "重新加载",
|
||||
"ExtensionsActionConnect": "连接",
|
||||
"ExtensionsActionReconnect": "重新连接",
|
||||
"ExtensionsActionReauth": "重新登录",
|
||||
"ExtensionsActionDiagnose": "诊断",
|
||||
"ExtensionsActionFocus": "焦点",
|
||||
"ExtensionsActionFold": "折叠",
|
||||
"ExtensionsActionTabs": "标签页",
|
||||
@@ -293,6 +297,10 @@
|
||||
"ConfigLabelAutoCompactThreshold": "压缩阈值",
|
||||
"ConfigLabelMaxHistory": "输入历史",
|
||||
"ConfigLabelMcpConfigPath": "MCP 配置路径",
|
||||
"ConfigLabelMcpOpen": "MCP 管理器",
|
||||
"ConfigLabelMcpReconnect": "重新连接 MCP",
|
||||
"ConfigLabelMcpDiagnose": "诊断 MCP",
|
||||
"ConfigLabelPluginsOpen": "插件",
|
||||
"ConfigLabelFleetSpawnDepth": "Fleet 递归深度",
|
||||
"ConfigLabelGoalCommand": "目标命令",
|
||||
"ConfigLabelWorkflow": "工作流",
|
||||
@@ -302,6 +310,10 @@
|
||||
"ConfigColumnScope": "范围",
|
||||
"ConfigActionOpenProvider": "按 Enter 打开提供商选择器",
|
||||
"ConfigActionOpenModel": "按 Enter 打开模型选择器",
|
||||
"ConfigActionOpenMcp": "Enter 打开 MCP 管理器",
|
||||
"ConfigActionMcpReconnect": "Enter 重新连接 MCP",
|
||||
"ConfigActionMcpDiagnose": "Enter 诊断 MCP",
|
||||
"ConfigActionOpenPlugins": "Enter 打开插件",
|
||||
"ConfigActionToggle": "按 Enter/空格切换",
|
||||
"ConfigActionChoose": "按 Enter 打开选项",
|
||||
"ConfigActionEdit": "按 Enter 编辑",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"ExtensionsActionAdd": "新增",
|
||||
"ExtensionsActionEnable": "啟用",
|
||||
"ExtensionsActionReload": "重新載入",
|
||||
"ExtensionsActionConnect": "連線",
|
||||
"ExtensionsActionReconnect": "重新連線",
|
||||
"ExtensionsActionReauth": "重新登入",
|
||||
"ExtensionsActionDiagnose": "診斷",
|
||||
"ExtensionsActionFocus": "焦點",
|
||||
"ExtensionsActionFold": "摺疊",
|
||||
"ExtensionsActionTabs": "分頁",
|
||||
@@ -474,6 +478,10 @@
|
||||
"ConfigActionChoose": "按 Enter 開啟選項",
|
||||
"ConfigActionEdit": "按 Enter 編輯",
|
||||
"ConfigActionOpenModel": "按 Enter 開啟模型選擇器",
|
||||
"ConfigActionOpenMcp": "Enter 開啟 MCP 管理員",
|
||||
"ConfigActionMcpReconnect": "Enter 重新連線 MCP",
|
||||
"ConfigActionMcpDiagnose": "Enter 診斷 MCP",
|
||||
"ConfigActionOpenPlugins": "Enter 開啟外掛",
|
||||
"ConfigActionOpenProvider": "按 Enter 開啟提供商選擇器",
|
||||
"ConfigActionReadOnly": "唯讀",
|
||||
"ConfigActionToggle": "按 Enter/空格切換",
|
||||
@@ -527,6 +535,10 @@
|
||||
"ConfigLabelManagedApprovalPolicy": "新工作階段權限(受管理)",
|
||||
"ConfigLabelMaxHistory": "輸入歷史",
|
||||
"ConfigLabelMcpConfigPath": "MCP 設定路徑",
|
||||
"ConfigLabelMcpOpen": "MCP 管理員",
|
||||
"ConfigLabelMcpReconnect": "重新連線 MCP",
|
||||
"ConfigLabelMcpDiagnose": "診斷 MCP",
|
||||
"ConfigLabelPluginsOpen": "外掛",
|
||||
"ConfigLabelMentionMenuBehavior": "提及選單行為",
|
||||
"ConfigLabelMentionMenuLimit": "提及選單上限",
|
||||
"ConfigLabelMentionWalkDepth": "檔案提及深度",
|
||||
|
||||
@@ -1086,9 +1086,13 @@ fn canonical_openrouter_recent_model_id(model: &str) -> Option<&'static str> {
|
||||
OPENROUTER_QWEN_3_8_FLASH_MODEL | "qwen3.8-flash" | "qwen-3.8-flash" => {
|
||||
Some(OPENROUTER_QWEN_3_8_FLASH_MODEL)
|
||||
}
|
||||
OPENROUTER_TENCENT_HY3_PREVIEW_MODEL | "hy3-preview" | "tencent-hy3-preview" => {
|
||||
Some(OPENROUTER_TENCENT_HY3_PREVIEW_MODEL)
|
||||
}
|
||||
OPENROUTER_TENCENT_HY3_PREVIEW_MODEL
|
||||
| "hy3-preview"
|
||||
| "tencent-hy3-preview"
|
||||
| "hy3"
|
||||
| "hunyuan"
|
||||
| "tencent-hunyuan"
|
||||
| "hunyuan-hy3" => Some(OPENROUTER_TENCENT_HY3_PREVIEW_MODEL),
|
||||
OPENROUTER_XIAOMI_MIMO_V2_5_PRO_MODEL
|
||||
| "mimo-v2.5-pro"
|
||||
| "mimo-v2-5-pro"
|
||||
|
||||
@@ -6179,6 +6179,29 @@ fn retired_deepseek_aliases_do_not_escape_provider_owned_namespaces() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openrouter_hunyuan_aliases_resolve_to_hy3_preview() {
|
||||
for alias in [
|
||||
"tencent/hy3-preview",
|
||||
"hy3-preview",
|
||||
"hy3",
|
||||
"hunyuan",
|
||||
"tencent-hunyuan",
|
||||
"hunyuan-hy3",
|
||||
] {
|
||||
assert_eq!(
|
||||
canonical_model_id_for_provider(ApiProvider::Openrouter, alias).as_deref(),
|
||||
Some(OPENROUTER_TENCENT_HY3_PREVIEW_MODEL),
|
||||
"{alias} should select the public Hy3 preview id"
|
||||
);
|
||||
}
|
||||
assert_ne!(
|
||||
canonical_model_id_for_provider(ApiProvider::Openrouter, "hy4").as_deref(),
|
||||
Some(OPENROUTER_TENCENT_HY3_PREVIEW_MODEL),
|
||||
"hy4 is not a released public model id"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_default_model_canonicalizes_provider_prefixed_ids() {
|
||||
let _lock = lock_test_env();
|
||||
|
||||
@@ -308,6 +308,10 @@ pub enum MessageId {
|
||||
ConfigLabelAutoCompact,
|
||||
ConfigLabelAutoCompactThreshold,
|
||||
ConfigLabelMaxHistory,
|
||||
ConfigLabelMcpOpen,
|
||||
ConfigLabelMcpReconnect,
|
||||
ConfigLabelMcpDiagnose,
|
||||
ConfigLabelPluginsOpen,
|
||||
ConfigLabelMcpConfigPath,
|
||||
ConfigLabelFleetSpawnDepth,
|
||||
ConfigLabelGoalCommand,
|
||||
@@ -318,6 +322,10 @@ pub enum MessageId {
|
||||
ConfigColumnScope,
|
||||
ConfigActionOpenProvider,
|
||||
ConfigActionOpenModel,
|
||||
ConfigActionOpenMcp,
|
||||
ConfigActionMcpReconnect,
|
||||
ConfigActionMcpDiagnose,
|
||||
ConfigActionOpenPlugins,
|
||||
ConfigActionToggle,
|
||||
ConfigActionChoose,
|
||||
ConfigActionEdit,
|
||||
@@ -436,6 +444,10 @@ pub enum MessageId {
|
||||
ExtensionsActionAdd,
|
||||
ExtensionsActionEnable,
|
||||
ExtensionsActionReload,
|
||||
ExtensionsActionConnect,
|
||||
ExtensionsActionReconnect,
|
||||
ExtensionsActionReauth,
|
||||
ExtensionsActionDiagnose,
|
||||
ExtensionsActionFocus,
|
||||
ExtensionsActionFold,
|
||||
ExtensionsActionTabs,
|
||||
@@ -2104,6 +2116,10 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::ConfigLabelAutoCompact,
|
||||
MessageId::ConfigLabelAutoCompactThreshold,
|
||||
MessageId::ConfigLabelMaxHistory,
|
||||
MessageId::ConfigLabelMcpOpen,
|
||||
MessageId::ConfigLabelMcpReconnect,
|
||||
MessageId::ConfigLabelMcpDiagnose,
|
||||
MessageId::ConfigLabelPluginsOpen,
|
||||
MessageId::ConfigLabelMcpConfigPath,
|
||||
MessageId::ConfigLabelFleetSpawnDepth,
|
||||
MessageId::ConfigLabelGoalCommand,
|
||||
@@ -2114,6 +2130,10 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::ConfigColumnScope,
|
||||
MessageId::ConfigActionOpenProvider,
|
||||
MessageId::ConfigActionOpenModel,
|
||||
MessageId::ConfigActionOpenMcp,
|
||||
MessageId::ConfigActionMcpReconnect,
|
||||
MessageId::ConfigActionMcpDiagnose,
|
||||
MessageId::ConfigActionOpenPlugins,
|
||||
MessageId::ConfigActionToggle,
|
||||
MessageId::ConfigActionChoose,
|
||||
MessageId::ConfigActionEdit,
|
||||
@@ -2231,6 +2251,10 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::ExtensionsActionAdd,
|
||||
MessageId::ExtensionsActionEnable,
|
||||
MessageId::ExtensionsActionReload,
|
||||
MessageId::ExtensionsActionConnect,
|
||||
MessageId::ExtensionsActionReconnect,
|
||||
MessageId::ExtensionsActionReauth,
|
||||
MessageId::ExtensionsActionDiagnose,
|
||||
MessageId::ExtensionsActionFocus,
|
||||
MessageId::ExtensionsActionFold,
|
||||
MessageId::ExtensionsActionTabs,
|
||||
@@ -4893,7 +4917,7 @@ mod tests {
|
||||
.filter(|key| key.starts_with("Extensions"))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(keys.len(), 82, "the complete extensions locale set changed");
|
||||
assert_eq!(keys.len(), 86, "the complete extensions locale set changed");
|
||||
|
||||
let prose_keys = [
|
||||
"ExtensionsMarketplaceUnavailable",
|
||||
|
||||
@@ -3587,6 +3587,84 @@ pub struct McpManagerSnapshot {
|
||||
pub servers: Vec<McpServerSnapshot>,
|
||||
}
|
||||
|
||||
/// First-class recovery for a configured MCP server. Commands named here exist:
|
||||
/// `/mcp login`, `/mcp reload`, `/mcp validate`, `/mcp enable`. There is no
|
||||
/// `/mcp auth`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum McpRecoveryKind {
|
||||
Enable,
|
||||
Connect,
|
||||
Reconnect,
|
||||
Reauth,
|
||||
Diagnose,
|
||||
}
|
||||
|
||||
impl McpRecoveryKind {
|
||||
#[must_use]
|
||||
pub fn slash_command(self, name: &str) -> String {
|
||||
match self {
|
||||
Self::Enable => format!("/mcp enable {name}"),
|
||||
Self::Connect | Self::Reconnect => "/mcp reload".to_string(),
|
||||
Self::Reauth => format!("/mcp login {name}"),
|
||||
Self::Diagnose => "/mcp validate".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn label_key(self) -> crate::localization::MessageId {
|
||||
match self {
|
||||
Self::Enable => crate::localization::MessageId::ExtensionsActionEnable,
|
||||
Self::Connect => crate::localization::MessageId::ExtensionsActionConnect,
|
||||
Self::Reconnect => crate::localization::MessageId::ExtensionsActionReconnect,
|
||||
Self::Reauth => crate::localization::MessageId::ExtensionsActionReauth,
|
||||
Self::Diagnose => crate::localization::MessageId::ExtensionsActionDiagnose,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn mcp_name_is_command_safe(name: &str) -> bool {
|
||||
!name.is_empty()
|
||||
&& name
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn mcp_server_oauth_capable(config: &McpServerConfig) -> bool {
|
||||
config.url.is_some()
|
||||
&& (config.oauth.is_some() || !config.scopes.is_empty() || config.oauth_resource.is_some())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn mcp_recovery_kind(
|
||||
enabled: bool,
|
||||
inspected: bool,
|
||||
connected: bool,
|
||||
error: Option<&str>,
|
||||
oauth_capable: bool,
|
||||
) -> McpRecoveryKind {
|
||||
if !enabled {
|
||||
return McpRecoveryKind::Enable;
|
||||
}
|
||||
if let Some(error) = error {
|
||||
if oauth::error_text_looks_auth_required(error) {
|
||||
return McpRecoveryKind::Reauth;
|
||||
}
|
||||
return McpRecoveryKind::Diagnose;
|
||||
}
|
||||
if !inspected {
|
||||
return McpRecoveryKind::Connect;
|
||||
}
|
||||
if connected {
|
||||
return McpRecoveryKind::Diagnose;
|
||||
}
|
||||
if oauth_capable {
|
||||
return McpRecoveryKind::Reauth;
|
||||
}
|
||||
McpRecoveryKind::Reconnect
|
||||
}
|
||||
|
||||
pub fn load_config(path: &Path) -> Result<McpConfig> {
|
||||
validate_mcp_config_path(path)?;
|
||||
let Some(contents) = read_mcp_config_file(path)? else {
|
||||
|
||||
@@ -46,12 +46,19 @@ impl std::fmt::Display for McpAuthStatus {
|
||||
}
|
||||
|
||||
pub fn error_looks_auth_required(error: &anyhow::Error) -> bool {
|
||||
let text = format!("{error:#}").to_ascii_lowercase();
|
||||
error_text_looks_auth_required(&format!("{error:#}"))
|
||||
}
|
||||
|
||||
pub fn error_text_looks_auth_required(text: &str) -> bool {
|
||||
let text = text.to_ascii_lowercase();
|
||||
text.contains("401")
|
||||
|| text.contains("unauthorized")
|
||||
|| text.contains("authentication_required")
|
||||
|| text.contains("not logged in")
|
||||
|| text.contains("not-logged-in")
|
||||
|| text.contains("re-authorize")
|
||||
|| text.contains("/mcp login")
|
||||
|| text.contains("mcp login")
|
||||
}
|
||||
|
||||
pub fn auth_required_login_hint(server_name: &str) -> String {
|
||||
@@ -60,6 +67,16 @@ pub fn auth_required_login_hint(server_name: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// TUI recovery for a stale Streamable HTTP OAuth session. `/mcp auth` is not a
|
||||
/// command; login is `/mcp login <name>` (CLI: `codewhale mcp login <name>`).
|
||||
pub fn tui_reauth_hint() -> &'static str {
|
||||
"Re-authorize this server (/mcp login <name>) to continue."
|
||||
}
|
||||
|
||||
pub fn tui_reauth_refresh_failed_hint() -> &'static str {
|
||||
"Re-authorize this server (/mcp login <name>) or configure a fresh bearer token."
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct StoredMcpOAuthTokens {
|
||||
pub server_name: String,
|
||||
@@ -1229,6 +1246,25 @@ mod tests {
|
||||
let hint = auth_required_login_hint("nordic-mcp");
|
||||
assert!(hint.contains("nordic-mcp"));
|
||||
assert!(hint.contains("codewhale mcp login nordic-mcp"));
|
||||
assert!(!hint.contains("/mcp auth"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_reauth_hints_name_the_login_command() {
|
||||
for hint in [tui_reauth_hint(), tui_reauth_refresh_failed_hint()] {
|
||||
assert!(
|
||||
hint.contains("/mcp login <name>"),
|
||||
"OAuth recovery must name the implemented command"
|
||||
);
|
||||
assert!(
|
||||
!hint.contains("/mcp auth"),
|
||||
"OAuth recovery must not advertise a missing /mcp auth command"
|
||||
);
|
||||
}
|
||||
assert!(error_text_looks_auth_required(
|
||||
"MCP server rejected the request with 401 Unauthorized"
|
||||
));
|
||||
assert!(!error_text_looks_auth_required("connection refused"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -216,15 +216,16 @@ pub(super) async fn read_body_capped(
|
||||
Ok(String::from_utf8_lossy(&buf).into_owned())
|
||||
}
|
||||
|
||||
/// TUI recovery for a rejected OAuth session. The implemented command is
|
||||
/// `/mcp login <name>` (`codewhale mcp login`); `/mcp auth` is not a command.
|
||||
/// TUI recovery for a rejected OAuth session. Settings recovery and the
|
||||
/// Streamable HTTP path share the oauth helpers so `/mcp login <name>` stays
|
||||
/// the only advertised command; `/mcp auth` is not a command.
|
||||
fn oauth_refresh_failed_hint() -> &'static str {
|
||||
"Re-authorize this server (/mcp login <name>) or configure a fresh bearer token."
|
||||
super::oauth::tui_reauth_refresh_failed_hint()
|
||||
}
|
||||
|
||||
fn unauthorized_session_hint(oauth_configured: bool) -> &'static str {
|
||||
if oauth_configured {
|
||||
"Re-authorize this server (/mcp login <name>) to continue."
|
||||
super::oauth::tui_reauth_hint()
|
||||
} else {
|
||||
"Check the configured bearer token (or its environment variable)."
|
||||
}
|
||||
|
||||
@@ -5716,3 +5716,59 @@ fn removed_runtime_server_config_can_be_retried_with_same_name() {
|
||||
pool.add_runtime_server_config("retryable".to_string(), config)
|
||||
.expect("rollback must release the deterministic runtime name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_recovery_kind_names_real_login_and_reload_commands() {
|
||||
assert_eq!(
|
||||
mcp_recovery_kind(false, true, false, None, false),
|
||||
McpRecoveryKind::Enable
|
||||
);
|
||||
assert_eq!(
|
||||
mcp_recovery_kind(true, false, false, None, false),
|
||||
McpRecoveryKind::Connect
|
||||
);
|
||||
assert_eq!(
|
||||
mcp_recovery_kind(true, true, false, Some("connection refused"), false),
|
||||
McpRecoveryKind::Diagnose
|
||||
);
|
||||
assert_eq!(
|
||||
mcp_recovery_kind(true, true, false, Some("connection refused"), true),
|
||||
McpRecoveryKind::Diagnose
|
||||
);
|
||||
assert_eq!(
|
||||
mcp_recovery_kind(true, true, false, Some("401 Unauthorized"), true),
|
||||
McpRecoveryKind::Reauth
|
||||
);
|
||||
assert_eq!(
|
||||
mcp_recovery_kind(true, true, false, None, true),
|
||||
McpRecoveryKind::Reauth
|
||||
);
|
||||
assert_eq!(
|
||||
mcp_recovery_kind(true, true, false, None, false),
|
||||
McpRecoveryKind::Reconnect
|
||||
);
|
||||
assert_eq!(
|
||||
mcp_recovery_kind(true, true, true, None, false),
|
||||
McpRecoveryKind::Diagnose
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
McpRecoveryKind::Reauth.slash_command("github"),
|
||||
"/mcp login github"
|
||||
);
|
||||
assert_eq!(
|
||||
McpRecoveryKind::Connect.slash_command("github"),
|
||||
"/mcp reload"
|
||||
);
|
||||
assert_eq!(
|
||||
McpRecoveryKind::Diagnose.slash_command("github"),
|
||||
"/mcp validate"
|
||||
);
|
||||
assert!(
|
||||
!McpRecoveryKind::Reauth
|
||||
.slash_command("github")
|
||||
.contains("/mcp auth")
|
||||
);
|
||||
assert!(mcp_name_is_command_safe("github"));
|
||||
assert!(!mcp_name_is_command_safe("github mcp"));
|
||||
}
|
||||
@@ -34,10 +34,7 @@ pub(super) fn format_mcp_manager(snapshot: &McpManagerSnapshot, locale: Locale)
|
||||
}
|
||||
|
||||
lines.push(String::new());
|
||||
lines.push(
|
||||
"Actions: /mcp init, /mcp add stdio <name> <command> [args...], /mcp add http <name> <url>, /mcp enable <name>, /mcp disable <name>, /mcp remove <name>, /mcp validate, /mcp reload."
|
||||
.to_string(),
|
||||
);
|
||||
lines.push("Next: Connect /mcp reload · Diagnose /mcp validate".to_string());
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
@@ -65,6 +62,32 @@ fn push_server(lines: &mut Vec<String>, server: &McpServerSnapshot, locale: Loca
|
||||
if let Some(error) = server.error.as_ref() {
|
||||
lines.push(format!(" error: {error}"));
|
||||
}
|
||||
let recovery = crate::mcp::mcp_recovery_kind(
|
||||
server.enabled,
|
||||
true,
|
||||
server.connected,
|
||||
server.error.as_deref(),
|
||||
false,
|
||||
);
|
||||
let command = if crate::mcp::mcp_name_is_command_safe(&server.name)
|
||||
|| matches!(
|
||||
recovery,
|
||||
crate::mcp::McpRecoveryKind::Connect
|
||||
| crate::mcp::McpRecoveryKind::Reconnect
|
||||
| crate::mcp::McpRecoveryKind::Diagnose
|
||||
) {
|
||||
recovery.slash_command(&server.name)
|
||||
} else {
|
||||
"/mcp validate".to_string()
|
||||
};
|
||||
let verb = match recovery {
|
||||
crate::mcp::McpRecoveryKind::Enable => "Enable",
|
||||
crate::mcp::McpRecoveryKind::Connect => "Connect",
|
||||
crate::mcp::McpRecoveryKind::Reconnect => "Reconnect",
|
||||
crate::mcp::McpRecoveryKind::Reauth => "Re-auth",
|
||||
crate::mcp::McpRecoveryKind::Diagnose => "Diagnose",
|
||||
};
|
||||
lines.push(format!(" next: {verb} {command}"));
|
||||
lines.push(format!(
|
||||
" discovered: {} tools, {} resources, {} prompts",
|
||||
server.tools.len(),
|
||||
@@ -205,6 +228,37 @@ mod tests {
|
||||
assert!(text.contains("boom"));
|
||||
assert!(text.contains("Advertised capabilities: tools"));
|
||||
assert!(text.contains("not observed because the server is not connected"));
|
||||
assert!(text.contains("next: Diagnose /mcp validate"));
|
||||
assert!(text.contains("Next: Connect /mcp reload · Diagnose /mcp validate"));
|
||||
assert!(!text.contains("/mcp auth"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manager_text_names_login_for_stale_oauth() {
|
||||
let snapshot = McpManagerSnapshot {
|
||||
config_path: PathBuf::from("/tmp/mcp.json"),
|
||||
config_exists: true,
|
||||
reload_required: false,
|
||||
servers: vec![McpServerSnapshot {
|
||||
name: "github".to_string(),
|
||||
enabled: true,
|
||||
required: false,
|
||||
transport: "http".to_string(),
|
||||
command_or_url: "https://api.githubcopilot.com/mcp/".to_string(),
|
||||
connect_timeout: 10,
|
||||
execute_timeout: 60,
|
||||
read_timeout: 120,
|
||||
connected: false,
|
||||
error: Some("401 Unauthorized".to_string()),
|
||||
capability_metadata: McpServerCapabilityMetadata::NotObserved,
|
||||
tools: Vec::new(),
|
||||
resources: Vec::new(),
|
||||
prompts: Vec::new(),
|
||||
}],
|
||||
};
|
||||
let text = format_mcp_manager(&snapshot, Locale::En);
|
||||
assert!(text.contains("next: Re-auth /mcp login github"), "{text}");
|
||||
assert!(!text.contains("/mcp auth"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -639,7 +639,15 @@ fn plugins_model(app: &App, locale: Locale) -> ExtensionsTabModel {
|
||||
crate::plugins::types::PluginScope::Workspace => 2,
|
||||
};
|
||||
let diagnostic_count = plugin.diagnostics.len();
|
||||
let action = if plugin.active() {
|
||||
let has_error_diagnostics = plugin.diagnostics.iter().any(|diagnostic| {
|
||||
diagnostic.level == crate::plugins::types::PluginDiagnosticLevel::Error
|
||||
});
|
||||
let action = if has_error_diagnostics {
|
||||
ExtensionAction::Command {
|
||||
label: tr(locale, MessageId::ExtensionsActionDiagnose).into_owned(),
|
||||
command: format!("/plugin validate {}", plugin.name()),
|
||||
}
|
||||
} else if plugin.active() {
|
||||
ExtensionAction::Command {
|
||||
label: tr(locale, MessageId::LaunchHintOpen).into_owned(),
|
||||
command: format!("/plugin show {}", plugin.name()),
|
||||
@@ -698,7 +706,7 @@ fn plugins_model(app: &App, locale: Locale) -> ExtensionsTabModel {
|
||||
tr(locale, MessageId::ExtensionsGroupWorkspace).into_owned(),
|
||||
),
|
||||
];
|
||||
let groups = labels
|
||||
let mut groups = labels
|
||||
.into_iter()
|
||||
.zip(by_scope)
|
||||
.filter(|(_, items)| !items.is_empty())
|
||||
@@ -707,7 +715,40 @@ fn plugins_model(app: &App, locale: Locale) -> ExtensionsTabModel {
|
||||
label,
|
||||
items,
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
let problems = app
|
||||
.plugin_registry
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, diagnostic)| ExtensionItem {
|
||||
id: format!("plugin-diagnostic-{index}"),
|
||||
label: diagnostic.code.to_string(),
|
||||
description: diagnostic.message.clone(),
|
||||
state: if diagnostic.level == crate::plugins::types::PluginDiagnosticLevel::Error {
|
||||
tr(locale, MessageId::ExtensionsStateInvalid)
|
||||
} else {
|
||||
tr(locale, MessageId::ExtensionsStateWarning)
|
||||
}
|
||||
.into_owned(),
|
||||
detail: diagnostic
|
||||
.path
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| diagnostic.message.clone()),
|
||||
action: Some(ExtensionAction::Command {
|
||||
label: tr(locale, MessageId::ExtensionsActionDiagnose).into_owned(),
|
||||
command: "/plugin validate".into(),
|
||||
}),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !problems.is_empty() {
|
||||
groups.push(ExtensionGroup {
|
||||
id: "problems".into(),
|
||||
label: tr(locale, MessageId::ExtensionsGroupProblems).into_owned(),
|
||||
items: problems,
|
||||
});
|
||||
}
|
||||
ExtensionsTabModel {
|
||||
groups,
|
||||
problem: app.plugin_registry.state_error().map(ToString::to_string),
|
||||
@@ -906,23 +947,29 @@ fn mcp_model(app: &App, locale: Locale) -> ExtensionsTabModel {
|
||||
tr(locale, MessageId::PickerActionConfigured)
|
||||
}
|
||||
.into_owned();
|
||||
let action = if !enabled
|
||||
&& name
|
||||
.chars()
|
||||
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
|
||||
{
|
||||
let oauth_capable = config.is_some_and(crate::mcp::mcp_server_oauth_capable);
|
||||
let recovery = crate::mcp::mcp_recovery_kind(
|
||||
enabled,
|
||||
observed.is_some(),
|
||||
observed.is_some_and(|server| server.connected),
|
||||
observed.and_then(|server| server.error.as_deref()),
|
||||
oauth_capable,
|
||||
);
|
||||
let action = if crate::mcp::mcp_name_is_command_safe(&name)
|
||||
|| matches!(
|
||||
recovery,
|
||||
crate::mcp::McpRecoveryKind::Connect
|
||||
| crate::mcp::McpRecoveryKind::Reconnect
|
||||
| crate::mcp::McpRecoveryKind::Diagnose
|
||||
) {
|
||||
ExtensionAction::Command {
|
||||
label: tr(locale, MessageId::ExtensionsActionEnable).into_owned(),
|
||||
command: format!("/mcp enable {name}"),
|
||||
}
|
||||
} else if enabled && observed.is_none() {
|
||||
ExtensionAction::Command {
|
||||
label: tr(locale, MessageId::ExtensionsActionReload).into_owned(),
|
||||
command: "/mcp reload".into(),
|
||||
label: tr(locale, recovery.label_key()).into_owned(),
|
||||
command: recovery.slash_command(&name),
|
||||
}
|
||||
} else {
|
||||
ExtensionAction::Status {
|
||||
label: state.clone(),
|
||||
ExtensionAction::Command {
|
||||
label: tr(locale, MessageId::ExtensionsActionDiagnose).into_owned(),
|
||||
command: "/mcp validate".into(),
|
||||
}
|
||||
};
|
||||
ExtensionItem {
|
||||
@@ -1506,3 +1553,26 @@ impl ModalView for ExtensionsView {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mcp::McpRecoveryKind;
|
||||
|
||||
#[test]
|
||||
fn mcp_item_action_for_stale_oauth_is_login() {
|
||||
let recovery =
|
||||
crate::mcp::mcp_recovery_kind(true, true, false, Some("401 Unauthorized"), true);
|
||||
assert_eq!(recovery, McpRecoveryKind::Reauth);
|
||||
assert_eq!(recovery.slash_command("github"), "/mcp login github");
|
||||
assert_eq!(tr(Locale::En, recovery.label_key()).as_ref(), "re-auth");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_item_action_for_disconnected_server_is_reconnect() {
|
||||
let recovery = crate::mcp::mcp_recovery_kind(true, true, false, None, false);
|
||||
assert_eq!(recovery, McpRecoveryKind::Reconnect);
|
||||
assert_eq!(recovery.slash_command("playwright"), "/mcp reload");
|
||||
assert_eq!(tr(Locale::En, recovery.label_key()).as_ref(), "reconnect");
|
||||
}
|
||||
}
|
||||
@@ -1386,7 +1386,13 @@ impl SettingsRegistry {
|
||||
SettingKind::ReadOnly
|
||||
} else if matches!(
|
||||
row.key.as_str(),
|
||||
"provider" | "model" | "provider_templates"
|
||||
"provider"
|
||||
| "model"
|
||||
| "provider_templates"
|
||||
| "mcp_open"
|
||||
| "mcp_reconnect"
|
||||
| "mcp_diagnose"
|
||||
| "plugins_open"
|
||||
) {
|
||||
SettingKind::Action
|
||||
} else if config_boolean_key(&row.key) {
|
||||
@@ -1570,6 +1576,7 @@ pub struct ConfigView {
|
||||
last_render_scroll: Cell<usize>,
|
||||
last_row_hitboxes: RefCell<Vec<(u16, usize)>>,
|
||||
last_choice_hitboxes: RefCell<Vec<(u16, usize)>>,
|
||||
last_tab_hitboxes: RefCell<Vec<(u16, u16, u16, ConfigTab)>>,
|
||||
last_mouse_selected: Option<usize>,
|
||||
api_provider: ApiProvider,
|
||||
route_base_url: String,
|
||||
@@ -2097,6 +2104,34 @@ impl ConfigView {
|
||||
editable: true,
|
||||
scope: ConfigScope::Saved,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Mcp,
|
||||
key: "mcp_open".to_string(),
|
||||
value: "/mcp".to_string(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Session,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Mcp,
|
||||
key: "mcp_reconnect".to_string(),
|
||||
value: "/mcp reload".to_string(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Session,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Mcp,
|
||||
key: "mcp_diagnose".to_string(),
|
||||
value: "/mcp validate".to_string(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Session,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Mcp,
|
||||
key: "plugins_open".to_string(),
|
||||
value: "/plugin".to_string(),
|
||||
editable: true,
|
||||
scope: ConfigScope::Session,
|
||||
},
|
||||
ConfigRow {
|
||||
section: ConfigSection::Mcp,
|
||||
key: "mcp_config_path".to_string(),
|
||||
@@ -2215,6 +2250,7 @@ impl ConfigView {
|
||||
last_render_scroll: Cell::new(0),
|
||||
last_row_hitboxes: RefCell::new(Vec::new()),
|
||||
last_choice_hitboxes: RefCell::new(Vec::new()),
|
||||
last_tab_hitboxes: RefCell::new(Vec::new()),
|
||||
last_mouse_selected: None,
|
||||
api_provider: app.api_provider,
|
||||
route_base_url: app.active_route_base_url.clone(),
|
||||
@@ -2356,6 +2392,12 @@ impl ConfigView {
|
||||
"locale" => "UI language. Scope: saved settings.",
|
||||
"reasoning_effort" => "Default reasoning effort for capable models.",
|
||||
"default_mode" => "Startup mode (agent / plan / operate).",
|
||||
"mcp_open" => {
|
||||
"Open the MCP manager. Connect, reconnect, re-auth, or diagnose from there."
|
||||
}
|
||||
"mcp_reconnect" => "Reconnect configured MCP servers (/mcp reload).",
|
||||
"mcp_diagnose" => "Diagnose MCP servers (/mcp validate).",
|
||||
"plugins_open" => "Open plugins. Trust, enable, or diagnose invalid manifests.",
|
||||
_ => "Enter change · R reset · Esc close. Scope shown on the row badge.",
|
||||
}
|
||||
}
|
||||
@@ -2494,6 +2536,10 @@ impl ConfigView {
|
||||
"provider" if row.editable => "/provider",
|
||||
"provider_templates" if row.editable => "/provider templates",
|
||||
"model" if row.editable => "/model",
|
||||
"mcp_open" if row.editable => "/mcp",
|
||||
"mcp_reconnect" if row.editable => "/mcp reload",
|
||||
"mcp_diagnose" if row.editable => "/mcp validate",
|
||||
"plugins_open" if row.editable => "/plugin",
|
||||
_ => return None,
|
||||
};
|
||||
Some(ViewAction::Emit(ViewEvent::CommandPaletteSelected {
|
||||
@@ -2832,6 +2878,14 @@ impl ConfigView {
|
||||
MessageId::ConfigActionOpenProviderTemplates
|
||||
} else if row.key == "model" {
|
||||
MessageId::ConfigActionOpenModel
|
||||
} else if row.key == "mcp_open" {
|
||||
MessageId::ConfigActionOpenMcp
|
||||
} else if row.key == "mcp_reconnect" {
|
||||
MessageId::ConfigActionMcpReconnect
|
||||
} else if row.key == "mcp_diagnose" {
|
||||
MessageId::ConfigActionMcpDiagnose
|
||||
} else if row.key == "plugins_open" {
|
||||
MessageId::ConfigActionOpenPlugins
|
||||
} else if meta.kind == SettingKind::Boolean {
|
||||
MessageId::ConfigActionToggle
|
||||
} else if meta.kind == SettingKind::Choice {
|
||||
@@ -3013,6 +3067,10 @@ fn config_label_message(key: &str) -> Option<MessageId> {
|
||||
"auto_compact" => MessageId::ConfigLabelAutoCompact,
|
||||
"auto_compact_threshold_percent" => MessageId::ConfigLabelAutoCompactThreshold,
|
||||
"max_history" => MessageId::ConfigLabelMaxHistory,
|
||||
"mcp_open" => MessageId::ConfigLabelMcpOpen,
|
||||
"mcp_reconnect" => MessageId::ConfigLabelMcpReconnect,
|
||||
"mcp_diagnose" => MessageId::ConfigLabelMcpDiagnose,
|
||||
"plugins_open" => MessageId::ConfigLabelPluginsOpen,
|
||||
"mcp_config_path" => MessageId::ConfigLabelMcpConfigPath,
|
||||
"fleet.exec.max_spawn_depth" => MessageId::ConfigLabelFleetSpawnDepth,
|
||||
"goal_command" => MessageId::ConfigLabelGoalCommand,
|
||||
@@ -3165,6 +3223,10 @@ fn config_literal_hint_for_key(key: &str) -> &'static str {
|
||||
"reasoning_effort" => {
|
||||
"Per-model thinking ladder from the active route. default clears the saved value and uses that model's official default. Always-thinking models omit off."
|
||||
}
|
||||
"mcp_open" => "open the MCP manager · Connect / Reconnect / Re-auth / Diagnose",
|
||||
"mcp_reconnect" => "reconnect MCP · /mcp reload",
|
||||
"mcp_diagnose" => "diagnose MCP · /mcp validate",
|
||||
"plugins_open" => "open plugins · trust, enable, or diagnose",
|
||||
"mcp_config_path" => "path to mcp.json",
|
||||
"fleet.exec.max_spawn_depth" => {
|
||||
"0 blocks child agents; 3 default (same axis as sub-agents); capped at 8"
|
||||
@@ -3651,6 +3713,22 @@ impl ModalView for ConfigView {
|
||||
return ViewAction::None;
|
||||
}
|
||||
|
||||
let clicked_tab = self
|
||||
.last_tab_hitboxes
|
||||
.borrow()
|
||||
.iter()
|
||||
.find_map(|(x0, x1, y, tab)| {
|
||||
(*y == mouse.row && mouse.column >= *x0 && mouse.column < *x1).then_some(*tab)
|
||||
});
|
||||
if let Some(tab) = clicked_tab {
|
||||
if self.active_tab != tab {
|
||||
self.active_tab = tab;
|
||||
self.select_first_visible_row();
|
||||
}
|
||||
self.last_mouse_selected = None;
|
||||
return ViewAction::None;
|
||||
}
|
||||
|
||||
let selected = self
|
||||
.last_row_hitboxes
|
||||
.borrow()
|
||||
@@ -3686,6 +3764,7 @@ impl ModalView for ConfigView {
|
||||
render_underwater_surface(area, buf, self.tr(MessageId::ConfigModalTitle).to_string());
|
||||
let (lines, footer) = if let Some(edit) = self.editing.as_ref() {
|
||||
*self.last_choice_hitboxes.borrow_mut() = Vec::new();
|
||||
*self.last_tab_hitboxes.borrow_mut() = Vec::new();
|
||||
let footer_text = if edit.choices.is_some() {
|
||||
if inner.width < 56 || inner.height <= 8 {
|
||||
" ↑/↓ choose · Enter apply · Esc ".to_string()
|
||||
@@ -3907,13 +3986,20 @@ impl ModalView for ConfigView {
|
||||
]);
|
||||
// Category tabs — app-style shell, not ASCII table headers.
|
||||
let mut tab_spans = Vec::new();
|
||||
let tab_y = inner.y.saturating_add(u16::from(!compact));
|
||||
let mut tab_x = inner.x;
|
||||
let mut tab_hitboxes = Vec::new();
|
||||
for (i, tab) in ConfigTab::ALL.iter().enumerate() {
|
||||
if i > 0 {
|
||||
tab_spans.push(Span::styled(" ", Style::default()));
|
||||
tab_x = tab_x.saturating_add(2);
|
||||
}
|
||||
let label = format!(" {} ", tab.label());
|
||||
let width = UnicodeWidthStr::width(label.as_str()) as u16;
|
||||
tab_hitboxes.push((tab_x, tab_x.saturating_add(width), tab_y, *tab));
|
||||
let active = *tab == self.active_tab;
|
||||
tab_spans.push(Span::styled(
|
||||
format!(" {} ", tab.label()),
|
||||
label,
|
||||
if active {
|
||||
Style::default()
|
||||
.fg(palette::SELECTION_TEXT)
|
||||
@@ -3923,7 +4009,9 @@ impl ModalView for ConfigView {
|
||||
Style::default().fg(palette::TEXT_MUTED)
|
||||
},
|
||||
));
|
||||
tab_x = tab_x.saturating_add(width);
|
||||
}
|
||||
*self.last_tab_hitboxes.borrow_mut() = tab_hitboxes;
|
||||
let tab_line = Line::from(tab_spans);
|
||||
let mut lines: Vec<Line> = if compact {
|
||||
vec![tab_line, search_line]
|
||||
@@ -4879,7 +4967,7 @@ fn fit_config_column(text: &str, width: usize) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
ActionHint, ConfigListItem, ConfigScope, ConfigTab, ConfigView, EmptyState,
|
||||
ActionHint, ConfigListItem, ConfigScope, ConfigSection, ConfigTab, ConfigView, EmptyState,
|
||||
FocusTextureMode, HelpView, ListDetailLayout, ModalKind, ModalView, SettingKind,
|
||||
SettingsRegistry, ViewAction, ViewEvent, ViewStack, action_footer_lines,
|
||||
canonical_config_choice, centered_modal_area, config_choice_detail, config_choice_label,
|
||||
@@ -5682,6 +5770,10 @@ mod tests {
|
||||
assert!(keys.contains(&"bracketed_paste"));
|
||||
assert!(keys.contains(&"context_panel"));
|
||||
assert!(keys.contains(&"cost_currency"));
|
||||
assert!(keys.contains(&"mcp_open"));
|
||||
assert!(keys.contains(&"mcp_reconnect"));
|
||||
assert!(keys.contains(&"mcp_diagnose"));
|
||||
assert!(keys.contains(&"plugins_open"));
|
||||
assert!(keys.contains(&"mcp_config_path"));
|
||||
assert!(keys.contains(&"fleet.exec.max_spawn_depth"));
|
||||
assert!(keys.contains(&"features.vision_model"));
|
||||
@@ -7002,6 +7094,10 @@ context_window = 262144
|
||||
assert_eq!(kind_for("low_motion"), SettingKind::Boolean);
|
||||
assert_eq!(kind_for("default_mode"), SettingKind::Choice);
|
||||
assert_eq!(kind_for("mention_menu_limit"), SettingKind::Integer);
|
||||
assert_eq!(kind_for("mcp_open"), SettingKind::Action);
|
||||
assert_eq!(kind_for("mcp_reconnect"), SettingKind::Action);
|
||||
assert_eq!(kind_for("mcp_diagnose"), SettingKind::Action);
|
||||
assert_eq!(kind_for("plugins_open"), SettingKind::Action);
|
||||
assert_eq!(kind_for("mcp_config_path"), SettingKind::Text);
|
||||
assert_eq!(kind_for("fast_model"), SettingKind::ReadOnly);
|
||||
|
||||
@@ -7130,6 +7226,67 @@ context_window = 262144
|
||||
assert!(view.editing.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_view_tabs_are_clickable() {
|
||||
let app = create_test_app();
|
||||
let mut view = ConfigView::new_for_app(&app);
|
||||
assert_eq!(view.active_tab, ConfigTab::General);
|
||||
let area = Rect::new(0, 0, 100, 30);
|
||||
let mut buf = Buffer::empty(area);
|
||||
view.render(area, &mut buf);
|
||||
|
||||
let hitboxes = view.last_tab_hitboxes.borrow().clone();
|
||||
let (x0, _x1, y, tab) = hitboxes
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|(_, _, _, tab)| *tab == ConfigTab::Advanced)
|
||||
.expect("Advanced tab should have a hitbox");
|
||||
|
||||
let action = view.handle_mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: x0.saturating_add(1),
|
||||
row: y,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
});
|
||||
assert!(matches!(action, ViewAction::None));
|
||||
assert_eq!(tab, ConfigTab::Advanced);
|
||||
assert_eq!(view.active_tab, ConfigTab::Advanced);
|
||||
assert!(
|
||||
view.rows
|
||||
.get(view.selected)
|
||||
.is_some_and(|row| row.section == ConfigSection::Mcp
|
||||
|| row.section == ConfigSection::Fleet
|
||||
|| row.section == ConfigSection::Workflow
|
||||
|| row.section == ConfigSection::Session
|
||||
|| row.section == ConfigSection::Legacy
|
||||
|| row.section == ConfigSection::Experimental)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_view_mcp_action_rows_run_existing_commands() {
|
||||
let app = create_test_app();
|
||||
let mut view = ConfigView::new_for_app(&app);
|
||||
for (key, command) in [
|
||||
("mcp_open", "/mcp"),
|
||||
("mcp_reconnect", "/mcp reload"),
|
||||
("mcp_diagnose", "/mcp validate"),
|
||||
("plugins_open", "/plugin"),
|
||||
] {
|
||||
view.focus_key(key);
|
||||
let action = view.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
match action {
|
||||
ViewAction::Emit(ViewEvent::CommandPaletteSelected {
|
||||
action: CommandPaletteAction::ExecuteCommand { command: emitted },
|
||||
}) => {
|
||||
assert_eq!(emitted, command, "{key}");
|
||||
assert!(!emitted.contains("/mcp auth"), "{key}");
|
||||
}
|
||||
other => panic!("{key} should run {command}, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_view_bottom_hint_semantically_truncates_at_narrow_width() {
|
||||
// The dense bottom status line must truncate on a word boundary with an
|
||||
|
||||
Reference in new issue
Block a user