# AGENTS.md ## Structure - **`tui.tsx`** — only source file. Exports a `TuiPluginModule` as default. No build step; OpenCode compiles JSX at runtime. - **`package.json`** — declares peer deps (`@opencode-ai/plugin`, `@opentui/core`, `@opentui/solid`, `solid-js`) and an `engines.opencode` constraint (`>=1.3.14`). The `exports` field maps `./tui` to `tui.tsx`. ## Constraints - No tsconfig, no test runner, no lint/format. - `node_modules` is gitignored. - JSX uses `@opentui/solid` (`/** @jsxImportSource @opentui/solid */`). - The plugin registers via `api.slots.register` to render into `session_prompt_right`. ## TUI Plugin Loading OpenCode has TWO separate plugin loaders: - **Server plugins** — configured in `opencode.jsonc` - **TUI plugins** — configured in `~/.config/opencode/tui.json` under the `plugin` field Server plugins in `opencode.jsonc` do NOT load TUI plugins. TUI plugins must be listed in `tui.json`: ```json {"plugin": ["oc-ls-stats@latest"]} ``` OpenCode's `file://` path loader does not resolve `package.json` exports field for TUI plugins. TUI plugins require npm-style module resolution (package published to npm registry or private npm-compatible registry like Forgejo). ### Important: Plugin Installation - **npm plugins are installed using Bun at startup**, not npm - Cached in `~/.cache/opencode/node_modules/` - `~/.npmrc` is respected for private registry auth (Bun's native `.npmrc` support) - For private registries, ensure `~/.npmrc` has the correct format: ``` registry=https://your-registry/api/npm/ //your-registry/api/npm/:_authToken=YOUR_TOKEN ``` ### tui.json Format Limitations - Only plain strings are supported: `["plugin-name@latest"]` - **Tuple format with options is NOT supported**: `["plugin", { "url": "..." }]` will be rejected - Log shows: `skipping invalid tui config — Expected string | array, got {"url":"..."}` - To pass config to TUI plugins, hardcode in the plugin code or use environment variables ### Debugging Plugin Loading Issues - **Check logs**: `~/.local/share/opencode/log/` (most recent: `opencode.log`) - **Plugin loading errors**: grep for `skipping invalid tui config` or `plugin` - **Run with debug**: `opencode --log-level DEBUG` or `opencode --print-logs` - **Clear cache**: `rm -rf ~/.cache/opencode` (removes all plugin caches) - **Verify installed version**: check `~/.cache/opencode/packages/@latest/package.json` for pinned version ## Prefill Tracking (llama-server) The plugin polls llama-server's `GET /slots` endpoint to track prompt processing (prefill) timing. ### URL Discovery - Falls back to reading OpenCode config from `api.state.path.config` - Parses JSONC (strips `//`, `/* */` comments and trailing commas) - Extracts `baseURL`/`base_url` from `provider.*.options` where value contains "localhost" - Strips `/v1` path suffix to get the llama-server base URL - Falls back to `http://localhost:8080` default URL ### Slot Polling - Polls `GET /slots?model=...` every 2 seconds (`SLOT_POLL_MS = 2000`) - Prefill detection: `is_processing === true` AND `next_token[0].n_remain === -1` - Model parameter is required by the `/slots` endpoint - Falls back to `http://localhost:8080` default URL ### Display Format `PP {pp} | TPS {tps} | AVG {avg}` Example: `PP 1247 | TPS 25 | AVG 25` ### Display Rules - PP: integer only, no suffix, 4-char fixed-width padded - TPS: integer only, 4-char fixed-width padded - AVG: one decimal when < 100, integer when >= 100, 4-char fixed-width padded - "----" shown when no value available ### Limitations - Slot state tracked globally (not per-session) — slot IDs don't map to OpenCode sessions - Shows latest prefill data across all slots - Requires llama-server to have `/slots` endpoint enabled (default) ## Deployment - Forgejo npm registry: `https://git.sync.wtf/api/packages/troed/npm/` - Package name: `oc-ls-stats` (llama-server stats, distinct from `oc-tps` on npmjs.org) - Auth: token in `~/.npmrc` - Git remotes: `origin` → Forgejo, `upstream` → GitHub - To update: bump version in `package.json`, run `npm publish --registry https://git.sync.wtf/api/packages/troed/npm/`, clear cache (`rm -rf ~/.cache/opencode/packages/@troed`), restart OpenCode ## Debugging: Plugin Not Loading / No Output ### Symptom Plugin installs from private registry but produces no output in OpenCode UI. ### Root Causes Identified #### 1. Incorrect TUI Function Signature (CRITICAL) **Error:** Type mismatch prevented plugin from loading. ```typescript // ❌ WRONG - causes load failure const tui: TuiPlugin = async (api, options) => { ... } // ✅ CORRECT const tui: TuiPlugin = async (api) => { ... } ``` The `TuiPlugin` type from `@opencode-ai/plugin/tui` expects exactly one parameter: `api`. #### 2. Malformed Regex in JSONC Parser **Error:** Syntax error in character class regex. ```typescript // ❌ WRONG - invalid regex syntax text.indexOf(/[\s}])/), i + 1) /[\}])]/.test(text[next]) // ✅ CORRECT text.indexOf(/[\s}\]]/, i + 1) /[\s}\]]/.test(text[next]) ``` Regex literals with unescaped parentheses caused JavaScript parse errors. #### 3. Hardcoded Test Text Replaced Component **Error:** Development debug code left in production. ```tsx // ❌ WRONG - no actual output return *** SCOPED v0.0.17 LOADED *** // ✅ CORRECT return ``` #### 4. Extra Export in package.json **Error:** Non-standard exports configuration. ```json // ❌ WRONG - extra root export { "exports": { ".": { "import": "./tui.tsx" }, "./tui": { "import": "./tui.tsx" } } } // ✅ CORRECT - match working plugin config { "exports": { "./tui": { "import": "./tui.tsx" } } } ``` ### Verification Steps 1. **Check plugin loads:** Look for `[oc-ls-stats] TUI plugin loaded!` in OpenCode logs 2. **Clear cache between tests:** `rm -rf ~/.cache/opencode` 3. **Verify installed version:** `cat ~/.cache/opencode/packages/@troed/oc-ls-stats@latest/node_modules/@troed/oc-ls-stats/package.json` 4. **Compare with working plugin:** Diff against cached `@troed/oc-tps@latest` to spot discrepancies ### Resolution Timeline - Confirmed OpenCode CAN load scoped packages from Forgejo (tested with `@troed/oc-tps`) - Compared working `@troed/oc-tps` vs non-working `@troed/oc-ls-stats` line-by-line - Fixed all 4 issues above in version 0.0.20 - Published 0.0.20 to Forgejo registry ## Prefill Rate (PP) Calculation — v0.0.40+ ### Slot State Structure (from `GET /slots?model=...`) ```json { "id": 2, "n_ctx": 160000, "speculative": false, "is_processing": true, "n_prompt_tokens": 12345, "n_prompt_tokens_processed": 12345, "next_token": { "has_next_token": true, "has_new_line": false, "n_remain": -1, "n_decoded": 0 } } ``` ### Slot State Phases | Phase | `is_processing` | `n_remain` | `n_decoded` | `n_prompt_tokens` | |-------|-----------------|------------|-------------|-------------------| | Idle | `false` | — | — | stable | | Prefill | `true` | `-1` | `0` | incrementing | | Transition | `true` | `>0` (e.g. 31999) | `0` | stable (final) | | Generation | `true` | `>0` (decreasing) | `>0` (incrementing) | stable (final) | **Key insight:** `n_remain === -1` is the definitive prefill indicator. `n_decoded === 0` alone is not sufficient — a slot can have `n_decoded === 0` during generation start (transition phase). ### PP Calculation Method PP is calculated from the per-slot `n_prompt_tokens` field during prefill: 1. Detect prefill: slot with `is_processing === true` AND `next_token[0].n_remain === -1` 2. On first detection: capture `slot.id` and `slot.n_prompt_tokens` as baseline 3. On subsequent polls: `delta = current_n_prompt_tokens - baseline_tokens`, `dt = (now - startAt) / 1000`, `PP = delta / dt` 4. Reset when no slot has `n_remain === -1` (prefill ended) ### Why NOT `llamacpp:prompt_tokens_total` from `/metrics` `llamacpp:prompt_tokens_total` is a **global counter across all slots**, not per-model. When multiple slots are active simultaneously (e.g., slot A prefilling while slot B generating), the delta includes tokens from all slots, producing wildly inflated PP values. **Example from debug data (`test-debug-log.jsonl`):** - Line 15: slot 3 prefilling, `prompt_tokens_total` delta = 49 tokens in 1815ms → PP = 27 (correct but tiny delta) - Line 31: slot 2 prefilling, slot 3 still generating, `prompt_tokens_total` delta = 13793 tokens in 668ms → PP = 20,648 (wrong — includes slot 3 tokens) - Actual server console showed ~1200-1300 tokens/sec during prefill ### Why NOT `llamacpp:prompt_tokens_seconds` from `/metrics` `llamacpp:prompt_tokens_seconds` is an **average since server start**, not an instantaneous rate. It smooths out all prefill events into a single running average, making it useless for displaying current prefill speed. ### Slot Polling - Polls `GET /slots?model=...` every 2 seconds (`SLOT_POLL_MS = 2000`) - Model parameter is required by the `/slots` endpoint (returns error without it) - Model discovered from: `tui.json` options → route session → OpenCode config provider parsing - Falls back to `http://localhost:8080` default URL ### Display PP only shown during active prefill (`n_remain === -1`). Format: integer only, no suffix (e.g., "1247"). Fixed-width 4 chars, space-padded. Shows "----" when no value. ## Debugging: PP Value Wrong ### Symptom PP shows 3634 when llama-server console shows ~1200-1300 tokens/sec during prefill. ### Root Cause (fixed in v0.0.40) Used `llamacpp:prompt_tokens_total` from `/metrics` endpoint which is a global counter across all slots. Delta between polls included tokens from other active slots. ### Fix Track `n_prompt_tokens` from the slot data itself (per-slot field) during prefill state (`n_remain === -1`). ### Debug Log Location `/tmp/oc-ls-stats-debug.log` — TUI plugin writes here because `console.log` in TUI doesn't reach main OpenCode log. ### Debug Data Sample debug log with slot states and metrics values: `test-debug-log.jsonl`