From 6cd305877b523b652bd37a086efe514c3acba6b0 Mon Sep 17 00:00:00 2001 From: Rizky Faza Date: Mon, 22 Jun 2026 22:58:34 +0700 Subject: [PATCH 1/7] fix(opencode-go): read session.cost, fix QuickJS float truncation, refresh on panel open - Switch from message to session table (cost moved in OpenCode May 2026 migration) - Fix column: data -> model, JSON path: $.model.providerID -> $.providerID - CAST(cost AS TEXT) for QuickJS compat (sqlite3 -json REAL values get 18-decimal precision that QuickJS silently floors to integers) - clampPercent: Math.round -> Math.floor (match OpenCode server behavior) - Trigger refresh on panel visibility change (no more stale cache on reopen) - Accept background batchId in probe events (reserved for future Rust timer) --- plugins/opencode-go/plugin.js | 24 +++++++++++------------- plugins/opencode-go/plugin.test.js | 24 ++++++------------------ src/App.tsx | 11 +++++++++++ src/hooks/use-probe-events.ts | 2 +- 4 files changed, 29 insertions(+), 32 deletions(-) diff --git a/plugins/opencode-go/plugin.js b/plugins/opencode-go/plugin.js index 6a1ce28d2..97a1971fa 100644 --- a/plugins/opencode-go/plugin.js +++ b/plugins/opencode-go/plugin.js @@ -12,23 +12,21 @@ const HISTORY_EXISTS_SQL = ` SELECT 1 AS present - FROM message - WHERE json_valid(data) - AND json_extract(data, '$.providerID') = 'opencode-go' - AND json_extract(data, '$.role') = 'assistant' - AND json_type(data, '$.cost') IN ('integer', 'real') + FROM session + WHERE json_valid(model) + AND json_extract(model, '$.providerID') = 'opencode-go' + AND cost > 0 LIMIT 1 `; const HISTORY_ROWS_SQL = ` SELECT - CAST(COALESCE(json_extract(data, '$.time.created'), time_created) AS INTEGER) AS createdMs, - CAST(json_extract(data, '$.cost') AS REAL) AS cost - FROM message - WHERE json_valid(data) - AND json_extract(data, '$.providerID') = 'opencode-go' - AND json_extract(data, '$.role') = 'assistant' - AND json_type(data, '$.cost') IN ('integer', 'real') + time_updated AS createdMs, + CAST(cost AS TEXT) AS cost + FROM session + WHERE json_valid(model) + AND json_extract(model, '$.providerID') = 'opencode-go' + AND cost > 0 `; function readNumber(value) { @@ -45,7 +43,7 @@ return 0; const percent = (used / limit) * 100; if (!Number.isFinite(percent)) return 0; - return Math.round(Math.max(0, Math.min(100, percent)) * 10) / 10; + return Math.floor(Math.max(0, Math.min(100, percent))); } function toIso(ms) { diff --git a/plugins/opencode-go/plugin.test.js b/plugins/opencode-go/plugin.test.js index d7cdf6f18..69a0a0788 100644 --- a/plugins/opencode-go/plugin.test.js +++ b/plugins/opencode-go/plugin.test.js @@ -26,31 +26,19 @@ function setHistoryQuery(ctx, rows, options = {}) { if (String(sql).includes("SELECT 1 AS present")) { if (options.assertFilters !== false) { expect(String(sql)).toContain( - "json_extract(data, '$.providerID') = 'opencode-go'", - ); - expect(String(sql)).toContain( - "json_extract(data, '$.role') = 'assistant'", - ); - expect(String(sql)).toContain( - "json_type(data, '$.cost') IN ('integer', 'real')", + "json_extract(model, '$.providerID') = 'opencode-go'", ); + expect(String(sql)).toContain("cost > 0"); } return JSON.stringify(list.length > 0 ? [{ present: 1 }] : []); } if (options.assertFilters !== false) { expect(String(sql)).toContain( - "json_extract(data, '$.providerID') = 'opencode-go'", - ); - expect(String(sql)).toContain( - "json_extract(data, '$.role') = 'assistant'", - ); - expect(String(sql)).toContain( - "json_type(data, '$.cost') IN ('integer', 'real')", - ); - expect(String(sql)).toContain( - "COALESCE(json_extract(data, '$.time.created'), time_created)", + "json_extract(model, '$.providerID') = 'opencode-go'", ); + expect(String(sql)).toContain("time_updated"); + expect(String(sql)).toContain("cost > 0"); } return JSON.stringify(list); @@ -204,7 +192,7 @@ describe("opencode-go plugin", () => { const result = plugin.probe(ctx); const monthlyLine = result.lines.find((line) => line.label === "Monthly"); - expect(monthlyLine.used).toBe(4.5); + expect(monthlyLine.used).toBe(4); expect(monthlyLine.resetsAt).toBe("2026-03-25T07:53:16.000Z"); expect(monthlyLine.periodDurationMs).toBe(28 * 24 * 60 * 60 * 1000); }); diff --git a/src/App.tsx b/src/App.tsx index 918e74dcd..756623d90 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -255,6 +255,17 @@ function App() { [pluginStates] ) + useEffect(() => { + const refreshOnShow = () => { + if (!document.hidden) { + handleRefreshAll() + } + } + + document.addEventListener("visibilitychange", refreshOnShow) + return () => document.removeEventListener("visibilitychange", refreshOnShow) + }, [handleRefreshAll]) + return ( { const resultUnlisten = await listen("probe:result", (event) => { - if (activeBatchIds.current.has(event.payload.batchId)) { + if (activeBatchIds.current.has(event.payload.batchId) || event.payload.batchId === "background") { onResult(event.payload.output) } }) From 47d7e0af8c6cec306d53ddc7acf25549178617e9 Mon Sep 17 00:00:00 2001 From: Rizky Faza Date: Tue, 23 Jun 2026 06:55:31 +0700 Subject: [PATCH 2/7] docs(opencode-go): sync provider docs with new session table query - Update SQL examples to use session table, model column, CAST(cost AS TEXT) - Update detection text from 'assistant messages' to 'sessions with cost data' - Update usage scope from 'assistant spend' to 'session spend' --- docs/providers/opencode-go.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/providers/opencode-go.md b/docs/providers/opencode-go.md index 3cd934dd2..1394d1eb6 100644 --- a/docs/providers/opencode-go.md +++ b/docs/providers/opencode-go.md @@ -7,14 +7,14 @@ - **Source of truth:** `~/.local/share/opencode/opencode.db` - **Auth discovery:** `~/.local/share/opencode/auth.json` - **Provider ID:** `opencode-go` -- **Usage scope:** local observed assistant spend only +- **Usage scope:** local observed session spend only ## Detection The plugin enables when either condition is true: - `~/.local/share/opencode/auth.json` contains an `opencode-go` entry with a non-empty `key` -- local OpenCode history already contains `opencode-go` assistant messages with numeric `cost` +- local OpenCode history already contains `opencode-go` sessions with cost data If neither signal exists, the plugin stays hidden. @@ -24,16 +24,15 @@ OpenUsage reads the local OpenCode SQLite database directly: ```sql SELECT - CAST(COALESCE(json_extract(data, '$.time.created'), time_created) AS INTEGER) AS createdMs, - CAST(json_extract(data, '$.cost') AS REAL) AS cost -FROM message -WHERE json_valid(data) - AND json_extract(data, '$.providerID') = 'opencode-go' - AND json_extract(data, '$.role') = 'assistant' - AND json_type(data, '$.cost') IN ('integer', 'real') + time_updated AS createdMs, + CAST(cost AS TEXT) AS cost +FROM session +WHERE json_valid(model) + AND json_extract(model, '$.providerID') = 'opencode-go' + AND cost > 0 ``` -Only assistant messages with numeric `cost` count. Missing remote or other-device usage is not estimated. +Only sessions with a positive cost count. Missing remote or other-device usage is not estimated. ## Limits From 1744ec0df971a51f7f4e4d87b0d8bda4aa1dd4c9 Mon Sep 17 00:00:00 2001 From: Rizky Faza Date: Tue, 23 Jun 2026 07:09:37 +0700 Subject: [PATCH 3/7] test(opencode-go): add regression tests for CAST truncation and visibility refresh - plugin.test.js: verify fractional cost 0.7 parses to used=5 (not 0) - App.test.tsx: verify visibilitychange triggers startBatch - App.tsx: use startBatch directly (bypass handleRefreshAll cooldown) --- plugins/opencode-go/plugin.test.js | 18 ++++++++++++++++++ src/App.test.tsx | 12 ++++++++++++ src/App.tsx | 4 ++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/plugins/opencode-go/plugin.test.js b/plugins/opencode-go/plugin.test.js index 69a0a0788..d601becc0 100644 --- a/plugins/opencode-go/plugin.test.js +++ b/plugins/opencode-go/plugin.test.js @@ -233,6 +233,24 @@ describe("opencode-go plugin", () => { }); }); + it("parses fractional costs from CAST(cost AS TEXT) without integer truncation", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-03-06T12:00:00.000Z")); + + const ctx = makeCtx(); + // cost as a small decimal that would floor to 0 if truncated to integer + setHistoryQuery(ctx, [ + { createdMs: Date.parse("2026-03-06T11:00:00.000Z"), cost: 0.7 }, + ]); + + const plugin = await loadPlugin(); + const result = plugin.probe(ctx); + + // 0.7 / 12 * 100 = 5.83 → Math.floor = 5 + // If QuickJS truncated 0.7 to 0, this would be 0 + expect(result.lines[0].used).toBe(5); + }); + it("returns a soft empty state when sqlite returns malformed JSON and auth exists", async () => { const ctx = makeCtx(); setAuth(ctx); diff --git a/src/App.test.tsx b/src/App.test.tsx index f0a6fe411..f7fb22b42 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -468,6 +468,18 @@ describe("App", () => { await screen.findByText("Now") }) + it("triggers refresh on visibilitychange when panel becomes visible", async () => { + render() + await waitFor(() => expect(state.startBatchMock).toHaveBeenCalled()) + state.startBatchMock.mockClear() + + // Simulate panel becoming visible (document.hidden = false) + Object.defineProperty(document, "hidden", { configurable: true, value: false }) + fireEvent(document, new Event("visibilitychange")) + + await waitFor(() => expect(state.startBatchMock).toHaveBeenCalled()) + }) + it("updates tray icon on probe results when plugin has a primary progress", async () => { state.invokeMock.mockImplementation(async (cmd: string) => { if (cmd === "list_plugins") { diff --git a/src/App.tsx b/src/App.tsx index 756623d90..6fd4a13fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -258,13 +258,13 @@ function App() { useEffect(() => { const refreshOnShow = () => { if (!document.hidden) { - handleRefreshAll() + startBatch() } } document.addEventListener("visibilitychange", refreshOnShow) return () => document.removeEventListener("visibilitychange", refreshOnShow) - }, [handleRefreshAll]) + }, [startBatch]) return ( Date: Tue, 23 Jun 2026 07:11:51 +0700 Subject: [PATCH 4/7] refactor(use-probe-events): extract 'background' magic string into BACKGROUND_BATCH_ID constant --- src/hooks/use-probe-events.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hooks/use-probe-events.ts b/src/hooks/use-probe-events.ts index 8e07b4a67..b9d988954 100644 --- a/src/hooks/use-probe-events.ts +++ b/src/hooks/use-probe-events.ts @@ -17,6 +17,8 @@ type ProbeBatchStarted = { pluginIds: string[] } +export const BACKGROUND_BATCH_ID = "background" + type UseProbeEventsOptions = { onResult: (output: PluginOutput) => void onBatchComplete: () => void @@ -38,7 +40,7 @@ export function useProbeEvents({ onResult, onBatchComplete }: UseProbeEventsOpti const setup = async () => { const resultUnlisten = await listen("probe:result", (event) => { - if (activeBatchIds.current.has(event.payload.batchId) || event.payload.batchId === "background") { + if (activeBatchIds.current.has(event.payload.batchId) || event.payload.batchId === BACKGROUND_BATCH_ID) { onResult(event.payload.output) } }) From 170f1dae969bcb406e2ea73f15d7b40fa595de34 Mon Sep 17 00:00:00 2001 From: Rizky Faza Date: Tue, 23 Jun 2026 07:24:46 +0700 Subject: [PATCH 5/7] docs: confirm time_updated is in milliseconds, add note to plugin.js and provider docs --- docs/providers/opencode-go.md | 2 +- plugins/opencode-go/plugin.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/providers/opencode-go.md b/docs/providers/opencode-go.md index 1394d1eb6..2a24e80bb 100644 --- a/docs/providers/opencode-go.md +++ b/docs/providers/opencode-go.md @@ -32,7 +32,7 @@ WHERE json_valid(model) AND cost > 0 ``` -Only sessions with a positive cost count. Missing remote or other-device usage is not estimated. +Only sessions with a positive cost count. `time_updated` is stored in milliseconds (same unit as `Date.now()`), confirmed from OpenCode's `session` table schema. Missing remote or other-device usage is not estimated. ## Limits diff --git a/plugins/opencode-go/plugin.js b/plugins/opencode-go/plugin.js index 97a1971fa..99bec4931 100644 --- a/plugins/opencode-go/plugin.js +++ b/plugins/opencode-go/plugin.js @@ -184,6 +184,7 @@ } function loadHistory(ctx) { + // time_updated is in milliseconds (same unit as Date.now()), confirmed from OpenCode source const result = queryRows(ctx, HISTORY_ROWS_SQL); if (!result.ok) return result; From 5e67facdf48f9370ce0039ef87af0eb6e70306bf Mon Sep 17 00:00:00 2001 From: Rizky Faza Date: Tue, 23 Jun 2026 07:33:18 +0700 Subject: [PATCH 6/7] docs: update time_updated reference from anomaly code --- docs/providers/opencode-go.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/providers/opencode-go.md b/docs/providers/opencode-go.md index 2a24e80bb..cc8be20d2 100644 --- a/docs/providers/opencode-go.md +++ b/docs/providers/opencode-go.md @@ -32,7 +32,7 @@ WHERE json_valid(model) AND cost > 0 ``` -Only sessions with a positive cost count. `time_updated` is stored in milliseconds (same unit as `Date.now()`), confirmed from OpenCode's `session` table schema. Missing remote or other-device usage is not estimated. +Only sessions with a positive cost count. `time_updated` is stored in milliseconds (same unit as `Date.now()`), confirmed from OpenCode's [`packages/core/src/session/sql.ts`](https://github.com/anomalyco/opencode/blob/dev/packages/core/src/session/sql.ts) where `time_created` and `time_updated` use `Date.now()` as their default value. Missing remote or other-device usage is not estimated. ## Limits From bddf69207743aa009c2e900fa9b9ab4b0ab41858 Mon Sep 17 00:00:00 2001 From: Rizky Faza Date: Tue, 23 Jun 2026 08:19:04 +0700 Subject: [PATCH 7/7] bump: update opencode-go plugin to 0.0.2 --- plugins/opencode-go/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/opencode-go/plugin.json b/plugins/opencode-go/plugin.json index 1b010135d..8664a5f4b 100644 --- a/plugins/opencode-go/plugin.json +++ b/plugins/opencode-go/plugin.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "opencode-go", "name": "OpenCode Go", - "version": "0.0.1", + "version": "0.0.2", "entry": "plugin.js", "icon": "icon.svg", "brandColor": "#000000",