diff --git a/src/event-bus.ts b/src/event-bus.ts index b075a66..e43e0db 100644 --- a/src/event-bus.ts +++ b/src/event-bus.ts @@ -31,9 +31,10 @@ export interface BrainStatusEvent { current?: number; total?: number; scanning?: boolean; + searching?: boolean; + ingesting?: boolean; blocked?: boolean; busy?: boolean; - searching?: boolean; version?: string; error?: string; } diff --git a/src/four-opencode-brain.ts b/src/four-opencode-brain.ts index 83cc7e1..9d3ce19 100644 --- a/src/four-opencode-brain.ts +++ b/src/four-opencode-brain.ts @@ -53,7 +53,6 @@ function calculateIngestTimeout(fileCount: number): number { } /** Unified status updates — see src/status.ts */ -import { createToast } from "@four-bytes/opencode-plugin-lib"; import { updateStatus, initStatus, initVersion } from "./status"; @@ -64,16 +63,12 @@ const _serverPlugin = async (input: PluginInput) => { sessionCache.reset(); initStatus(client, directory); initVersion(VERSION); - const toast = createToast(client, "Brain 🧠"); // replaced inline with @four-bytes/opencode-plugin-lib log("info", "init", `v${VERSION} loaded`, { pid: process.pid }); setSilent(true); // suppress all subsequent console output // Status published via event bus — TUI subscribes to push-based updates - // Signal TUI we're initializing - updateStatus("busy", { text: "initializing" }); - // Ensure DB + schema on startup try { const db = initBrainDatabase(); @@ -100,19 +95,20 @@ const _serverPlugin = async (input: PluginInput) => { // Fire-and-forget — don't block plugin readiness (async () => { // Signal TUI we're scanning the directory tree - updateStatus("busy", { text: "scanning files" }); + updateStatus("busy", { text: "scanning files", scanning: true, total: 0 }); // Quick preliminary file count for toast + timeout calculation let fileCount = 0; try { const walked = await resolveFiles(directory, true); fileCount = walked.files.length; + updateStatus("busy", { text: "scanning files", scanning: true, total: fileCount }); const timeoutS = (calculateIngestTimeout(fileCount) / 1000).toFixed(0); toast( `Indexing ${fileCount} files… (timeout: ${timeoutS}s)`, "info", "Brain 🧠"); - updateStatus("busy", { text: `ingesting 0/${fileCount}`, progress: 0, current: 0, total: fileCount }); + updateStatus("busy", { text: `ingesting... 0/${fileCount}`, progress: 0, current: 0, total: fileCount, ingesting: true }); } catch { toast( `Indexing ${project?.name ?? "project"}…`, "info", "Brain 🧠"); - updateStatus("busy", { text: "ingesting…", progress: 0 }); + updateStatus("busy", { text: "ingesting...", progress: 0, ingesting: true }); } const ingestDb = initBrainDatabase(); @@ -126,7 +122,7 @@ const _serverPlugin = async (input: PluginInput) => { progressCallback: ({ current, total }) => { const pct = total > 0 ? Math.round((current / total) * 100) : 0; // Update status file every tick so TUI spinner stays live - updateStatus("busy", { text: `ingesting ${current}/${total} (${pct}%)`, progress: pct, current, total }); + updateStatus("busy", { text: `ingesting... ${current}/${total}`, progress: pct, current, total, ingesting: true }); }, }), timeoutMs, @@ -254,7 +250,7 @@ const _serverPlugin = async (input: PluginInput) => { progressCallback: ({ current, total }) => { const pct = total > 0 ? Math.round((current / total) * 100) : 0; // Update status file every tick so TUI spinner stays live - updateStatus("busy", { text: `ingesting ${current}/${total} (${pct}%)`, progress: pct, current, total }); + updateStatus("busy", { text: `ingesting... ${current}/${total}`, progress: pct, current, total, ingesting: true }); }, }), timeoutMs, @@ -299,7 +295,7 @@ const _serverPlugin = async (input: PluginInput) => { project: s.string().optional().describe("Project name or hash to scope search"), }, execute: async (args, toolCtx) => { - updateStatus("busy", { text: "searching" }); + updateStatus("busy", { text: "searching...", searching: true }); const db = initBrainDatabase(); try { const results = await withTimeout( diff --git a/src/hooks/auto-capture.ts b/src/hooks/auto-capture.ts index bf4a0e2..b1edf0f 100644 --- a/src/hooks/auto-capture.ts +++ b/src/hooks/auto-capture.ts @@ -12,7 +12,6 @@ import { log } from "../logger"; /** * TUI toast notification — silently handles all errors. */ -import { createToast } from "@four-bytes/opencode-plugin-lib"; // --------------------------------------------------------------------------- // Constants @@ -346,7 +345,7 @@ export async function onSessionIdle( }); if (client && (decisionsSaved + consideredSaved + errorsSaved) > 0) { - createToast(client, "Brain 🧠")(`Auto-captured ${summaryText}`, "success"); + client.tui.showToast({ body: { title: "Brain 🧠", message: `Auto-captured ${summaryText}`, variant: "success", duration: 5000 } }); } } finally { if (!db) conn.close(); diff --git a/src/status.ts b/src/status.ts index fe0385d..1514354 100644 --- a/src/status.ts +++ b/src/status.ts @@ -1,5 +1,4 @@ import { writeFileSync, mkdirSync, existsSync } from "fs"; -import { createToast } from "@four-bytes/opencode-plugin-lib"; import type { PluginInput } from "@opencode-ai/plugin"; import { brainBus, type BrainStatusEvent } from "./event-bus"; import { getBrainStatusFile } from "./shared"; @@ -17,13 +16,16 @@ export interface StatusOpts { progress?: number; current?: number; total?: number; + scanning?: boolean; + searching?: boolean; + ingesting?: boolean; } /** Merged state — published via event bus on every update */ let currentStatus: Record = { phase: "init", version: "" }; let _version = ""; -let toastFn: ReturnType | null = null; +let _client: PluginInput["client"] | null = null; let _statusFile = ""; /** Initialize with client for toast support */ @@ -33,7 +35,7 @@ export function initVersion(v: string): void { } export function initStatus(client: PluginInput["client"], directory: string): void { - toastFn = createToast(client, "Brain 🧠"); + _client = client; _statusFile = getBrainStatusFile(directory); } @@ -68,7 +70,9 @@ export function updateStatus(state: StatusState, opts?: StatusOpts): void { progress: opts?.progress, current: opts?.current, total: opts?.total, - scanning: false, + scanning: opts?.scanning ?? false, + searching: opts?.searching ?? false, + ingesting: opts?.ingesting ?? false, blocked: false, }); break; @@ -79,8 +83,8 @@ export function updateStatus(state: StatusState, opts?: StatusOpts): void { statusText: opts?.text ?? "", busy: false, }); - if (opts?.toast && toastFn) { - toastFn(opts.toast, opts.toastVariant ?? "success"); + if (opts?.toast && _client) { + _client.tui.showToast({ body: { title: "Brain 🧠", message: opts.toast, variant: opts.toastVariant ?? "success", duration: 5000 } }); } break; @@ -90,8 +94,8 @@ export function updateStatus(state: StatusState, opts?: StatusOpts): void { statusText: opts?.text ?? "", busy: false, }); - if (opts?.toast && toastFn) { - toastFn(opts.toast, opts.toastVariant ?? "warning"); + if (opts?.toast && _client) { + _client.tui.showToast({ body: { title: "Brain 🧠", message: opts.toast, variant: opts.toastVariant ?? "warning", duration: 5000 } }); } break; @@ -101,8 +105,8 @@ export function updateStatus(state: StatusState, opts?: StatusOpts): void { statusText: opts?.text ?? "error occurred", busy: false, }); - if (opts?.toast && toastFn) { - toastFn(opts.toast, "error"); + if (opts?.toast && _client) { + _client.tui.showToast({ body: { title: "Brain 🧠", message: opts.toast, variant: "error", duration: 7000 } }); } break; @@ -118,5 +122,5 @@ export function updateStatus(state: StatusState, opts?: StatusOpts): void { /** Convenience wrapper for direct toast calls without status file updates */ export function toast(msg: string, variant: "info" | "success" | "warning" | "error" = "info", _title?: string): void { - if (toastFn) toastFn(msg, variant); + _client?.tui.showToast({ body: { title: "Brain 🧠", message: msg, variant, duration: variant === "error" ? 7000 : 5000 } }); } diff --git a/src/tui.tsx b/src/tui.tsx index a3dc8a1..e30e1cb 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -33,28 +33,32 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) { return; } - if (data.phase === "busy") { + if (data.phase === "init") { setBusy(true); - setStatus(data.statusText ?? "working"); + setStatus(data.statusText ?? "initializing..."); setFg(pulse % 2 === 0 ? theme().warning : theme().accent); } else if (data.scanning) { setBusy(true); - setStatus("scanning files"); + setStatus("scanning files... " + (data.total ?? 0)); setFg(pulse % 2 === 0 ? theme().warning : theme().accent); - } else if (data.phase === "init") { + } else if (data.ingesting) { setBusy(true); - setStatus(data.statusText ?? "initializing..."); - setFg(pulse % 2 === 0 ? theme().warning : theme().accent); + setCurrent(data.current ?? 0); + setTotal(data.total ?? 0); + setPct(data.progress ?? 0); + setStatus("ingesting... " + (data.current ?? 0) + "/" + (data.total ?? 0) + " (" + (data.progress ?? 0).toFixed(1) + "%)"); + setFg(pulse % 2 === 0 ? theme().warning : theme().success); } else if (data.phase === "ingest") { + // fallback for legacy ingest phase setBusy(true); setCurrent(data.current ?? 0); setTotal(data.total ?? 0); setPct(data.progress ?? 0); - setStatus("ingesting " + (data.current ?? 0) + "/" + (data.total ?? 0) + " (" + (data.progress ?? 0).toFixed(1) + "%)"); + setStatus("ingesting... " + (data.current ?? 0) + "/" + (data.total ?? 0) + " (" + (data.progress ?? 0).toFixed(1) + "%)"); setFg(pulse % 2 === 0 ? theme().warning : theme().success); } else if (data.searching) { setBusy(true); - setStatus("searching"); + setStatus("searching..."); setFg(pulse % 2 === 0 ? theme().warning : theme().success); } else if (data.blocked) { setBusy(false);