Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/persist-native-console-profile-management.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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`.
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -348,9 +348,6 @@
"displayName": "OpenCode Console",
"configuration": {
"type": "object",
"required": [
"profile"
],
"properties": {
"profile": {
"type": "string",
Expand Down
4 changes: 2 additions & 2 deletions src/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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" },
];
Expand Down
7 changes: 7 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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<Readonly<Record<string, OpenCodeUsageSnapshot>>>(USAGE_STATE_KEY)
?? { "zen:legacy": context.globalState.get<OpenCodeUsageSnapshot>(LEGACY_USAGE_STATE_KEY) ?? {} };
const activeConsoleProfile = activeConsoleProfileFromState(context.globalState.get<unknown>(ACTIVE_CONSOLE_PROFILE_STATE_KEY));
const metadata = new ModelsDevMetadata(context.globalState);
const providers = Object.fromEntries(Object.values(OPENCODE_PROVIDER_DEFINITIONS).map((definition) => [
definition.mode,
Expand All @@ -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<keyof typeof OPENCODE_PROVIDER_DEFINITIONS, OpenCodeProvider>;
const usageStatus = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 90);
Expand All @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions src/provider-config.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -20,7 +20,7 @@ test("declares native API-key and Console-profile provider entries", () => {
properties?: Record<string, { secret?: boolean }>;
};
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"]) {
Expand All @@ -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");
});
9 changes: 9 additions & 0 deletions src/provider-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
9 changes: 7 additions & 2 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,15 +26,17 @@ export interface OpenCodeModelInformation extends vscode.LanguageModelChatInform
export class OpenCodeProvider implements vscode.LanguageModelChatProvider<OpenCodeModelInformation> {
private readonly changeEmitter = new vscode.EventEmitter<void>();
private readonly usageEmitter = new vscode.EventEmitter<{ scope: string; usage: OpenCodeUsageSnapshot }>();
private readonly activeConsoleProfileEmitter = new vscode.EventEmitter<string>();
private readonly catalogs = new Map<string, ModelCatalog>();
private readonly credentials = new Map<string, Credential>();
private readonly usageByScope = new Map<string, OpenCodeUsageSnapshot>();
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,
Expand All @@ -43,10 +45,12 @@ export class OpenCodeProvider implements vscode.LanguageModelChatProvider<OpenCo
private readonly mode: OpenCodeMode,
private readonly catalogFactory: () => ModelCatalog = () => new ModelCatalog(),
initialUsage: Readonly<Record<string, OpenCodeUsageSnapshot>> = {},
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(); }
Expand All @@ -56,6 +60,7 @@ export class OpenCodeProvider implements vscode.LanguageModelChatProvider<OpenCo
setActiveConsoleProfile(profile: string): void {
this.activeProfile = normalizeConsoleProfile(profile);
this.activeCredentialId = `profile-${this.activeProfile}`;
this.activeConsoleProfileEmitter.fire(this.activeProfile);
}
getUsageSnapshot(): OpenCodeUsageSnapshot { return this.usageByScope.get(this.getActiveScope()) ?? {}; }
getManagementUsageSnapshot(): OpenCodeUsageSnapshot { return this.usageByScope.get(this.scopeFor(this.activeCredentialId)) ?? {}; }
Expand Down