@ymys4ever/opencode-throughput-v2 4.0.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,39 +0,0 @@
1
- import type { LogEntry } from "../types.js"
2
-
3
- /** Format a millisecond duration; null (no sample) renders as "N/A". */
4
- export function formatMs(ms: number | null): string {
5
- if (ms == null) return "N/A"
6
- if (ms >= 60000) return (ms / 60000).toFixed(1) + "m"
7
- if (ms >= 1000) return (ms / 1000).toFixed(1) + "s"
8
- return ms.toFixed(0) + "ms"
9
- }
10
-
11
- /** Shared implementation for compact number rendering (k/M suffixes). */
12
- export function formatTokenCount(n: number): string {
13
- if (n >= 1e6) return (n / 1e6).toFixed(2) + "M"
14
- if (n >= 1e3) return (n / 1e3).toFixed(1) + "k"
15
- return n.toFixed(n >= 100 ? 0 : 1)
16
- }
17
-
18
- /** Format a numeric metric; null (no sample) renders as "N/A". */
19
- export function formatNum(n: number | null): string {
20
- if (n == null) return "N/A"
21
- return formatTokenCount(n)
22
- }
23
-
24
- /** Single project-wide truncation: overlong strings become (max - 1) chars + "…". */
25
- export function truncate(s: string, max: number): string {
26
- return s.length > max ? s.slice(0, max - 1) + "…" : s
27
- }
28
-
29
- /** Toast line: model | TTFT | tok/s | latency | ↑in ↓out. */
30
- export function buildLogMsg(entry: LogEntry): string {
31
- const parts = [
32
- truncate(entry.modelID, 28),
33
- formatMs(entry.ttft_ms) + " TTFT",
34
- formatNum(entry.tps) + " tok/s",
35
- formatMs(entry.latency_ms),
36
- "\u2191" + formatNum(entry.inputTokens) + " \u2193" + formatNum(entry.outputTokens),
37
- ]
38
- return parts.join(" | ")
39
- }
@@ -1,14 +0,0 @@
1
- import type { LogEntry } from "../types.js"
2
- import { formatMs, formatNum } from "./entry.js"
3
-
4
- /** Markdown perf-file line (frozen format; trailing newline included). */
5
- export function formatPerfLine(entry: LogEntry): string {
6
- const finishSuffix = entry.finish && entry.finish !== "stop" ? " | " + entry.finish : ""
7
- return (
8
- `${entry.ts} | ${entry.modelID} | TTFT ${formatMs(entry.ttft_ms)}` +
9
- ` | TPS ${formatNum(entry.tps)} tok/s | Latency ${formatMs(entry.latency_ms)}` +
10
- ` | \u2191${formatNum(entry.inputTokens)} \u2193${formatNum(entry.outputTokens)}` +
11
- ` \u2193r${formatNum(entry.reasoningTokens)} | Cost $${entry.cost.toFixed(4)}` +
12
- `${finishSuffix}\n`
13
- )
14
- }
package/src/index.ts DELETED
@@ -1,19 +0,0 @@
1
- import { aggregateAt } from "./core/stats.js"
2
- import type { LogEntry, ModelStats } from "./types.js"
3
-
4
- export { createThroughputPlugin, ThroughputPlugin } from "./plugins/throughput.js"
5
- export { default } from "./plugins/throughput.js"
6
- export type { ThroughputPluginOptions } from "./plugins/throughput.js"
7
- export { createLogStore } from "./io/log-store.js"
8
- export type { LogStore } from "./io/log-store.js"
9
-
10
- /**
11
- * Deprecated facade over aggregateAt — the public API is frozen, semantics
12
- * unchanged. The library's single default-clock point lives here on purpose:
13
- * index.ts is outside the src/core purity guard (test/guard.test.ts).
14
- */
15
- export function aggregate(entries: readonly LogEntry[]): Map<string, ModelStats> {
16
- return aggregateAt(entries, Date.now())
17
- }
18
-
19
- export type { LogEntry, ModelStats }
@@ -1,55 +0,0 @@
1
- import type { LogEntry } from "../types.js"
2
- import { decodeLogContent, encodeLogEntry } from "../core/log-codec.js"
3
- import fs from "fs"
4
- import os from "os"
5
- import path from "path"
6
-
7
- export interface LogStore {
8
- /**
9
- * Fault-tolerant append: dir creation memoized; sync appendFileSync; IO
10
- * errors go to onError (default console.warn), never thrown.
11
- */
12
- append(entry: LogEntry): void
13
- /**
14
- * ENOENT -> empty; decode-tolerant (skips corrupt lines, counts them);
15
- * other IO errors (e.g. EACCES) are thrown to caller.
16
- */
17
- read(): { entries: LogEntry[]; skipped: number }
18
- }
19
-
20
- export function createLogStore(opts?: {
21
- file?: string
22
- onError?: (e: unknown) => void
23
- }): LogStore {
24
- const file = opts?.file ?? path.join(os.homedir(), ".opencode", "throughput.jsonl")
25
- const onError = opts?.onError ?? ((e: unknown) => console.warn(e))
26
-
27
- // Memoized on success only: a failed mkdir retries on the next append.
28
- let dirReady = false
29
- const ensureDir = (): void => {
30
- if (dirReady) return
31
- fs.mkdirSync(path.dirname(file), { recursive: true })
32
- dirReady = true
33
- }
34
-
35
- return {
36
- append(entry) {
37
- try {
38
- ensureDir()
39
- fs.appendFileSync(file, encodeLogEntry(entry), "utf-8")
40
- } catch (e) {
41
- onError(e)
42
- }
43
- },
44
- read() {
45
- let content: string
46
- try {
47
- content = fs.readFileSync(file, "utf-8")
48
- } catch (e) {
49
- if ((e as NodeJS.ErrnoException).code === "ENOENT") return { entries: [], skipped: 0 }
50
- throw e
51
- }
52
- return decodeLogContent(content)
53
- },
54
- }
55
- }
@@ -1,30 +0,0 @@
1
- import type { LogEntry } from "../types.js"
2
- import { formatPerfLine } from "../format/perf.js"
3
- import fs from "fs"
4
- import path from "path"
5
-
6
- /**
7
- * Appends formatPerfLine(entry) to <directory>/.opencode/throughput.md.
8
- * Fully fault-tolerant: any IO error is reported to onError (default: silent)
9
- * and never thrown. Dir creation is memoized on success.
10
- */
11
- export function createPerfFileWriter(
12
- directory: string,
13
- onError?: (e: unknown) => void
14
- ): (entry: LogEntry) => void {
15
- const file = path.join(directory, ".opencode", "throughput.md")
16
- const handleError = onError ?? (() => {})
17
-
18
- let dirReady = false
19
- return (entry: LogEntry) => {
20
- try {
21
- if (!dirReady) {
22
- fs.mkdirSync(path.dirname(file), { recursive: true })
23
- dirReady = true
24
- }
25
- fs.appendFileSync(file, formatPerfLine(entry), "utf-8")
26
- } catch (e) {
27
- handleError(e)
28
- }
29
- }
30
- }
@@ -1,124 +0,0 @@
1
- import { Plugin } from "@opencode/plugin"
2
- import type { LogEntry } from "../types.js"
3
- import { StepTracker } from "../core/step-tracker.js"
4
- import { createPipeline, makeSafeOnError } from "../runtime/pipeline.js"
5
- import { makeOwns } from "../core/ownership.js"
6
- import { createLogStore, type LogStore } from "../io/log-store.js"
7
- import { createPerfFileWriter } from "../io/perf-file.js"
8
- import { executeBenchmark } from "../benchmark.js"
9
-
10
- export type ThroughputPluginOptions = {
11
- /** Injected persistence; defaults to the global ~/.opencode/throughput.jsonl store. */
12
- logStore?: LogStore
13
- /** Diagnostics sink for per-event failures (default: console.error). Never kills the subscription. */
14
- onError?: (e: unknown) => void
15
- /** Clock injection for tests. */
16
- now?: () => number
17
- /** Grace window for late terminal events after session.idle before an entry is recorded as finish:"aborted" (default 5000). */
18
- abortTtlMs?: number
19
- /** Fallback eviction window for active steps that receive neither a terminal event nor session.idle, in ms (default 600000 = 10 min; 0 or negative disables). */
20
- activeTtlMs?: number
21
- }
22
-
23
- export function createThroughputPlugin(options: ThroughputPluginOptions = {}) {
24
- return Plugin.define({
25
- id: "opencode-throughput",
26
- async setup(ctx) {
27
- const onError = options.onError ?? ((e: unknown) => console.error(e))
28
- const tracker = new StepTracker({ activeTtlMs: options.activeTtlMs })
29
- const logStore = options.logStore ?? createLogStore({ onError })
30
- const writePerf = createPerfFileWriter(ctx.location.directory, onError)
31
- const owns = makeOwns(ctx.location.directory)
32
- const now = options.now ?? Date.now
33
- const abortTtlMs = options.abortTtlMs ?? 5000
34
-
35
- await ctx.tool.transform((editor) => {
36
- editor.add({
37
- name: "benchmark",
38
- description:
39
- "Query model performance benchmarks including latency, throughput (TTFT, TPS), token usage, and cost. Shows stats aggregated by model.",
40
- input: {
41
- type: "object",
42
- properties: {
43
- model: {
44
- type: "string",
45
- description:
46
- "Filter by model name (partial match, e.g. 'claude' or 'gpt'). Omit for all models.",
47
- },
48
- last: {
49
- type: "integer",
50
- minimum: 1,
51
- description: "Only show the last N entries (default: show all)",
52
- },
53
- },
54
- additionalProperties: false,
55
- },
56
- async execute(args) {
57
- return await executeBenchmark(args, { logStore })
58
- },
59
- })
60
- })
61
-
62
- const controller = new AbortController()
63
-
64
- // Per-sink isolation: each sink gets its own boundary, so a failing
65
- // append never skips the perf file (and vice versa). Default sinks
66
- // never throw; injected sinks may.
67
- function record(entry: LogEntry) {
68
- try {
69
- logStore.append(entry)
70
- } catch (e) {
71
- onError(e)
72
- }
73
- try {
74
- writePerf(entry)
75
- } catch (e) {
76
- onError(e)
77
- }
78
- }
79
-
80
- const pipeline = createPipeline({
81
- tracker,
82
- owns,
83
- now,
84
- abortTtlMs,
85
- onEntry: record,
86
- onError,
87
- timers: {
88
- set: (cb, ms) => setTimeout(cb, ms),
89
- clear: (h) => clearTimeout(h as Parameters<typeof clearTimeout>[0]),
90
- },
91
- })
92
-
93
- const task = (async () => {
94
- // dispatch() is designed never to throw (it owns the per-event error
95
- // boundary); task.catch below remains the last resort if the
96
- // subscription itself dies.
97
- for await (const ev of ctx.event.subscribe({ signal: controller.signal })) {
98
- pipeline.dispatch(ev)
99
- }
100
- })()
101
- // Last resort if the subscription itself dies (e.g. the host adapter
102
- // fails to encode an event before it reaches the plugin). Report with
103
- // plugin identity, keep the original as `cause`, and stay fail-stop:
104
- // no resubscribe. A throwing onError here is swallowed by safeOnError.
105
- const reportSubscriptionDeath = makeSafeOnError(onError)
106
- task.catch((e: unknown) => {
107
- reportSubscriptionDeath(
108
- new Error(`throughput: event subscription died: ${e instanceof Error ? e.message : String(e)}`, {
109
- cause: e,
110
- })
111
- )
112
- })
113
-
114
- return () => {
115
- pipeline.dispose()
116
- controller.abort()
117
- }
118
- },
119
- })
120
- }
121
-
122
- /** Back-compat default instance (path & exports unchanged). */
123
- export const ThroughputPlugin = createThroughputPlugin()
124
- export default ThroughputPlugin
@@ -1,163 +0,0 @@
1
- import type { StepTracker } from "../core/step-tracker.js"
2
- import { buildAbortedEntry } from "../core/step-tracker.js"
3
- import { normalizeEvent, applyThroughputEvent } from "../core/events.js"
4
- import type { LocationLike } from "../core/ownership.js"
5
- import type { LogEntry } from "../types.js"
6
-
7
- /**
8
- * Timer port: a minimal injectable subset of the host timer API. Wrapping
9
- * calls (arrow functions at the assembly layer) avoids `this`-binding issues
10
- * with bare host references.
11
- */
12
- export type TimerPort = {
13
- set: (cb: () => void, ms: number) => unknown
14
- clear: (handle: unknown) => void
15
- }
16
-
17
- export type PipelineDeps = {
18
- tracker: StepTracker
19
- owns: (ev: LocationLike) => boolean
20
- now: () => number
21
- abortTtlMs: number
22
- onEntry: (entry: LogEntry) => void
23
- onError: (e: unknown) => void
24
- timers?: TimerPort
25
- }
26
-
27
- /**
28
- * Wrap a user-supplied onError so a reporter that throws itself never
29
- * escapes the pipeline. The failure is reported via console.error; if
30
- * console.error itself throws, that propagates unchecked by design.
31
- * (Stage labels — owns/handler/drain/timer — live in code comments only;
32
- * the public onError signature stays single-arg.)
33
- *
34
- * Internal export: shared with the assembly layers (e.g. the server's
35
- * subscription-death reporter); not part of the public index.ts surface.
36
- */
37
- export function makeSafeOnError(onError: (e: unknown) => void): (e: unknown) => void {
38
- return (e) => {
39
- try {
40
- onError(e)
41
- } catch (reporterError) {
42
- console.error(reporterError)
43
- }
44
- }
45
- }
46
-
47
- /**
48
- * Shared engine for both entrypoints: event normalization + application,
49
- * opportunistic expiry drain, and single-timer deadline management.
50
- *
51
- * Semantics (phase-3 rulings):
52
- * - Non-owned events return immediately: no drain, no re-arm (server rule).
53
- * - Owned events always drain + re-arm in a finally — including unknown
54
- * event types (previous switch fall-through) and after handler failures.
55
- * - `recordedAt = now()` is taken only for known events, right before apply.
56
- * - The flush callback re-arms in a finally, so a drain failure never
57
- * breaks the deadline chain.
58
- * - After dispose(), dispatch is a no-op and the armed timer is cleared.
59
- *
60
- * Internal module: not part of the public index.ts surface (tests may deep
61
- * path import).
62
- */
63
- export function createPipeline(deps: PipelineDeps): {
64
- dispatch(raw: unknown): void
65
- dispose(): void
66
- } {
67
- const timers: TimerPort =
68
- deps.timers ?? {
69
- set: (cb, ms) => setTimeout(cb, ms),
70
- clear: (h) => clearTimeout(h as Parameters<typeof clearTimeout>[0]),
71
- }
72
- const safeOnError = makeSafeOnError(deps.onError)
73
- const { tracker, now, abortTtlMs, onEntry } = deps
74
-
75
- let flushTimer: unknown = null
76
- let disposed = false
77
-
78
- function drainExpired(): void {
79
- for (const { assistantMessageID, state } of tracker.collectExpired(now())) {
80
- try {
81
- // Per-record boundary: one failing record never drops the remaining expired entries.
82
- onEntry(buildAbortedEntry(state, assistantMessageID, now()))
83
- } catch (e) {
84
- // drain stage
85
- safeOnError(e)
86
- }
87
- }
88
- }
89
-
90
- // One timer targeting the earliest parked deadline; only its callback clears the handle.
91
- function rearm(): void {
92
- if (flushTimer != null) timers.clear(flushTimer)
93
- flushTimer = null
94
- if (disposed) return
95
- const next = tracker.nextDeadline()
96
- if (next == null) return
97
- flushTimer = timers.set(() => {
98
- flushTimer = null
99
- if (disposed) return
100
- try {
101
- // timer stage; drainExpired is per-record guarded — this outer try
102
- // only catches unexpected collectExpired/nextDeadline failures.
103
- drainExpired()
104
- } catch (e) {
105
- safeOnError(e)
106
- } finally {
107
- rearm()
108
- }
109
- }, Math.max(0, next - now()))
110
- }
111
-
112
- function dispatch(raw: unknown): void {
113
- if (disposed) return // post-dispose dispatch is a no-op
114
-
115
- // owns stage: reading ev.location may throw; current semantics = report
116
- // and skip this turn entirely (no drain).
117
- let owned: boolean
118
- try {
119
- owned = deps.owns(raw as LocationLike)
120
- } catch (e) {
121
- safeOnError(e)
122
- return
123
- }
124
- if (!owned) return // not owned: no drain, no re-arm
125
-
126
- const n = normalizeEvent(raw)
127
- if (!n.ok && n.reason === "malformed") {
128
- // Reserved channel, currently unreachable: normalizeEvent never
129
- // returns "malformed". Wired conservatively: report and skip the turn.
130
- safeOnError(new Error("throughput: malformed event rejected"))
131
- return
132
- }
133
-
134
- try {
135
- if (n.ok) {
136
- // handler stage: recordedAt is taken only for known events, just
137
- // before apply (unknown events never read the clock here).
138
- const recordedAt = now()
139
- const entry = applyThroughputEvent(tracker, n.event, { recordedAt, abortTtlMs })
140
- if (entry) onEntry(entry)
141
- }
142
- } catch (e) {
143
- // handler stage (payload property access or the entry sink)
144
- safeOnError(e)
145
- } finally {
146
- // Owned events always drain + re-arm — unknown types included, and
147
- // even when the handler above failed.
148
- drainExpired()
149
- rearm()
150
- }
151
- }
152
-
153
- function dispose(): void {
154
- if (disposed) return
155
- disposed = true
156
- if (flushTimer != null) {
157
- timers.clear(flushTimer)
158
- flushTimer = null
159
- }
160
- }
161
-
162
- return { dispatch, dispose }
163
- }
@@ -1,191 +0,0 @@
1
- // No solid-js here: under host worker conditions it may resolve to the SSR
2
- // build or a different instance than the renderer's; reactivity flows through the host store.
3
- import type { LogEntry } from "../types.js"
4
- import { StepTracker } from "../core/step-tracker.js"
5
- import { SUBSCRIBED_EVENT_TYPES } from "../core/events.js"
6
- import { createPipeline } from "../runtime/pipeline.js"
7
- import { makeOwns } from "../core/ownership.js"
8
- import { aggregateAt } from "../core/stats.js"
9
- import { buildLogMsg } from "../format/entry.js"
10
- import { buildDisplayIds, type StatsRow } from "../ui/sidebar.js"
11
-
12
- export type ThroughputTuiOptions = {
13
- /** Diagnostics sink for per-event failures (default: console.error). Never kills a subscription. */
14
- onError?: (e: unknown) => void
15
- /** Clock injection for tests. */
16
- now?: () => number
17
- /** Grace window for late terminal events after session.idle before an entry is recorded as finish:"aborted" (default 5000). */
18
- abortTtlMs?: number
19
- /** Fallback eviction window for active steps that receive neither a terminal event nor session.idle, in ms (default 600000 = 10 min; 0 or negative disables). */
20
- activeTtlMs?: number
21
- }
22
-
23
- /** Structural subset of the real TUI plugin context (kept local for testability). */
24
- export type TuiContext = {
25
- location?: { directory?: string }
26
- data: { on: (type: string, handler: (ev: any) => void) => () => void }
27
- ui: {
28
- toast: {
29
- show: (toast: { title: string; message: string; variant: string; duration: number }) => void
30
- }
31
- }
32
- /** Host render hook; keeps the sidebar live regardless of the plugin-side solid build. */
33
- renderer?: { requestRender: () => void }
34
- /** Host live store — the only reactive channel reaching the JSX runtime; optional so tests inject a plain fallback. */
35
- storage?: {
36
- memory: <Value extends object>(
37
- key: string,
38
- options: { readonly initial: Value }
39
- ) => readonly [Value, (mutation: (draft: Value) => void) => void]
40
- }
41
- }
42
-
43
- // StatsRow now lives with the sidebar layout code (ui/sidebar.ts); re-exported
44
- // here so the controller's public type surface is unchanged for consumers.
45
- export type { StatsRow } from "../ui/sidebar.js"
46
-
47
- /** Recent entries shown in the sidebar; keeps the widget small. */
48
- const MAX_RECENT = 10
49
- /** Per-model history cap feeding aggregate(); bounds memory on long sessions. */
50
- const BUFFER_CAP = 200
51
-
52
- /** Sidebar state published to the host store. */
53
- export type ThroughputLiveState = {
54
- open: boolean
55
- recent: LogEntry[]
56
- rows: StatsRow[]
57
- }
58
-
59
- /**
60
- * Testable core of the TUI entry: owns the same event wiring as the server
61
- * plugin but publishes sidebar state through the host's live store.
62
- */
63
- export function createThroughputController(
64
- context: TuiContext,
65
- options: ThroughputTuiOptions = {}
66
- ): {
67
- recent: () => LogEntry[]
68
- statsRows: () => StatsRow[]
69
- open: () => boolean
70
- toggleOpen: () => void
71
- unsubscribe: () => void
72
- } {
73
- const onError = options.onError ?? ((e: unknown) => console.error(e))
74
- const now = options.now ?? Date.now
75
- const abortTtlMs = options.abortTtlMs ?? 5000
76
-
77
- const tracker = new StepTracker({ activeTtlMs: options.activeTtlMs })
78
- // Sidebar-only history: the TUI renders per-model aggregates from this
79
- // in-memory buffer; persistent logging is the server plugin's job.
80
- const buffers = new Map<string, LogEntry[]>()
81
- const owns = makeOwns(context.location?.directory)
82
-
83
- // Live state via the host store (reactive in the renderer's world) with a
84
- // plain-object fallback for hostless environments (tests).
85
- const initial: ThroughputLiveState = { open: true, recent: [], rows: [] }
86
- let fallback: ThroughputLiveState = { ...initial }
87
- const store = context.storage?.memory("opencode-throughput:tui", { initial })
88
- // Hot-reload reset (PR-B): unconditional construction-time reset. The
89
- // closure buffers are per-instance and start empty, and the store is only
90
- // an in-process channel between this controller and the renderer —
91
- // cross-generation persistence (any shape) would render as ghost rows
92
- // that collapse on the first record. Construction happens exactly once
93
- // per generation, so resetting here is strictly correct; a throwing store
94
- // mutation is reported, never fatal.
95
- if (store) {
96
- try {
97
- store[1]((draft) => {
98
- draft.open = true
99
- draft.recent = []
100
- draft.rows = []
101
- })
102
- } catch (e) {
103
- onError(e)
104
- }
105
- }
106
- const read = (): ThroughputLiveState => (store ? store[0] : fallback)
107
- const mutate = (fn: (draft: ThroughputLiveState) => void): void => {
108
- if (store) store[1](fn)
109
- else fn(fallback)
110
- context.renderer?.requestRender()
111
- }
112
-
113
- // Stats are recomputed eagerly on every record(); every recompute returns
114
- // brand-new row/stats objects so identity-based consumers always see
115
- // fresh rows.
116
- function recomputeRows(): StatsRow[] {
117
- const modelIDs = new Set<string>()
118
- for (const entries of buffers.values()) modelIDs.add(entries[entries.length - 1].modelID)
119
- // Display IDs are stable across recomputes: first-seen keeps the 3-char form (see buildDisplayIds).
120
- const displayIds = buildDisplayIds([...modelIDs])
121
- const rows: StatsRow[] = []
122
- for (const [modelKey, entries] of buffers) {
123
- const stats = aggregateAt(entries, now()).get(modelKey)
124
- if (!stats) continue
125
- // modelID taken from the model's most recent entry (structured, not
126
- // split from the aggregate key).
127
- const modelID = entries[entries.length - 1].modelID
128
- rows.push({ stats, modelID, displayID: displayIds.get(modelID)! })
129
- }
130
- return rows
131
- }
132
-
133
- function showToast(entry: LogEntry) {
134
- context.ui.toast.show({
135
- title: "Throughput",
136
- message: buildLogMsg(entry),
137
- // "aborted" is a normal lifecycle outcome, not a failure; every other non-stop finish warns.
138
- variant: entry.finish === "stop" || entry.finish === "aborted" ? "info" : "warning",
139
- duration: 4000,
140
- })
141
- }
142
-
143
- function record(entry: LogEntry) {
144
- const buffer = buffers.get(entry.model) ?? []
145
- buffer.push(entry)
146
- if (buffer.length > BUFFER_CAP) buffer.splice(0, buffer.length - BUFFER_CAP)
147
- buffers.set(entry.model, buffer)
148
- const rows = recomputeRows()
149
- mutate((draft) => {
150
- const next = [...draft.recent, entry]
151
- draft.recent = next.length > MAX_RECENT ? next.slice(-MAX_RECENT) : next
152
- draft.rows = rows
153
- })
154
- showToast(entry)
155
- }
156
-
157
- // Shared engine: owns routing, event application, expiry drain and the
158
- // single flush timer (see runtime/pipeline.ts).
159
- const pipeline = createPipeline({
160
- tracker,
161
- owns,
162
- now,
163
- abortTtlMs,
164
- onEntry: record,
165
- onError,
166
- timers: {
167
- set: (cb, ms) => setTimeout(cb, ms),
168
- clear: (h) => clearTimeout(h as Parameters<typeof clearTimeout>[0]),
169
- },
170
- })
171
-
172
- // Single subscription handler for all monitored event types (the type
173
- // routing itself lives in core/events.ts). `ev: any` is the one allowed
174
- // controlled cast at the host boundary.
175
- const onSessionEvent = (ev: any) => pipeline.dispatch(ev)
176
-
177
- const stops = SUBSCRIBED_EVENT_TYPES.map((t) => context.data.on(t, onSessionEvent))
178
-
179
- function unsubscribe() {
180
- pipeline.dispose()
181
- for (const off of stops) off()
182
- }
183
-
184
- return {
185
- recent: () => read().recent,
186
- statsRows: () => read().rows,
187
- open: () => read().open,
188
- toggleOpen: () => mutate((draft) => { draft.open = !draft.open }),
189
- unsubscribe,
190
- }
191
- }
package/src/tui.tsx DELETED
@@ -1,36 +0,0 @@
1
- import { Plugin } from "@opencode/plugin/tui"
2
- import {
3
- createThroughputController,
4
- type TuiContext,
5
- type ThroughputTuiOptions,
6
- } from "./tui/controller.js"
7
- import { ThroughputWidget, type ThemeTokens } from "./ui/throughput-widget.js"
8
-
9
- export function createThroughputTuiPlugin(options: ThroughputTuiOptions = {}) {
10
- return Plugin.define({
11
- id: "opencode-throughput",
12
- setup(context) {
13
- // Structural bridge to keep the controller testable without host SDK types.
14
- const controller = createThroughputController(context as TuiContext, options)
15
- const stopSlot = context.ui.slot({
16
- append: "sidebar.content",
17
- render: () =>
18
- ThroughputWidget({
19
- theme: context.theme as ThemeTokens,
20
- recent: controller.recent,
21
- stats: controller.statsRows,
22
- open: controller.open,
23
- onToggle: controller.toggleOpen,
24
- }),
25
- })
26
- return () => {
27
- stopSlot()
28
- controller.unsubscribe()
29
- }
30
- },
31
- })
32
- }
33
-
34
- /** Back-compat default instance (path & exports unchanged). */
35
- export const ThroughputTuiPlugin = createThroughputTuiPlugin()
36
- export default ThroughputTuiPlugin