diff --git a/.changeset/persist-native-console-profile-management.md b/.changeset/persist-native-console-profile-management.md new file mode 100644 index 0000000..bb60f04 --- /dev/null +++ b/.changeset/persist-native-console-profile-management.md @@ -0,0 +1,5 @@ +--- +"opencode-bridge-copilot-chat": patch +--- + +Restore the selected Console profile for usage and management after restart, clarify that native model entries retain their own account routing, and keep legacy default Console entries valid. diff --git a/docs/setup.md b/docs/setup.md index 4f8d224..e4775a0 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -3,8 +3,8 @@ Open **Manage Language Models** and use **Add Models** for each OpenCode entry you want: - Zen and Go accept an API key directly in the native provider form. VS Code stores the field as a secret. Add another named entry to use another account or key. -- Console first requires **OpenCode: Add Console Account**. Choose a profile ID, complete device authorization and organization selection, then enter the same profile ID when adding the native Console provider entry. -- **OpenCode: Select Active Console Profile** chooses the profile targeted by usage and management commands. +- Console first requires **OpenCode: Add Console Account**. Choose a profile ID, complete device authorization and organization selection, then enter the same profile ID when adding the native Console provider entry. Leaving the profile field empty preserves the legacy `default` Console account. +- **OpenCode: Select Console Profile for Usage and Management** chooses the profile targeted by those commands. VS Code restores it after restart; it does not change the account attached to a native model entry. - **OpenCode Console: Import Local Session** performs a read-only, one-way import from the active account in local `opencode.db` when that file and schema are available. Zen, Go, and Console remain separate providers, and each provider can now have multiple named entries. `opencode.defaultMode` only chooses the provider targeted by generic legacy connection, refresh, and test commands; it does not hide other entries. Console model discovery is profile- and organization-scoped and is read from `/api/config` with `x-org-id`; models marked `disabled` in that response are excluded. @@ -14,4 +14,4 @@ Reasoning-capable models show a **Thinking** or **Thinking Effort** submenu in t For Zen and Go, a signed-in refresh treats the account's `/models` response as authoritative and fills omitted fields from the canonical `opencode` or `opencode-go` provider in models.dev. Zen and Go share one six-hour, stale-while-revalidate metadata snapshot in VS Code `globalState`. The last successful live result is cached for up to 24 hours under a credential-scoped key; Console configuration is never persisted in that public cache. Request limits reserve context headroom, and only rejected optional parameters or transient 502–504/router failures are retried. -Use **OpenCode: Show Usage** or click the status-bar indicator to inspect inference tokens for the most recently used provider entry. Usage snapshots and catalogs remain isolated by API-key fingerprint or Console profile. The generic **OpenCode: Test Connection (Legacy Key / Active Console Profile)** and **OpenCode: Refresh Models (Legacy Key / Active Console Profile)** commands intentionally target only the command-managed Zen/Go key or active Console profile. `opencode.defaultMode` chooses the provider family targeted by these commands; when it is Console, **OpenCode: Select Active Console Profile** chooses the profile. Native Zen/Go entries are discovered and refreshed by VS Code through **Manage Language Models** and the model picker. `opencode.catalogCacheMinutes` controls normal live-catalog freshness, while `opencode.requestTimeoutSeconds` and `opencode.streamIdleTimeoutSeconds` bound total request time and stalled streams separately. Secret-safe request metadata can be enabled with `opencode.debugLogging`. +Use **OpenCode: Show Usage** or click the status-bar indicator to inspect inference tokens for the most recently used provider entry. Usage snapshots and catalogs remain isolated by API-key fingerprint or Console profile. The generic **OpenCode: Test Connection (Legacy Key / Active Console Profile)** and **OpenCode: Refresh Models (Legacy Key / Active Console Profile)** commands intentionally target only the command-managed Zen/Go key or active Console profile. `opencode.defaultMode` chooses the provider family targeted by these commands; when it is Console, **OpenCode: Select Console Profile for Usage and Management** chooses the profile. Native Zen/Go entries are discovered and refreshed by VS Code through **Manage Language Models** and the model picker. `opencode.catalogCacheMinutes` controls normal live-catalog freshness, while `opencode.requestTimeoutSeconds` and `opencode.streamIdleTimeoutSeconds` bound total request time and stalled streams separately. Secret-safe request metadata can be enabled with `opencode.debugLogging`. diff --git a/package.json b/package.json index e8123ef..398afb9 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ }, { "command": "opencodeCopilot.selectConsoleProfile", - "title": "OpenCode: Select Active Console Profile" + "title": "OpenCode: Select Console Profile for Usage and Management" }, { "command": "opencodeCopilot.refreshModels", @@ -348,9 +348,6 @@ "displayName": "OpenCode Console", "configuration": { "type": "object", - "required": [ - "profile" - ], "properties": { "profile": { "type": "string", diff --git a/src/commands/commands.ts b/src/commands/commands.ts index 8a65d0a..d0a2ef3 100644 --- a/src/commands/commands.ts +++ b/src/commands/commands.ts @@ -40,7 +40,7 @@ async function manage(auth: OpenCodeAuth, providers: OpenCodeProviders, output: { label: `$(check) Test ${label(mode)} inference`, action: "test" }, { label: `$(refresh) Refresh ${label(mode)} models`, action: "refresh" }, ...(mode === "console" ? [{ label: "$(organization) Switch Console organization", action: "org" }] : []), - ...(mode === "console" ? [{ label: "$(account) Switch Console profile", action: "profile" }, { label: "$(add) Add Console account", action: "addConsole" }] : []), + ...(mode === "console" ? [{ label: "$(account) Select Console profile for usage and management", action: "profile" }, { label: "$(add) Add Console account", action: "addConsole" }] : []), { label: "$(key) Add or switch OpenCode credential", action: "switch" }, { label: "$(sign-out) Sign out", action: "signout" }, { label: "$(output) Show OpenCode logs", action: "logs" }, @@ -50,7 +50,7 @@ async function manage(auth: OpenCodeAuth, providers: OpenCodeProviders, output: { label: "$(key) Sign in with OpenCode Go API key", action: "go" }, { label: "$(device-mobile) Sign in with OpenCode Console device code", action: "console" }, { label: "$(add) Add named Console account", action: "addConsole" }, - { label: "$(account) Switch Console profile", action: "profile" }, + { label: "$(account) Select Console profile for usage and management", action: "profile" }, { label: "$(database) Import Console session from local OpenCode", action: "importConsole" }, { label: "$(output) Show OpenCode logs", action: "logs" }, ]; diff --git a/src/extension.ts b/src/extension.ts index 3976ed4..c701619 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,9 +8,11 @@ import { ModelsDevMetadata } from "./models/metadata"; import { OPENCODE_PROVIDER_DEFINITIONS } from "./provider/definitions"; import { formatUsageStatus, formatUsageTooltip } from "./usage/presentation"; import type { OpenCodeUsageSnapshot } from "./usage/domain"; +import { activeConsoleProfileFromState } from "./provider-profile"; const LEGACY_USAGE_STATE_KEY = "opencode.usageSnapshot.v1"; const USAGE_STATE_KEY = "opencode.usageSnapshots.v2"; +const ACTIVE_CONSOLE_PROFILE_STATE_KEY = "opencode.activeConsoleProfile.v1"; export function activate(context: vscode.ExtensionContext): void { const output = vscode.window.createOutputChannel("OpenCode"); @@ -19,6 +21,7 @@ export function activate(context: vscode.ExtensionContext): void { const userAgent = `opencode-copilot-chat/${version} VSCode/${vscode.version}`; const initialUsage = context.globalState.get>>(USAGE_STATE_KEY) ?? { "zen:legacy": context.globalState.get(LEGACY_USAGE_STATE_KEY) ?? {} }; + const activeConsoleProfile = activeConsoleProfileFromState(context.globalState.get(ACTIVE_CONSOLE_PROFILE_STATE_KEY)); const metadata = new ModelsDevMetadata(context.globalState); const providers = Object.fromEntries(Object.values(OPENCODE_PROVIDER_DEFINITIONS).map((definition) => [ definition.mode, @@ -29,6 +32,7 @@ export function activate(context: vscode.ExtensionContext): void { definition.mode, () => new ModelCatalog(fetch, context.globalState, metadata), initialUsage, + definition.mode === "console" ? activeConsoleProfile : undefined, ), ])) as Record; const usageStatus = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 90); @@ -40,6 +44,9 @@ export function activate(context: vscode.ExtensionContext): void { context.subscriptions.push( output, usageStatus, + providers.console.onDidChangeActiveConsoleProfile((profile) => { + void context.globalState.update(ACTIVE_CONSOLE_PROFILE_STATE_KEY, profile); + }), ...Object.values(providers).map((provider) => provider.onDidChangeUsage(({ scope, usage }) => { if (scope === provider.getActiveScope()) { activeUsageProvider = provider; diff --git a/src/provider-config.test.ts b/src/provider-config.test.ts index 9b39bfd..28e57c0 100644 --- a/src/provider-config.test.ts +++ b/src/provider-config.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; -import { consoleProfileFromConfiguration, qualifiedModelId } from "./provider-profile"; +import { activeConsoleProfileFromState, consoleProfileFromConfiguration, qualifiedModelId } from "./provider-profile"; test("declares native API-key and Console-profile provider entries", () => { const manifest = JSON.parse(readFileSync("package.json", "utf8")) as { @@ -20,7 +20,7 @@ test("declares native API-key and Console-profile provider entries", () => { properties?: Record; }; const required = configuration.required; - assert.deepEqual(required, [vendor === "opencodeconsole" ? "profile" : "apiKey"]); + assert.deepEqual(required, vendor === "opencodeconsole" ? undefined : ["apiKey"]); if (vendor !== "opencodeconsole") assert.equal(configuration.properties?.apiKey.secret, true); } for (const command of ["opencodeCopilot.refreshModels", "opencodeCopilot.testConnection"]) { @@ -40,3 +40,9 @@ test("qualifies model IDs and reports invalid saved Console profiles", () => { /Update this provider entry in Manage Language Models/, ); }); + +test("restores only a valid persisted Console management profile", () => { + assert.equal(activeConsoleProfileFromState("Work"), "work"); + assert.equal(activeConsoleProfileFromState("work profile"), "default"); + assert.equal(activeConsoleProfileFromState(undefined), "default"); +}); diff --git a/src/provider-profile.ts b/src/provider-profile.ts index f7e1180..1e78e62 100644 --- a/src/provider-profile.ts +++ b/src/provider-profile.ts @@ -14,3 +14,12 @@ export function qualifiedModelId(credentialId: string, modelId: string): string ? modelId : `${credentialId}::${modelId}`; } + +/** Restores a command-management profile without allowing malformed state to prevent activation. */ +export function activeConsoleProfileFromState(value: unknown): string { + try { + return typeof value === "string" ? normalizeConsoleProfile(value) : DEFAULT_CONSOLE_PROFILE; + } catch { + return DEFAULT_CONSOLE_PROFILE; + } +} diff --git a/src/provider.ts b/src/provider.ts index 8d95657..af20715 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -13,7 +13,7 @@ import { buildFunctionTools, buildResponsesTools, originalToolName } from "./too import { endpointUrl, buildRequestHeaders, type OpenCodeMode } from "./transport/protocol"; import { OpenCodeStreamParser } from "./transport/sse"; import { recordRequestUsage, type OpenCodeUsageSnapshot } from "./usage/domain"; -import { consoleProfileFromConfiguration, qualifiedModelId } from "./provider-profile"; +import { activeConsoleProfileFromState, consoleProfileFromConfiguration, qualifiedModelId } from "./provider-profile"; export interface OpenCodeModelInformation extends vscode.LanguageModelChatInformation { readonly rawModelId: string; @@ -26,15 +26,17 @@ export interface OpenCodeModelInformation extends vscode.LanguageModelChatInform export class OpenCodeProvider implements vscode.LanguageModelChatProvider { private readonly changeEmitter = new vscode.EventEmitter(); private readonly usageEmitter = new vscode.EventEmitter<{ scope: string; usage: OpenCodeUsageSnapshot }>(); + private readonly activeConsoleProfileEmitter = new vscode.EventEmitter(); private readonly catalogs = new Map(); private readonly credentials = new Map(); private readonly usageByScope = new Map(); private activeCredentialId = "legacy"; private lastUsedCredentialId = "legacy"; - private activeProfile = DEFAULT_CONSOLE_PROFILE; + private activeProfile: string; readonly onDidChangeLanguageModelChatInformation = this.changeEmitter.event; readonly onDidChangeUsage = this.usageEmitter.event; + readonly onDidChangeActiveConsoleProfile = this.activeConsoleProfileEmitter.event; constructor( private readonly auth: OpenCodeAuth, @@ -43,10 +45,12 @@ export class OpenCodeProvider implements vscode.LanguageModelChatProvider ModelCatalog = () => new ModelCatalog(), initialUsage: Readonly> = {}, + initialActiveConsoleProfile: unknown = DEFAULT_CONSOLE_PROFILE, ) { for (const [scope, usage] of Object.entries(initialUsage)) { if (scope.startsWith(`${mode}:`)) this.usageByScope.set(scope, usage); } + this.activeProfile = activeConsoleProfileFromState(initialActiveConsoleProfile); } fireDidChange(): void { this.changeEmitter.fire(); } @@ -56,6 +60,7 @@ export class OpenCodeProvider implements vscode.LanguageModelChatProvider