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
3 changes: 2 additions & 1 deletion src/event-bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
18 changes: 7 additions & 11 deletions src/four-opencode-brain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";


Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 1 addition & 2 deletions src/hooks/auto-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { log } from "../logger";
/**
* TUI toast notification — silently handles all errors.
*/
import { createToast } from "@four-bytes/opencode-plugin-lib";

// ---------------------------------------------------------------------------
// Constants
Expand Down Expand Up @@ -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();
Expand Down
26 changes: 15 additions & 11 deletions src/status.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<string, unknown> = { phase: "init", version: "" };
let _version = "";

let toastFn: ReturnType<typeof createToast> | null = null;
let _client: PluginInput["client"] | null = null;
let _statusFile = "";

/** Initialize with client for toast support */
Expand All @@ -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);
}

Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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;

Expand All @@ -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;

Expand All @@ -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 } });
}
20 changes: 12 additions & 8 deletions src/tui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading