@sofer_agent/cli 0.3.13 → 0.3.14
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.js +11 -12
- package/dist/chat.js.map +1 -1
- package/dist/tui.d.ts +37 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +316 -190
- package/dist/tui.js.map +1 -1
- package/package.json +7 -15
- package/src/chat.ts +12 -12
- package/src/tui.ts +341 -0
- package/src/tui.tsx +0 -227
package/src/tui.tsx
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { render, useKeyboard, useTerminalDimensions } from "@opentui/solid";
|
|
2
|
-
import type { SoferAgent, SoferConfig } from "@sofer_agent/core";
|
|
3
|
-
import { For, Show, createEffect, createSignal, onCleanup } from "solid-js";
|
|
4
|
-
|
|
5
|
-
const SPINNER_FRAMES = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"] as const;
|
|
6
|
-
const SPINNER_MS = 80;
|
|
7
|
-
|
|
8
|
-
export interface ChatLine {
|
|
9
|
-
role: "you" | "agent" | "system" | "error";
|
|
10
|
-
text: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface TuiInfo {
|
|
14
|
-
lastTurnIn: number;
|
|
15
|
-
lastTurnOut: number;
|
|
16
|
-
balance?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// ── State signals (module-level so chat.ts can push into them) ──────────────
|
|
20
|
-
|
|
21
|
-
const [rows, setRows] = createSignal<ChatLine[]>([]);
|
|
22
|
-
const [status, setStatus] = createSignal<"idle" | "thinking">("idle");
|
|
23
|
-
const [input, setInput] = createSignal("");
|
|
24
|
-
const [info, setInfoSignal] = createSignal<TuiInfo>({ lastTurnIn: 0, lastTurnOut: 0 });
|
|
25
|
-
const [turnStartedAt, setTurnStartedAt] = createSignal<number | null>(null);
|
|
26
|
-
const [spinnerFrame, setSpinnerFrame] = createSignal(0);
|
|
27
|
-
const [activeAbort, setActiveAbort] = createSignal<AbortController | null>(null);
|
|
28
|
-
|
|
29
|
-
// ── Exported imperative API (used by chat.ts) ────────────────────────────────
|
|
30
|
-
|
|
31
|
-
export function addMessage(role: ChatLine["role"], text: string): void {
|
|
32
|
-
setRows((prev) => [...prev, { role, text }]);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function setInfo(patch: Partial<TuiInfo>): void {
|
|
36
|
-
setInfoSignal((prev) => ({ ...prev, ...patch }));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ── Helper ───────────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
function formatElapsed(startedAt: number | null): string {
|
|
42
|
-
if (!startedAt) return "";
|
|
43
|
-
const sec = Math.floor((Date.now() - startedAt) / 1000);
|
|
44
|
-
if (sec < 60) return `${sec}s`;
|
|
45
|
-
const m = Math.floor(sec / 60);
|
|
46
|
-
return `${m}m${String(sec % 60).padStart(2, "0")}s`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function shortModel(model: string): string {
|
|
50
|
-
const m = model.toLowerCase();
|
|
51
|
-
if (m.includes("sonnet")) return "sonnet-4";
|
|
52
|
-
if (m.includes("haiku")) return "haiku";
|
|
53
|
-
if (m.includes("opus")) return "opus-4";
|
|
54
|
-
return model.length > 16 ? `${model.slice(0, 15)}…` : model;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const LABEL: Record<ChatLine["role"], string> = {
|
|
58
|
-
you: "you",
|
|
59
|
-
agent: "agent",
|
|
60
|
-
system: "sys",
|
|
61
|
-
error: "err",
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const COLOR: Record<ChatLine["role"], string> = {
|
|
65
|
-
you: "#67e8f9",
|
|
66
|
-
agent: "#86efac",
|
|
67
|
-
system: "#9ca3af",
|
|
68
|
-
error: "#fca5a5",
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// ── Components ────────────────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
function MessageRow(props: { line: ChatLine }) {
|
|
74
|
-
return (
|
|
75
|
-
<box flexDirection="row" marginBottom={1}>
|
|
76
|
-
<text fg={COLOR[props.line.role]} flexShrink={0}>
|
|
77
|
-
{` ${LABEL[props.line.role].padEnd(5)} `}
|
|
78
|
-
</text>
|
|
79
|
-
<text wrapMode="word" flexGrow={1} fg="#e5e7eb">
|
|
80
|
-
{props.line.text}
|
|
81
|
-
</text>
|
|
82
|
-
</box>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function App(props: {
|
|
87
|
-
agent: SoferAgent;
|
|
88
|
-
config: SoferConfig;
|
|
89
|
-
onSubmit: (line: string, signal: AbortSignal) => Promise<void>;
|
|
90
|
-
onExit: () => void;
|
|
91
|
-
}) {
|
|
92
|
-
const dims = useTerminalDimensions();
|
|
93
|
-
|
|
94
|
-
// Spinner tick — only while thinking
|
|
95
|
-
createEffect(() => {
|
|
96
|
-
if (status() !== "thinking") { setSpinnerFrame(0); return; }
|
|
97
|
-
const id = setInterval(
|
|
98
|
-
() => setSpinnerFrame((f) => (f + 1) % SPINNER_FRAMES.length),
|
|
99
|
-
SPINNER_MS,
|
|
100
|
-
);
|
|
101
|
-
onCleanup(() => clearInterval(id));
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
useKeyboard((evt) => {
|
|
105
|
-
if (evt.ctrl && evt.name === "c") { evt.preventDefault(); props.onExit(); return; }
|
|
106
|
-
|
|
107
|
-
if (evt.name === "escape") {
|
|
108
|
-
const ab = activeAbort();
|
|
109
|
-
if (ab && !ab.signal.aborted) ab.abort();
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (status() === "thinking") return; // block input during turns
|
|
114
|
-
|
|
115
|
-
if (evt.name === "return") {
|
|
116
|
-
const text = input().trim();
|
|
117
|
-
if (!text) return;
|
|
118
|
-
if (text === "/exit" || text === "/quit") { props.onExit(); return; }
|
|
119
|
-
addMessage("you", text);
|
|
120
|
-
setInput("");
|
|
121
|
-
const ctrl = new AbortController();
|
|
122
|
-
setActiveAbort(ctrl);
|
|
123
|
-
setStatus("thinking");
|
|
124
|
-
setTurnStartedAt(Date.now());
|
|
125
|
-
props.onSubmit(text, ctrl.signal).finally(() => {
|
|
126
|
-
setStatus("idle");
|
|
127
|
-
setTurnStartedAt(null);
|
|
128
|
-
setActiveAbort(null);
|
|
129
|
-
});
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (evt.name === "backspace" || evt.name === "delete") {
|
|
134
|
-
setInput((p) => p.slice(0, -1));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (evt.sequence && !evt.ctrl && !evt.meta && !evt.option) {
|
|
139
|
-
setInput((p) => p + evt.sequence);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
const { agent, config } = props;
|
|
144
|
-
const addr = () => `${agent.address.slice(0, 6)}…${agent.address.slice(-4)}`;
|
|
145
|
-
|
|
146
|
-
return (
|
|
147
|
-
<box flexDirection="column" width={dims().width} height={dims().height}>
|
|
148
|
-
{/* Scrollable chat history */}
|
|
149
|
-
<scrollbox
|
|
150
|
-
flexGrow={1}
|
|
151
|
-
flexShrink={1}
|
|
152
|
-
stickyScroll
|
|
153
|
-
stickyStart="bottom"
|
|
154
|
-
contentOptions={{ flexDirection: "column", paddingLeft: 0, paddingRight: 1, paddingTop: 1, paddingBottom: 1 }}
|
|
155
|
-
>
|
|
156
|
-
<For each={rows()}>{(line) => <MessageRow line={line} />}</For>
|
|
157
|
-
</scrollbox>
|
|
158
|
-
|
|
159
|
-
{/* Spinner / hint row */}
|
|
160
|
-
<box flexDirection="row" flexShrink={0} paddingLeft={3} paddingRight={2} marginTop={1}>
|
|
161
|
-
<text fg="#67e8f9" flexGrow={1}>
|
|
162
|
-
{(() => {
|
|
163
|
-
if (status() !== "thinking") return " ";
|
|
164
|
-
spinnerFrame(); // reactive dependency
|
|
165
|
-
const elapsed = formatElapsed(turnStartedAt());
|
|
166
|
-
const frame = SPINNER_FRAMES[spinnerFrame()];
|
|
167
|
-
return elapsed
|
|
168
|
-
? `${frame} thinking… ${elapsed} (esc to interrupt)`
|
|
169
|
-
: `${frame} thinking… (esc to interrupt)`;
|
|
170
|
-
})()}
|
|
171
|
-
</text>
|
|
172
|
-
</box>
|
|
173
|
-
|
|
174
|
-
{/* Input box */}
|
|
175
|
-
<box
|
|
176
|
-
flexDirection="row"
|
|
177
|
-
flexShrink={0}
|
|
178
|
-
minHeight={3}
|
|
179
|
-
maxHeight={8}
|
|
180
|
-
borderStyle="rounded"
|
|
181
|
-
borderColor="#374151"
|
|
182
|
-
paddingLeft={1}
|
|
183
|
-
paddingRight={1}
|
|
184
|
-
marginLeft={2}
|
|
185
|
-
marginRight={2}
|
|
186
|
-
>
|
|
187
|
-
<text fg="#67e8f9" flexShrink={0}>{"› "}</text>
|
|
188
|
-
<text wrapMode="word" flexGrow={1} fg="#e5e7eb">
|
|
189
|
-
{`${input()}${status() === "idle" ? "▋" : ""}`}
|
|
190
|
-
</text>
|
|
191
|
-
</box>
|
|
192
|
-
|
|
193
|
-
{/* Footer */}
|
|
194
|
-
<box flexDirection="row" flexShrink={0} paddingLeft={2} paddingRight={2}>
|
|
195
|
-
<text fg="#9ca3af">
|
|
196
|
-
{[
|
|
197
|
-
addr(),
|
|
198
|
-
`${config.network}`,
|
|
199
|
-
shortModel(config.brain.model),
|
|
200
|
-
info().balance ? `${info().balance} SUI` : null,
|
|
201
|
-
`in ${agent.usage.tokens.inputTokens} out ${agent.usage.tokens.outputTokens} $${agent.usage.costUsd.toFixed(4)}`,
|
|
202
|
-
"/exit",
|
|
203
|
-
].filter(Boolean).join(" · ")}
|
|
204
|
-
</text>
|
|
205
|
-
</box>
|
|
206
|
-
</box>
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ── Main entry ────────────────────────────────────────────────────────────────
|
|
211
|
-
|
|
212
|
-
export async function runTui(
|
|
213
|
-
agent: SoferAgent,
|
|
214
|
-
config: SoferConfig,
|
|
215
|
-
onSubmit: (line: string, signal: AbortSignal) => Promise<void>,
|
|
216
|
-
): Promise<void> {
|
|
217
|
-
return new Promise<void>((resolve) => {
|
|
218
|
-
render(() => (
|
|
219
|
-
<App
|
|
220
|
-
agent={agent}
|
|
221
|
-
config={config}
|
|
222
|
-
onSubmit={onSubmit}
|
|
223
|
-
onExit={resolve}
|
|
224
|
-
/>
|
|
225
|
-
));
|
|
226
|
-
});
|
|
227
|
-
}
|