From a52efe1f70decaea6cc1cddb45ab0fe3e8a58390 Mon Sep 17 00:00:00 2001 From: "nhat.nguyenhong" Date: Wed, 16 Sep 2026 14:34:49 +0000 Subject: [PATCH] add zdr and debug status to the sidebar --- AGENTS.md | 29 +++++++++++------- README.md | 24 ++++++++------- package.json | 10 ++++--- src/tui-shims.d.ts | 19 ++++++++++++ src/{tui.ts => tui.tsx} | 65 ++++++++++++++++++++++++++++++++++------- tsconfig.json | 3 +- tsconfig.tui.json | 18 ++++++++++++ 7 files changed, 132 insertions(+), 36 deletions(-) create mode 100644 src/tui-shims.d.ts rename src/{tui.ts => tui.tsx} (50%) create mode 100644 tsconfig.tui.json diff --git a/AGENTS.md b/AGENTS.md index f915c6f..4e9ee5e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,8 +23,8 @@ Run from the repository root. ```powershell npm install # dev deps: typescript, @types/node, @ai-sdk/provider -npm run typecheck # tsc --noEmit -npm run build # tsc -> dist/ +npm run typecheck # tsc --noEmit (provider) + tsc -p tsconfig.tui.json (TUI plugin) +npm run build # tsc -> dist/ (provider only; the TUI plugin is loaded from source) npm run smoke # live request against CommandCode (needs a key) npm run sync-models # regenerate provider.commandcode.models from the live catalog ``` @@ -46,7 +46,9 @@ src/usage.ts finish event -> LanguageModelV3Usage; finish-reason unificatio src/redact.ts Credential scrubbing for error surfaces. src/log.ts Opt-in tracing (COMMANDCODE_DEBUG) to a log file. src/toggles.ts Shared toggle file (~/.config/opencode/commandcode-toggles.json). -src/tui.ts opencode TUI plugin exposing /cc-zdr, /cc-debug, /cc-status. +src/tui.tsx opencode TUI plugin: /cc-zdr, /cc-debug, /cc-status + sidebar status panel. +src/tui-shims.d.ts Ambient types for the opencode-provided solid-js/@opentui/solid runtime. +tsconfig.tui.json Typechecks src/tui.tsx against the shim (no dependencies). src/constants.ts Defaults, paths, headers, passthrough params, static config block. scripts/smoke.mjs Live end-to-end check. scripts/sync-models.mjs Catalog -> provider.commandcode.models generator (with vision probing). @@ -116,13 +118,20 @@ These are load-bearing. Breaking one causes silent failures in opencode. `provider.commandcode.models` in `opencode.json` and never asks a custom `file://` provider to discover models (only internal providers can register `discoverModels`). Refresh the map with `npm run sync-models`; do not expect a discovery hook in `src/` to populate it. -12. **The TUI plugin lives outside the provider entrypoint.** `src/tui.ts` compiles to - `dist/tui.js`, is exposed as `./tui` in `package.json`, and is installed with - `opencode plugin `, which writes a `tui.json` `plugin` entry. Verified on opencode - 1.18.31: the module's **default export must be `{ id, tui }`** (a bare `tui` named export is - imported but never invoked), `id` is required, and `tui` is `async (api) => {}`. Commands - register via `api.keymap.registerLayer({ commands })` with `{ name, run, title, desc, - category, namespace: "palette", slashName }`. +12. **The TUI plugin lives outside the provider entrypoint.** `src/tui.tsx` is loaded from + **source** (not `dist/`) via `opencode plugin `, which writes a `tui.json` `plugin` + entry; it is exposed as `./tui` in `package.json`. opencode compiles the `.tsx` at load with + its Bun/Solid transform and maps `solid-js` / `@opentui/solid` to its internal modules, so the + package stays dependency-free — do not add a build step or real imports of those packages. + `src/tui-shims.d.ts` is the only local stand-in for types; keep it in sync with what the file + actually imports. Verified on opencode 1.18.31: the module's **default export must be + `{ id, tui }`** (a bare `tui` named export is imported but never invoked), `id` is required, + and `tui` is `async (api) => {}`. Commands register via + `api.keymap.registerLayer({ commands })` with `{ name, run, title, desc, category, + namespace: "palette", slashName }`. The sidebar panel registers via + `api.slots.register({ order, slots: { sidebar_content } })`; `order: 90` keeps it above the + built-in panels (100 context, 200 mcp, 300 lsp, 400 todo, 500 files). The slot renderer + returns Solid JSX and is reactive; it relies on `api.theme` and `api.lifecycle.onDispose`. 13. **Slash commands and `Ctrl+P` share one registry.** In opencode 1.x the slash menu queries the same `namespace: "palette"` commands the palette lists (both filter out `hidden: true`). A slash-only entry is not expressible; registering `/cc-*` also adds them to `Ctrl+P`. diff --git a/README.md b/README.md index 2b47e44..ea7d194 100644 --- a/README.md +++ b/README.md @@ -188,18 +188,21 @@ reads on every request, so both settings change without restarting opencode. `/c the current state. The file is `~/.config/opencode/commandcode-toggles.json` (overridable with `COMMANDCODE_TOGGLES_FILE`); the environment variables above are used when a key is absent. -These commands come from a small TUI plugin shipped in this package (`src/tui.ts`, built to -`dist/tui.js`). Register it once: +These commands come from a small TUI plugin shipped in this package (`src/tui.tsx`). It is loaded +from **source** — opencode compiles the TSX and provides the Solid runtime itself — so there is no +build step and no runtime dependency for it. Register it once: ```powershell -opencode plugin file:///C:/DevTools/pienv/ccprovider/dist/tui.js +opencode plugin file:///C:/DevTools/pienv/ccprovider/src/tui.tsx ``` That writes a `tui.json` `plugin` entry (project-local `.opencode/tui.json`, or global with -`--global`). Restart opencode after installing; then type `/cc-` for autocomplete. Note that in -opencode 1.x the slash menu and the `Ctrl+P` palette read the same command registry, so these -entries appear in both. If you run opencode against a remote server, the TUI-side file is not -visible to the provider on the server host — use the env vars or `providerOptions` there. +`--global`). Restart opencode after installing; then type `/cc-` for autocomplete. The plugin also +renders a **CommandCode panel in the session sidebar** (`zdr` / `debug`, above the built-in +panels) that updates live as you toggle. Note that in opencode 1.x the slash menu and the `Ctrl+P` +palette read the same command registry, so these entries appear in both. If you run opencode +against a remote server, the TUI-side file is not visible to the provider on the server host — use +the env vars or `providerOptions` there. ## Features @@ -221,6 +224,7 @@ visible to the provider on the server host — use the env vars or `providerOpti | Retry with backoff | Yes — 429/5xx and network errors, honouring `Retry-After` | | Credential redaction | Yes — error bodies are scrubbed before surfacing | | Runtime toggles | Yes — `/cc-zdr` and `/cc-debug` flip the shared toggle file without a restart | +| Sidebar status panel | Yes — a `sidebar_content` panel shows `zdr`/`debug` and updates live | ### `tool_choice` handling @@ -255,7 +259,7 @@ src/usage.ts finish event -> V3 usage; finish-reason unification. src/redact.ts Credential scrubbing for error surfaces. src/log.ts Opt-in tracing (COMMANDCODE_DEBUG) to a log file. src/toggles.ts Shared toggle file read by the provider and written by the TUI plugin. -src/tui.ts opencode TUI plugin registering /cc-zdr, /cc-debug, /cc-status. +src/tui.tsx opencode TUI plugin: /cc-zdr, /cc-debug, /cc-status + sidebar status panel. src/constants.ts Defaults, header names, passthrough params, static config block. scripts/smoke.mjs Live end-to-end check against api.commandcode.ai. scripts/sync-models.mjs Generate provider.commandcode.models from the live catalog. @@ -283,8 +287,8 @@ scripts/sync-models.mjs Generate provider.commandcode.models from the live catal ```powershell npm install # installs typescript, @types/node, @ai-sdk/provider -npm run typecheck # tsc --noEmit -npm run build # tsc -> dist/ +npm run typecheck # tsc --noEmit (provider) + tsc -p tsconfig.tui.json (TUI plugin) +npm run build # tsc -> dist/ (provider only; the TUI plugin loads from source) npm run smoke # live request against CommandCode npm run sync-models # regenerate provider.commandcode.models from the catalog ``` diff --git a/package.json b/package.json index 33be08a..6baeb7c 100644 --- a/package.json +++ b/package.json @@ -11,16 +11,18 @@ "types": "./dist/index.d.ts" }, "./tui": { - "import": "./dist/tui.js", - "types": "./dist/tui.d.ts" + "import": "./src/tui.tsx" } }, "files": [ - "dist" + "dist", + "src/tui.tsx", + "src/tui-shims.d.ts", + "src/toggles.ts" ], "scripts": { "build": "tsc", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && tsc -p tsconfig.tui.json", "smoke": "node scripts/smoke.mjs", "sync-models": "node scripts/sync-models.mjs" }, diff --git a/src/tui-shims.d.ts b/src/tui-shims.d.ts new file mode 100644 index 0000000..495025b --- /dev/null +++ b/src/tui-shims.d.ts @@ -0,0 +1,19 @@ +// Ambient shims for the symbols the TUI plugin imports from opencode's bundled runtime. +// opencode resolves `solid-js` and `@opentui/solid` to its own internal modules when it +// loads the `.tsx` plugin, so this package declares just enough surface for `tsc` without +// taking a dependency. The JSX namespace is intentionally permissive: only a handful of +// intrinsic elements are used and their props come from opencode's theme at runtime. + +declare module "solid-js" { + export function createSignal(value: Value): [get: () => Value, set: (value: Value) => void]; +} + +declare namespace JSX { + type Element = unknown; + interface ElementChildrenAttribute { + children: {}; + } + interface IntrinsicElements { + [name: string]: any; + } +} diff --git a/src/tui.ts b/src/tui.tsx similarity index 50% rename from src/tui.ts rename to src/tui.tsx index b52177d..f154c98 100644 --- a/src/tui.ts +++ b/src/tui.tsx @@ -1,14 +1,21 @@ -// opencode TUI plugin exposing /cc-zdr, /cc-debug and /cc-status. It flips the shared -// toggle file (see ./toggles.ts) that the provider reads on every request, so a change -// takes effect without restarting opencode. +// opencode TUI plugin exposing /cc-zdr, /cc-debug and /cc-status, plus a sidebar panel +// showing the current toggle state. It flips the shared toggle file (see ./toggles.ts) +// that the provider reads on every request, so a change takes effect without restarting +// opencode. // // opencode loads this module's default export (`{ id, tui }`) from a `tui.json` plugin -// entry; verified against @opencode-ai/plugin@1.18.31. The local structural types below -// keep this package runtime-dependency-free. Note: in this opencode version slash -// commands and the Ctrl+P palette read the same `namespace: "palette"` command list, so -// these entries appear in both; `hidden: true` would remove them from both. +// entry. It is loaded from source (not dist) so opencode's Bun/Solid transform compiles +// the JSX and maps `solid-js` / `@opentui/solid` to its internal modules; that keeps this +// package free of runtime and build dependencies. The ambient types in ./tui-shims.d.ts +// back the JSX and `createSignal` references for `tsc -p tsconfig.tui.json`. +// +// Note: in this opencode version slash commands and the Ctrl+P palette read the same +// `namespace: "palette"` command list, so these entries appear in both; `hidden: true` +// would remove them from both. -import { readToggles, toggle, type ToggleName } from "./toggles.js"; +import { createSignal } from "solid-js"; + +import { readToggles, toggle, type ToggleName, type Toggles } from "./toggles.js"; type ToastVariant = "info" | "success" | "warning" | "error"; @@ -26,33 +33,66 @@ type TuiCommand = { run: () => void | Promise; }; +type TuiTheme = { current: { text: unknown; textMuted: unknown; success: unknown } }; + +type TuiSlotHandler = (ctx: { theme: TuiTheme }, props: { session_id: string }) => unknown; + +type TuiSlotPlugin = { order?: number; slots: Record }; + type TuiApi = { keymap: { registerLayer(layer: { commands?: readonly TuiCommand[] }): () => void }; ui: { toast(input: { title?: string; message: string; variant?: ToastVariant; duration?: number }): void }; + slots: { register(plugin: TuiSlotPlugin): string }; + theme: TuiTheme; + lifecycle: { onDispose(fn: () => void): () => void }; }; const ID = "commandcode-toggles"; const CATEGORY = "CommandCode"; +const SIDEBAR_ORDER = 90; function state(value: boolean | undefined): string { return value === true ? "on" : "off"; } -function summary(): string { - const toggles = readToggles(); +function summary(toggles: Toggles = readToggles()): string { return `zdr=${state(toggles.zdr)}, debug=${state(toggles.debug)}`; } export const tui = async (api: TuiApi): Promise => { + const [toggles, setToggles] = createSignal(readToggles()); + + const refresh = (): void => setToggles(readToggles()); + const flip = (name: ToggleName): void => { const next = toggle(name); + setToggles(next); api.ui.toast({ title: CATEGORY, - message: `${name} ${state(next[name])} (${summary()})`, + message: `${name} ${state(next[name])} (${summary(next)})`, variant: next[name] === true ? "success" : "info", }); }; + const Status = () => ( + + + CommandCode + + zdr: {state(toggles().zdr)} + debug: {state(toggles().debug)} + + ); + + api.slots.register({ + order: SIDEBAR_ORDER, + slots: { + sidebar_content() { + return ; + }, + }, + }); + api.keymap.registerLayer({ commands: [ { @@ -84,6 +124,9 @@ export const tui = async (api: TuiApi): Promise => { }, ], }); + + const timer = setInterval(refresh, 1500); + api.lifecycle.onDispose(() => clearInterval(timer)); }; export const id = ID; diff --git a/tsconfig.json b/tsconfig.json index 2d4608e..881a476 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,5 +14,6 @@ "noUncheckedIndexedAccess": true, "verbatimModuleSyntax": true }, - "include": ["src"] + "include": ["src"], + "exclude": ["src/tui.tsx", "src/tui-shims.d.ts"] } diff --git a/tsconfig.tui.json b/tsconfig.tui.json new file mode 100644 index 0000000..59edd9d --- /dev/null +++ b/tsconfig.tui.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "jsx": "preserve", + "noEmit": true + }, + "include": ["src/tui.tsx", "src/tui-shims.d.ts"] +}