- Remove unused formatAvg, formatPercent, activeDurationMs functions - Remove unused constants (LIVE_STALE_MS, SINGLE_SAMPLE_MS) - Remove unused session average tracking (totalTokens, totalDurationMs, messageCount, totalTtftMs) - Remove unused lastToolCallAt from MessageTiming - Remove prefillTotalTokens/prefillProcessedTokens (set but never read) - Remove unused nt variable from prefill detection loop - Simplify display: xxxx tps (PP) / xxxx tps (TG) / - tps (TG)
11 KiB
AGENTS.md
Structure
tui.tsx— only source file. Exports aTuiPluginModuleas default. No build step; OpenCode compiles JSX at runtime.package.json— declares peer deps (@opencode-ai/plugin,@opentui/core,@opentui/solid,solid-js) and anengines.opencodeconstraint (>=1.3.14). Theexportsfield maps./tuitotui.tsx.
Constraints
- No tsconfig, no test runner, no lint/format.
node_modulesis gitignored.- JSX uses
@opentui/solid(/** @jsxImportSource @opentui/solid */). - The plugin registers via
api.slots.registerto render intosession_prompt_right.
TUI Plugin Loading
OpenCode has TWO separate plugin loaders:
- Server plugins — configured in
opencode.jsonc - TUI plugins — configured in
~/.config/opencode/tui.jsonunder thepluginfield
Server plugins in opencode.jsonc do NOT load TUI plugins. TUI plugins must be listed in tui.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/ ~/.npmrcis respected for private registry auth (Bun's native.npmrcsupport)- For private registries, ensure
~/.npmrchas 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 configorplugin - Run with debug:
opencode --log-level DEBUGoropencode --print-logs - Clear cache:
rm -rf ~/.cache/opencode(removes all plugin caches) - Verify installed version: check
~/.cache/opencode/packages/<plugin>@latest/package.jsonfor 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_urlfromprovider.*.optionswhere value contains "localhost" - Strips
/v1path suffix to get the llama-server base URL - Falls back to
http://localhost:8080default URL
Slot Polling
- Polls
GET /slots?model=...every 500ms (SLOT_POLL_MS = 500) - Prefill detection:
is_processing === trueANDnext_token[0].n_decoded === 0 - Model parameter is required by the
/slotsendpoint - Falls back to
http://localhost:8080default URL
Display Format
PP xxxx tps | GEN xxxx tps
Example: PP 1247 tps | GEN 25 tps
Display Rules
- PP: integer only, no suffix, 4-char fixed-width padded
- TPS: integer only, no suffix, 4-char fixed-width padded
- "----" shown when no value available
- PP only shown during active prefill (
n_decoded === 0) - GEN only shown during active generation (
n_decoded > 0)
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
/slotsendpoint enabled (default)
Deployment
- Forgejo npm registry:
https://git.sync.wtf/api/packages/troed/npm/ - Package name:
oc-ls-stats(llama-server stats, distinct fromoc-tpson npmjs.org) - Auth: token in
~/.npmrc - Git remotes:
origin→ Forgejo,upstream→ GitHub - To update: bump version in
package.json, runnpm 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.
// ❌ 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.
// ❌ 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.
// ❌ WRONG - no actual output
return <text fg="#ff0000" bg="#000000">*** SCOPED v0.0.17 LOADED ***</text>
// ✅ CORRECT
return <SessionPromptRight api={api} sessionID={value.session_id} tracker={tracker} version={version} clock={clock} />
4. Extra Export in package.json
Error: Non-standard exports configuration.
// ❌ WRONG - extra root export
{
"exports": {
".": { "import": "./tui.tsx" },
"./tui": { "import": "./tui.tsx" }
}
}
// ✅ CORRECT - match working plugin config
{
"exports": {
"./tui": { "import": "./tui.tsx" }
}
}
Verification Steps
- Check plugin loads: Look for
[oc-ls-stats] TUI plugin loaded!in OpenCode logs - Clear cache between tests:
rm -rf ~/.cache/opencode - Verify installed version:
cat ~/.cache/opencode/packages/@troed/oc-ls-stats@latest/node_modules/@troed/oc-ls-stats/package.json - Compare with working plugin: Diff against cached
@troed/oc-tps@latestto spot discrepancies
Resolution Timeline
- Confirmed OpenCode CAN load scoped packages from Forgejo (tested with
@troed/oc-tps) - Compared working
@troed/oc-tpsvs non-working@troed/oc-ls-statsline-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=...)
{
"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": 31943,
"n_decoded": 0
}
}
Slot State Phases
| Phase | is_processing |
n_remain |
n_decoded |
n_prompt_tokens |
|---|---|---|---|---|
| Idle | false |
— | — | stable |
| Prefill | true |
>0 (e.g. 31943) |
0 |
incrementing |
| Transition | true |
>0 (e.g. 31999) |
0 |
stable (final) |
| Generation | true |
>0 (decreasing) |
>0 (incrementing) |
stable (final) |
Key insight: n_decoded === 0 is the definitive prefill indicator (when is_processing === true). n_remain === -1 only occurs when n_predict == -1 (unlimited generation), which is rare in practice.
PP Calculation Method
PP is calculated from the per-slot n_prompt_tokens field during prefill:
- Detect prefill: slot with
is_processing === trueANDnext_token[0].n_decoded === 0 - On first detection: capture
slot.idandslot.n_prompt_tokensas baseline - On subsequent polls:
delta = current_n_prompt_tokens - baseline_tokens,dt = (now - startAt) / 1000,PP = delta / dt - Reset when no slot has
n_decoded === 0(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_totaldelta = 49 tokens in 1815ms → PP = 27 (correct but tiny delta) - Line 31: slot 2 prefilling, slot 3 still generating,
prompt_tokens_totaldelta = 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 500ms (SLOT_POLL_MS = 500) - Model parameter is required by the
/slotsendpoint (returns error without it) - Model discovered from: route session → OpenCode config provider parsing
- Falls back to
http://localhost:8080default URL
Display
PP only shown during active prefill (n_decoded === 0). Format: integer only, no suffix (e.g., "1247"). Fixed-width 4 chars, space-padded. Shows "----" when no value.
Why Progress Percentage Is Not Shown
The /slots endpoint does not expose the final prompt size (task->n_tokens() from llama.cpp). During prefill, n_prompt_tokens and n_prompt_tokens_processed are always equal because every token is processed as it's added to the prompt. Progress percentage requires the ratio n_prompt_tokens_processed / task->n_tokens(), but task->n_tokens() is not returned in /slots JSON.
Required llama.cpp changes:
- PR #14685 (merged as part of #14728): adds
prompt_progressstreaming to/completionendpoint withtotal,cache,processed,time_ms. This enables progress for streaming clients but not for/slotspolling. - PR #23454 (merged May 2026): added
n_prompt_tokens,n_prompt_tokens_processed,n_prompt_tokens_cacheto/slots— but NOT the total prompt size. - Needed: expose
n_prompt_tokens_total(orn_tokensfromtask->n_tokens()) in/slotsoutput so polling clients can compute(n_prompt_tokens_processed / n_prompt_tokens_total) * 100.
Why n_remain === -1 Does NOT Indicate Prefill
n_remain === -1 only occurs when n_predict == -1 (unlimited generation). When n_predict is set (e.g., 32000), n_remain = n_predict - n_decoded, which is always positive during prefill. Prefill detection must use n_decoded === 0 instead.
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_decoded === 0).
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