@sofer_agent/cli 0.3.12 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat.d.ts +0 -1
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +24 -46
- package/dist/chat.js.map +1 -1
- package/dist/tui.d.ts +3 -37
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +190 -337
- package/dist/tui.jsx +170 -0
- package/dist/tui.jsx.map +1 -0
- package/package.json +20 -5
- package/src/bin.ts +1 -0
- package/src/chat.ts +2 -2
- package/bin/sofer +0 -6
- package/src/tui.ts +0 -371
package/dist/tui.jsx
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { render, useKeyboard, useTerminalDimensions } from "@opentui/solid";
|
|
2
|
+
import { For, Show, createEffect, createSignal, onCleanup } from "solid-js";
|
|
3
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
4
|
+
const SPINNER_MS = 80;
|
|
5
|
+
// ── State signals (module-level so chat.ts can push into them) ──────────────
|
|
6
|
+
const [rows, setRows] = createSignal([]);
|
|
7
|
+
const [status, setStatus] = createSignal("idle");
|
|
8
|
+
const [input, setInput] = createSignal("");
|
|
9
|
+
const [info, setInfoSignal] = createSignal({ lastTurnIn: 0, lastTurnOut: 0 });
|
|
10
|
+
const [turnStartedAt, setTurnStartedAt] = createSignal(null);
|
|
11
|
+
const [spinnerFrame, setSpinnerFrame] = createSignal(0);
|
|
12
|
+
const [activeAbort, setActiveAbort] = createSignal(null);
|
|
13
|
+
// ── Exported imperative API (used by chat.ts) ────────────────────────────────
|
|
14
|
+
export function addMessage(role, text) {
|
|
15
|
+
setRows((prev) => [...prev, { role, text }]);
|
|
16
|
+
}
|
|
17
|
+
export function setInfo(patch) {
|
|
18
|
+
setInfoSignal((prev) => ({ ...prev, ...patch }));
|
|
19
|
+
}
|
|
20
|
+
// ── Helper ───────────────────────────────────────────────────────────────────
|
|
21
|
+
function formatElapsed(startedAt) {
|
|
22
|
+
if (!startedAt)
|
|
23
|
+
return "";
|
|
24
|
+
const sec = Math.floor((Date.now() - startedAt) / 1000);
|
|
25
|
+
if (sec < 60)
|
|
26
|
+
return `${sec}s`;
|
|
27
|
+
const m = Math.floor(sec / 60);
|
|
28
|
+
return `${m}m${String(sec % 60).padStart(2, "0")}s`;
|
|
29
|
+
}
|
|
30
|
+
function shortModel(model) {
|
|
31
|
+
const m = model.toLowerCase();
|
|
32
|
+
if (m.includes("sonnet"))
|
|
33
|
+
return "sonnet-4";
|
|
34
|
+
if (m.includes("haiku"))
|
|
35
|
+
return "haiku";
|
|
36
|
+
if (m.includes("opus"))
|
|
37
|
+
return "opus-4";
|
|
38
|
+
return model.length > 16 ? `${model.slice(0, 15)}…` : model;
|
|
39
|
+
}
|
|
40
|
+
const LABEL = {
|
|
41
|
+
you: "you",
|
|
42
|
+
agent: "agent",
|
|
43
|
+
system: "sys",
|
|
44
|
+
error: "err",
|
|
45
|
+
};
|
|
46
|
+
const COLOR = {
|
|
47
|
+
you: "#67e8f9",
|
|
48
|
+
agent: "#86efac",
|
|
49
|
+
system: "#9ca3af",
|
|
50
|
+
error: "#fca5a5",
|
|
51
|
+
};
|
|
52
|
+
// ── Components ────────────────────────────────────────────────────────────────
|
|
53
|
+
function MessageRow(props) {
|
|
54
|
+
return (<box flexDirection="row" marginBottom={1}>
|
|
55
|
+
<text fg={COLOR[props.line.role]} flexShrink={0}>
|
|
56
|
+
{` ${LABEL[props.line.role].padEnd(5)} `}
|
|
57
|
+
</text>
|
|
58
|
+
<text wrapMode="word" flexGrow={1} fg="#e5e7eb">
|
|
59
|
+
{props.line.text}
|
|
60
|
+
</text>
|
|
61
|
+
</box>);
|
|
62
|
+
}
|
|
63
|
+
function App(props) {
|
|
64
|
+
const dims = useTerminalDimensions();
|
|
65
|
+
// Spinner tick — only while thinking
|
|
66
|
+
createEffect(() => {
|
|
67
|
+
if (status() !== "thinking") {
|
|
68
|
+
setSpinnerFrame(0);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const id = setInterval(() => setSpinnerFrame((f) => (f + 1) % SPINNER_FRAMES.length), SPINNER_MS);
|
|
72
|
+
onCleanup(() => clearInterval(id));
|
|
73
|
+
});
|
|
74
|
+
useKeyboard((evt) => {
|
|
75
|
+
if (evt.ctrl && evt.name === "c") {
|
|
76
|
+
evt.preventDefault();
|
|
77
|
+
props.onExit();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (evt.name === "escape") {
|
|
81
|
+
const ab = activeAbort();
|
|
82
|
+
if (ab && !ab.signal.aborted)
|
|
83
|
+
ab.abort();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (status() === "thinking")
|
|
87
|
+
return; // block input during turns
|
|
88
|
+
if (evt.name === "return") {
|
|
89
|
+
const text = input().trim();
|
|
90
|
+
if (!text)
|
|
91
|
+
return;
|
|
92
|
+
if (text === "/exit" || text === "/quit") {
|
|
93
|
+
props.onExit();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
addMessage("you", text);
|
|
97
|
+
setInput("");
|
|
98
|
+
const ctrl = new AbortController();
|
|
99
|
+
setActiveAbort(ctrl);
|
|
100
|
+
setStatus("thinking");
|
|
101
|
+
setTurnStartedAt(Date.now());
|
|
102
|
+
props.onSubmit(text, ctrl.signal).finally(() => {
|
|
103
|
+
setStatus("idle");
|
|
104
|
+
setTurnStartedAt(null);
|
|
105
|
+
setActiveAbort(null);
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (evt.name === "backspace" || evt.name === "delete") {
|
|
110
|
+
setInput((p) => p.slice(0, -1));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (evt.sequence && !evt.ctrl && !evt.meta && !evt.option) {
|
|
114
|
+
setInput((p) => p + evt.sequence);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const { agent, config } = props;
|
|
118
|
+
const addr = () => `${agent.address.slice(0, 6)}…${agent.address.slice(-4)}`;
|
|
119
|
+
return (<box flexDirection="column" width={dims().width} height={dims().height}>
|
|
120
|
+
{/* Scrollable chat history */}
|
|
121
|
+
<scrollbox flexGrow={1} flexShrink={1} stickyScroll stickyStart="bottom" contentOptions={{ flexDirection: "column", paddingLeft: 0, paddingRight: 1, paddingTop: 1, paddingBottom: 1 }}>
|
|
122
|
+
<For each={rows()}>{(line) => <MessageRow line={line}/>}</For>
|
|
123
|
+
</scrollbox>
|
|
124
|
+
|
|
125
|
+
{/* Spinner / hint row */}
|
|
126
|
+
<box flexDirection="row" flexShrink={0} paddingLeft={3} paddingRight={2} marginTop={1}>
|
|
127
|
+
<text fg="#67e8f9" flexGrow={1}>
|
|
128
|
+
{(() => {
|
|
129
|
+
if (status() !== "thinking")
|
|
130
|
+
return " ";
|
|
131
|
+
spinnerFrame(); // reactive dependency
|
|
132
|
+
const elapsed = formatElapsed(turnStartedAt());
|
|
133
|
+
const frame = SPINNER_FRAMES[spinnerFrame()];
|
|
134
|
+
return elapsed
|
|
135
|
+
? `${frame} thinking… ${elapsed} (esc to interrupt)`
|
|
136
|
+
: `${frame} thinking… (esc to interrupt)`;
|
|
137
|
+
})()}
|
|
138
|
+
</text>
|
|
139
|
+
</box>
|
|
140
|
+
|
|
141
|
+
{/* Input box */}
|
|
142
|
+
<box flexDirection="row" flexShrink={0} minHeight={3} maxHeight={8} borderStyle="rounded" borderColor="#374151" paddingLeft={1} paddingRight={1} marginLeft={2} marginRight={2}>
|
|
143
|
+
<text fg="#67e8f9" flexShrink={0}>{"› "}</text>
|
|
144
|
+
<text wrapMode="word" flexGrow={1} fg="#e5e7eb">
|
|
145
|
+
{`${input()}${status() === "idle" ? "▋" : ""}`}
|
|
146
|
+
</text>
|
|
147
|
+
</box>
|
|
148
|
+
|
|
149
|
+
{/* Footer */}
|
|
150
|
+
<box flexDirection="row" flexShrink={0} paddingLeft={2} paddingRight={2}>
|
|
151
|
+
<text fg="#9ca3af">
|
|
152
|
+
{[
|
|
153
|
+
addr(),
|
|
154
|
+
`${config.network}`,
|
|
155
|
+
shortModel(config.brain.model),
|
|
156
|
+
info().balance ? `${info().balance} SUI` : null,
|
|
157
|
+
`in ${agent.usage.tokens.inputTokens} out ${agent.usage.tokens.outputTokens} $${agent.usage.costUsd.toFixed(4)}`,
|
|
158
|
+
"/exit",
|
|
159
|
+
].filter(Boolean).join(" · ")}
|
|
160
|
+
</text>
|
|
161
|
+
</box>
|
|
162
|
+
</box>);
|
|
163
|
+
}
|
|
164
|
+
// ── Main entry ────────────────────────────────────────────────────────────────
|
|
165
|
+
export async function runTui(agent, config, onSubmit) {
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
render(() => (<App agent={agent} config={config} onSubmit={onSubmit} onExit={resolve}/>));
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=tui.jsx.map
|
package/dist/tui.jsx.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui.jsx","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sofer_agent/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
|
-
"sofer": "./bin
|
|
6
|
+
"sofer": "./dist/bin.js"
|
|
7
7
|
},
|
|
8
|
-
"files": [
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
9
12
|
"publishConfig": {
|
|
10
13
|
"access": "public"
|
|
11
14
|
},
|
|
@@ -13,9 +16,16 @@
|
|
|
13
16
|
"type": "git",
|
|
14
17
|
"url": "https://github.com/anomalyco/sofer"
|
|
15
18
|
},
|
|
16
|
-
"keywords": [
|
|
19
|
+
"keywords": [
|
|
20
|
+
"sui",
|
|
21
|
+
"ai",
|
|
22
|
+
"agent",
|
|
23
|
+
"walrus",
|
|
24
|
+
"sovereign",
|
|
25
|
+
"cli"
|
|
26
|
+
],
|
|
17
27
|
"scripts": {
|
|
18
|
-
"build": "tsc -b && chmod +x bin
|
|
28
|
+
"build": "tsc -b && npx babel src/tui.tsx --out-file dist/tui.js --config-file ./babel.config.json && chmod +x dist/bin.js",
|
|
19
29
|
"typecheck": "tsc --noEmit"
|
|
20
30
|
},
|
|
21
31
|
"dependencies": {
|
|
@@ -26,7 +36,12 @@
|
|
|
26
36
|
"solid-js": "1.9.13"
|
|
27
37
|
},
|
|
28
38
|
"devDependencies": {
|
|
39
|
+
"@babel/cli": "^7.27.0",
|
|
40
|
+
"@babel/core": "^7.27.0",
|
|
41
|
+
"@babel/plugin-syntax-jsx": "^8.0.0",
|
|
42
|
+
"@babel/preset-typescript": "^7.27.0",
|
|
29
43
|
"@types/node": "^22.10.2",
|
|
44
|
+
"babel-preset-solid": "^1.9.0",
|
|
30
45
|
"typescript": "^5.7.2"
|
|
31
46
|
}
|
|
32
47
|
}
|
package/src/bin.ts
CHANGED
package/src/chat.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
SoferAgent,
|
|
9
9
|
} from "@sofer_agent/core";
|
|
10
10
|
import { loadSecrets } from "./env.js";
|
|
11
|
-
import { addMessage, setInfo, runTui } from "./tui.
|
|
11
|
+
import { addMessage, setInfo, runTui } from "./tui.js";
|
|
12
12
|
|
|
13
13
|
interface SessionMessage {
|
|
14
14
|
role: string;
|
|
@@ -59,7 +59,7 @@ export async function chatCommand(): Promise<void> {
|
|
|
59
59
|
setInfo({ balance: (Number(bal) / 1e9).toFixed(4) });
|
|
60
60
|
}).catch(() => {});
|
|
61
61
|
|
|
62
|
-
await runTui(agent, config, async (line, signal) => {
|
|
62
|
+
await runTui(agent, config, async (line: string, signal: AbortSignal) => {
|
|
63
63
|
try {
|
|
64
64
|
const { text, turnUsage } = await agent.chat(line, signal);
|
|
65
65
|
addMessage("agent", text);
|
package/bin/sofer
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// Register the @opentui/solid JSX transform plugin BEFORE any .tsx file is parsed.
|
|
3
|
-
// Static imports are all hoisted by bun, so we must use a dynamic import for the
|
|
4
|
-
// app entry so the plugin is active before bun touches tui.tsx.
|
|
5
|
-
import '@opentui/solid/preload'
|
|
6
|
-
await import('../src/bin.ts')
|
package/src/tui.ts
DELETED
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
import type { SoferAgent, SoferConfig } from "@sofer_agent/core";
|
|
2
|
-
|
|
3
|
-
const SIDEBAR_W = 28;
|
|
4
|
-
const MIN_CHAT_W = 34;
|
|
5
|
-
const GUTTER = " ";
|
|
6
|
-
const LABEL_W = 6;
|
|
7
|
-
const INDENT = `${GUTTER}${" ".repeat(LABEL_W + 1)}`;
|
|
8
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
9
|
-
const SPINNER_MS = 80;
|
|
10
|
-
|
|
11
|
-
// Box-drawing: ╭ ─ ╮ │ ╰ ╯ ├ ┤ ┬ ┴ ┼
|
|
12
|
-
const B = {
|
|
13
|
-
tl: "╭", tr: "╮", bl: "╰", br: "╯",
|
|
14
|
-
h: "─", v: "│",
|
|
15
|
-
lt: "├", rt: "┤", tt: "┬", bt: "┴",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export interface ChatLine {
|
|
19
|
-
role: "you" | "agent" | "system" | "error";
|
|
20
|
-
text: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface TuiInfo {
|
|
24
|
-
lastTurnIn: number;
|
|
25
|
-
lastTurnOut: number;
|
|
26
|
-
balance?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class SoferTui {
|
|
30
|
-
private state: ChatLine[] = [];
|
|
31
|
-
private inputBuf = "";
|
|
32
|
-
private cursor = 0;
|
|
33
|
-
private columns = 90;
|
|
34
|
-
private rows = 28;
|
|
35
|
-
private chatW = 58;
|
|
36
|
-
private sideW = 26;
|
|
37
|
-
private bodyH = 22;
|
|
38
|
-
private scrollOff = 0;
|
|
39
|
-
// spinner
|
|
40
|
-
private status: "idle" | "thinking" = "idle";
|
|
41
|
-
private spinnerFrame = 0;
|
|
42
|
-
private spinnerTimer: ReturnType<typeof setInterval> | null = null;
|
|
43
|
-
private turnStartedAt = 0;
|
|
44
|
-
// interrupt
|
|
45
|
-
private abortCtrl: AbortController | null = null;
|
|
46
|
-
// sidebar
|
|
47
|
-
private info: TuiInfo = { lastTurnIn: 0, lastTurnOut: 0 };
|
|
48
|
-
// callbacks
|
|
49
|
-
private resolve: (() => void) | null = null;
|
|
50
|
-
private onLine: ((line: string, signal?: AbortSignal) => Promise<void>) | null = null;
|
|
51
|
-
|
|
52
|
-
constructor(
|
|
53
|
-
private readonly agent: SoferAgent,
|
|
54
|
-
private readonly config: SoferConfig,
|
|
55
|
-
) {}
|
|
56
|
-
|
|
57
|
-
private measure(): void {
|
|
58
|
-
this.columns = Math.max(80, process.stdout.columns ?? 80);
|
|
59
|
-
this.rows = Math.max(24, process.stdout.rows ?? 24);
|
|
60
|
-
this.chatW = Math.max(MIN_CHAT_W, this.columns - SIDEBAR_W - 3);
|
|
61
|
-
this.sideW = this.columns - this.chatW - 3;
|
|
62
|
-
this.bodyH = this.rows - 5; // top border, input box(3), footer(1)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
addMessage(role: ChatLine["role"], text: string): void {
|
|
66
|
-
this.state.push({ role, text });
|
|
67
|
-
this.scrollOff = Number.MAX_SAFE_INTEGER; // will be clamped to maxScroll in buildFrame
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
setInfo(info: Partial<TuiInfo>): void {
|
|
71
|
-
Object.assign(this.info, info);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
private startSpinner(): void {
|
|
75
|
-
if (this.spinnerTimer) return;
|
|
76
|
-
this.status = "thinking";
|
|
77
|
-
this.turnStartedAt = Date.now();
|
|
78
|
-
this.spinnerFrame = 0;
|
|
79
|
-
this.spinnerTimer = setInterval(() => {
|
|
80
|
-
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
81
|
-
this.render();
|
|
82
|
-
}, SPINNER_MS);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private stopSpinner(): void {
|
|
86
|
-
this.status = "idle";
|
|
87
|
-
this.turnStartedAt = 0;
|
|
88
|
-
if (this.spinnerTimer) {
|
|
89
|
-
clearInterval(this.spinnerTimer);
|
|
90
|
-
this.spinnerTimer = null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async run(onLine: (line: string, signal?: AbortSignal) => Promise<void>): Promise<void> {
|
|
95
|
-
this.onLine = onLine;
|
|
96
|
-
this.measure();
|
|
97
|
-
if (process.stdin.isTTY) process.stdin.setRawMode?.(true);
|
|
98
|
-
process.stdin.on("data", this.handleInput);
|
|
99
|
-
process.stdout.on("resize", this.handleResize);
|
|
100
|
-
process.on("SIGWINCH", this.handleResize);
|
|
101
|
-
this.render();
|
|
102
|
-
return new Promise<void>((resolve) => { this.resolve = resolve; });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private shutdown(): void {
|
|
106
|
-
this.stopSpinner();
|
|
107
|
-
process.stdin.setRawMode?.(false);
|
|
108
|
-
process.stdin.removeAllListeners("data");
|
|
109
|
-
process.stdout.removeAllListeners("resize");
|
|
110
|
-
process.removeAllListeners("SIGWINCH");
|
|
111
|
-
this.render();
|
|
112
|
-
process.stdout.write("\n");
|
|
113
|
-
this.resolve?.();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ── Input handling ──────────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
private handleInput = (data: Buffer): void => {
|
|
119
|
-
const str = data.toString();
|
|
120
|
-
for (const ch of str) {
|
|
121
|
-
const code = ch.charCodeAt(0);
|
|
122
|
-
if (code === 3) { this.shutdown(); return; }
|
|
123
|
-
if (code === 4 && this.inputBuf.length === 0) { this.shutdown(); return; }
|
|
124
|
-
if (code === 27) {
|
|
125
|
-
if (this.status === "thinking" && this.abortCtrl) { this.abortCtrl.abort(); return; }
|
|
126
|
-
this.inputBuf = "";
|
|
127
|
-
this.cursor = 0;
|
|
128
|
-
this.render();
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (this.status === "thinking") continue;
|
|
132
|
-
if (code === 13) { this.submit(); return; }
|
|
133
|
-
if (code === 127) {
|
|
134
|
-
if (this.cursor > 0) {
|
|
135
|
-
this.inputBuf = this.inputBuf.slice(0, this.cursor - 1) + this.inputBuf.slice(this.cursor);
|
|
136
|
-
this.cursor--;
|
|
137
|
-
}
|
|
138
|
-
this.render();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (ch >= " ") {
|
|
142
|
-
this.inputBuf = this.inputBuf.slice(0, this.cursor) + ch + this.inputBuf.slice(this.cursor);
|
|
143
|
-
this.cursor++;
|
|
144
|
-
this.render();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
private async submit(): Promise<void> {
|
|
150
|
-
const line = this.inputBuf.trim();
|
|
151
|
-
this.inputBuf = "";
|
|
152
|
-
this.cursor = 0;
|
|
153
|
-
if (!line) { this.render(); return; }
|
|
154
|
-
if (line === "/exit" || line === "/quit") { this.shutdown(); return; }
|
|
155
|
-
this.addMessage("you", line);
|
|
156
|
-
// scrollOff already set to MAX by addMessage (pinned to bottom)
|
|
157
|
-
this.abortCtrl = new AbortController();
|
|
158
|
-
this.startSpinner();
|
|
159
|
-
this.render();
|
|
160
|
-
await this.onLine?.(line, this.abortCtrl.signal);
|
|
161
|
-
this.abortCtrl = null;
|
|
162
|
-
this.stopSpinner();
|
|
163
|
-
this.render();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ── Rendering ───────────────────────────────────────────────────────────
|
|
167
|
-
|
|
168
|
-
private handleResize = (): void => { this.measure(); this.render(); };
|
|
169
|
-
|
|
170
|
-
render(): void {
|
|
171
|
-
this.measure();
|
|
172
|
-
process.stdout.write("\x1b[?25l\x1b[H" + this.buildFrame() + "\x1b[?25h");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private buildFrame(): string {
|
|
176
|
-
const { columns, chatW, sideW, bodyH } = this;
|
|
177
|
-
const innerSideW = sideW - 2;
|
|
178
|
-
const sep = `${B.lt}${B.h.repeat(chatW)}${B.bt}${B.h.repeat(sideW)}${B.rt}`;
|
|
179
|
-
const inputW = columns - 2; // inside border
|
|
180
|
-
|
|
181
|
-
let buf = "";
|
|
182
|
-
|
|
183
|
-
// ── Top border ────────────────────────────────────────────────────────
|
|
184
|
-
buf += `${B.tl}${B.h.repeat(chatW)}${B.tt}${B.h.repeat(sideW)}${B.tr}\n`;
|
|
185
|
-
|
|
186
|
-
// ── Body ──────────────────────────────────────────────────────────────
|
|
187
|
-
const wrapped = this.state.flatMap((m) => this.wrapMessage(m, chatW));
|
|
188
|
-
const maxScroll = Math.max(0, wrapped.length - bodyH);
|
|
189
|
-
if (this.scrollOff > maxScroll) this.scrollOff = maxScroll;
|
|
190
|
-
if (this.scrollOff < 0) this.scrollOff = 0;
|
|
191
|
-
const visible = wrapped.slice(this.scrollOff, this.scrollOff + bodyH);
|
|
192
|
-
|
|
193
|
-
const sidebarLines = this.buildSidebar(innerSideW);
|
|
194
|
-
const sbTop = Math.max(0, Math.floor((bodyH - sidebarLines.length) / 2));
|
|
195
|
-
|
|
196
|
-
for (let r = 0; r < bodyH; r++) {
|
|
197
|
-
buf += B.v;
|
|
198
|
-
buf += visible[r] ? padTrunc(visible[r]!, chatW) : " ".repeat(chatW);
|
|
199
|
-
buf += B.v;
|
|
200
|
-
const sbIdx = r - sbTop;
|
|
201
|
-
const sbLine = (sbIdx >= 0 && sbIdx < sidebarLines.length) ? sidebarLines[sbIdx]! : "";
|
|
202
|
-
buf += ` ${padRight(sbLine, innerSideW)} `;
|
|
203
|
-
buf += `${B.v}\n`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ── Separator ─────────────────────────────────────────────────────────
|
|
207
|
-
buf += `${sep}\n`;
|
|
208
|
-
|
|
209
|
-
// ── Spinner row ───────────────────────────────────────────────────────
|
|
210
|
-
if (this.status === "thinking") {
|
|
211
|
-
const frame = SPINNER_FRAMES[this.spinnerFrame]!;
|
|
212
|
-
const elapsed = this.turnStartedAt ? formatElapsed(Date.now() - this.turnStartedAt) : "";
|
|
213
|
-
const spinnerText = `${frame} thinking\u2026${elapsed ? ` ${elapsed}` : ""} (esc to interrupt)`;
|
|
214
|
-
buf += `${B.v} \x1b[36m${spinnerText}\x1b[0m${" ".repeat(Math.max(0, inputW - 1 - spinnerText.length))}${B.v}\n`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ── Input bar (bordered box) ─────────────────────────────────────────
|
|
218
|
-
buf += `${B.tl}${B.h.repeat(inputW)}${B.tr}\n`;
|
|
219
|
-
const cursorCh = this.status === "idle" ? "\x1b[5m▋\x1b[25m" : "";
|
|
220
|
-
const inputMaxW = inputW - 3; // "> " prefix + 1 padding
|
|
221
|
-
const visibleInput = this.inputBuf.length > inputMaxW
|
|
222
|
-
? this.inputBuf.slice(this.inputBuf.length - inputMaxW)
|
|
223
|
-
: this.inputBuf;
|
|
224
|
-
const inputPad = inputMaxW - visibleInput.length;
|
|
225
|
-
buf += `${B.v} \x1b[36m>\x1b[0m ${visibleInput}${cursorCh}${" ".repeat(Math.max(0, inputPad))} ${B.v}\n`;
|
|
226
|
-
buf += `${B.bl}${B.h.repeat(inputW)}${B.br}\n`;
|
|
227
|
-
|
|
228
|
-
// ── Footer ────────────────────────────────────────────────────────────
|
|
229
|
-
const totalIn = this.agent.usage.tokens.inputTokens;
|
|
230
|
-
const totalOut = this.agent.usage.tokens.outputTokens;
|
|
231
|
-
const cost = this.agent.usage.costUsd.toFixed(4);
|
|
232
|
-
const ft = `\x1b[90m${this.agent.address.slice(0, 10)}… · in ${totalIn} out ${totalOut} $${cost} /exit\x1b[0m`;
|
|
233
|
-
buf += `${padTrunc(ft, columns)}\n`;
|
|
234
|
-
|
|
235
|
-
// ── Cursor position ───────────────────────────────────────────────────
|
|
236
|
-
// Row layout: 1(top border) + bodyH(body) + 1(sep) + [1 spinner] + 1(input top) + 1(input content) = bodyH+4|5
|
|
237
|
-
const cursorRow = bodyH + 3 + (this.status === "thinking" ? 1 : 0); // 1-indexed: body+sep+[spinner]+input-top+input-content
|
|
238
|
-
const cursorCol = 4 + Math.min(this.cursor, inputMaxW);
|
|
239
|
-
buf += `\x1b[${cursorRow};${cursorCol}H`;
|
|
240
|
-
|
|
241
|
-
return buf;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ── Messages ────────────────────────────────────────────────────────────
|
|
245
|
-
|
|
246
|
-
private wrapMessage(msg: ChatLine, width: number): string[] {
|
|
247
|
-
const label = this.roleLabel(msg.role);
|
|
248
|
-
const bodyWidth = Math.max(10, width - GUTTER.length - LABEL_W - 1);
|
|
249
|
-
const lines = wrapText(msg.text, bodyWidth);
|
|
250
|
-
return lines.map((l, i) =>
|
|
251
|
-
i === 0 ? `${GUTTER}${label} ${l}` : `${INDENT}${l}`,
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
private roleLabel(role: ChatLine["role"]): string {
|
|
256
|
-
switch (role) {
|
|
257
|
-
case "you": return "\x1b[36myou \x1b[0m";
|
|
258
|
-
case "agent": return `\x1b[32m${this.agent.name.toLowerCase().padEnd(LABEL_W)}\x1b[0m`;
|
|
259
|
-
case "system": return "\x1b[90msys \x1b[0m";
|
|
260
|
-
case "error": return "\x1b[31merr \x1b[0m";
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ── Sidebar ─────────────────────────────────────────────────────────────
|
|
265
|
-
|
|
266
|
-
private buildSidebar(width: number): string[] {
|
|
267
|
-
const { agent, config, info } = this;
|
|
268
|
-
const addr = `${agent.address.slice(0, 6)}…${agent.address.slice(-4)}`;
|
|
269
|
-
const totalIn = agent.usage.tokens.inputTokens;
|
|
270
|
-
const totalOut = agent.usage.tokens.outputTokens;
|
|
271
|
-
const cost = agent.usage.costUsd.toFixed(6);
|
|
272
|
-
|
|
273
|
-
const rows = [
|
|
274
|
-
`\x1b[1;36m${agent.name}\x1b[0m`,
|
|
275
|
-
`\x1b[90m${addr}\x1b[0m`,
|
|
276
|
-
"",
|
|
277
|
-
`${B.h.repeat(width)}`,
|
|
278
|
-
`\x1b[90mnetwork\x1b[0m ${config.network}`,
|
|
279
|
-
`\x1b[90mmodel\x1b[0m ${shortModel(config.brain.model)}`,
|
|
280
|
-
];
|
|
281
|
-
|
|
282
|
-
if (info.balance) {
|
|
283
|
-
rows.push("", `\x1b[90mbalance\x1b[0m \x1b[33m${info.balance} SUI\x1b[0m`);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
rows.push(
|
|
287
|
-
"",
|
|
288
|
-
`\x1b[90m▸ last turn\x1b[0m`,
|
|
289
|
-
` in ${info.lastTurnIn}`,
|
|
290
|
-
` out ${info.lastTurnOut}`,
|
|
291
|
-
"",
|
|
292
|
-
`\x1b[90m▸ total\x1b[0m`,
|
|
293
|
-
` in ${totalIn}`,
|
|
294
|
-
` out ${totalOut}`,
|
|
295
|
-
` \x1b[33m$${cost}\x1b[0m`,
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
return rows;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
303
|
-
|
|
304
|
-
function wrapText(text: string, width: number): string[] {
|
|
305
|
-
if (!text) return [""];
|
|
306
|
-
const lines: string[] = [];
|
|
307
|
-
// First split on hard newlines, then word-wrap each paragraph
|
|
308
|
-
for (const para of text.split("\n")) {
|
|
309
|
-
if (!para) { lines.push(""); continue; }
|
|
310
|
-
const words = para.split(" ");
|
|
311
|
-
let cur = "";
|
|
312
|
-
for (const w of words) {
|
|
313
|
-
// Break words longer than width
|
|
314
|
-
if (w.length > width) {
|
|
315
|
-
if (cur) { lines.push(cur); cur = ""; }
|
|
316
|
-
for (let i = 0; i < w.length; i += width) lines.push(w.slice(i, i + width));
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
if (cur && cur.length + 1 + w.length > width) { lines.push(cur); cur = w; }
|
|
320
|
-
else cur = cur ? `${cur} ${w}` : w;
|
|
321
|
-
}
|
|
322
|
-
if (cur) lines.push(cur);
|
|
323
|
-
}
|
|
324
|
-
return lines.length > 0 ? lines : [""];
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function stripAnsi(s: string): string {
|
|
328
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping
|
|
329
|
-
return s.replace(/\x1b\[\d*;?\d*m/g, "");
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function visibleLen(s: string): number {
|
|
333
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping
|
|
334
|
-
const re = /\x1b\[\d*;?\d*m/g;
|
|
335
|
-
let len = 0;
|
|
336
|
-
let last = 0;
|
|
337
|
-
let m = re.exec(s);
|
|
338
|
-
while (m !== null) {
|
|
339
|
-
len += m.index - last;
|
|
340
|
-
last = m.index + m[0].length;
|
|
341
|
-
m = re.exec(s);
|
|
342
|
-
}
|
|
343
|
-
return len + s.length - last;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function padRight(s: string, w: number): string {
|
|
347
|
-
const vl = visibleLen(s);
|
|
348
|
-
return vl >= w ? s : s + " ".repeat(w - vl);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function padTrunc(s: string, w: number): string {
|
|
352
|
-
const stripped = stripAnsi(s);
|
|
353
|
-
if (stripped.length <= w) return s + " ".repeat(w - stripped.length);
|
|
354
|
-
return stripped.slice(0, w);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function formatElapsed(ms: number): string {
|
|
358
|
-
const sec = Math.floor(ms / 1000);
|
|
359
|
-
if (sec < 60) return `${sec}s`;
|
|
360
|
-
const m = Math.floor(sec / 60);
|
|
361
|
-
return `${m}m${String(sec % 60).padStart(2, "0")}s`;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function shortModel(model: string): string {
|
|
365
|
-
const m = model.toLowerCase();
|
|
366
|
-
if (m.includes("sonnet")) return "Sonnet 4";
|
|
367
|
-
if (m.includes("haiku")) return "Haiku";
|
|
368
|
-
if (m.includes("opus")) return "Opus 4";
|
|
369
|
-
if (m.includes("fable")) return "Fable 5";
|
|
370
|
-
return model.length > 14 ? `${model.slice(0, 13)}…` : model;
|
|
371
|
-
}
|