refactor: rombak jadi full CLI/TUI only — hapus semua interface server (api/ws/grpc/web/daemon) + wire TUI ke agent beneran

- Hapus total interfaces: api (REST), ws (WebSocket), grpc, web, daemon (IPC)
- Hapus infra server-only yang jadi dead code: infrastructure/ipc + seluruh
  stack auth (domain/auth, application/auth, persistence/iam, infrastructure/auth jwt/password/oauth_loopback)
- TUI (OpenTUI) jadi interface utama: wire ke runSingleProcess() real runtime,
  ganti mock '(no LLM configured)' dengan turnService.runTurn asli + streaming
  stream_token/reasoning/tool_result/system_note/error ke transcript
- Tampilkan model asli di header (mis. claude-opus-5), abort turn via Ctrl+C
- CLI jadi mode headless tambahan: zesdex --headless '<prompt>' one-shot turn
- Bersihkan package.json (script tui/headless), tsconfig paths, docker-compose,
  AGENTS.md; hapus StartOAuth/login stub
- verified: tsc 0 err, 70 test pass, build dist/zesdex; headless 'pong' real; TUI
  interactive render + real turn 'say pong in one word' -> assistant 'pong'
This commit is contained in:
asepharyana committed 2026-09-03 01:19:27 +07:00
1 parent 34322e492a
commit 7e6ed34f0d
68 files changed
+250 -3661

No files matched your search

+1 -5
View File
@@ -40,7 +40,6 @@ export type Action =
| { tag: "LessonAccept"; name: string }
| { tag: "LessonReject"; name: string }
| { tag: "LessonDelete"; name: string }
| { tag: "StartOAuth"; provider: string }
| { tag: "OpenEditor"; path: string }
| { tag: "McpAdd"; name: string; command: string }
| { tag: "ModelList" }
@@ -154,16 +153,13 @@ export function applyAction(state: AppStateRest, action: Action): void {
toastInfo(state, `Lesson deleted: ${action.name}`);
break;
case "StartOAuth":
toastInfo(state, `OAuth login started for ${action.provider}`);
break;
case "McpAdd":
toastInfo(state, `MCP server added: ${action.name} (${action.command})`);
break;
case "AbortTurn":
state.abortFlag.value = true;
state.turnAbort?.abort();
toastInfo(state, "Aborting current turn...");
break;
-5
View File
@@ -13,7 +13,6 @@ export type Command =
| { tag: "McpOpen" }
| { tag: "Clear" }
| { tag: "ClearConfirm" }
| { tag: "Login"; provider: string }
| { tag: "Edit"; path: string }
| { tag: "McpAdd"; name: string; command: string }
| { tag: "ModelList" }
@@ -41,8 +40,6 @@ export function parseCommand(text: string): Command {
return { tag: "Quit" };
case "/clear":
return arg1 === "" ? { tag: "ClearConfirm" } : { tag: "Clear" };
case "/login":
return arg1 === "" ? { tag: "Login", provider: "" } : { tag: "Login", provider: arg1 };
case "/edit":
return { tag: "Edit", path: arg1 === "" ? "." : arg1 };
case "/mcp":
@@ -86,8 +83,6 @@ export function applyCommand(cmd: Command): Action[] {
return [{ tag: "SystemNote", kind: "clear", message: "Transcript cleared." }];
case "ClearConfirm":
return [{ tag: "OpenOverlay", overlay: "clear_confirm" }];
case "Login":
return [{ tag: "StartOAuth", provider: cmd.provider }];
case "Edit":
return [{ tag: "OpenEditor", path: cmd.path }];
case "McpAdd":
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env bun
/**
* Zesdex TUI — standalone entry point.
* Mirrors the default (TUI) mode of the Rust gateway.
*/
import { runTui } from "./run.ts";
void runTui();
+53 -10
View File
@@ -1,22 +1,65 @@
#!/usr/bin/env bun
/**
* Zesdex TUI — OpenTUI entry point.
* Zesdex TUI — bootstrapper / composition root.
*
* Boots the OpenTUI CLI renderer and mounts `ZesdexApp`, which routes input
* through `handleKey` → `applyAction`. Replaced the earlier raw-mode stdin
* event loop with OpenTUI's native renderer/keyboard handling.
* Wires the real agent runtime (via `runSingleProcess` from the CLI compose
* root) into the OpenTUI app, and installs a `turnRunner` that executes real
* turns against the LLM + tool executor, streaming events into the transcript.
*/
import * as fs from "node:fs";
import { createTuiState } from "./state.ts";
import { newStore, userMessage, type TurnEventSink } from "@zesdex/domain";
import { runSingleProcess, type WiredRuntime } from "../cli/compose.ts";
import { createTuiState, type AppStateRest, type TurnRunner } from "./state.ts";
import { bootTui } from "./ui.tsx";
/** Run the interactive OpenTUI until the user quits. */
/** Build the real turn runner from the wired runtime. */
function makeTurnRunner(runtime: WiredRuntime): TurnRunner {
return async (text, onEvent) => {
const abort = new AbortController();
// Live sink: forward every event to the UI immediately as it happens.
const sink: TurnEventSink = {
push(ev) {
onEvent(ev);
},
drain() {
return [];
},
};
const messages = [userMessage(text)];
await runtime.turnService.runTurn({
messages,
session_dir: runtime.store.base_dir,
workspace_roots: [process.cwd()],
turn_events: sink,
in_flight: { value: false },
abort,
api_key: runtime.apiKey,
model: runtime.model,
api_base: runtime.apiBase,
});
};
}
/** Run the interactive OpenTUI with the real agent wired in. */
export async function runTui(): Promise<void> {
const cwd = process.cwd();
const dataDir = `${process.env.HOME ?? ""}/.local/share/zesdex`;
const sessionDir = `${dataDir}/sessions/${Date.now()}`;
fs.mkdirSync(sessionDir, { recursive: true });
const state = createTuiState([cwd], sessionDir, `${dataDir}/memory`);
// Build the real runtime once (LLM client + tool executor + turn service).
const runtime = await runSingleProcess();
const store = newStore();
void store;
const dataDir = `${process.env.HOME ?? ""}/.local/share/zesdex`;
fs.mkdirSync(dataDir, { recursive: true });
const sessionLabel = new Date().toISOString().replace(/[:T]/g, "-").slice(0, 19);
const sessionDir = `${dataDir}/sessions/${sessionLabel}`;
fs.mkdirSync(sessionDir, { recursive: true });
const memoryDir = `${dataDir}/memory`;
const state: AppStateRest = createTuiState([cwd], sessionDir, memoryDir);
state.turnRunner = makeTurnRunner(runtime);
state.misc.apiConnected = true;
// Surface the real effective model/provider in the header.
state.settings = { provider: runtime.model } as Record<string, unknown>;
await bootTui(state);
}
+18 -4
View File
@@ -7,7 +7,18 @@
* unit-testable in Bun.
*/
import { Roles, type Role } from "@zesdex/domain";
import { Roles, type Role, type TurnEvent } from "@zesdex/domain";
/**
* Driver for executing a single agent turn in the TUI. Implemented by the
* composition root (`run.ts` → `runSingleProcess`), keeping the TUI logic
* layer free of direct wiring. Returns a promise that resolves when the turn
* (including all tool calls) finishes or is aborted.
*/
export type TurnRunner = (
text: string,
onEvent: (ev: TurnEvent) => void,
) => Promise<void>;
/* ── Input state ──────────────────────────────────────────────────── */
@@ -23,9 +34,6 @@ export const COMMANDS: string[] = [
"/help",
"/quit",
"/clear",
"/login",
"/login zen",
"/login openai",
"/edit",
"/mcp add",
"/model",
@@ -390,6 +398,10 @@ export interface AppStateRest {
renderWidthAtCache: number;
mentionFiles: string[];
pendingSubmit: string | null; // set when a turn should be spawned
/** Real agent turn driver (wired by the composition root). */
turnRunner: TurnRunner | null;
/** Abort controller for the in-flight turn (driven by AbortTurn). */
turnAbort: AbortController | null;
}
export const DEFAULT_HELP_TEXT = ` Zesdex TUI — Keyboard Shortcuts
@@ -459,6 +471,8 @@ export function createTuiState(
renderWidthAtCache: 0,
mentionFiles: [],
pendingSubmit: null,
turnRunner: null,
turnAbort: null,
};
}
+79 -15
View File
@@ -119,27 +119,78 @@ export function ZesdexApp(props: { state: AppStateRest; onQuit: () => void }): R
const dimensions = useTerminalDimensionsHook();
// Local-only "turn drain": when a turn is submitted without a live LLM,
// simulate a short thinking delay then append a note. Real wiring happens
// in the CLI. Replaced the old raw-loop poll.
// Turn driver: when a turn is submitted, run it against the real agent and
// stream every event into the transcript as it happens.
useEffect(() => {
const timer = setInterval(() => {
if (state.pendingSubmit != null) {
const text = state.pendingSubmit;
let turnRunning = false;
const poll = async () => {
if (turnRunning) return;
const text = state.pendingSubmit;
if (text == null) return;
if (state.turnRunner == null) {
appendToLastTranscript(
state,
"\n(no agent runtime — run via `zesdex` on the CLI / ensure settings)",
false,
);
state.pendingSubmit = null;
state.turnInFlightFlag.value = true;
setTimeout(() => {
state.turnInFlightFlag.value = false;
appendToLastTranscript(state, `(no LLM configured — processed: "${text}")`, false);
setTick((x: number) => x + 1);
}, 50);
return;
}
setTick((x: number) => x + 1);
}, 120);
turnRunning = true;
const captured = state.pendingSubmit;
state.pendingSubmit = null;
state.turnInFlightFlag.value = true;
const abort = new AbortController();
state.turnAbort = abort;
// Open an assistant message we stream into.
pushTranscript(state, makeChatMessage(Roles.Assistant, ""));
try {
await state.turnRunner(captured ?? text, (ev) => {
switch (ev.kind) {
case "stream_token":
appendToLastTranscript(state, ev.content, false);
break;
case "stream_reasoning":
appendToLastTranscript(state, ev.content, true);
break;
case "system_note":
pushTranscript(
state,
makeChatMessage(Roles.System, `[${ev.systemKind}] ${ev.message}`),
);
break;
case "error":
pushTranscript(state, makeChatMessage(Roles.System, `error: ${ev.message}`));
break;
case "tool_result":
pushTranscript(
state,
makeChatMessage(Roles.Tool, `⇄ ${ev.tool_name} · ${ev.is_error ? "err" : "ok"}`),
);
break;
default:
break;
}
});
} finally {
state.turnInFlightFlag.value = false;
state.turnAbort = null;
state.abortFlag.value = false;
// Re-prime the next poll so queued turns run.
setTimeout(() => {
turnRunning = false;
setTick((x: number) => x + 1);
}, 0);
}
};
const timer = setInterval(() => poll(), 120);
return () => clearInterval(timer);
}, [state, setTick]);
const header = ` zesdex · ${state.settings.provider ?? "?"} `;
const model = String(state.settings.provider ?? "?");
const header = ` zesdex · ${model} `;
const messages = state.transcriptCache.messages;
const scrollFromEnd = messages.length - state.scroll.offset;
const visible = messages.slice(Math.max(0, scrollFromEnd - 40), scrollFromEnd);
@@ -159,6 +210,19 @@ export function ZesdexApp(props: { state: AppStateRest; onQuit: () => void }): R
{oneLine(m.content)}
</text>
))}
{/* Reasoning block for the last assistant message (dimmed). */}
{messages.length > 0 &&
(() => {
const last = messages[messages.length - 1];
if (last && last.role === Roles.Assistant && last.reasoning.length > 0) {
return (
<text fg="gray">
{`reasoning ${oneLine(last.reasoning)}`}
</text>
);
}
return null;
})()}
</scrollbox>
{/* Overlay or input area */}