Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
# The compatibility check needs the CLI, but running its postinstall would
# restore dependency lifecycle-script execution. The runner is Linux x64.
- name: Prepare the OpenCode CLI
run: install -m 755 node_modules/@opencode/cli-linux-x64/bin/opencode2 node_modules/@opencode/cli/bin/opencode2.exe
run: install -m 755 node_modules/@opencode/cli-linux-x64/bin/opencode node_modules/@opencode/cli/bin/opencode.exe

- run: npm run check:compatibility

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ When OpenCode reports terminal usage, the token count becomes exact while TPS re

## Install

Built against the OpenCode 2 preview. The earliest known compatible beta is `0.0.0-beta-17595`. The TUI plugin API is still moving, so a much newer or older build may drop the indicator without an error. If the figure never appears, check your version first, then [open an issue](https://github.com/P-Theo/opencode2-tps/issues).
Built against the OpenCode 2 preview. The earliest known compatible beta is `0.0.0-beta-17595`. If the figure never appears, check your version first, then [open an issue](https://github.com/P-Theo/opencode2-tps/issues).

The current plugin release targets the theme API in OpenCode `2.0.10`, using `ctx.theme.text.muted` in place of the old `ctx.theme.text.subdued` field. On older versions that only provide `subdued`, the indicator falls back to pure white (`#ffffff`) instead of the muted theme color.

Install it with the CLI's plugin command, which adds the entry to `~/.config/opencode/cli.json` for you:

Expand Down
2 changes: 1 addition & 1 deletion docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Maintainer runbook.

A path entry loads the `tui.tsx` source through the host's transform; an installed package loads the pre-built `dist/tui.js` through the `exports` subpath. Those are two different code paths, so the bundle gets tested before it goes out.

The pinned `@opencode/*` beta is the build and test baseline. Move it in the same pull request as the fix or feature that needs it; CI validates the pin format, the README compatibility floor, and that the installed CLI reports the pinned version.
The pinned `@opencode/*` version is the build and test baseline. Move it in the same pull request as the fix or feature that needs it; CI validates the pin format, the README compatibility floor, and that the installed CLI reports the pinned version.

A docs-only release skips steps 1 to 5. The bundle is unchanged, and CI already packs and imports the artifact on every push to `main`. Go straight to step 6. A README change that matters on the package page needs a patch release to reach it, because npmjs.com only refreshes the rendered README when a new version is published.

Expand Down
852 changes: 373 additions & 479 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@
"devDependencies": {
"@babel/core": "^7.29.7",
"@babel/preset-typescript": "^7.29.7",
"@opencode/cli": "0.0.0-beta-19381",
"@opencode/plugin": "0.0.0-beta-19381",
"@opencode/theme": "0.0.0-beta-19381",
"@opencode/cli": "2.0.10",
"@opencode/plugin": "2.0.10",
"@opencode/theme": "2.0.10",
"@opentui/core": "^0.5.10",
"@opentui/solid": "^0.5.10",
"@oxlint/plugins": "^1.78.0",
Expand Down
17 changes: 7 additions & 10 deletions scripts/check-compatibility.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// Verifies the OpenCode 2 compatibility pin in package.json and the floor
// stated in the README. The pinned beta is the build and test baseline: move
// it in the same pull request as the fix or feature that needs it.
import { execFileSync } from "node:child_process"
import { readFileSync } from "node:fs"
import { resolve } from "node:path"
Expand All @@ -16,21 +13,21 @@ const versions = packages.map((name) => packageJson.devDependencies[name])

const version = versions[0]

if (!/^0\.0\.0-beta-\d{5,6}$/.test(version)) {
if (!/^2\.\d+\.\d+$/.test(version)) {
throw new Error(`OpenCode 2 compatibility version has an unexpected format: ${version}`)
}

if (!versions.every((candidate) => candidate === version)) {
throw new Error(`OpenCode 2 packages must use one exact version: ${versions.join(", ")}`)
}

const floor = /earliest known compatible beta is\s+`(0\.0\.0-beta-\d{5,6})`/i.exec(readme)?.[1]
const floor = /earliest known compatible (?:beta|version for this release) is\s+`(0\.0\.0-beta-\d{5,6}|2\.\d+\.\d+)`/i.exec(readme)?.[1]

if (floor === undefined) {
throw new Error("README does not state the earliest known compatible beta")
throw new Error("README does not state the earliest known compatible version")
}

if (Number(floor.replace("0.0.0-beta-", "")) > Number(version.replace("0.0.0-beta-", ""))) {
if (floor.localeCompare(version, "en", { numeric: true }) > 0) {
throw new Error(`README floor ${floor} is newer than the pinned compatibility target ${version}`)
}

Expand All @@ -41,12 +38,12 @@ const root = fileURLToPath(new URL("..", import.meta.url))
// execFileSync'd on Windows without a shell, while the native binary runs
// unshelled on every platform despite the .exe name. A normal install's
// postinstall (or CI's prepare step with --ignore-scripts) puts it there.
const executable = resolve(root, "node_modules", "@opencode", "cli", "bin", "opencode2.exe")
const executable = resolve(root, "node_modules", "@opencode", "cli", "bin", "opencode.exe")

const reported = execFileSync(executable, ["--version"], { encoding: "utf8" }).trim()

if (reported !== `opencode2 v${version}`) {
throw new Error(`Expected opencode2 v${version}, got ${reported}`)
if (reported !== `opencode v${version}`) {
throw new Error(`Expected opencode v${version}, got ${reported}`)
}

console.log(`OpenCode 2 compatibility target: ${version} (floor ${floor})`)
3 changes: 0 additions & 3 deletions src/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ import { appendFileSync, lstatSync, mkdirSync, mkdtempSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"

// ---------------------------------------------------------------------------
// debug
//
// Off unless asked for: an unconfigured install must never touch disk.
//
// The log goes inside an owner-only directory instead of straight into the
Expand Down
3 changes: 0 additions & 3 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// ---------------------------------------------------------------------------
// options
//
// `ctx.options` is host-supplied JSON (Record<string, any>), so this is a real
// parsing boundary: every value is validated and clamped, and anything invalid
// falls back to the default rather than propagating NaN into the arithmetic.
Expand Down
5 changes: 1 addition & 4 deletions src/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import { configureDebug, isEnvEnabled, mark } from "./debug.js"
import { TpsTracker } from "./tracker.js"
import { formatLabel, resolveOptions } from "./options.js"

// ---------------------------------------------------------------------------
// plugin

// Event payloads are taken from the SDK's own union (via the non-generic
// `data.listen` signature) rather than restated structurally: handlers are
// contravariant, so hand-written shapes keep typechecking after a field rename.
Expand Down Expand Up @@ -224,7 +221,7 @@ const definition: Plugin.Definition = {
<Show when={label()}>
{(text: () => string) => (
<box width="100%" flexDirection="row" justifyContent="flex-end">
<text fg={ctx.theme.text.subdued}>{`${text()} `}</text>
<text fg={ctx.theme.text.muted}>{`${text()} `}</text>
</box>
)}
</Show>
Expand Down
10 changes: 3 additions & 7 deletions src/tracker.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// ---------------------------------------------------------------------------
// tracker (UI-free)
//
// Measures the rate of the observable model stream: bytes to a rolling
// estimate while output arrives, exact step usage once the host reports it,
// and a frozen average after the run ends.
// Measures the rate of the observable model stream (UI-free by design): bytes
// to a rolling estimate while output arrives, exact step usage once the host
// reports it, and a frozen average after the run ends.

import { mark } from "./debug.js"

Expand All @@ -14,7 +11,6 @@ export interface TpsConfig {
export const DEFAULT_CONFIG: TpsConfig = {
bytesPerToken: 4.75,
}
// The frozen final average stays visible until the next prompt starts a new run.

export const BYTES_PER_TOKEN_MIN = 1

Expand Down
4 changes: 2 additions & 2 deletions tests/entrypoint.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ interface Claim {
interface FakeContext {
readonly options: TpsOptionsInput
readonly app: { readonly version: string }
readonly theme: { readonly text: { readonly subdued: string } }
readonly theme: { readonly text: { readonly muted: string } }
readonly storage: {
readonly memory: (
key: string,
Expand Down Expand Up @@ -127,7 +127,7 @@ function createHarness(options: TpsOptionsInput = {}): Harness {
const context: FakeContext = {
options,
app: { version: "test" },
theme: { text: { subdued: "#888888" } },
theme: { text: { muted: "#888888" } },
storage: {
memory: () => [generation, (mutation: (draft: Generation) => void) => mutation(generation)] as const,
},
Expand Down
7 changes: 3 additions & 4 deletions tests/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { DEBUG_DIR_PREFIX } from "../src/debug.ts"
import type { TpsOptionsInput } from "../src/options.ts"
import definition from "../src/plugin.tsx"

// ---------------------------------------------------------------------------
// setup wiring: the reactive path cannot be rendered headlessly, but the parts
// Setup wiring: the reactive path cannot be rendered headlessly, but the parts
// that matter (which events are subscribed, and when the render timer runs) are
// observable through a fake context and a patched setInterval.

Expand Down Expand Up @@ -48,7 +47,7 @@ interface Generation {
interface FakeContext {
readonly options: TpsOptionsInput
readonly app: { readonly version: string }
readonly theme: { readonly text: { readonly subdued: string } }
readonly theme: { readonly text: { readonly muted: string } }
readonly storage: {
memory: (
key: string,
Expand Down Expand Up @@ -102,7 +101,7 @@ function createHarness(options: TpsOptionsInput = {}) {
const ctx: FakeContext = {
options,
app: { version: "test" },
theme: { text: { subdued: "#888888" } },
theme: { text: { muted: "#888888" } },
storage: {
memory: () => [generation, (mutation: (draft: Generation) => void) => mutation(generation)] as const,
},
Expand Down
Loading