@sofer_agent/cli 0.3.11 → 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/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
@@ -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.11",
3
+ "version": "0.3.13",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sofer": "./dist/bin.js"
7
7
  },
8
- "files": ["dist", "src"],
8
+ "files": [
9
+ "dist",
10
+ "src"
11
+ ],
9
12
  "publishConfig": {
10
13
  "access": "public"
11
14
  },
@@ -13,17 +16,32 @@
13
16
  "type": "git",
14
17
  "url": "https://github.com/anomalyco/sofer"
15
18
  },
16
- "keywords": ["sui", "ai", "agent", "walrus", "sovereign", "cli"],
19
+ "keywords": [
20
+ "sui",
21
+ "ai",
22
+ "agent",
23
+ "walrus",
24
+ "sovereign",
25
+ "cli"
26
+ ],
17
27
  "scripts": {
18
- "build": "tsc -b && chmod +x dist/bin.js",
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": {
22
32
  "@clack/prompts": "^1.5.1",
23
- "@sofer_agent/core": "0.3.3"
33
+ "@opentui/core": "0.4.1",
34
+ "@opentui/solid": "0.4.1",
35
+ "@sofer_agent/core": "0.3.3",
36
+ "solid-js": "1.9.13"
24
37
  },
25
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",
26
43
  "@types/node": "^22.10.2",
44
+ "babel-preset-solid": "^1.9.0",
27
45
  "typescript": "^5.7.2"
28
46
  }
29
47
  }
package/src/chat.ts CHANGED
@@ -8,52 +8,34 @@ import {
8
8
  SoferAgent,
9
9
  } from "@sofer_agent/core";
10
10
  import { loadSecrets } from "./env.js";
11
- import { SoferTui } from "./tui.js";
11
+ import { addMessage, setInfo, runTui } from "./tui.js";
12
12
 
13
- /** Serializable message for JSONL session storage. */
14
13
  interface SessionMessage {
15
14
  role: string;
16
- content: unknown; // Anthropic ContentBlock[]
17
- }
18
-
19
- interface SessionLine {
20
- role: SessionMessage["role"];
21
- content: SessionMessage["content"];
15
+ content: unknown;
22
16
  }
23
17
 
24
18
  function sessionPath(config: SoferConfig): string | null {
25
- if (config.agentObjectId) {
26
- return agentPaths.agent(shortAgentId(config.agentObjectId)).activityLog;
27
- }
28
- if (config.memwal.accountId) {
29
- return agentPaths.agent(shortAgentId(config.memwal.accountId)).activityLog;
30
- }
19
+ if (config.agentObjectId) return agentPaths.agent(shortAgentId(config.agentObjectId)).activityLog;
20
+ if (config.memwal.accountId) return agentPaths.agent(shortAgentId(config.memwal.accountId)).activityLog;
31
21
  return null;
32
22
  }
33
23
 
34
24
  function loadSession(path: string): SessionMessage[] {
35
25
  if (!existsSync(path)) return [];
36
26
  const messages: SessionMessage[] = [];
37
- const raw = readFileSync(path, "utf8");
38
- for (const line of raw.split("\n")) {
27
+ for (const line of readFileSync(path, "utf8").split("\n")) {
39
28
  if (!line.trim()) continue;
40
- try {
41
- const entry = JSON.parse(line) as SessionLine;
42
- messages.push({ role: entry.role, content: entry.content });
43
- } catch {
44
- // skip malformed lines
45
- }
29
+ try { messages.push(JSON.parse(line) as SessionMessage); } catch { /* skip */ }
46
30
  }
47
31
  return messages;
48
32
  }
49
33
 
50
34
  function saveSession(path: string, messages: SessionMessage[]): void {
51
- const lines = messages.map((m) => JSON.stringify({ role: m.role, content: m.content }));
52
35
  mkdirSync(dirname(path), { recursive: true });
53
- writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
36
+ writeFileSync(path, messages.map((m) => JSON.stringify(m)).join("\n") + "\n", "utf8");
54
37
  }
55
38
 
56
- /** Interactive chat TUI with promus-style polish + session resume. */
57
39
  export async function chatCommand(): Promise<void> {
58
40
  const config = loadConfig();
59
41
  const secrets = loadSecrets();
@@ -61,50 +43,35 @@ export async function chatCommand(): Promise<void> {
61
43
  console.error("Missing secrets. Run `sofer init` first.");
62
44
  process.exit(1);
63
45
  }
64
- const agent = await SoferAgent.create(config, { suiSecretKey: secrets.suiSecretKey, anthropicApiKey: secrets.anthropicApiKey });
46
+ const agent = await SoferAgent.create(config, {
47
+ suiSecretKey: secrets.suiSecretKey,
48
+ anthropicApiKey: secrets.anthropicApiKey,
49
+ });
65
50
 
66
- // Session resume
67
51
  const sp = sessionPath(config);
68
52
  if (sp) {
69
53
  const history = loadSession(sp);
70
- if (history.length > 0) {
71
- agent.loadHistory(history);
72
- }
54
+ if (history.length > 0) agent.loadHistory(history);
73
55
  }
74
56
 
75
- const tui = new SoferTui(agent, config);
76
-
77
- // Show initial balance
78
- try {
79
- const bal = await agent.getBalance();
80
- tui.setInfo({ balance: (Number(bal) / 1e9).toFixed(4) });
81
- } catch {
82
- // RPC unavailable, skip
83
- }
57
+ // Fetch initial balance non-blocking
58
+ agent.getBalance().then((bal) => {
59
+ setInfo({ balance: (Number(bal) / 1e9).toFixed(4) });
60
+ }).catch(() => {});
84
61
 
85
- await tui.run(async (line, signal) => {
62
+ await runTui(agent, config, async (line: string, signal: AbortSignal) => {
86
63
  try {
87
64
  const { text, turnUsage } = await agent.chat(line, signal);
88
- tui.addMessage("agent", text);
89
- tui.setInfo({ lastTurnIn: turnUsage.inputTokens, lastTurnOut: turnUsage.outputTokens });
65
+ addMessage("agent", text);
66
+ setInfo({ lastTurnIn: turnUsage.inputTokens, lastTurnOut: turnUsage.outputTokens });
90
67
  } catch (e) {
91
- if (signal?.aborted) {
92
- tui.addMessage("system", "turn interrupted (esc).");
68
+ if (signal.aborted) {
69
+ addMessage("system", "turn interrupted.");
93
70
  } else {
94
- tui.addMessage("error", e instanceof Error ? e.message : String(e));
71
+ addMessage("error", e instanceof Error ? e.message : String(e));
95
72
  }
96
73
  }
97
- // Query SUI balance (non-blocking skip if RPC down)
98
- try {
99
- const bal = await agent.getBalance();
100
- tui.setInfo({ balance: (Number(bal) / 1e9).toFixed(4) });
101
- } catch {
102
- // RPC unavailable, skip
103
- }
104
- // Save session after each turn
105
- if (sp) {
106
- saveSession(sp, agent.getHistory());
107
- }
108
- tui.render();
74
+ agent.getBalance().then((bal) => setInfo({ balance: (Number(bal) / 1e9).toFixed(4) })).catch(() => {});
75
+ if (sp) saveSession(sp, agent.getHistory());
109
76
  });
110
77
  }
package/src/tui.tsx ADDED
@@ -0,0 +1,227 @@
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
+ }