diff --git a/docs/superpowers/plans/2026-08-21-explicit-server-option.md b/docs/superpowers/plans/2026-08-21-explicit-server-option.md new file mode 100644 index 0000000..2b4cee8 --- /dev/null +++ b/docs/superpowers/plans/2026-08-21-explicit-server-option.md @@ -0,0 +1,286 @@ +# Explicit `server` Option Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Let users configure the llama-server endpoint explicitly via the plugin's `server` option in `tui.json`, independent of provider naming. + +**Architecture:** OpenCode passes plugin options (`[name, options]` tuples in `tui.json`) as the second argument to the TUI plugin function. A new pure function `resolveServerUrls(options, config)` in `src/stats.ts` implements the resolution order (explicit `server` → name-based detection → localhost fallback); `tui.tsx` calls it at startup. Helpers `stripBaseUrlPath` and `extractProviderUrls` move from `tui.tsx` into `src/stats.ts` so the resolution logic is pure and testable. + +**Tech Stack:** TypeScript, Solid JSX (TUI), `node:test` + `tsx` for tests (existing pattern in `test-backoff.ts`). + +**Spec:** `docs/superpowers/specs/2026-08-21-explicit-server-option-design.md` + +--- + +### Task 1: `resolveServerUrls` with moved helpers in `src/stats.ts` + +**Files:** +- Modify: `src/stats.ts` +- Test: `test-backoff.ts` + +- [ ] **Step 1: Write the failing tests** + +Append to `test-backoff.ts` (keep existing imports/tests intact; change the import line): + +```ts +import { backoffDelayMs, resolveServerUrls } from "./src/stats.ts" +``` + +Add at the end of the file: + +```ts +const llamaConfig = { + provider: { + llama: { options: { baseURL: "http://localhost:9090/v1" } }, + }, +} + +test("explicit server wins over provider detection", () => { + assert.deepEqual( + resolveServerUrls({ server: "http://headless.local:8080" }, llamaConfig), + ["http://headless.local:8080"], + ) +}) + +test("explicit server strips /v1 path", () => { + assert.deepEqual( + resolveServerUrls({ server: "http://headless.local:8080/v1" }, {}), + ["http://headless.local:8080"], + ) +}) + +test("non-string server falls back to detection", () => { + assert.deepEqual(resolveServerUrls({ server: 42 }, llamaConfig), [ + "http://localhost:9090", + ]) +}) + +test("empty server falls back to detection", () => { + assert.deepEqual(resolveServerUrls({ server: "" }, llamaConfig), [ + "http://localhost:9090", + ]) +}) + +test("no options uses detected providers", () => { + assert.deepEqual(resolveServerUrls(undefined, llamaConfig), [ + "http://localhost:9090", + ]) +}) + +test("no options and no matching providers falls back to localhost", () => { + assert.deepEqual( + resolveServerUrls(undefined, { + provider: { vision: { options: { baseURL: "http://headless.local:8080/v1" } } }, + }), + ["http://localhost:8080"], + ) +}) +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npx -y tsx test-backoff.ts` +Expected: FAIL — `SyntaxError: The requested module './src/stats.ts' does not provide an export named 'resolveServerUrls'` + +- [ ] **Step 3: Implement** + +Append to `src/stats.ts` (after `processPoll`). These two helpers are moved verbatim from `tui.tsx` (lines 46–74 there; they will be deleted from `tui.tsx` in Task 2): + +```ts +export function stripBaseUrlPath(baseURL: string): string { + try { + const url = new URL(baseURL) + return `${url.origin}` + } catch { + return baseURL.replace(/\/v1(\/.*)?$/, "").replace(/\/$/, "") || baseURL + } +} + +export function extractProviderUrls(config: unknown): string[] { + if (!config || typeof config !== "object") return [] + const obj = config as Record + const provider = obj.provider + if (!provider || typeof provider !== "object") return [] + + const urls: string[] = [] + for (const [key, val] of Object.entries(provider as Record)) { + if (val && typeof val === "object") { + const opts = (val as Record).options + if (opts && typeof opts === "object") { + const baseURL = (opts as Record).baseURL || (opts as Record).base_url + if (typeof baseURL === "string" && key.toLowerCase().includes("llama") && !key.toLowerCase().includes("ollama")) { + urls.push(stripBaseUrlPath(baseURL)) + } + } + } + } + return urls +} + +export function resolveServerUrls(options: unknown, config: unknown): string[] { + const opts = options && typeof options === "object" ? (options as Record) : undefined + const server = opts?.server + if (typeof server === "string" && server.trim() !== "") { + return [stripBaseUrlPath(server)] + } + if (server !== undefined) { + console.warn("[oc-ls-stats] ignoring invalid 'server' plugin option, falling back to provider detection") + } + const detected = extractProviderUrls(config) + return detected.length > 0 ? detected : ["http://localhost:8080"] +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `npx -y tsx test-backoff.ts` +Expected: PASS — all existing backoff tests plus the six new tests (a `console.warn` line appears for the non-string case; that is expected) + +- [ ] **Step 5: Commit** + +```bash +git add src/stats.ts test-backoff.ts +git commit -m "feat: resolve server endpoint from plugin options" +``` + +--- + +### Task 2: Wire options into `tui.tsx` + +**Files:** +- Modify: `tui.tsx:5` (import), `tui.tsx:46-82` (delete moved helpers), `tui.tsx:151` (signature), `tui.tsx:345-351` (startup block) + +- [ ] **Step 1: Update the import** + +Change line 5 from: + +```ts +import { backoffDelayMs, processPoll } from "./src/stats.ts" +``` + +to: + +```ts +import { backoffDelayMs, processPoll, resolveServerUrls } from "./src/stats.ts" +``` + +- [ ] **Step 2: Delete the moved helpers** + +Delete these three functions from `tui.tsx` (now duplicated in `src/stats.ts`): +- `stripBaseUrlPath` (lines 46–53) +- `extractProviderUrls` (lines 55–74) +- `loadConfigUrls` (lines 76–82) + +They are only referenced by the startup block replaced in Step 4. + +- [ ] **Step 3: Accept the options argument** + +Change line 151 from: + +```ts +const tui: TuiPlugin = async (api) => { +``` + +to: + +```ts +const tui: TuiPlugin = async (api, options) => { +``` + +(`TuiPlugin`'s second parameter is `PluginOptions | undefined`; passing it to an `unknown` parameter needs no cast.) + +- [ ] **Step 4: Replace the startup block** + +Replace lines 345–351: + +```ts + try { + const configUrls = await loadConfigUrls(api) + llamaServerUrls = configUrls.length > 0 ? configUrls : ["http://localhost:8080"] + } catch (e) { + console.error("[oc-ls-stats] loadConfigUrls error:", e) + llamaServerUrls = ["http://localhost:8080"] + } +``` + +with: + +```ts + try { + llamaServerUrls = resolveServerUrls(options, api.state.config) + } catch (e) { + console.error("[oc-ls-stats] resolveServerUrls error:", e) + llamaServerUrls = ["http://localhost:8080"] + } +``` + +- [ ] **Step 5: Verify tests still pass and no stale references remain** + +Run: `npx -y tsx test-backoff.ts` +Expected: PASS + +Run: `grep -n "loadConfigUrls\|extractProviderUrls\|stripBaseUrlPath" tui.tsx` +Expected: no output + +- [ ] **Step 6: Commit** + +```bash +git add tui.tsx +git commit -m "feat: read explicit server endpoint from plugin options" +``` + +--- + +### Task 3: Document the option in README + +**Files:** +- Modify: `README.md` ("### Server Discovery" section, around line 48) + +- [ ] **Step 1: Update the Server Discovery section** + +Replace the section body: + +```markdown +The plugin discovers the llama-server URL by reading the OpenCode configuration via the TUI API and extracting `baseURL`/`base_url` fields from provider options whose name contains "llama" (but excludes providers whose name contains "ollama"). Falls back to `http://localhost:8080` if no matching provider is found. +``` + +with: + +```markdown +By default the plugin discovers the llama-server URL by reading the OpenCode configuration via the TUI API and extracting `baseURL`/`base_url` fields from provider options whose name contains "llama" (but excludes providers whose name contains "ollama"). Falls back to `http://localhost:8080` if no matching provider is found. + +If your llama-server provider is named differently, configure the endpoint explicitly via the plugin's `server` option in `tui.json`. An explicit server replaces discovery entirely: + +```json +{ + "plugin": [ + ["@troed/oc-ls-stats@latest", { "server": "http://headless.local:8080" }] + ] +} +``` + +A `baseURL`-style value ending in `/v1` is also accepted. +``` + +- [ ] **Step 2: Commit** + +```bash +git add README.md +git commit -m "docs: document explicit server option" +``` + +--- + +### Task 4: Final verification + +- [ ] **Step 1: Run the full prepack suite** + +Run: `npm run prepack` +Expected: all tests PASS, exit code 0 + +- [ ] **Step 2: Confirm clean tree** + +Run: `git status --short` +Expected: no output (all changes committed) + +No version bump: tagging 1.3.0 is deferred until the planned batch of work is complete (per spec).