docs: implementation plan for explicit server option

This commit is contained in:
Troed Sångberg
2026-08-21 10:59:42 +02:00
parent 3683c47d94
commit ab79c67d7c
@@ -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 4674 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<string, unknown>
const provider = obj.provider
if (!provider || typeof provider !== "object") return []
const urls: string[] = []
for (const [key, val] of Object.entries(provider as Record<string, unknown>)) {
if (val && typeof val === "object") {
const opts = (val as Record<string, unknown>).options
if (opts && typeof opts === "object") {
const baseURL = (opts as Record<string, unknown>).baseURL || (opts as Record<string, unknown>).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<string, unknown>) : 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 4653)
- `extractProviderUrls` (lines 5574)
- `loadConfigUrls` (lines 7682)
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 345351:
```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).