diff --git a/.gitignore b/.gitignore index c5942d9..9079b96 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ out/ .DS_Store scripts/validate-models.mjs scripts/validate-models.mjs.map +tmp/ diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..eddce7a --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,12 @@ +{ + "config": { + "MD013": false, + "MD024": { + "siblings_only": true, + }, + "MD033": false, + "MD041": false, + "MD060": false, + }, + "gitignore": true, +} diff --git a/.markdownlint.json b/.markdownlint.json deleted file mode 100644 index 99971f5..0000000 --- a/.markdownlint.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "MD013": false, - "MD024": { - "siblings_only": true - }, - "MD033": false, - "MD041": false, - "MD060": false -} diff --git a/.vscodeignore b/.vscodeignore index 0a4ae1d..2372dff 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,7 +1,7 @@ .gitignore .github/** .husky/** -.markdownlint.json +.markdownlint-cli2.jsonc .prettierrc.json .vscode/** .vscode-test/** @@ -23,3 +23,4 @@ src/** tsconfig.json package-lock.json media/opencodego.svg +tmp/** diff --git a/CHANGELOG.md b/CHANGELOG.md index fec7ca3..824e6b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to the **OpenCode Go BYOK Provider** extension are documente ## [Unreleased] +### Added + +- **`[VS Code]` Remove OpenCode Go / OpenCode Zen from Language Models.** Providers can now be removed from the Language Models list and every model picker like in GitHub Copilot's Manage Language Models: new `opencodego.enabled` / `opencodezen.enabled` settings (with matching `when` clauses on the vendor contributions) skip provider registration, plus `OpenCode Go/Zen: Remove/Re-add Provider in Language Models` commands and a **Remove from Language Models** action in the Manage Provider QuickPick. API keys and BYOK group settings are kept so re-enabling restores the provider unchanged. A window reload is required after toggling. +- **`[Agents]` OpenCode Go/Zen in the VS Code Agents window (#122).** VS Code ≥1.129 runs the Agents window in a separate agent host process and keeps the two mechanisms this feature depends on off by default: the experimental BYOK model bridge (`chat.agentHost.byokModels.enabled`) that mirrors extension BYOK models into agent-host sessions, and `extensions.supportAgentsWindow`, without which code extensions are disabled in the Agents window process entirely — so OpenCode Go/Zen were missing from both the Agents window model picker and its "+ Add Models" vendor list. The extension now auto-enables both settings on activation (gated by the new `opencodego.autoEnableAgentsWindow` setting, default `true`, merging with any existing user values), records what it flipped, and offers a reload button the first time. When the user disables `opencodego.agentsWindow` afterwards, only the settings the extension itself enabled are reverted. The legacy agent-host providers (`targetChatSessionType: "copilotcli"`) remain registered for VS Code 1.125–1.128, where the bridge does not exist. + ### Fixed - **`[VS Code]` Provider context menu unresponsive; "+ Add Models" dead; leftover groups undeletable (#121).** The `languageModelChatProviders` contributions declared both `managementCommand` and a `configuration` schema. VS Code's native BYOK flow (`configureLanguageModelsProviderGroup`) short-circuits on `managementCommand` — it re-resolves models and returns without prompting for a group name or API key — so a BYOK group could never be created through "+ Add Models", and every built-in context-menu action (Rename Group, Update API Key, Delete, Open in Language Models (JSON)) throws "group not found", failing silently. Dropped `managementCommand` from the `opencodego`, `opencodezen`, and agent-variant contributions; "+ Add Models" now runs the native prompt flow, the context-menu actions work against the created group, and leftover groups (e.g. created by per-model configuration) can finally be deleted. The extension's own commands (`OpenCode Go: Manage Provider`, `Set API Key`, etc.) remain available in the Command Palette and keep working as a legacy fallback. diff --git a/README.md b/README.md index c24d5e3..5229fe7 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ | 📊 **Live usage tracking** | Status bar shows Go subscription burn-rate across 5h / weekly / monthly tiers | | 🔌 **Dual providers** | OpenCode **Go** ($10/mo subscription) + OpenCode **Zen** (free + paid models) — run both at once, switch instantly | | 🎯 **Smart routing** | Each model family auto-routes to its native transport (`/responses`, `/messages`, `streamGenerateContent`, `/chat/completions`) | -| 🖼️ **Vision + PDF + Audio** | Multimodal models pass through image, PDF, audio, and video inputs. Oversized images auto-resize to 2000×2000 / 5MB to match the gateway contract. | +| 🖼️ **Vision + PDF + Audio** | Multimodal models pass through image, PDF, audio, and video inputs. Oversized images auto-resize to 2000×2000 / 5MB to match the gateway contract. | | 📐 **Context-size picker** | Kimi K3 and other tiered-context models expose `256K` vs full-window selection in the per-model configuration, with the cheaper tier selected by default. | | 🔒 **Your key, your control** | API key stored in VS Code SecretStorage — never leaves your machine | @@ -270,36 +270,34 @@ Delete Active Profile` help you manage your profiles. The label you give ### 🪟 Agents Window (Copilot CLI) Support -OpenCode models appear in the VS Code **Agents window** model picker when starting a Copilot CLI / Background agent session — not just the regular Chat view. Two sets of models are available: +OpenCode models appear in the VS Code **Agents window** model picker when starting a Copilot CLI / Background agent session — not just the regular Chat view: -| Provider | Appears under | Notes | -| ---------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `opencodego` / `opencodezen` | **Local** | Normal models, no `targetChatSessionType`. From VS Code ≥1.126 they appear naturally in the Local section (once the extension loads in the sessions window). | -| `opencodego-agent` / `opencodezen-agent` | **Copilot** | Agent variants with `targetChatSessionType: "copilotcli"`. Picked up by `CopilotChatSessionsProvider` for agent sessions. | +| Provider | Appears under | Notes | +| ---------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `opencodego` / `opencodezen` | **Local** | Normal models, no `targetChatSessionType`. On VS Code 1.129+ they reach agent-host sessions through VS Code's BYOK model bridge. | +| `opencodego-agent` / `opencodezen-agent` | **Copilot** | Agent variants with `targetChatSessionType: "copilotcli"`. Picked up by `CopilotChatSessionsProvider` for legacy agent sessions (VS Code ≤1.128). | **How it works:** -| Setting | Default | What it controls | -| ----------------------------------------- | ------- | ------------------------------------------------------- | -| `opencodego.agentsWindow` | `true` | Registers agent-host providers at runtime | -| `opencodego.showAgentModelsInManagePanel` | `false` | Shows agent vendors in the Manage Language Models panel | +VS Code ≥1.129 runs the Agents window in a separate agent host process and keeps two knobs **off by default** that this extension depends on: -**Setup:** +1. `chat.agentHost.byokModels.enabled` (experimental) — the BYOK language-model bridge that mirrors extension-provided BYOK models into agent-host sessions. +2. `extensions.supportAgentsWindow` — without it, code extensions are **disabled in the Agents window process**, so OpenCode Go/Zen don't appear in the Agents window picker nor in its **+ Add Models** list. -1. Agent models are enabled by default (`agentsWindow: true`). No changes needed for basic usage. -2. Add this to your VS Code `settings.json` to enable the extension in the Agents window process: +The extension auto-enables both when `agentsWindow` is on: - ```json - "extensions.supportAgentsWindow": { - "ltmoerdani.opencode-copilot-chat": true - } - ``` +| Setting | Default | What it controls | +| ----------------------------------------- | ------- | ------------------------------------------------------------------------------------------------- | +| `opencodego.agentsWindow` | `true` | Master switch for Agents window support | +| `opencodego.autoEnableAgentsWindow` | `true` | Auto-manage the two VS Code core settings above (and revert them when `agentsWindow` is disabled) | +| `opencodego.showAgentModelsInManagePanel` | `false` | Shows agent vendors in the Manage Language Models panel | -3. Reload the window (`Developer: Reload Window`). -4. Open the **Agents window** → start a new session → select **Copilot CLI** as the agent type. -5. Open the model picker — OpenCode models appear under **Local** (normal models) and **Copilot** (agent-host variants). +**Setup:** -Normal OpenCode models (`opencodego`, `opencodezen`) appear in the **Local** section of the Agents window picker from VS Code ≥1.126 onwards. On ≤1.125 they may require the `supportAgentsWindow` setting. Agent-host variants (`opencodego-agent`, `opencodezen-agent`) appear under **Copilot** because they carry `targetChatSessionType: "copilotcli"` and are matched by `CopilotChatSessionsProvider`. +1. Agent models are enabled by default (`agentsWindow: true`). The extension auto-enables the required VS Code settings on first activation and offers a **Reload Now** button — a window reload is required for them to take effect. +2. Reload the window (`Developer: Reload Window`) if you haven't been prompted. +3. Open the **Agents window** → start a new session → select **Copilot CLI** as the agent type. +4. Open the model picker — OpenCode models appear under their provider; the **+ Add Models** list in the Agents window's Language Models view now includes **OpenCode Go** and **OpenCode Zen**. To manage agent API keys separately or see agent vendors in the Manage panel, enable: @@ -307,6 +305,23 @@ To manage agent API keys separately or see agent vendors in the Manage panel, en "opencodego.showAgentModelsInManagePanel": true ``` +#### ❌ Removing a provider from Language Models + +Like deleting a provider in GitHub Copilot's Manage Language Models, you can +remove **OpenCode Go** or **OpenCode Zen** from the Language Models list and +every model picker: + +- **Command Palette** — `OpenCode Go: Remove/Re-add Provider in Language +Models` (same for Zen), or use **Manage Provider → Remove from Language + Models**. +- **Settings** — set `opencodego.enabled` / `opencodezen.enabled` to `false`. + +The provider's vendor row and models disappear from the Manage Language +Models view, the `+ Add Models` list, the Chat picker, and the Agents window. +Your API key and BYOK group settings are kept, so re-enabling (or the +`Re-add to Language Models` action) restores everything. A window reload is +required after toggling. + ### 🛠️ Smart Routing & Reliability - **Native endpoint routing** per family (see [Models](#-models) table) diff --git a/eslint.config.mjs b/eslint.config.mjs index 7601852..b1a56f7 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,14 +1,11 @@ -import { readFileSync } from "node:fs"; import tseslint from "typescript-eslint"; - -const gitignore = readFileSync(new URL(".gitignore", import.meta.url), "utf8") - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("#") && !line.startsWith("!")); +import { readGitignorePatterns } from "./scripts/gitignore-patterns.mjs"; export default tseslint.config( { - ignores: gitignore, + // Ignore everything `.gitignore` ignores (tmp/, out/, generated scripts, + // …) so linting stays in sync with the repo's ignore rules at runtime. + ignores: readGitignorePatterns(), }, ...tseslint.configs.recommended, { diff --git a/package.json b/package.json index 8a13bdf..37f72bb 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,10 @@ "command": "opencodego.setApiKey", "title": "OpenCode Go: Set API Key" }, + { + "command": "opencodego.toggleProvider", + "title": "OpenCode Go: Remove/Re-add Provider in Language Models" + }, { "command": "opencodego.refreshModels", "title": "OpenCode Go: Refresh Models" @@ -90,6 +94,10 @@ "command": "opencodezen.manage", "title": "OpenCode Zen: Manage Provider" }, + { + "command": "opencodezen.toggleProvider", + "title": "OpenCode Zen: Remove/Re-add Provider in Language Models" + }, { "command": "opencodezen.refreshModels", "title": "OpenCode Zen: Refresh Models" @@ -186,10 +194,25 @@ "default": true, "description": "When enabled, only free OpenCode Zen models are shown in the Copilot model selector. Disable to include paid Zen models as well." }, + "opencodego.enabled": { + "type": "boolean", + "default": true, + "markdownDescription": "Register the OpenCode Go provider. Set to `false` to remove OpenCode Go from the Language Models list and all model pickers (like deleting the provider in GitHub Copilot's Manage Language Models). Models you previously configured stay in your language-models config so they come back if you re-enable. Requires a window reload to take effect." + }, + "opencodezen.enabled": { + "type": "boolean", + "default": true, + "markdownDescription": "Register the OpenCode Zen provider. Set to `false` to remove OpenCode Zen from the Language Models list and all model pickers (like deleting the provider in GitHub Copilot's Manage Language Models). Models you previously configured stay in your language-models config so they come back if you re-enable. Requires a window reload to take effect." + }, "opencodego.agentsWindow": { "type": "boolean", "default": true, - "markdownDescription": "Register separate agent-host providers for the Copilot Agents window. Disable to hide the Agents section entirely. Requires a window reload to take effect." + "markdownDescription": "Enable OpenCode models in the VS Code Agents window. On VS Code 1.129+ this enables VS Code's BYOK model bridge (`chat.agentHost.byokModels.enabled`) so OpenCode Go/Zen models run in agent-host sessions; on all versions it also opts the extension in to run in the Agents window process (`extensions.supportAgentsWindow`) so OpenCode Go/Zen appear in the Agents window's '+ Add Models' list. Both core settings are auto-managed (see `#opencodego.autoEnableAgentsWindow#`). Disable to hide the Agents section entirely and revert the auto-managed settings. Requires a window reload to take effect." + }, + "opencodego.autoEnableAgentsWindow": { + "type": "boolean", + "default": true, + "markdownDescription": "When `#opencodego.agentsWindow#` is on, automatically enable the VS Code core settings the Agents window support depends on: `chat.agentHost.byokModels.enabled` (BYOK model bridge, experimental, off by default) and `extensions.supportAgentsWindow: { \"ltmoerdani.opencode-copilot-chat\": true }` (without this the extension is disabled in the Agents window process and its providers, including the '+ Add Models' vendors, are missing). The extension records the values it flips and reverts them when `agentsWindow` is disabled. A window reload is required after the first auto-enable. Disable to manage those settings manually." }, "opencodego.showAgentModelsInManagePanel": { "type": "boolean", @@ -285,6 +308,7 @@ { "vendor": "opencodego", "displayName": "OpenCode Go", + "when": "config.opencodego.enabled", "configuration": { "type": "object", "required": [ @@ -303,6 +327,7 @@ { "vendor": "opencodezen", "displayName": "OpenCode Zen", + "when": "config.opencodezen.enabled", "configuration": { "type": "object", "required": [ @@ -334,10 +359,10 @@ "lint": "npm run lint:js && npm run lint:md", "lint:js": "eslint .", "lint:fix": "eslint . --fix", - "lint:md": "markdownlint-cli2 --config .markdownlint.json \"**/*.md\" \"#node_modules\" \"#docs/**\"", - "lint:md:all": "markdownlint-cli2 --config .markdownlint.json \"**/*.md\" \"#node_modules\"", - "format": "npx prettier --write .", - "format:check": "npx prettier --check .", + "lint:md": "markdownlint-cli2 --config .markdownlint-cli2.jsonc \"**/*.md\" \"#docs/**\"", + "lint:md:all": "markdownlint-cli2 --config .markdownlint-cli2.jsonc \"**/*.md\"", + "format": "npx prettier --write . --ignore-path .gitignore", + "format:check": "npx prettier --check . --ignore-path .gitignore", "clean": "node -e \"require('node:fs').rmSync('out', { recursive: true, force: true })\"", "compile": "npm run clean && tsc -p ./", "test": "npm run compile && node scripts/run-unit-tests.mjs", diff --git a/scripts/gitignore-patterns.mjs b/scripts/gitignore-patterns.mjs new file mode 100644 index 0000000..fa76478 --- /dev/null +++ b/scripts/gitignore-patterns.mjs @@ -0,0 +1,31 @@ +import { readFileSync } from "node:fs"; + +/** + * Read `.gitignore` and return its patterns as an array suitable for tools + * that accept gitignore-style patterns (ESLint flat config `ignores`). + * + * This is the single source of truth for "which files are temporary / + * generated / excluded", so every linter and formatter that supports + * gitignore-style patterns should derive its ignores from here (or point + * straight at `.gitignore` itself) instead of maintaining a parallel, + * drifting ignore list: + * + * - ESLint: `ignores: readGitignorePatterns()` (this module). + * - Prettier: `--ignore-path .gitignore` (native gitignore parsing). + * - markdownlint-cli2: `gitignore: true` in `.markdownlint-cli2.jsonc` + * (native gitignore parsing, including nested `.gitignore` files). + * + * CONTRACT: + * - Blank lines and `#` comments are dropped. + * - `!` negations are dropped: ESLint's flat-config `ignores` handles them + * differently from git (relative vs. anchored semantics), and this + * repository's `.gitignore` does not use negations. + * - Lines are trimmed so accidental leading/trailing whitespace can't + * silently disable an ignore rule. + */ +export function readGitignorePatterns(file = ".gitignore") { + return readFileSync(new URL(`../${file}`, import.meta.url), "utf8") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#") && !line.startsWith("!")); +} diff --git a/src/extension.ts b/src/extension.ts index ef9d75e..ed87950 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -44,6 +44,7 @@ import { type AllProviderVendor, type ProviderVendor, } from "./providerTypes"; +import { providerEnabledSetting } from "./providerEnablement"; import { isInternalDataPart } from "./chatParts"; import { getImageDataUrlBase64Bytes, MAX_IMAGE_BASE64_BYTES, normalizeImageDataUrl } from "./imageNormalizer"; import { imageDescriptionKey, lookupImageDescriptions, storeImageDescriptions } from "./visionProxyCache"; @@ -84,6 +85,28 @@ const SECRET_KEY = "opencodego.apiKey"; const RECENT_TRANSPORT_SUMMARY_LIMIT = 25; const RECENT_TRANSPORT_SUMMARY_STORAGE_PREFIX = "opencode.recentTransportSummaries"; +/** + * VS Code core settings the extension manages (auto-configures and reverts) + * so OpenCode models work in the Agents window (issue #122): + * + * - `chat.agentHost.byokModels.enabled`: wires the agent-host BYOK bridge + * (VS Code 1.129+); off by default, so extension-provided BYOK models never + * reach agent-host sessions until it is flipped on. + * - `extensions.supportAgentsWindow.`: the ONLY way a code extension is + * allowed to run in the Agents window (sessions window) process. Without + * it the extension is disabled there, its `languageModelChatProviders` + * vendors are not registered, and neither the model picker nor the + * "+ Add Models" list knows OpenCode Go/Zen. + */ +const AGENT_HOST_BYOK_ENABLED_SETTING = "byokModels.enabled"; +const SUPPORT_AGENTS_WINDOW_SETTING = "supportAgentsWindow"; +const EXTENSION_ID = "ltmoerdani.opencode-copilot-chat"; +/** How many VS Code versions old the agent-host BYOK bridge goes back to. */ +const AGENT_HOST_BYOK_MINOR_VERSION = 129; +/** globalState keys tracking that the extension enabled each setting itself. */ +const AGENTS_BYOK_BRIDGE_STATE_KEY = "opencode.agentsByokBridge.v1"; +const SUPPORT_AGENTS_WINDOW_STATE_KEY = "opencode.supportAgentsWindow.v1"; + let usageStatusBarItem: vscode.StatusBarItem | undefined; let goUsageStatusBarItem: vscode.StatusBarItem | undefined; /** Singleton tracker — the first/legacy account. Used for backward compat until first migration. */ @@ -696,21 +719,33 @@ export function activate(context: vscode.ExtensionContext) { ensureUsageStatusBar(context); ensureGoUsageStatusBar(context); + // Read from the root configuration with the FULL setting key: section-scoped + // reads (getConfiguration("opencodego")) resolve keys relative to the + // section, which would misread the Zen flag as opencodego.opencodezen.enabled. + const goProviderEnabled = vscode.workspace.getConfiguration().get(providerEnabledSetting(GO_VENDOR), true); + const zenProviderEnabled = vscode.workspace.getConfiguration().get(providerEnabledSetting(ZEN_VENDOR), true); const goProvider = new OpenCodeProvider(context, PROVIDERS[GO_VENDOR]); const zenProvider = new OpenCodeProvider(context, PROVIDERS[ZEN_VENDOR]); const modelInfoProviders: OpenCodeProvider[] = [goProvider, zenProvider]; const subscriptions: vscode.Disposable[] = [ - vscode.lm.registerLanguageModelChatProvider(GO_VENDOR, goProvider), - vscode.lm.registerLanguageModelChatProvider(ZEN_VENDOR, zenProvider), + // Register the chat providers only while the matching `opencodego.enabled` + // / `opencodezen.enabled` setting is on, so a disabled provider disappears + // from the Language Models list and every model picker (its vendor + // contribution carries the same `when` clause). The provider instances are + // still created so the management commands keep working for re-enabling. + ...(goProviderEnabled ? [vscode.lm.registerLanguageModelChatProvider(GO_VENDOR, goProvider)] : []), + ...(zenProviderEnabled ? [vscode.lm.registerLanguageModelChatProvider(ZEN_VENDOR, zenProvider)] : []), vscode.commands.registerCommand("opencodego.manage", () => goProvider.manage()), vscode.commands.registerCommand("opencodego.diagnostics", () => goProvider.showDiagnostics()), vscode.commands.registerCommand("opencodego.setApiKey", () => goProvider.setApiKey()), vscode.commands.registerCommand("opencodego.refreshModels", () => goProvider.refreshModels()), + vscode.commands.registerCommand("opencodego.toggleProvider", () => toggleProviderEnabled("opencodego", "OpenCode Go")), vscode.commands.registerCommand("opencodego.configureUtilityModels", () => configureUtilityModels()), vscode.commands.registerCommand("opencodezen.diagnostics", () => zenProvider.showDiagnostics()), vscode.commands.registerCommand("opencodezen.manage", () => zenProvider.manage()), vscode.commands.registerCommand("opencodezen.refreshModels", () => zenProvider.refreshModels()), + vscode.commands.registerCommand("opencodezen.toggleProvider", () => toggleProviderEnabled("opencodezen", "OpenCode Zen")), vscode.commands.registerCommand("opencodego.modelPickerDiagnostics", () => showModelPickerDiagnostics()), vscode.commands.registerCommand("opencodego.setThinkingEffort", () => showThinkingEffortPicker()), vscode.commands.registerCommand("opencodego.showUsageDetails", () => showUsageWebview(context)), @@ -868,14 +903,19 @@ export function activate(context: vscode.ExtensionContext) { // Agent-host providers for the Copilot Agents window (opt-in via config). const enableAgents = vscode.workspace.getConfiguration("opencodego").get("agentsWindow", true); - if (enableAgents) { + if (enableAgents && (goProviderEnabled || zenProviderEnabled)) { const agentGoProvider = new OpenCodeProvider(context, PROVIDERS[AGENT_GO_VENDOR]); const agentZenProvider = new OpenCodeProvider(context, PROVIDERS[AGENT_ZEN_VENDOR]); modelInfoProviders.push(agentGoProvider, agentZenProvider); subscriptions.push( - vscode.lm.registerLanguageModelChatProvider(AGENT_GO_VENDOR, agentGoProvider), - vscode.lm.registerLanguageModelChatProvider(AGENT_ZEN_VENDOR, agentZenProvider), + ...(goProviderEnabled ? [vscode.lm.registerLanguageModelChatProvider(AGENT_GO_VENDOR, agentGoProvider)] : []), + ...(zenProviderEnabled ? [vscode.lm.registerLanguageModelChatProvider(AGENT_ZEN_VENDOR, agentZenProvider)] : []), ); + // On VS Code 1.129+ the Agents window runs in the agent host process, + // where extension BYOK models are only reachable through VS Code's BYOK + // language-model bridge — which is off by default. Make sure it is on so + // OpenCode models actually show up there (issue #122). + void ensureAgentsWindowSupport(context); } context.subscriptions.push(...subscriptions); @@ -890,6 +930,18 @@ export function activate(context: vscode.ExtensionContext) { provider.notifyModelInfoChanged(); } } + if (event.affectsConfiguration("opencodego.agentsWindow") || event.affectsConfiguration("opencodego.autoEnableAgentsWindow")) { + const agentsWindowEnabled = vscode.workspace.getConfiguration("opencodego").get("agentsWindow", true); + const autoEnabled = vscode.workspace.getConfiguration("opencodego").get("autoEnableAgentsWindow", true); + if (agentsWindowEnabled && autoEnabled) { + void ensureAgentsWindowSupport(context); + } else if (!agentsWindowEnabled) { + // We may have enabled core settings for the Agents window; revert + // them when the user turns the feature off so the user's global + // configuration is restored. + void revertAgentsWindowSupport(context); + } + } }), ); @@ -903,9 +955,145 @@ async function configureUtilityModels(): Promise { ); } +/** + * Toggle whether a provider (`opencodego` / `opencodezen`) is registered at + * all. Disabling removes the provider from the Language Models list and every + * model picker — the provider's vendor contribution is gated by the same + * `when` clause (`config..enabled`) and its runtime registration is + * skipped. Previously configured BYOK groups and API keys are kept, so + * re-enabling restores the provider exactly as it was. + * + * Provider registration happens at startup, so a window reload is required + * for the change to take effect. + */ +async function toggleProviderEnabled(vendor: string, displayName: string): Promise { + const cfg = vscode.workspace.getConfiguration(vendor); + const current = cfg.get("enabled", true); + const next = !current; + await cfg.update("enabled", next, vscode.ConfigurationTarget.Global); + + const reload = await vscode.window.showInformationMessage( + next + ? `${displayName} re-enabled. Reload the window for the provider to appear in Language Models again.` + : `${displayName} removed from Language Models. Reload the window for it to disappear from the model picker and the manage list. Your API key and group settings are kept.`, + "Reload Now", + ); + if (reload === "Reload Now") { + await vscode.commands.executeCommand("workbench.action.reloadWindow"); + } +} + +/** + * Whether this VS Code has the modern agent-host BYOK bridge (1.129+). + * + * From VS Code 1.129 the Agents window runs in a separate agent host + * process. Extension-provided BYOK models (isBYOK, no `targetChatSessionType`) + * are mirrored into agent-host sessions exclusively through the BYOK + * language-model bridge, which VS Code keeps OFF by default + * (`chat.agentHost.byokModels.enabled`, experimental). On older versions the + * extension's own agent-host providers (`targetChatSessionType: "copilotcli"`) + * are the only path, which is why they stay registered. + */ +function isModernAgentHostVscode(): boolean { + const [major = 1, minor = 0] = (vscode.version ?? "").split(".").map(Number); + return major > 1 || (major === 1 && minor >= AGENT_HOST_BYOK_MINOR_VERSION); +} + +/** + * Ensure the VS Code core settings that make OpenCode Go/Zen models usable in + * the Agents window are enabled (issue #122): + * + * 1. `extensions.supportAgentsWindow.` — the only way a code extension is + * allowed to run in the Agents window (sessions window) process. VS Code + * disables any extension with a `main` entry there by default, so without + * this setting the extension's `languageModelChatProviders` vendors are + * not registered in that window: neither the model picker nor the + * "+ Add Models" list can show OpenCode Go/Zen. + * 2. `chat.agentHost.byokModels.enabled` (VS Code 1.129+) — the BYOK + * language-model bridge that mirrors extension BYOK models into + * agent-host sessions. Off by default and experimental. + * + * CONTRACT: + * - Only writes the settings while the user keeps `opencodego.agentsWindow` + * and `opencodego.autoEnableAgentsWindow` on; the settings are merged with + * existing user values (never clobbering unrelated entries). + * - Records in globalState which settings the extension flipped itself, so + * {@link revertAgentsWindowSupport} can restore them when the user disables + * the Agents feature. + * - Both settings take effect after a window reload (extension host / + * agent host restart) — surface an actionable notification the first time + * anything was changed. + */ +async function ensureAgentsWindowSupport(context: vscode.ExtensionContext): Promise { + const opencodeCfg = vscode.workspace.getConfiguration("opencodego"); + if (!opencodeCfg.get("agentsWindow", true) || !opencodeCfg.get("autoEnableAgentsWindow", true)) { + return; + } + + let changed = false; + const extensionCfg = vscode.workspace.getConfiguration("extensions"); + const support = extensionCfg.get>(SUPPORT_AGENTS_WINDOW_SETTING, {}); + if (!support[EXTENSION_ID]) { + await extensionCfg.update(SUPPORT_AGENTS_WINDOW_SETTING, { ...support, [EXTENSION_ID]: true }, vscode.ConfigurationTarget.Global); + await context.globalState.update(SUPPORT_AGENTS_WINDOW_STATE_KEY, true); + changed = true; + } + + if (isModernAgentHostVscode()) { + const agentHostCfg = vscode.workspace.getConfiguration("chat.agentHost"); + if (!agentHostCfg.get(AGENT_HOST_BYOK_ENABLED_SETTING, false)) { + await agentHostCfg.update(AGENT_HOST_BYOK_ENABLED_SETTING, true, vscode.ConfigurationTarget.Global); + await context.globalState.update(AGENTS_BYOK_BRIDGE_STATE_KEY, true); + changed = true; + } + } + + if (changed) { + const reload = await vscode.window.showInformationMessage( + "OpenCode: enabled VS Code's Agents window support so OpenCode Go/Zen models can run in the Agents window. Reload the window for it to take effect.", + "Reload Now", + ); + if (reload === "Reload Now") { + await vscode.commands.executeCommand("workbench.action.reloadWindow"); + } + } +} + +/** + * Revert the core settings that {@link ensureAgentsWindowSupport} enabled on + * this machine (and only those — settings the user configured manually are + * left untouched). + */ +async function revertAgentsWindowSupport(context: vscode.ExtensionContext): Promise { + const extensionCfg = vscode.workspace.getConfiguration("extensions"); + if (context.globalState.get(SUPPORT_AGENTS_WINDOW_STATE_KEY)) { + const support = extensionCfg.get>(SUPPORT_AGENTS_WINDOW_SETTING, {}); + if (support[EXTENSION_ID]) { + const next = { ...support }; + delete next[EXTENSION_ID]; + await extensionCfg.update( + SUPPORT_AGENTS_WINDOW_SETTING, + Object.keys(next).length > 0 ? next : undefined, + vscode.ConfigurationTarget.Global, + ); + } + await context.globalState.update(SUPPORT_AGENTS_WINDOW_STATE_KEY, undefined); + } + + if (context.globalState.get(AGENTS_BYOK_BRIDGE_STATE_KEY)) { + await vscode.workspace + .getConfiguration("chat.agentHost") + .update(AGENT_HOST_BYOK_ENABLED_SETTING, false, vscode.ConfigurationTarget.Global); + await context.globalState.update(AGENTS_BYOK_BRIDGE_STATE_KEY, undefined); + } +} + async function warmModelPickerMetadata(): Promise { - const vendors: string[] = [GO_VENDOR, ZEN_VENDOR]; - if (vscode.workspace.getConfiguration("opencodego").get("agentsWindow", true)) { + const vendors: string[] = [ + ...(vscode.workspace.getConfiguration().get(providerEnabledSetting(GO_VENDOR), true) ? [GO_VENDOR] : []), + ...(vscode.workspace.getConfiguration().get(providerEnabledSetting(ZEN_VENDOR), true) ? [ZEN_VENDOR] : []), + ]; + if (vscode.workspace.getConfiguration("opencodego").get("agentsWindow", true) && vendors.length > 0) { vendors.push(AGENT_GO_VENDOR, AGENT_ZEN_VENDOR); } await Promise.allSettled(vendors.map((v) => vscode.lm.selectChatModels({ vendor: v }))); @@ -1625,13 +1813,9 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider { - const apiKey = await this.context.secrets.get(SECRET_KEY); - - if (!apiKey) { - await this.setApiKey(); - return; - } - + // Read via the base-vendor full key so agent variants (opencodego-agent, + // opencodezen-agent) follow the same switch as the vendor they mirror. + const providerEnabled = vscode.workspace.getConfiguration().get(providerEnabledSetting(this.definition.vendor), true); const choice = await vscode.window.showQuickPick( [ { label: "Set API Key", action: "set" as const }, @@ -1640,6 +1824,9 @@ class OpenCodeProvider implements vscode.LanguageModelChatProvider 0) { const fallbackDescription = descriptions.values().next().value ?? ""; for (let i = 0; i < flatMessages.length; i++) { @@ -3934,10 +4132,7 @@ function collectRequestParts( * the proxy describe ONLY the message that contains a new image, instead of * re-sending the whole conversation on every turn. */ -function buildVisionRequestMessage( - msg: vscode.LanguageModelChatRequestMessage, - visionPrompt: string, -): vscode.LanguageModelChatMessage[] { +function buildVisionRequestMessage(msg: vscode.LanguageModelChatRequestMessage, visionPrompt: string): vscode.LanguageModelChatMessage[] { const requestMessages: vscode.LanguageModelChatMessage[] = []; const parts = collectRequestParts(msg); if (parts.length > 0) { @@ -4062,8 +4257,7 @@ async function proxyVision( for (let index = 0; index < messages.length; index++) { const msg = messages[index]; const imageParts = msg.content.filter( - (part): part is vscode.LanguageModelDataPart => - part instanceof vscode.LanguageModelDataPart && part.mimeType.startsWith("image/"), + (part): part is vscode.LanguageModelDataPart => part instanceof vscode.LanguageModelDataPart && part.mimeType.startsWith("image/"), ); if (imageParts.length === 0) { continue; @@ -4092,8 +4286,7 @@ async function proxyVision( for (let index = 0; index < messages.length; index++) { const msg = messages[index]; const imageParts = msg.content.filter( - (part): part is vscode.LanguageModelDataPart => - part instanceof vscode.LanguageModelDataPart && part.mimeType.startsWith("image/"), + (part): part is vscode.LanguageModelDataPart => part instanceof vscode.LanguageModelDataPart && part.mimeType.startsWith("image/"), ); if (imageParts.length === 0) { continue; diff --git a/src/providerEnablement.ts b/src/providerEnablement.ts new file mode 100644 index 0000000..805d3eb --- /dev/null +++ b/src/providerEnablement.ts @@ -0,0 +1,18 @@ +import { resolveBaseVendor, type AllProviderVendor } from "./providerTypes"; + +/** + * The full configuration key that gates whether a provider is registered at + * all (`opencodego.enabled` / `opencodezen.enabled`). Agent-host variants + * resolve to their base vendor, so the agent providers follow the same switch + * as the vendor they mirror. + * + * CONTRACT: callers must read this from the ROOT configuration + * (`vscode.workspace.getConfiguration().get(key, ...)`), never from a + * section-scoped configuration — `getConfiguration("opencodego")` resolves + * keys relative to that section, so passing the full `opencodezen.enabled` + * key there would silently read `opencodego.opencodezen.enabled` and always + * fall back to the default. + */ +export function providerEnabledSetting(vendor: AllProviderVendor): string { + return `${resolveBaseVendor(vendor)}.enabled`; +} diff --git a/src/test/providerEnablement.test.ts b/src/test/providerEnablement.test.ts new file mode 100644 index 0000000..bf22bcb --- /dev/null +++ b/src/test/providerEnablement.test.ts @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { providerEnabledSetting } from "../providerEnablement"; +import { AGENT_GO_VENDOR, AGENT_ZEN_VENDOR, GO_VENDOR, ZEN_VENDOR } from "../providerTypes"; + +test("providerEnabledSetting — base vendors map to their own setting", () => { + assert.equal(providerEnabledSetting(GO_VENDOR), "opencodego.enabled"); + assert.equal(providerEnabledSetting(ZEN_VENDOR), "opencodezen.enabled"); +}); + +test("providerEnabledSetting — agent-host variants follow their base vendor", () => { + assert.equal(providerEnabledSetting(AGENT_GO_VENDOR), "opencodego.enabled"); + assert.equal(providerEnabledSetting(AGENT_ZEN_VENDOR), "opencodezen.enabled"); +}); + +test("providerEnabledSetting — keys are full root-configuration keys (regression: #125 review)", () => { + // Section-scoped reads (getConfiguration("opencodego")) resolve keys relative + // to the section. The Zen flag must be read from the root configuration with + // the full "opencodezen.enabled" key, otherwise the read silently hits + // "opencodego.opencodezen.enabled" and always falls back to the default. + assert.ok(providerEnabledSetting(ZEN_VENDOR).startsWith("opencodezen.")); + assert.ok(providerEnabledSetting(GO_VENDOR).startsWith("opencodego.")); + assert.ok(!providerEnabledSetting(ZEN_VENDOR).startsWith("opencodego.")); +});