@sofer_agent/cli 0.3.18 → 0.3.20
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/bin/sofer +2 -3
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +35 -35
- package/dist/chat.js.map +1 -1
- package/dist/tui.d.ts +36 -12
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +330 -0
- package/dist/tui.js.map +1 -0
- package/package.json +2 -5
- package/src/chat.ts +29 -43
- package/src/tui.ts +320 -0
- package/dist/tui.jsx +0 -182
- package/dist/tui.jsx.map +0 -1
- package/src/tui.tsx +0 -247
package/src/tui.tsx
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import { render, useKeyboard, useTerminalDimensions } from "@opentui/solid";
|
|
2
|
-
import type { SoferAgent, SoferConfig } from "@sofer_agent/core";
|
|
3
|
-
import { For, createEffect, createSignal, onCleanup } from "solid-js";
|
|
4
|
-
|
|
5
|
-
const SPINNER_FRAMES = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"] as const;
|
|
6
|
-
const SPINNER_MS = 80;
|
|
7
|
-
|
|
8
|
-
export interface TuiState {
|
|
9
|
-
rows: ReturnType<typeof createSignal<ChatLine[]>>;
|
|
10
|
-
status: ReturnType<typeof createSignal<"idle" | "thinking">>;
|
|
11
|
-
input: ReturnType<typeof createSignal<string>>;
|
|
12
|
-
info: ReturnType<typeof createSignal<TuiInfo>>;
|
|
13
|
-
spinnerFrame: ReturnType<typeof createSignal<number>>;
|
|
14
|
-
turnStartedAt: ReturnType<typeof createSignal<number | null>>;
|
|
15
|
-
activeAbort: ReturnType<typeof createSignal<AbortController | null>>;
|
|
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
|
-
function formatElapsed(startedAt: number | null): string {
|
|
30
|
-
if (!startedAt) return "";
|
|
31
|
-
const sec = Math.floor((Date.now() - startedAt) / 1000);
|
|
32
|
-
if (sec < 60) return `${sec}s`;
|
|
33
|
-
const m = Math.floor(sec / 60);
|
|
34
|
-
return `${m}m${String(sec % 60).padStart(2, "0")}s`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function shortModel(model: string): string {
|
|
38
|
-
const m = model.toLowerCase();
|
|
39
|
-
if (m.includes("sonnet")) return "sonnet-4";
|
|
40
|
-
if (m.includes("haiku")) return "haiku";
|
|
41
|
-
if (m.includes("opus")) return "opus-4";
|
|
42
|
-
return model.length > 14 ? `${model.slice(0, 13)}…` : model;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function App(props: {
|
|
46
|
-
state: TuiState;
|
|
47
|
-
agent: SoferAgent;
|
|
48
|
-
config: SoferConfig;
|
|
49
|
-
onSubmit: (line: string, signal: AbortSignal) => Promise<void>;
|
|
50
|
-
onExit: () => void;
|
|
51
|
-
}) {
|
|
52
|
-
const { state, agent, config } = props;
|
|
53
|
-
const dims = useTerminalDimensions();
|
|
54
|
-
|
|
55
|
-
const [rows] = state.rows;
|
|
56
|
-
const [status] = state.status;
|
|
57
|
-
const [input, setInput] = state.input;
|
|
58
|
-
const [info] = state.info;
|
|
59
|
-
const [spinnerFrame, setSpinnerFrame] = state.spinnerFrame;
|
|
60
|
-
const [turnStartedAt] = state.turnStartedAt;
|
|
61
|
-
const [, setActiveAbort] = state.activeAbort;
|
|
62
|
-
const [activeAbort] = state.activeAbort;
|
|
63
|
-
|
|
64
|
-
createEffect(() => {
|
|
65
|
-
if (status() !== "thinking") { setSpinnerFrame(0); return; }
|
|
66
|
-
const id = setInterval(
|
|
67
|
-
() => setSpinnerFrame((f) => (f + 1) % SPINNER_FRAMES.length),
|
|
68
|
-
SPINNER_MS,
|
|
69
|
-
);
|
|
70
|
-
onCleanup(() => clearInterval(id));
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
useKeyboard((evt) => {
|
|
74
|
-
if (evt.ctrl && evt.name === "c") { evt.preventDefault(); props.onExit(); return; }
|
|
75
|
-
|
|
76
|
-
if (evt.name === "escape") {
|
|
77
|
-
const ab = activeAbort();
|
|
78
|
-
if (ab && !ab.signal.aborted) ab.abort();
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (status() === "thinking") return;
|
|
83
|
-
|
|
84
|
-
if (evt.name === "return") {
|
|
85
|
-
const text = input().trim();
|
|
86
|
-
if (!text) return;
|
|
87
|
-
if (text === "/exit" || text === "/quit") { props.onExit(); return; }
|
|
88
|
-
const ctrl = new AbortController();
|
|
89
|
-
setActiveAbort(ctrl);
|
|
90
|
-
state.rows[1](prev => [...prev, { role: "you", text }]);
|
|
91
|
-
setInput("");
|
|
92
|
-
state.status[1]("thinking");
|
|
93
|
-
state.turnStartedAt[1](Date.now());
|
|
94
|
-
props.onSubmit(text, ctrl.signal).finally(() => {
|
|
95
|
-
state.status[1]("idle");
|
|
96
|
-
state.turnStartedAt[1](null);
|
|
97
|
-
setActiveAbort(null);
|
|
98
|
-
});
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (evt.name === "backspace" || evt.name === "delete") {
|
|
103
|
-
setInput((p) => p.slice(0, -1));
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (evt.sequence && !evt.ctrl && !evt.meta && !evt.option) {
|
|
108
|
-
setInput((p) => p + evt.sequence);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const sidebarW = Math.min(30, Math.floor(dims().width * 0.22));
|
|
113
|
-
const chatW = dims().width - sidebarW;
|
|
114
|
-
const addr = () => `${agent.address.slice(0, 6)}…${agent.address.slice(-4)}`;
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<box flexDirection="row" width={dims().width} height={dims().height}>
|
|
118
|
-
{/* Chat area */}
|
|
119
|
-
<box flexDirection="column" width={chatW} height={dims().height}>
|
|
120
|
-
<scrollbox
|
|
121
|
-
flexGrow={1}
|
|
122
|
-
flexShrink={1}
|
|
123
|
-
stickyScroll
|
|
124
|
-
stickyStart="bottom"
|
|
125
|
-
contentOptions={{ flexDirection: "column", paddingLeft: 1, paddingRight: 1, paddingTop: 1, paddingBottom: 1 }}
|
|
126
|
-
>
|
|
127
|
-
<For each={rows()}>
|
|
128
|
-
{(line) => {
|
|
129
|
-
const color = line.role === "you" ? "#67e8f9"
|
|
130
|
-
: line.role === "agent" ? "#86efac"
|
|
131
|
-
: line.role === "error" ? "#fca5a5"
|
|
132
|
-
: "#9ca3af";
|
|
133
|
-
const label = line.role === "you" ? "you"
|
|
134
|
-
: line.role === "agent" ? agent.name.toLowerCase().slice(0, 5)
|
|
135
|
-
: line.role === "error" ? "err"
|
|
136
|
-
: "sys";
|
|
137
|
-
return (
|
|
138
|
-
<box flexDirection="row">
|
|
139
|
-
<text fg={color} flexShrink={0}>{` ${label.padEnd(5)} `}</text>
|
|
140
|
-
<text wrapMode="word" flexGrow={1} fg="#e5e7eb">
|
|
141
|
-
{line.text}
|
|
142
|
-
</text>
|
|
143
|
-
</box>
|
|
144
|
-
);
|
|
145
|
-
}}
|
|
146
|
-
</For>
|
|
147
|
-
</scrollbox>
|
|
148
|
-
|
|
149
|
-
<box flexDirection="row" flexShrink={0} paddingLeft={3} paddingRight={2} marginTop={1}>
|
|
150
|
-
<text fg="#67e8f9" flexGrow={1}>
|
|
151
|
-
{(() => {
|
|
152
|
-
if (status() !== "thinking") return " ";
|
|
153
|
-
const frame = SPINNER_FRAMES[spinnerFrame()];
|
|
154
|
-
const elapsed = formatElapsed(turnStartedAt());
|
|
155
|
-
return elapsed
|
|
156
|
-
? `${frame} thinking… ${elapsed} (esc to interrupt)`
|
|
157
|
-
: `${frame} thinking… (esc to interrupt)`;
|
|
158
|
-
})()}
|
|
159
|
-
</text>
|
|
160
|
-
</box>
|
|
161
|
-
|
|
162
|
-
<box
|
|
163
|
-
flexDirection="row"
|
|
164
|
-
flexShrink={0}
|
|
165
|
-
minHeight={3}
|
|
166
|
-
maxHeight={8}
|
|
167
|
-
borderStyle="rounded"
|
|
168
|
-
borderColor="#374151"
|
|
169
|
-
paddingLeft={1} paddingRight={1}
|
|
170
|
-
marginLeft={2} marginRight={2}
|
|
171
|
-
>
|
|
172
|
-
<text fg="#67e8f9" flexShrink={0}>{"› "}</text>
|
|
173
|
-
<text wrapMode="word" flexGrow={1} fg="#e5e7eb">
|
|
174
|
-
{`${input()}${status() === "idle" ? "▋" : ""}`}
|
|
175
|
-
</text>
|
|
176
|
-
</box>
|
|
177
|
-
|
|
178
|
-
<box flexDirection="row" flexShrink={0} paddingLeft={2} paddingRight={2}>
|
|
179
|
-
<text fg="#9ca3af">
|
|
180
|
-
{[
|
|
181
|
-
addr(),
|
|
182
|
-
config.network,
|
|
183
|
-
shortModel(config.brain.model),
|
|
184
|
-
info().balance ? `${info().balance} SUI` : null,
|
|
185
|
-
`in ${agent.usage.tokens.inputTokens} out ${agent.usage.tokens.outputTokens} $${agent.usage.costUsd.toFixed(4)}`,
|
|
186
|
-
"/exit",
|
|
187
|
-
].filter(Boolean).join(" · ")}
|
|
188
|
-
</text>
|
|
189
|
-
</box>
|
|
190
|
-
</box>
|
|
191
|
-
|
|
192
|
-
{/* Sidebar */}
|
|
193
|
-
<box
|
|
194
|
-
flexDirection="column"
|
|
195
|
-
width={sidebarW}
|
|
196
|
-
borderStyle="single"
|
|
197
|
-
borderColor="#374151"
|
|
198
|
-
paddingLeft={2} paddingRight={1}
|
|
199
|
-
>
|
|
200
|
-
<text fg="#86efac">{agent.name}</text>
|
|
201
|
-
<text fg="#9ca3af">{addr()}</text>
|
|
202
|
-
<text>{" "}</text>
|
|
203
|
-
<text fg="#6b7280">{"─".repeat(sidebarW - 4)}</text>
|
|
204
|
-
<text fg="#9ca3af">{`network ${config.network}`}</text>
|
|
205
|
-
<text fg="#9ca3af">{`model ${shortModel(config.brain.model)}`}</text>
|
|
206
|
-
{info().balance ? <text fg="#fbbf24">{`balance ${info().balance} SUI`}</text> : null}
|
|
207
|
-
<text>{" "}</text>
|
|
208
|
-
<text fg="#6b7280">{"─".repeat(sidebarW - 4)}</text>
|
|
209
|
-
<text fg="#9ca3af">{`last in ${info().lastTurnIn} out ${info().lastTurnOut}`}</text>
|
|
210
|
-
<text fg="#9ca3af">{`total in ${agent.usage.tokens.inputTokens}`}</text>
|
|
211
|
-
<text fg="#9ca3af">{` out ${agent.usage.tokens.outputTokens}`}</text>
|
|
212
|
-
<text fg="#fbbf24">{` $${agent.usage.costUsd.toFixed(6)}`}</text>
|
|
213
|
-
</box>
|
|
214
|
-
</box>
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function createTuiState(): TuiState {
|
|
219
|
-
return {
|
|
220
|
-
rows: createSignal<ChatLine[]>([]),
|
|
221
|
-
status: createSignal<"idle" | "thinking">("idle"),
|
|
222
|
-
input: createSignal(""),
|
|
223
|
-
info: createSignal<TuiInfo>({ lastTurnIn: 0, lastTurnOut: 0 }),
|
|
224
|
-
spinnerFrame: createSignal(0),
|
|
225
|
-
turnStartedAt: createSignal<number | null>(null),
|
|
226
|
-
activeAbort: createSignal<AbortController | null>(null),
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export async function runTui(
|
|
231
|
-
state: TuiState,
|
|
232
|
-
agent: SoferAgent,
|
|
233
|
-
config: SoferConfig,
|
|
234
|
-
onSubmit: (line: string, signal: AbortSignal) => Promise<void>,
|
|
235
|
-
): Promise<void> {
|
|
236
|
-
return new Promise<void>((resolve) => {
|
|
237
|
-
render(() => (
|
|
238
|
-
<App
|
|
239
|
-
state={state}
|
|
240
|
-
agent={agent}
|
|
241
|
-
config={config}
|
|
242
|
-
onSubmit={onSubmit}
|
|
243
|
-
onExit={resolve}
|
|
244
|
-
/>
|
|
245
|
-
));
|
|
246
|
-
});
|
|
247
|
-
}
|