refactor: rombak dari monorepo ke project tunggal — flat src/{domain,application,infrastructure,interfaces}, satu package.json tanpa workspaces, tsconfig paths → src/*, update install.sh/Dockerfile/CI

This commit is contained in:
asepharyana
2026-09-02 22:56:26 +07:00
parent e91fda3691
commit 0ee0cc4350
196 changed files with 31 additions and 337 deletions
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bun
/**
* Zesdex TUI — interactive event loop.
*
* Reads keys from the terminal in raw mode, decodes them into `KeyEvent`s,
* routes through `handleKey` → `applyAction`, and re-renders the frame.
* Streaming agent output is applied to the transcript via
* `appendToLastTranscript`.
*
* Mirrors the conceptual flow of `apps/interfaces/tui/src/run.rs` (event
* loop + turn drain) using a plain raw-mode stdin reader instead of
* crossterm/ratatui.
*/
import * as fs from "node:fs";
import { createTuiState, pushTranscript, appendToLastTranscript, makeChatMessage } from "./state.ts";
import { applyAction } from "./action.ts";
import type { Action } from "./action.ts";
import { decodeKey, handleKey } from "./controller.ts";
import type { KeyEvent } from "./controller.ts";
import { renderFrame } from "./render.ts";
import { Roles } from "@zesdex/domain";
function readRaw(): () => void {
const prev = { ...(process.stdout as unknown as { attrs?: unknown }) };
const stdin = process.stdin as unknown as { setRawMode: (b: boolean) => void };
if (typeof stdin.setRawMode === "function") stdin.setRawMode(true);
// Show cursor-off control sequences are handled at frame draw.
void prev;
return () => {
if (typeof stdin.setRawMode === "function") stdin.setRawMode(false);
process.stdout.write("\x1b[?25h\x1b[0m"); // show cursor + reset
};
}
/** Run the interactive TUI until the user quits. */
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`);
const restore = readRaw();
// Seed some transcript so the pane isn't empty on first render.
pushTranscript(state, makeChatMessage(Roles.System, "Zesdex ready. Type a message or /help."));
const stdin = process.stdin as unknown as {
setRawMode: (b: boolean) => void;
on: (ev: string, cb: (chunk: Buffer) => void) => void;
resume: () => void;
};
stdin.resume();
let buffer = "";
stdin.on("data", (chunk: Buffer) => {
buffer += chunk.toString("utf8");
// Process complete escape sequences (each is self-contained).
while (buffer.length > 0) {
const ev = decodeKey(buffer);
if (!ev) {
// Might be a partial escape sequence; wait for more bytes.
break;
}
// consume the decoded sequence length
const consumed = consumedLen(buffer, ev);
buffer = buffer.slice(consumed);
dispatch(ev, state);
}
draw(state);
});
// Turn drain: poll a fake "in-flight" by checking pendingSubmit periodically.
const drainTimer = setInterval(() => {
if (state.pendingSubmit !== null) {
const text = state.pendingSubmit;
state.pendingSubmit = null;
state.turnInFlightFlag.value = true;
state.dirty = true;
// With no live LLM wiring in the pure TUI, simulate a short "thinking"
// then append a fixed assistant note. Real wiring happens in the CLI.
setTimeout(() => {
state.turnInFlightFlag.value = false;
appendToLastTranscript(state, `(no LLM configured — processed: "${text}")`, false);
state.dirty = true;
draw(state);
}, 50);
}
if (state.dirty) draw(state);
}, 100);
draw(state);
// Block until quit.
await new Promise<void>((resolve) => {
const check = setInterval(() => {
if (state.quit) {
clearInterval(check);
clearInterval(drainTimer);
restore();
resolve();
}
}, 50);
});
}
/** Determine how many characters of the input buffer a decoded event consumed. */
function consumedLen(buffer: string, _ev: KeyEvent): number {
// Escape sequences are multi-byte; a plain char is 1 codepoint.
if (buffer.startsWith("\u001b[")) {
// consume through the terminating letter (~ or A-D).
const m = buffer.match(/^\u001b\[([0-9;]*)([A-Z~])/);
return m ? m[0].length : buffer.length;
}
if (buffer.startsWith("\u001b")) {
return buffer.length; // alt sequence
}
return [...buffer][0]!.length;
}
/** Dispatch a decoded key via the controller and apply resulting actions. */
function dispatch(key: KeyEvent, state: ReturnType<typeof createTuiState>): void {
const actions = handleKey(key, state);
for (const action of actions) {
const act = action as Action;
if (act.tag === "QuitConfirm") {
// Go straight to quit on Ctrl+C (simplified confirm).
state.quit = true;
continue;
}
applyAction(state, act);
}
}
/** Draw the current frame. */
function draw(state: ReturnType<typeof createTuiState>): void {
if (!state.dirty) return;
state.dirty = false;
const h = process.stdout.rows ?? 24;
const w = process.stdout.columns ?? 80;
const lines = renderFrame(state, h, w);
process.stdout.write("\x1b[2J\x1b[H"); // clear + home
process.stdout.write(lines.join("\n"));
process.stdout.write("\x1b[?25h");
}