From 19d24568c9f2ca1f8060eac85d3fa7e76c577523 Mon Sep 17 00:00:00 2001 From: 4 Bytes Robby Date: Sat, 13 Jun 2026 10:06:01 +0200 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20migrate=20brain=20to=20bus=20system?= =?UTF-8?q?=20=E2=80=94=20real-time=20push=20with=20HTTP=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bun.lock | 4 ++-- package.json | 2 +- src/status.ts | 23 +++++++++++++++++++++-- src/tui.tsx | 30 +++++++++++++++++++++++++----- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/bun.lock b/bun.lock index 97d9463..021c683 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "@four-bytes/four-opencode-brain", "dependencies": { - "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.3.0", + "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.4.0", "@opencode-ai/plugin": "1.16.2", "@opentui/core": "0.3.2", "@opentui/solid": "0.3.2", @@ -81,7 +81,7 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@four-bytes/opencode-plugin-lib": ["@four-bytes/opencode-plugin-lib@github:four-bytes/four-opencode-plugin-lib#af1d900", { "peerDependencies": { "@opencode-ai/plugin": ">=1.16.0" } }, "four-bytes-four-opencode-plugin-lib-af1d900", "sha512-zssN7Eo524vy3NLzJ/VyqscwBFWn3t5ri3abxHOTT29x4bKfTqjJpx7EtHNDeoJ7l3oV+hhCLIY8LZq1Lbdv7A=="], + "@four-bytes/opencode-plugin-lib": ["@four-bytes/opencode-plugin-lib@github:four-bytes/four-opencode-plugin-lib#8eb3a93", { "peerDependencies": { "@opencode-ai/plugin": ">=1.16.0" } }, "four-bytes-four-opencode-plugin-lib-8eb3a93", "sha512-WUtySumBXL7pfo5/w2mu54eCL6pHBvJsF4N3XkQCqtfuMw74T4e/iAKDdwGNNzlmVmuq2fCeMwan3bJH2Q68VA=="], "@huggingface/jinja": ["@huggingface/jinja@0.5.9", "", {}, "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw=="], diff --git a/package.json b/package.json index d92bfd3..e9ea274 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "four-bytes" ], "dependencies": { - "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.3.0", + "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.4.0", "@opencode-ai/plugin": "1.16.2", "@opentui/core": "0.3.2", "@opentui/solid": "0.3.2", diff --git a/src/status.ts b/src/status.ts index c30a674..12300a7 100644 --- a/src/status.ts +++ b/src/status.ts @@ -3,7 +3,8 @@ import { createHash } from "crypto"; import { homedir } from "os"; import { join } from "path"; import type { PluginInput } from "@opencode-ai/plugin"; -import { brainBus, type BrainStatusEvent } from "./event-bus"; +import { BusClient } from "@four-bytes/opencode-plugin-lib"; +import type { BrainStatusEvent } from "./event-bus"; export type StatusState = "busy" | "success" | "warning" | "error" | "ready"; @@ -27,6 +28,7 @@ let _version = ""; let _client: PluginInput["client"] | null = null; let _server: ReturnType | null = null; let _port = 0; +let _busPromise: Promise | null = null; /** Initialize with client for toast support */ export function initVersion(v: string): void { @@ -39,6 +41,16 @@ export function initStatus(client: PluginInput["client"], directory: string): vo startStatusServer(directory); } +function getBus(): Promise { + if (!_busPromise) { + _busPromise = BusClient.connect().catch((err) => { + console.warn("[brain] BusClient connect failed:", (err as Error).message); + throw err; + }); + } + return _busPromise; +} + export function startStatusServer(directory: string): void { if (_server) return; @@ -84,7 +96,14 @@ export function stopStatusServer(): void { function write(data: Record): void { _state.current = { ..._state.current, ...data }; - brainBus.emit("status", { ..._state.current, version: _version } as BrainStatusEvent); + const payload = { ..._state.current, version: _version } as BrainStatusEvent; + + // Real-time push via plugin bus (HTTP fallback still serves status endpoint) + getBus() + .then((bus) => bus.publish("brain/status", payload)) + .catch((err) => { + console.warn("[brain] Bus publish failed:", (err as Error).message); + }); } /** diff --git a/src/tui.tsx b/src/tui.tsx index e980ed2..336ec9c 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -3,7 +3,8 @@ import { createSignal, onMount, onCleanup } from "solid-js"; import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"; import type { RGBA } from "@opentui/core"; -import { brainBus, type BrainStatusEvent } from "./event-bus"; +import { BusTui } from "@four-bytes/opencode-plugin-lib/tui"; +import type { BrainStatusEvent } from "./event-bus"; import { Spinner } from "./spinner"; import { createHash } from "crypto"; import { homedir } from "os"; @@ -60,9 +61,29 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) { }; onMount(() => { - const unsub = brainBus.on("status", handleStatus); + let bus: BusTui | null = null; + let unsub: (() => void) | null = null; + let timer: ReturnType | null = null; - // Resolve port from discovery file, then poll HTTP endpoint + onCleanup(() => { + unsub?.(); + bus?.close(); + if (timer) clearInterval(timer); + }); + + // Real-time WebSocket subscription via plugin bus + BusTui.connect() + .then((b) => { + bus = b; + unsub = b.subscribe("brain/status", (envelope) => { + handleStatus(envelope.payload as BrainStatusEvent); + }); + }) + .catch((err) => { + console.warn("[brain TUI] BusTui connect failed:", (err as Error).message); + }); + + // HTTP fallback for when bus is unavailable (cross-process) let statusUrl = ""; const hash = createHash("md5").update(props.api.state.path.directory).digest("hex").slice(0, 12); const portFile = join(homedir(), ".cache", "opencode", "brain", `status-port-${hash}.json`); @@ -92,8 +113,7 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) { }; poll(); - const timer = setInterval(poll, 200); - onCleanup(() => { unsub(); clearInterval(timer); }); + timer = setInterval(poll, 200); }); const StatusRow = () => ( From b24d980c827a0e5f09f88ec0eb1790e3a262a346 Mon Sep 17 00:00:00 2001 From: 4 Bytes Robby Date: Sat, 13 Jun 2026 10:13:29 +0200 Subject: [PATCH 2/6] feat: use session-scoped bus channels (brain/{sessionID}) --- src/four-opencode-brain.ts | 3 ++- src/status.ts | 10 +++++++++- src/tui.tsx | 7 ++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/four-opencode-brain.ts b/src/four-opencode-brain.ts index 595d57c..ae59a72 100644 --- a/src/four-opencode-brain.ts +++ b/src/four-opencode-brain.ts @@ -53,7 +53,7 @@ function calculateIngestTimeout(fileCount: number): number { } /** Unified status updates — see src/status.ts */ -import { updateStatus, initStatus, initVersion, stopStatusServer, toast } from "./status"; +import { updateStatus, initStatus, initVersion, setSessionId, stopStatusServer, toast } from "./status"; @@ -691,6 +691,7 @@ const _serverPlugin = async (input: PluginInput) => { output.system.push(brainSystemPrompt()); }, "chat.message": async (_hookInput, output) => { + if (_hookInput?.sessionID) setSessionId(_hookInput.sessionID); if (output.message?.role === "user" && output.message?.content) { const stored = await onChatMessage(input, output.message as { role: string; content: string }); if (stored && output.parts) { diff --git a/src/status.ts b/src/status.ts index 12300a7..4f68d1e 100644 --- a/src/status.ts +++ b/src/status.ts @@ -24,6 +24,8 @@ export interface StatusOpts { const _state = { current: {} as Record }; _state.current = { status: "init", statusText: "", version: "" }; let _version = ""; +let _sessionId = ""; +let _channel = "brain/status"; let _client: PluginInput["client"] | null = null; let _server: ReturnType | null = null; @@ -36,6 +38,12 @@ export function initVersion(v: string): void { write({ status: "init", statusText: "initializing..." }); } +export function setSessionId(id: string): void { + if (id === _sessionId) return; + _sessionId = id; + _channel = `brain/${id}`; +} + export function initStatus(client: PluginInput["client"], directory: string): void { _client = client; startStatusServer(directory); @@ -100,7 +108,7 @@ function write(data: Record): void { // Real-time push via plugin bus (HTTP fallback still serves status endpoint) getBus() - .then((bus) => bus.publish("brain/status", payload)) + .then((bus) => bus.publish(_channel, payload)) .catch((err) => { console.warn("[brain] Bus publish failed:", (err as Error).message); }); diff --git a/src/tui.tsx b/src/tui.tsx index 336ec9c..ca426b9 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -10,7 +10,7 @@ import { createHash } from "crypto"; import { homedir } from "os"; import { join } from "path"; -function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) { +function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi; sessionId?: string }) { const [indicator, setIndicator] = createSignal("•"); const [status, setStatus] = createSignal("connecting..."); const [version, setVersion] = createSignal(""); @@ -75,7 +75,8 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) { BusTui.connect() .then((b) => { bus = b; - unsub = b.subscribe("brain/status", (envelope) => { + const channel = `brain/${props.sessionId || "unknown"}`; + unsub = b.subscribe(channel, (envelope) => { handleStatus(envelope.payload as BrainStatusEvent); }); }) @@ -157,7 +158,7 @@ const tui: TuiPlugin = (api) => { api.slots.register({ order: 60, // below deepseek-meter (55) slots: { - sidebar_content: () => , + sidebar_content: (_ctx: any, props: any) => , home_bottom: () => , }, }); From 822572623ed512751ab51e1c52efb1ab6c368944 Mon Sep 17 00:00:00 2001 From: 4 Bytes Robby Date: Sat, 13 Jun 2026 11:22:05 +0200 Subject: [PATCH 3/6] feat: show ingest progress with shared ProgressBar component --- bun.lock | 20 +++++++++++--------- package.json | 2 +- src/tui.tsx | 4 ++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/bun.lock b/bun.lock index 021c683..bbe7aee 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "@four-bytes/four-opencode-brain", "dependencies": { - "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.4.0", + "@four-bytes/opencode-plugin-lib": "file:../four-opencode-plugin-lib", "@opencode-ai/plugin": "1.16.2", "@opentui/core": "0.3.2", "@opentui/solid": "0.3.2", @@ -81,7 +81,7 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@four-bytes/opencode-plugin-lib": ["@four-bytes/opencode-plugin-lib@github:four-bytes/four-opencode-plugin-lib#8eb3a93", { "peerDependencies": { "@opencode-ai/plugin": ">=1.16.0" } }, "four-bytes-four-opencode-plugin-lib-8eb3a93", "sha512-WUtySumBXL7pfo5/w2mu54eCL6pHBvJsF4N3XkQCqtfuMw74T4e/iAKDdwGNNzlmVmuq2fCeMwan3bJH2Q68VA=="], + "@four-bytes/opencode-plugin-lib": ["@four-bytes/opencode-plugin-lib@file:../four-opencode-plugin-lib", { "devDependencies": { "@opencode-ai/plugin": "^1.16.0", "bun-types": "^1.3.0", "typescript": "^6.0.3" }, "peerDependencies": { "@opencode-ai/plugin": ">=1.16.0", "@opentui/solid": "^0.4.1", "solid-js": "^1.9.13" } }], "@huggingface/jinja": ["@huggingface/jinja@0.5.9", "", {}, "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw=="], @@ -187,7 +187,7 @@ "@tinyhttp/content-disposition": ["@tinyhttp/content-disposition@2.2.4", "", {}, "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA=="], - "@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + "@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="], @@ -205,7 +205,7 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.34", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.37", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig=="], "brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], @@ -217,7 +217,7 @@ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - "caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="], + "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -257,7 +257,7 @@ "effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - "electron-to-chromium": ["electron-to-chromium@1.5.368", "", {}, "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.372", "", {}, "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -355,7 +355,7 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@2.0.3", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-vFKpMYFTEQujRQxvdS/u6zlfesws0J40K74w6E1fVsYnIa9WKJKB5xIVVON8L7S39hCNrCVGXcPjrYmCb9lT+w=="], + "msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="], "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], @@ -423,7 +423,7 @@ "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], - "semver": ["semver@7.8.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg=="], + "semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="], "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], @@ -469,7 +469,7 @@ "tree-sitter-typescript": ["tree-sitter-typescript@0.23.2", "", { "dependencies": { "node-addon-api": "^8.2.2", "node-gyp-build": "^4.8.2", "tree-sitter-javascript": "^0.23.1" }, "peerDependencies": { "tree-sitter": "^0.21.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], @@ -515,6 +515,8 @@ "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "bun-ffi-structs/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/package.json b/package.json index e9ea274..f69b85b 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "four-bytes" ], "dependencies": { - "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.4.0", + "@four-bytes/opencode-plugin-lib": "file:../four-opencode-plugin-lib", "@opencode-ai/plugin": "1.16.2", "@opentui/core": "0.3.2", "@opentui/solid": "0.3.2", diff --git a/src/tui.tsx b/src/tui.tsx index ca426b9..0ab9fc4 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -4,6 +4,7 @@ import { createSignal, onMount, onCleanup } from "solid-js"; import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"; import type { RGBA } from "@opentui/core"; import { BusTui } from "@four-bytes/opencode-plugin-lib/tui"; +import { ProgressBar } from "@four-bytes/opencode-plugin-lib/tui-components"; import type { BrainStatusEvent } from "./event-bus"; import { Spinner } from "./spinner"; import { createHash } from "crypto"; @@ -146,6 +147,9 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi; sessionI {busy() ? : {indicator()}} {connecting() ? "connecting..." : status()} + {current() > 0 && total() > 0 && ( + + )} )} From 8d1afe525aecba00c17a949b5ef30afe130f3daa Mon Sep 17 00:00:00 2001 From: 4 Bytes Robby Date: Sun, 14 Jun 2026 09:08:43 +0200 Subject: [PATCH 4/6] fix: use plugin-lib v0.5.0 --- bun.lock | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index bbe7aee..e78e09a 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "@four-bytes/four-opencode-brain", "dependencies": { - "@four-bytes/opencode-plugin-lib": "file:../four-opencode-plugin-lib", + "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.5.0", "@opencode-ai/plugin": "1.16.2", "@opentui/core": "0.3.2", "@opentui/solid": "0.3.2", @@ -81,7 +81,7 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], - "@four-bytes/opencode-plugin-lib": ["@four-bytes/opencode-plugin-lib@file:../four-opencode-plugin-lib", { "devDependencies": { "@opencode-ai/plugin": "^1.16.0", "bun-types": "^1.3.0", "typescript": "^6.0.3" }, "peerDependencies": { "@opencode-ai/plugin": ">=1.16.0", "@opentui/solid": "^0.4.1", "solid-js": "^1.9.13" } }], + "@four-bytes/opencode-plugin-lib": ["@four-bytes/opencode-plugin-lib@github:four-bytes/four-opencode-plugin-lib#6e3e32c", { "peerDependencies": { "@opencode-ai/plugin": ">=1.16.0", "@opentui/solid": "^0.4.1", "solid-js": "^1.9.13" } }, "four-bytes-four-opencode-plugin-lib-6e3e32c", "sha512-TXCpPGPttPsUMX8fVeAIp24Xk8WvbUxXXwtm5LtXawJmN6/pahs/n131WhXP6y0D/ftt+/ZO1nRU9NPye39hog=="], "@huggingface/jinja": ["@huggingface/jinja@0.5.9", "", {}, "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw=="], diff --git a/package.json b/package.json index f69b85b..4e5c0a6 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "four-bytes" ], "dependencies": { - "@four-bytes/opencode-plugin-lib": "file:../four-opencode-plugin-lib", + "@four-bytes/opencode-plugin-lib": "github:four-bytes/four-opencode-plugin-lib#v0.5.0", "@opencode-ai/plugin": "1.16.2", "@opentui/core": "0.3.2", "@opentui/solid": "0.3.2", From dc5ecaf45c45379339bb4da5cfbe930962988b43 Mon Sep 17 00:00:00 2001 From: 4 Bytes Robby Date: Sun, 14 Jun 2026 09:34:44 +0200 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20cubic=20review=20=E2=80=94=20bus=20r?= =?UTF-8?q?econnect,=20unmount=20guard,=20fallback=20channel,=20polling=20?= =?UTF-8?q?stop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/status.ts | 1 + src/tui.tsx | 26 +++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/status.ts b/src/status.ts index 4f68d1e..8008f85 100644 --- a/src/status.ts +++ b/src/status.ts @@ -53,6 +53,7 @@ function getBus(): Promise { if (!_busPromise) { _busPromise = BusClient.connect().catch((err) => { console.warn("[brain] BusClient connect failed:", (err as Error).message); + _busPromise = null; // allow retry on next call throw err; }); } diff --git a/src/tui.tsx b/src/tui.tsx index 0ab9fc4..aa812d6 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -65,8 +65,11 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi; sessionI let bus: BusTui | null = null; let unsub: (() => void) | null = null; let timer: ReturnType | null = null; + let unmounted = false; + let busConnected = false; onCleanup(() => { + unmounted = true; unsub?.(); bus?.close(); if (timer) clearInterval(timer); @@ -75,14 +78,19 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi; sessionI // Real-time WebSocket subscription via plugin bus BusTui.connect() .then((b) => { + if (unmounted) { b.close(); return; } bus = b; - const channel = `brain/${props.sessionId || "unknown"}`; + busConnected = true; + if (timer) { clearInterval(timer); timer = null; } + const channel = props.sessionId ? `brain/${props.sessionId}` : "brain/status"; unsub = b.subscribe(channel, (envelope) => { handleStatus(envelope.payload as BrainStatusEvent); }); }) .catch((err) => { console.warn("[brain TUI] BusTui connect failed:", (err as Error).message); + busConnected = false; + if (!timer) timer = setInterval(poll, 200); }); // HTTP fallback for when bus is unavailable (cross-process) @@ -122,7 +130,13 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi; sessionI 🧠 {version()} {busy() ? : {indicator()}} - {connecting() ? "connecting..." : status()} + {connecting() ? "connecting…" : status()} + {busy() && current() > 0 && total() > 0 && ( + + )} + {busy() && current() > 0 && total() > 0 && ( + + )} ); @@ -145,7 +159,13 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi; sessionI {busy() ? : {indicator()}} - {connecting() ? "connecting..." : status()} + {connecting() ? "connecting…" : status()} + {busy() && current() > 0 && total() > 0 && ( + + )} + {busy() && current() > 0 && total() > 0 && ( + + )} {current() > 0 && total() > 0 && ( From 4c480bf99a1a8fceea3593014d7aadd4892cf3ce Mon Sep 17 00:00:00 2001 From: 4 Bytes Robby Date: Sun, 14 Jun 2026 09:43:59 +0200 Subject: [PATCH 6/6] fix: remove duplicate ProgressBar in non-centered layout #133 --- src/tui.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tui.tsx b/src/tui.tsx index aa812d6..5dcd33f 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -167,9 +167,6 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi; sessionI )} - {current() > 0 && total() > 0 && ( - - )} )}