diff --git a/opencode_bridge/config.py b/opencode_bridge/config.py index 45c1166..3267ec4 100644 --- a/opencode_bridge/config.py +++ b/opencode_bridge/config.py @@ -1,18 +1,25 @@ """Configuration resolution for the opencode-bridge extension. -Priority order (highest to lowest): - 1. jupyter settings dict (from schema/plugin.json) - 2. Environment variables (OPENCODE_BRIDGE_URL, _USER, _PASSWORD) - 3. Built-in defaults +All connection fields (URL / user / password / request timeout) are resolved +exclusively from environment variables + built-in defaults at server startup. +They are NOT stored in the JupyterLab plugin settings. + +Env vars: + OPENCODE_BRIDGE_URL — OpenCode Serve base URL (default http://127.0.0.1:4096) + OPENCODE_BRIDGE_USER — HTTP Basic Auth username (default 'opencode') + OPENCODE_BRIDGE_PASSWORD — HTTP Basic Auth password (default '' = no auth) + OPENCODE_BRIDGE_TIMEOUT — request timeout in seconds (default 120) """ from __future__ import annotations import os from typing import NamedTuple, Optional, Tuple + ENV_URL = "OPENCODE_BRIDGE_URL" ENV_USER = "OPENCODE_BRIDGE_USER" ENV_PASSWORD = "OPENCODE_BRIDGE_PASSWORD" +ENV_TIMEOUT = "OPENCODE_BRIDGE_TIMEOUT" DEFAULT_URL = "http://127.0.0.1:4096" DEFAULT_USER = "opencode" @@ -23,7 +30,7 @@ class OpenCodeConfig(NamedTuple): url: str user: str password: str - request_timeout_seconds: int = DEFAULT_REQUEST_TIMEOUT + request_timeout_seconds: int @property def auth(self) -> Optional[Tuple[str, str]]: @@ -34,18 +41,18 @@ class OpenCodeConfig(NamedTuple): def resolve_config(jupyter_settings: dict) -> OpenCodeConfig: - """Resolve OpenCode connection config from jupyter settings + env + defaults.""" - bridge = jupyter_settings.get("opencode_bridge", {}) or {} + """Resolve OpenCode connection config from environment variables + defaults. + The `jupyter_settings` argument is accepted for API compatibility but no + fields are read from it — all connection config is now startup-only + (environment variables). + """ + _ = jupyter_settings # intentionally unused; see module docstring return OpenCodeConfig( - url=bridge.get("opencodeServerUrl") or os.environ.get(ENV_URL) or DEFAULT_URL, - user=bridge.get("opencodeServerUser") or os.environ.get(ENV_USER) or DEFAULT_USER, - password=( - bridge.get("opencodeServerPassword") - or os.environ.get(ENV_PASSWORD) - or "" - ), + url=os.environ.get(ENV_URL) or DEFAULT_URL, + user=os.environ.get(ENV_USER) or DEFAULT_USER, + password=os.environ.get(ENV_PASSWORD) or "", request_timeout_seconds=int( - bridge.get("requestTimeoutSeconds", DEFAULT_REQUEST_TIMEOUT) - ), + os.environ.get(ENV_TIMEOUT) or DEFAULT_REQUEST_TIMEOUT + ) ) diff --git a/opencode_bridge/tests/test_client.py b/opencode_bridge/tests/test_client.py index 4a1797b..c1d76da 100644 --- a/opencode_bridge/tests/test_client.py +++ b/opencode_bridge/tests/test_client.py @@ -118,7 +118,9 @@ async def test_send_message_sync_omits_model_when_provider_or_model_missing( @pytest.mark.asyncio async def test_send_message_sync_includes_system_when_provided() -> None: """system param is forwarded into the request body when not None.""" - config = OpenCodeConfig(url="http://x:1", user="u", password="") + config = OpenCodeConfig( + url="http://x:1", user="u", password="", request_timeout_seconds=120 + ) mock = MockHTTPClient() mock.responses.append((200, {"info": {}, "parts": []})) client = OpenCodeClient(config, http_client=mock) diff --git a/opencode_bridge/tests/test_config.py b/opencode_bridge/tests/test_config.py index 0d2dc67..b0aa6a9 100644 --- a/opencode_bridge/tests/test_config.py +++ b/opencode_bridge/tests/test_config.py @@ -17,21 +17,23 @@ def test_env_url_overrides_default(monkeypatch) -> None: assert config.url == "http://x:1" -def test_jupyter_settings_override_env(monkeypatch) -> None: +def test_jupyter_settings_are_ignored(monkeypatch) -> None: monkeypatch.setenv("OPENCODE_BRIDGE_URL", "http://x:1") settings = {"opencode_bridge": {"opencodeServerUrl": "http://y:2"}} config = resolve_config(settings) - assert config.url == "http://y:2" + assert config.url == "http://x:1" def test_all_env_vars(monkeypatch) -> None: monkeypatch.setenv("OPENCODE_BRIDGE_URL", "http://x:1") monkeypatch.setenv("OPENCODE_BRIDGE_USER", "u") monkeypatch.setenv("OPENCODE_BRIDGE_PASSWORD", "p") + monkeypatch.setenv("OPENCODE_BRIDGE_TIMEOUT", "300") config = resolve_config({}) assert config.url == "http://x:1" assert config.user == "u" assert config.password == "p" + assert config.request_timeout_seconds == 300 def test_auth_none_when_password_empty() -> None: @@ -44,11 +46,12 @@ def test_auth_when_password_set() -> None: url="http://127.0.0.1:4096", user="opencode", password="secret", + request_timeout_seconds=120, ) assert config.auth == ("opencode", "secret") -def test_request_timeout_from_settings() -> None: - settings = {"opencode_bridge": {"requestTimeoutSeconds": 300}} - config = resolve_config(settings) +def test_request_timeout_from_env(monkeypatch) -> None: + monkeypatch.setenv("OPENCODE_BRIDGE_TIMEOUT", "300") + config = resolve_config({}) assert config.request_timeout_seconds == 300 diff --git a/schema/plugin.json b/schema/plugin.json index 4f55ac1..7e3135f 100644 --- a/schema/plugin.json +++ b/schema/plugin.json @@ -6,44 +6,6 @@ "title": "opencode_bridge", "description": "opencode_bridge settings.", "type": "object", - "properties": { - "opencodeServerUrl": { - "type": "string", - "title": "OpenCode Server URL", - "description": "opencode serve 监听的 HTTP 地址。覆盖 OPENCODE_BRIDGE_URL 环境变量。", - "default": "http://127.0.0.1:4096" - }, - "opencodeServerUser": { - "type": "string", - "title": "OpenCode Server 用户名", - "description": "HTTP Basic Auth 用户名。默认 'opencode'。可被 OPENCODE_BRIDGE_USER 环境变量覆盖。", - "default": "opencode" - }, - "opencodeServerPassword": { - "type": "string", - "title": "OpenCode Server 密码", - "description": "HTTP Basic Auth 密码。空字符串表示无认证。可被 OPENCODE_BRIDGE_PASSWORD 环境变量覆盖。", - "default": "" - }, - "opencodeProvider": { - "type": "string", - "title": "OpenCode Provider", - "description": "Provider id (e.g. 'anthropic' or 'openai'). Get available values from the browser console after activating this extension (logged on startup), or hit GET /opencode-bridge/providers directly. Leave empty to use OpenCode's default.", - "default": "" - }, - "opencodeModel": { - "type": "string", - "title": "OpenCode Model", - "description": "Model id (e.g. 'claude-sonnet-4-20250514'). Get available values from the browser console after activation. Leave empty to use the provider's default.", - "default": "" - }, - "requestTimeoutSeconds": { - "type": "integer", - "title": "请求超时(秒)", - "default": 120, - "minimum": 5, - "maximum": 600 - } - }, + "properties": {}, "additionalProperties": false } diff --git a/src/__tests__/opencode_bridge.spec.ts b/src/__tests__/opencode_bridge.spec.ts deleted file mode 100644 index 6c4c480..0000000 --- a/src/__tests__/opencode_bridge.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DEFAULT_OPENCODE_SETTINGS, readOpenCodeSettings } from '../types'; - -describe('opencode_bridge types', () => { - it('returns defaults when given empty composite', () => { - const s = readOpenCodeSettings({}); - expect(s).toEqual(DEFAULT_OPENCODE_SETTINGS); - expect(s.opencodeProvider).toBe(''); - expect(s.opencodeModel).toBe(''); - }); - - it('overrides defaults with user values', () => { - const s = readOpenCodeSettings({ - opencodeServerUrl: 'http://x:1', - opencodeProvider: 'anthropic', - opencodeModel: 'claude-sonnet-4-20250514' - }); - expect(s.opencodeServerUrl).toBe('http://x:1'); - expect(s.opencodeProvider).toBe('anthropic'); - expect(s.opencodeModel).toBe('claude-sonnet-4-20250514'); - expect(s.opencodeServerUser).toBe( - DEFAULT_OPENCODE_SETTINGS.opencodeServerUser - ); - }); - - it('treats empty string provider/model as "use default"', () => { - const s = readOpenCodeSettings({ - opencodeProvider: '', - opencodeModel: '' - }); - expect(s.opencodeProvider).toBe(''); - expect(s.opencodeModel).toBe(''); - }); -}); diff --git a/src/index.ts b/src/index.ts index cf5fb97..d58fc80 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,32 +5,38 @@ import { import { IToolbarWidgetRegistry } from '@jupyterlab/apputils'; import { Cell, CodeCell } from '@jupyterlab/cells'; -import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { Widget } from '@lumino/widgets'; import { OpenCodeCellActions, - setOpenCodeRuntime + setOpenCodeProviders, + setOpenCodeServerSettings } from './components/opencode_cell_actions'; import { requestAPI } from './request'; -import { readOpenCodeSettings } from './types'; import { callOpenCodeProviders } from './api/opencode_client'; /** * Initialization data for the opencode_bridge extension. + * + * v3-final: no JupyterLab plugin settings. Connection config is read from + * environment variables by the server extension; the model is picked + * dynamically in the inline prompt. */ const plugin: JupyterFrontEndPlugin = { id: 'opencode_bridge:plugin', description: 'A JupyterLab extension bridging the UI to OpenCode Serve.', autoStart: true, - optional: [ISettingRegistry, IToolbarWidgetRegistry], + optional: [IToolbarWidgetRegistry], activate: ( app: JupyterFrontEnd, - settingRegistry: ISettingRegistry | null, toolbarRegistry: IToolbarWidgetRegistry | null ) => { console.log('JupyterLab extension opencode_bridge is activated!'); + // Push the Jupyter server settings into the actions module so that + // callOpenCodeEdit can build the right base URL. + setOpenCodeServerSettings(app.serviceManager.serverSettings); + // Register the per-cell AI actions into the native Cell toolbar // (top-right of the active cell, next to move up/down). Non-code // cells get an empty widget so they show no AI buttons. @@ -47,44 +53,28 @@ const plugin: JupyterFrontEndPlugin = { ); } - // Load settings + push into the actions module. - if (settingRegistry) { - void settingRegistry - .load(plugin.id) - .then(settings => { - const apply = () => { - const bridge = readOpenCodeSettings(settings.composite); - setOpenCodeRuntime({ - settings: bridge, - serverSettings: app.serviceManager.serverSettings - }); - console.log('opencode_bridge settings applied:', bridge); - }; - apply(); - settings.changed.connect(apply); - - void callOpenCodeProviders(app.serviceManager.serverSettings) - .then(data => { - const lines: string[] = [ - '[opencode_bridge] Available OpenCode providers:' - ]; - for (const p of data.providers) { - const models = p.models.map(m => m.id).join(', '); - lines.push(` - ${p.id}: ${models || '(no models)'}`); - } - console.log(lines.join('\n')); - }) - .catch(reason => { - console.warn( - '[opencode_bridge] Could not fetch providers (is the opencode-bridge server extension enabled and opencode serve running?):', - reason - ); - }); - }) - .catch(reason => { - console.error('Failed to load settings for opencode_bridge.', reason); - }); - } + // Fetch available providers once at activation and cache them for the + // inline prompt's model picker. Failure is non-fatal (the picker hides). + void callOpenCodeProviders(app.serviceManager.serverSettings) + .then(data => { + setOpenCodeProviders(data); + const lines: string[] = [ + '[opencode_bridge] Available OpenCode providers:' + ]; + for (const p of data.providers) { + const models = p.models.map(m => m.id).join(', '); + lines.push(` - ${p.id}: ${models || '(no models)'}`); + } + // eslint-disable-next-line no-console + console.log(lines.join('\n')); + }) + .catch(reason => { + // eslint-disable-next-line no-console ++ console.warn( + '[opencode_bridge] Could not fetch providers (is the opencode-bridge server extension enabled and opencode serve running?):', + reason + ); + }); requestAPI('hello', app.serviceManager.serverSettings) .then(data => { diff --git a/src/types.ts b/src/types.ts index d1a119b..8002ed5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,10 @@ * * These mirror the Python server's `CellContext` / `OpenCodeRequest` / `OpenCodeResponse` * shapes in `opencode_bridge/routes.py`. Keep them in sync. + * + * v3-final: the JupyterLab plugin settings have been removed entirely. + * All connection config lives in startup environment variables (see + * `opencode_bridge/config.py`); the inline model picker is fully dynamic. */ export interface ErrorOutput { @@ -43,40 +47,6 @@ export interface OpenCodeFailure { export type OpenCodeResponse = OpenCodeSuccess | OpenCodeFailure; -/** Frontend view of the 4 schema/plugin.json fields. */ -export interface OpenCodeSettings { - opencodeServerUrl: string; - opencodeServerUser: string; - opencodeServerPassword: string; - requestTimeoutSeconds: number; - opencodeProvider: string; - opencodeModel: string; -} - -export const DEFAULT_OPENCODE_SETTINGS: OpenCodeSettings = { - opencodeServerUrl: 'http://127.0.0.1:4096', - opencodeServerUser: 'opencode', - opencodeServerPassword: '', - requestTimeoutSeconds: 120, - opencodeProvider: '', - opencodeModel: '', -}; - -export function readOpenCodeSettings(composite: unknown): OpenCodeSettings { - const c = (composite ?? {}) as Partial; - return { - opencodeServerUrl: c.opencodeServerUrl || DEFAULT_OPENCODE_SETTINGS.opencodeServerUrl, - opencodeServerUser: c.opencodeServerUser || DEFAULT_OPENCODE_SETTINGS.opencodeServerUser, - opencodeServerPassword: c.opencodeServerPassword || '', - requestTimeoutSeconds: - typeof c.requestTimeoutSeconds === 'number' - ? c.requestTimeoutSeconds - : DEFAULT_OPENCODE_SETTINGS.requestTimeoutSeconds, - opencodeProvider: c.opencodeProvider || '', - opencodeModel: c.opencodeModel || '', - }; -} - /** Response from GET /opencode-bridge/providers. */ export interface OpenCodeProvider { id: string;