diff --git a/branding/apply.ts b/branding/apply.ts index ac4616026b3..bac79d2d964 100644 --- a/branding/apply.ts +++ b/branding/apply.ts @@ -122,6 +122,38 @@ function buildReplacements(config: Branding): Replacement[] { const r = config.replacements const replacements: Replacement[] = [] + // URL replacements MUST come before generic name replacements. + // Otherwise "anomalyco/opencode" becomes "anomalyco/codeq" before + // the GitHub URL regex can match the full original URL. + if (r.urls?.github) { + replacements.push({ + search: /https:\/\/github\.com\/anomalyco\/opencode/g, + replace: r.urls.github, + description: `github repo -> ${r.urls.github}`, + }) + } + + if (r.urls?.website) { + replacements.push({ + search: /https:\/\/opencode\.ai/g, + replace: r.urls.website, + description: `opencode.ai -> ${r.urls.website}`, + }) + } + + if (r.urls?.api) { + replacements.push({ + search: /https:\/\/api\.opencode\.ai/g, + replace: r.urls.api, + description: `api.opencode.ai -> ${r.urls.api}`, + }) + replacements.push({ + search: /https:\/\/api\.dev\.opencode\.ai/g, + replace: r.urls.api, + description: `api.dev.opencode.ai -> ${r.urls.api}`, + }) + } + // Product name replacements (case-sensitive) // Use negative lookbehind/lookahead to avoid matching: // - @opencode-ai package names @@ -158,36 +190,6 @@ function buildReplacements(config: Branding): Replacement[] { }) } - // URL replacements - if (r.urls?.website) { - replacements.push({ - search: /https:\/\/opencode\.ai/g, - replace: r.urls.website, - description: `opencode.ai -> ${r.urls.website}`, - }) - } - - if (r.urls?.api) { - replacements.push({ - search: /https:\/\/api\.opencode\.ai/g, - replace: r.urls.api, - description: `api.opencode.ai -> ${r.urls.api}`, - }) - replacements.push({ - search: /https:\/\/api\.dev\.opencode\.ai/g, - replace: r.urls.api, - description: `api.dev.opencode.ai -> ${r.urls.api}`, - }) - } - - if (r.urls?.github) { - replacements.push({ - search: /https:\/\/github\.com\/anomalyco\/opencode/g, - replace: r.urls.github, - description: `github repo -> ${r.urls.github}`, - }) - } - return replacements } @@ -482,7 +484,7 @@ const PURPLE = RGBA.fromHex("#9370DB")`, transform: (content, config) => { return content.replace( /You are OpenCode, the best coding agent on the planet\./, - `You are CodeQ, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, + `You are ${config.replacements.displayName}, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, ) }, }, @@ -491,7 +493,7 @@ const PURPLE = RGBA.fromHex("#9370DB")`, transform: (content, config) => { return content.replace( /You are OpenCode, the best coding agent on the planet\./, - `You are CodeQ, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, + `You are ${config.replacements.displayName}, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, ) }, }, @@ -500,7 +502,7 @@ const PURPLE = RGBA.fromHex("#9370DB")`, transform: (content, config) => { return content.replace( /You are OpenCode, the best coding agent on the planet\./, - `You are CodeQ, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, + `You are ${config.replacements.displayName}, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, ) }, }, @@ -509,7 +511,7 @@ const PURPLE = RGBA.fromHex("#9370DB")`, transform: (content, config) => { return content.replace( /You are OpenCode, the best coding agent on the planet\./, - `You are CodeQ, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, + `You are ${config.replacements.displayName}, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, ) }, }, @@ -518,7 +520,7 @@ const PURPLE = RGBA.fromHex("#9370DB")`, transform: (content, config) => { return content.replace( /You are OpenCode, the best coding agent on the planet\./, - `You are CodeQ, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, + `You are ${config.replacements.displayName}, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, ) }, }, @@ -527,7 +529,7 @@ const PURPLE = RGBA.fromHex("#9370DB")`, transform: (content, config) => { return content.replace( /You are OpenCode, the best coding agent on the planet\./, - `You are CodeQ, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, + `You are ${config.replacements.displayName}, built by qBraid - the leading quantum software company. You are the universe's most powerful coding agent.`, ) }, }, diff --git a/branding/qbraid/README.md b/branding/qbraid/README.md index eba069b02f2..3b02e414011 100644 --- a/branding/qbraid/README.md +++ b/branding/qbraid/README.md @@ -1,10 +1,10 @@ -# CodeQ by qBraid +# Codeq by qBraid -CodeQ is qBraid's branded version of opencode - the universe's most powerful coding agent for quantum software development. +Codeq is qBraid's branded version of opencode - the universe's most powerful coding agent for quantum software development. ## Configuration -CodeQ is configured by qBraid's platform. The configuration file is placed at: +Codeq is configured by qBraid's platform. The configuration file is placed at: - **Project-level**: `.codeq/opencode.json` in your project directory - **Global**: `~/.config/codeq/config.json` @@ -18,7 +18,7 @@ CodeQ is configured by qBraid's platform. The configuration file is placed at: "qbraid": { "options": { "apiKey": "qbr_...", - "baseURL": "https://account-v2.qbraid.com/api/ai/v1" + "baseURL": "https://account.qbraid.com/api/ai/v1" } } } @@ -27,7 +27,7 @@ CodeQ is configured by qBraid's platform. The configuration file is placed at: ## Available Models -CodeQ provides access to the following models through qBraid: +Codeq provides access to the following models through qBraid: | Model ID | Name | Features | | -------------------------- | ----------------- | ----------------------------------- | @@ -45,7 +45,7 @@ codeq models ## Usage ```bash -# Start CodeQ TUI +# Start Codeq TUI codeq # Run with a message @@ -57,17 +57,18 @@ codeq /path/to/project ## Environment Variables -CodeQ uses the `CODEQ_` prefix for environment variables: +Codeq uses the `CODEQ_` prefix for environment variables: -| Variable | Description | -| ------------------------- | ------------------------------------ | -| `CODEQ_MODEL` | Default model to use | -| `CODEQ_DISABLE_TELEMETRY` | Disable usage telemetry | -| `CODEQ_LOG_LEVEL` | Log level (DEBUG, INFO, WARN, ERROR) | +| Variable | Description | +| ------------------------- | ------------------------------------------------ | +| `CODEQ_MODEL` | Default model to use | +| `CODEQ_DISABLE_TELEMETRY` | Disable usage telemetry | +| `CODEQ_LOG_LEVEL` | Log level (DEBUG, INFO, WARN, ERROR) | +| `QBRAID_API_URL` | Override qBraid API endpoint (e.g. staging) | ## Data Storage -CodeQ stores data in: +Codeq stores data in: - **Config**: `~/.config/codeq/` - **Cache**: `~/.cache/codeq/` @@ -75,4 +76,4 @@ CodeQ stores data in: ## Support -For issues with CodeQ, contact qBraid support at https://qbraid.com/support +For issues with Codeq, contact qBraid support at https://qbraid.com/support diff --git a/branding/qbraid/brand.json b/branding/qbraid/brand.json index 31745d1a96a..e188e324b24 100644 --- a/branding/qbraid/brand.json +++ b/branding/qbraid/brand.json @@ -2,7 +2,7 @@ "$schema": "../schema.json", "version": 1, "id": "qbraid", - "name": "qBraid CodeQ", + "name": "qBraid Codeq", "logo": { "cli": [ [" ", " "], @@ -17,7 +17,7 @@ }, "replacements": { "productName": "codeq", - "displayName": "CodeQ", + "displayName": "Codeq", "npmPackage": "codeq", "binaryName": "codeq", "envPrefix": "CODEQ", diff --git a/branding/qbraid/generate-models.ts b/branding/qbraid/generate-models.ts index 12c54a6847d..09b4222be3c 100644 --- a/branding/qbraid/generate-models.ts +++ b/branding/qbraid/generate-models.ts @@ -11,6 +11,7 @@ import { parseArgs } from "util" import path from "path" +import { QBRAID_DEFAULT_API_URL } from "../../packages/opencode/src/provider/sdk/qbraid/index" const { values } = parseArgs({ args: Bun.argv.slice(2), @@ -28,7 +29,7 @@ Options: --output, -o Output file path (default: models.json) --help, -h Show this help message -This script generates the models.json configuration for qBraid CodeQ. +This script generates the models.json configuration for qBraid Codeq. It defines the AI models available through qBraid's platform. `) process.exit(0) @@ -42,7 +43,7 @@ const models = { name: "qBraid", env: ["QBRAID_API_KEY"], npm: "@ai-sdk/openai-compatible", - api: "https://api.qbraid.com/ai/v1", + api: QBRAID_DEFAULT_API_URL, models: { "claude-sonnet-4": { id: "claude-sonnet-4", diff --git a/branding/qbraid/models.json b/branding/qbraid/models.json index 1e69207eada..0ad213399b4 100644 --- a/branding/qbraid/models.json +++ b/branding/qbraid/models.json @@ -4,7 +4,7 @@ "name": "qBraid", "env": ["QBRAID_API_KEY"], "npm": "@ai-sdk/qbraid", - "api": "https://account-v2.qbraid.com/api/ai/v1", + "api": "https://account.qbraid.com/api/ai/v1", "models": { "claude-opus-4-6": { "id": "claude-opus-4-6", diff --git a/branding/schema.ts b/branding/schema.ts index 47dcd6f5842..1696e6ca465 100644 --- a/branding/schema.ts +++ b/branding/schema.ts @@ -56,7 +56,7 @@ export const ModelsSchema = z.object({ export const ReplacementsSchema = z.object({ /** Product name (e.g., "codeq" instead of "opencode") */ productName: z.string(), - /** Display name with proper casing (e.g., "CodeQ" instead of "OpenCode") */ + /** Display name with proper casing (e.g., "Codeq" instead of "OpenCode") */ displayName: z.string(), /** Package name for npm (e.g., "codeq" instead of "opencode-ai") */ npmPackage: z.string().optional(), diff --git a/docs/qbraid-fork-rationale.md b/docs/qbraid-fork-rationale.md new file mode 100644 index 00000000000..6baeade8194 --- /dev/null +++ b/docs/qbraid-fork-rationale.md @@ -0,0 +1,152 @@ +# qBraid Fork Rationale: Why Source Modifications Are Required + +This document explains which qBraid/Codeq features require source-level modifications +to OpenCode and which could theoretically be implemented via the existing plugin/MCP +extension points. It serves as a reference for upstream discussions and future +architecture decisions. + +## Extension Points Available in OpenCode + +OpenCode provides three extension mechanisms: + +| Mechanism | Capabilities | +|-----------|-------------| +| **Plugins** | Server-side hooks for auth, chat pipeline (messages, params, headers, system prompt), tool registration, event listening, permission overrides, shell env injection. Plugins are async functions loaded from npm packages or local `.ts`/`.js` files. | +| **MCP servers** | External processes providing tools, resources, and prompts via the Model Context Protocol. Auto-connected from config. Tools appear alongside built-in tools. | +| **Config** | Keybinds, themes, agents (markdown), slash commands (markdown), skills, permissions, MCP server definitions, diff style, scroll behavior. | + +### Key limitation + +The plugin system is a **server-side hooks API** for the LLM conversation pipeline. +It has **zero TUI extensibility surface**. The TUI is a self-contained SolidJS +application with a hardcoded component tree. There is no mechanism for plugins or MCP +servers to inject sidebar sections, dialogs, footer elements, spinner styles, or any +other visual components. + +## Feature-by-Feature Analysis + +### Features that COULD be plugins or MCP + +| Feature | Mechanism | Notes | +|---------|-----------|-------| +| Quantum tools (list devices, submit job, get result, cancel, estimate cost, list jobs) | MCP server | An MCP server wrapping the qBraid quantum API would provide the same 6 tools. Config-only, zero source changes. | +| Chat header injection | Plugin `chat.headers` hook | Injecting `X-API-Key` or other headers into LLM requests. | +| System prompt customization | Plugin `experimental.chat.system.transform` hook | Adding quantum-specific instructions to the system prompt. | +| Event listening for analytics | Plugin `event` hook | Plugins receive all bus events and could forward them to an analytics backend. | +| Provider auth flow (API key) | Plugin `auth` hook | The `auth` hook can define API key and OAuth flows for a provider. The CodexAuthPlugin is an example. | + +### Features that REQUIRE source modifications + +#### 1. Quantum Sidebar Dashboard + +**Files**: `quantum-status.tsx`, `sidebar.tsx`, `quantum/state.ts`, `quantum/poller.ts`, `quantum/client.ts` + +The sidebar component tree is hardcoded in `sidebar.tsx`. The section order +(Title, Context, **qBraid**, MCP, LSP, Todo, Modified Files) is defined in JSX +with no injection point. `QuantumSidebarSection` is imported and rendered directly. + +There is no plugin hook for adding sidebar sections. The `command.register()` API +adds items to the command palette, not the sidebar. MCP servers appear in the sidebar +only as connection status indicators (colored dot + name + status text) -- they cannot +provide custom widgets. + +The quantum sidebar also requires: +- A background **scheduler task** (`quantum.poll`, 15s interval) -- `Scheduler.register()` + is not exposed to plugins +- A custom **bus event** (`quantum.state.updated`) -- `BusEvent.define()` is a + compile-time operation; plugins can listen but cannot publish new event types +- An **SSE event pipeline** to deliver state updates from the worker thread to the + main TUI thread + +#### 2. Telemetry System and Consent Dialog + +**Files**: `telemetry/index.ts`, `telemetry/integration.ts`, `telemetry/types.ts`, `dialog-telemetry-consent.tsx`, `app.tsx` + +The telemetry system subscribes to bus events server-side and forwards metrics to the +qBraid analytics endpoint. While a plugin's `event` hook can listen to events, it +cannot: +- Ship a consent dialog (dialogs are hardcoded in `app.tsx`) +- Control startup sequencing (consent -> auth -> provider connect) +- Persist consent state to the KV store from the server side +- Conditionally enable/disable itself based on user tier + +#### 3. qBraid Auth Startup Dialog + +**Files**: `dialog-qbraid-auth.tsx`, `app.tsx` + +The first-run dialog that prompts users for their qBraid API key requires a TUI dialog +component. Plugin `auth` hooks can define auth *methods* (API key, OAuth) that appear +in the `/connect` flow, but they cannot trigger a dialog at startup or control the +dialog sequencing order. + +#### 4. Provider SDK (`@ai-sdk/qbraid`) + +**Files**: `provider/sdk/qbraid/index.ts`, `provider/provider.ts` + +The qBraid provider extends `@ai-sdk/openai-compatible` with a custom default endpoint +(`QBRAID_DEFAULT_API_URL`), env var override (`QBRAID_API_URL`), and registration in +the `BUNDLED_PROVIDERS` map. Plugins can modify models within an existing provider via +the `auth` hook, but they cannot: +- Register new entries in `BUNDLED_PROVIDERS` +- Add new provider factory functions to `getSDK()` +- Set `includeUsage: true` for specific providers (line-level logic in `provider.ts`) + +#### 5. Interference Spinner Animation + +**Files**: `spinner.ts`, `prompt/index.tsx` + +The spinner style is hardcoded in `prompt/index.tsx` using `createFrames()` and +`createColors()` with fixed parameters. There is no configuration option or plugin +hook to customize spinner appearance. + +#### 6. SSE Event Pipeline Fix + +**Files**: `context/sdk.tsx` + +The fix that moved RPC event listener registration from `onMount` to the init phase +is core infrastructure. The timing race between worker-thread event emission and +main-thread listener registration cannot be addressed externally. + +#### 7. Branding + +**Files**: `branding/apply.ts`, `branding/qbraid/brand.json`, hundreds of source files + +The `opencode` -> `codeq` rename touches package names, binary names, config paths, +data directories, system prompts, and user-facing strings across 274 files. This is a +build-system transformation that is fundamentally source-level. + +## Summary + +``` +Source modification required: + - TUI components (sidebar, dialogs, footer) -- no plugin UI API + - Bus event definitions -- compile-time only + - Scheduler tasks -- not exposed to plugins + - Provider SDK registration -- internal map + - Spinner/animation customization -- hardcoded + - SSE/event pipeline infrastructure -- core plumbing + - Branding -- build system + - Startup dialog sequencing -- hardcoded in app.tsx + +Could be external (plugin or MCP): + - Quantum tools (6 tools) -- MCP server + - Chat hooks (headers, system prompt) -- plugin hooks + - Event listening for analytics -- plugin event hook + - Provider auth flow -- plugin auth hook +``` + +Roughly **30% of the quantum work** (the tools themselves) could be an MCP server. +The remaining **70%** (UI, infrastructure, branding) requires source changes because +OpenCode's TUI has no component injection mechanism. + +## Recommendations for Upstream + +If OpenCode added the following extension points, a significant portion of the +qBraid customizations could move to plugins: + +1. **Sidebar widget hook** -- allow plugins to register sidebar section components +2. **Dialog hook** -- allow plugins to show dialogs and control startup sequencing +3. **Scheduler hook** -- expose `Scheduler.register()` to plugins +4. **Bus publish hook** -- allow plugins to define and publish custom event types +5. **Provider registration hook** -- allow plugins to register new provider SDKs +6. **Spinner/theme hook** -- allow config-level spinner style selection diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index f3fc7392b8f..ceda8ba2ba6 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -37,6 +37,7 @@ import open from "open" import { writeHeapSnapshot } from "v8" import { PromptRefProvider, usePromptRef } from "./context/prompt" import { DialogTelemetryConsent, KV_TELEMETRY_CONSENT_SHOWN, KV_TELEMETRY_ENABLED } from "@tui/component/dialog-telemetry-consent" +import { DialogQBraidAuth, KV_QBRAID_AUTH_SHOWN } from "@tui/component/dialog-qbraid-auth" import { Telemetry } from "@/telemetry" async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { @@ -300,9 +301,30 @@ function App() { ), ) + // --- First-run qBraid API key dialog --- + // Fires once after telemetry consent is handled and user hasn't seen it yet. + let authShown = false createEffect( on( - () => sync.status === "complete" && sync.data.provider.length === 0, + () => sync.status === "complete" && kv.get(KV_TELEMETRY_CONSENT_SHOWN) !== undefined && kv.get(KV_QBRAID_AUTH_SHOWN) === undefined, + (needsAuth, prev) => { + if (!needsAuth || prev || authShown) return + authShown = true + DialogQBraidAuth.show(dialog).then((connected) => { + kv.set(KV_QBRAID_AUTH_SHOWN, true) + if (connected) { + toast.show({ variant: "info", message: "qBraid connected", duration: 3000 }) + } + }) + }, + ), + ) + + createEffect( + on( + // Wait for first-run dialogs (consent + auth) before showing provider connect. + // On subsequent runs KV_QBRAID_AUTH_SHOWN is already set so this fires immediately. + () => sync.status === "complete" && sync.data.provider.length === 0 && kv.get(KV_QBRAID_AUTH_SHOWN) !== undefined, (isEmpty, wasEmpty) => { // only trigger when we transition into an empty-provider state if (!isEmpty || wasEmpty) return diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-qbraid-auth.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-qbraid-auth.tsx new file mode 100644 index 00000000000..8c11e20d4dc --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-qbraid-auth.tsx @@ -0,0 +1,128 @@ +/** + * First-run qBraid API key dialog. + * + * Shown after telemetry consent on first launch. Prompts the user + * for a qBraid API key and stores it via the auth API. + * + * Exports: + * - KV_QBRAID_AUTH_SHOWN: KV key to track whether the dialog has been shown + * - DialogQBraidAuth.show(dialog): returns Promise (true if key was set) + */ + +import { TextAttributes } from "@opentui/core" +import { useTheme } from "@tui/context/theme" +import { useDialog, type DialogContext } from "@tui/ui/dialog" +import { useSDK } from "@tui/context/sdk" +import { useSync } from "@tui/context/sync" +import { createSignal, onMount, Show } from "solid-js" +import { useKeyboard } from "@opentui/solid" +import type { TextareaRenderable } from "@opentui/core" + +export const KV_QBRAID_AUTH_SHOWN = "qbraid_auth_shown" + +function DialogQBraidAuthContent(props: { onResult: (connected: boolean) => void }) { + const dialog = useDialog() + const { theme } = useTheme() + const sdk = useSDK() + const sync = useSync() + let textarea: TextareaRenderable + const [error, setError] = createSignal("") + const [saving, setSaving] = createSignal(false) + + useKeyboard((evt) => { + if (evt.name === "return" && !saving()) { + submit() + } + if (evt.name === "escape") { + props.onResult(false) + } + }) + + onMount(() => { + dialog.setSize("medium") + setTimeout(() => { + if (!textarea || textarea.isDestroyed) return + textarea.focus() + }, 1) + }) + + async function submit() { + const value = textarea.plainText.trim() + if (!value) { + props.onResult(false) + return + } + setSaving(true) + setError("") + try { + await sdk.client.auth.set({ + providerID: "qbraid", + auth: { type: "api", key: value }, + }) + await sdk.client.instance.dispose() + await sync.bootstrap() + props.onResult(true) + } catch (e) { + setError(String(e)) + setSaving(false) + } + } + + return ( + + + + Connect qBraid + + props.onResult(false)} + > + esc to skip + + + + + Enter your qBraid API key to enable quantum computing features — device + listing, job submission, credit tracking, and more. + + + Get a key at https://account.qbraid.com/api-keys + +