mirror of
https://git.sync.wtf/troed/oc-ls-stats.git
synced 2026-08-31 09:43:38 +03:00
docs: implementation plan for explicit server option
This commit is contained in:
@@ -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<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 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).
|
||||
Reference in New Issue
Block a user