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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ After moving or renaming exports: run full `bun test`; `tests/module-load.test.t
- **`PLUGIN_ROOT`** in `load-config.ts` is `fileURLToPath(new URL("..", import.meta.url))` — do **not** wrap with an extra `dirname` (breaks config path).
- **Sub-agent ids**: only from `session.list` overwrite in `child-session-sync.ts`; do not append via `session.get`.
- **Agents UI totals**: child sessions only; main session excluded by design (see design doc).
- **Eager-safe JSX**: this plugin ships raw TSX; npm installs land under `node_modules`, where opencode/opentui skip the Solid transform (see [opencode#39986](https://github.com/anomalyco/opencode/issues/39986)), so bun's generic JSX compiles `<Show>`/`<For>` children eagerly — accessing a guard variable's property inside children can throw on `undefined` before `when`/`each` runs. Never `!`-assert a guard variable in control-flow children; use `?.`/`??`, bind a local accessor, or accept `| undefined` in child props. Guard against `tests/eager-safe-jsx.test.ts`.
- Comments only for non-obvious behavior.

## Configuration
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ Then reinstall via `Ctrl+P` → install plugin, and **restart OpenCode**.
To avoid the pinning issue entirely, install a **pinned version** instead of `@latest`:

```jsonc
{ "plugin": ["opencode-cache-hit@0.6.3"] }
{ "plugin": ["opencode-cache-hit@0.6.4"] }
```

## Compatibility
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ rm -rf ~/.cache/opencode/packages/opencode-cache-hit@latest
要彻底避免固定问题,可安装**固定版本**而非 `@latest`:

```jsonc
{ "plugin": ["opencode-cache-hit@0.6.3"] }
{ "plugin": ["opencode-cache-hit@0.6.4"] }
```

## 兼容性
Expand Down
23 changes: 22 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"name": "opencode-cache-hit",
"version": "0.6.3",
"version": "0.6.4",
"description": "OpenCode TUI sidebar: prompt cache hit rate, tokens & cost with sub-agent rollup. Works with opencode-visual-cache; optional per-call JSONL timeline.",
"type": "module",
"license": "MIT",
"author": "mengzhu.zhu <mengzhu.loveyou@gmail.com>",
"repository": {
"type": "git",
"url": "https://github.com/zhumengzhu/opencode-cache-hit"
Expand Down Expand Up @@ -65,6 +66,7 @@
"@opentui/solid": ">=0.2.0"
},
"devDependencies": {
"@opentui/solid": "^0.4.5",
"simple-git-hooks": "^2.13.1"
}
}
9 changes: 5 additions & 4 deletions src/cache-ttl-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { type CacheTTLConfig, DEFAULT_CACHE_TTL } from "./plugin-config.ts"
import { getTTL, formatElapsed, DEFAULT_TTL_MS } from "./cache-ttl.ts"
import type { PanelPalette, PanelLayout } from "./tui-panel/index.ts"

function findLastCacheActivity(messages: Accessor<AssistantMessage[]>): AssistantMessage | null {
const msgs = messages()
function findLastCacheActivity(messages: Accessor<AssistantMessage[]> | undefined): AssistantMessage | null {
const msgs = messages?.()
if (!msgs) return null
for (let i = msgs.length - 1; i >= 0; i--) {
const m = msgs[i]
if (
Expand All @@ -26,7 +27,7 @@ function findLastCacheActivity(messages: Accessor<AssistantMessage[]>): Assistan
}

export function CacheTTLView(props: {
messages: Accessor<AssistantMessage[]>
messages?: Accessor<AssistantMessage[]>
config?: CacheTTLConfig
pal: PanelPalette
layout: PanelLayout
Expand Down Expand Up @@ -77,7 +78,7 @@ export function CacheTTLView(props: {
return (
<Show when={elapsed() !== null}>
<text fg={statusColor()}>
{props.layout.row(props.label, `${statusIcon()} ${formatElapsed(elapsed()!)}`, "")}
{props.layout.row(props.label, `${statusIcon()} ${formatElapsed(elapsed() ?? 0)}`, "")}
</text>
</Show>
)
Expand Down
2 changes: 1 addition & 1 deletion src/main-session-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function MainSessionView(props: {
<TuiMetricRow pal={m.pal()} layout={layout} label={m.t().totalHit} value={m.sessionPct()} />
<Show when={props.cacheTTL?.enabled && props.cacheTTL?.providers && props.messages}>
<CacheTTLView
messages={props.messages!}
messages={props.messages}
config={props.cacheTTL}
pal={m.pal()}
layout={layout}
Expand Down
2 changes: 1 addition & 1 deletion src/tui-panel/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export function TuiHitRow(props: {
<span style={{ fg: props.barColor }}>[{props.bar}] </span>
<span style={{ fg: props.textColor }}>{props.pct}</span>
<Show when={props.trend}>
<span style={{ fg: props.trend!.color }}> {props.trend!.text}</span>
<span style={{ fg: props.trend?.color }}> {props.trend?.text}</span>
</Show>
</text>
)
Expand Down
42 changes: 42 additions & 0 deletions tests/eager-render.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, test, expect } from "bun:test"
import { testRender } from "@opentui/solid"
import { TuiHitRow } from "../src/tui-panel/components.tsx"

/**
* Regression guard for opencode#5/#6: npm-installed plugins live under
* node_modules, where opencode/opentui skip babel-preset-solid, so bun's
* generic JSX (jsxDEV) eagerly evaluates <Show> children before the guard
* runs. bun test loads this raw TSX the same way (no solid transform), so
* rendering with an undefined optional prop must not throw.
*/
describe("eager render smoke (npm plugin load path)", () => {
test("TuiHitRow with trend=undefined does not throw", async () => {
await expect(
testRender(() =>
TuiHitRow({
label: "Hit",
bar: "||",
pct: "50%",
barColor: "blue",
textColor: "white",
trend: undefined,
}),
),
).resolves.toBeDefined()
})

test("TuiHitRow with a trend value renders", async () => {
await expect(
testRender(() =>
TuiHitRow({
label: "Hit",
bar: "||",
pct: "50%",
barColor: "blue",
textColor: "white",
trend: { text: "\u21932.0%", color: "green" },
}),
),
).resolves.toBeDefined()
})
})
31 changes: 31 additions & 0 deletions tests/eager-safe-jsx.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, test, expect } from "bun:test"
import { readdirSync, readFileSync } from "node:fs"
import path from "node:path"

const srcDir = path.resolve(import.meta.dir, "../src")

function collectTsx(dir: string, out: string[] = []): string[] {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) collectTsx(full, out)
else if (entry.name.endsWith(".tsx")) out.push(full)
}
return out
}

describe("eager-safe JSX (npm plugin loads under bun's generic JSX transform)", () => {
for (const file of collectTsx(srcDir)) {
test(path.relative(srcDir, file), () => {
const code = readFileSync(file, "utf8")
const problems: string[] = []
const deref = code.match(/\w+!\s*[.\[]/)
if (deref) problems.push(`non-null assertion followed by deref: ${deref[0]}`)
const optChain = code.match(/\?\.\w+\s*[.\[]/)
if (optChain) problems.push(`optional chain then bare deref: ${optChain[0]}`)
expect(
problems,
`${path.relative(srcDir, file)}: opencode loads this raw TSX with bun's eager JSX (solid transform skips node_modules, see AGENTS.md "Eager-safe JSX"). These patterns throw on undefined before <Show>/<For> guards run. Use ?. / ?? / a local accessor instead.`,
).toEqual([])
})
}
})
Loading