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
5 changes: 4 additions & 1 deletion src/four-opencode-brain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function calculateIngestTimeout(fileCount: number): number {
}

/** Unified status updates — see src/status.ts */
import { updateStatus, initStatus, initVersion } from "./status";
import { updateStatus, initStatus, initVersion, stopStatusServer } from "./status";



Expand Down Expand Up @@ -738,6 +738,9 @@ const _serverPlugin = async (input: PluginInput) => {
brain_kb_search,
brain_kb_stats,
},
dispose: async () => {
stopStatusServer();
},
};
};

Expand Down
9 changes: 1 addition & 8 deletions src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1 @@
import { createHash } from "crypto";
import { homedir } from "os";
import { join } from "path";

export function getBrainStatusFile(directory: string): string {
const hash = createHash("md5").update(directory).digest("hex").slice(0, 12);
return join(homedir(), ".cache", "opencode", "brain", `status-${hash}.json`);
}
// shared.ts — utilities extracted; HTTP status endpoint replaces file-based communication
62 changes: 50 additions & 12 deletions src/status.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { writeFileSync, mkdirSync, existsSync } from "fs";
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 { getBrainStatusFile } from "./shared";

export type StatusState = "busy" | "success" | "warning" | "error" | "ready";

Expand All @@ -26,7 +28,8 @@ let currentStatus: Record<string, unknown> = { phase: "init", version: "" };
let _version = "";

let _client: PluginInput["client"] | null = null;
let _statusFile = "";
let _server: ReturnType<typeof Bun.serve> | null = null;
let _port = 0;

/** Initialize with client for toast support */
export function initVersion(v: string): void {
Expand All @@ -36,20 +39,55 @@ export function initVersion(v: string): void {

export function initStatus(client: PluginInput["client"], directory: string): void {
_client = client;
_statusFile = getBrainStatusFile(directory);
startStatusServer(directory);
}

export function startStatusServer(directory: string): void {
if (_server) return;

try {
_server = Bun.serve({
port: 0, // OS assigns free port — no collision possible
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/status") {
const payload = { ...currentStatus, version: _version, updated: Date.now() };
return new Response(JSON.stringify(payload), {
headers: { "Content-Type": "application/json" },
});
}
return new Response("Not Found", { status: 404 });
},
});
} catch (err) {
// Fallback: if Bun.serve fails (e.g., out of FDs), TUI will use default state
console.error("[brain] Failed to start status server:", err);
return;
}

_port = _server.port;

// Write port to session-scoped discovery file
try {
const hash = createHash("md5").update(directory).digest("hex").slice(0, 12);
const portFile = join(homedir(), ".cache", "opencode", "brain", `status-port-${hash}.json`);
const dir = portFile.replace(/\/[^/]+$/, "");
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
writeFileSync(portFile, JSON.stringify({ port: _port }));
} catch { /* silently ignore */ }
}

export function stopStatusServer(): void {
if (_server) {
_server.stop();
_server = null;
_port = 0;
}
}

function write(data: Record<string, unknown>): void {
currentStatus = { ...currentStatus, ...data };
const payload = { ...currentStatus, version: _version, updated: Date.now() };
brainBus.emit("status", payload as BrainStatusEvent);
try {
if (_statusFile) {
const dir = _statusFile.replace(/\/[^/]+$/, "");
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
writeFileSync(_statusFile, JSON.stringify(payload));
}
} catch { /* never crash on status file failure */ }
brainBus.emit("status", { ...currentStatus, version: _version } as BrainStatusEvent);
}

/**
Expand Down
33 changes: 28 additions & 5 deletions src/tui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ 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 { getBrainStatusFile } from "./shared";
import { Spinner } from "./spinner";
import { createHash } from "crypto";
import { homedir } from "os";
import { join } from "path";

function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) {
const [indicator, setIndicator] = createSignal("•");
Expand Down Expand Up @@ -81,15 +83,36 @@ function BrainStatusBar(props: { centered?: boolean; api: TuiPluginApi }) {

onMount(() => {
const unsub = brainBus.on("status", handleStatus);
const poll = async () => {

// Resolve port from discovery file, then poll HTTP endpoint
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`);

const resolvePort = async () => {
try {
const statusFile = getBrainStatusFile(props.api.state.path.directory);
const file = Bun.file(statusFile);
const file = Bun.file(portFile);
if (!(await file.exists())) return;
const data = await file.json();
if (data.port) {
statusUrl = `http://127.0.0.1:${data.port}/status`;
}
} catch { /* port file not ready yet */ }
};

const poll = async () => {
if (!statusUrl) {
await resolvePort();
if (!statusUrl) return;
}
try {
const res = await fetch(statusUrl);
if (!res.ok) return;
const data = await res.json();
handleStatus(data as BrainStatusEvent);
} catch { /* silent */ }
} catch { /* server not ready */ }
};

poll();
const timer = setInterval(poll, 200);
onCleanup(() => { unsub(); clearInterval(timer); });
Expand Down
Loading