@sofer_agent/cli 0.3.16 → 0.3.17

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 ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ import "@opentui/solid/preload";
3
+ import "../src/bin.ts";
package/dist/bin.js CHANGED
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"AA+CA,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAwCjD"}
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"AAsCA,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAwCjD"}
package/dist/chat.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { agentPaths, loadConfig, shortAgentId, SoferAgent, } from "@sofer_agent/core";
4
4
  import { loadSecrets } from "./env.js";
5
- import { SoferTui } from "./tui.js";
5
+ import { createTuiState, runTui } from "./tui.jsx";
6
6
  function sessionPath(config) {
7
7
  if (config.agentObjectId)
8
8
  return agentPaths.agent(shortAgentId(config.agentObjectId)).activityLog;
@@ -28,17 +28,6 @@ function saveSession(path, messages) {
28
28
  mkdirSync(dirname(path), { recursive: true });
29
29
  writeFileSync(path, messages.map((m) => JSON.stringify(m)).join("\n") + "\n", "utf8");
30
30
  }
31
- function cleanError(raw) {
32
- try {
33
- const obj = JSON.parse(raw);
34
- if (obj?.error?.type === "overloaded_error")
35
- return "Claude is overloaded — try again in a moment.";
36
- if (obj?.error?.message)
37
- return obj.error.message;
38
- }
39
- catch { /* not JSON */ }
40
- return raw;
41
- }
42
31
  export async function chatCommand() {
43
32
  const config = loadConfig();
44
33
  const secrets = loadSecrets();
@@ -56,28 +45,39 @@ export async function chatCommand() {
56
45
  if (history.length > 0)
57
46
  agent.loadHistory(history);
58
47
  }
59
- const tui = new SoferTui(agent, config);
60
- agent.getBalance().then((bal) => tui.setInfo({ balance: (Number(bal) / 1e9).toFixed(4) })).catch(() => { });
61
- await tui.run(async (line, signal) => {
48
+ const tuiState = createTuiState();
49
+ const [, setRows] = tuiState.rows;
50
+ const [, setInfo] = tuiState.info;
51
+ agent.getBalance().then((bal) => setInfo((prev) => ({ ...prev, balance: (Number(bal) / 1e9).toFixed(4) }))).catch(() => { });
52
+ await runTui(tuiState, agent, config, async (line, signal) => {
62
53
  try {
63
54
  const { text, turnUsage } = await agent.chat(line, signal);
64
- tui.addMessage("agent", text);
65
- tui.setInfo({ lastTurnIn: turnUsage.inputTokens, lastTurnOut: turnUsage.outputTokens });
55
+ setRows((prev) => [...prev, { role: "agent", text }]);
56
+ setInfo((prev) => ({ ...prev, lastTurnIn: turnUsage.inputTokens, lastTurnOut: turnUsage.outputTokens }));
66
57
  }
67
58
  catch (e) {
68
- if (signal?.aborted) {
69
- tui.addMessage("system", "turn interrupted (esc).");
59
+ if (signal.aborted) {
60
+ setRows((prev) => [...prev, { role: "system", text: "turn interrupted." }]);
70
61
  }
71
62
  else {
72
63
  const raw = e instanceof Error ? e.message : String(e);
73
- const clean = cleanError(raw);
74
- tui.addMessage("error", clean);
64
+ setRows((prev) => [...prev, { role: "error", text: cleanError(raw) }]);
75
65
  }
76
66
  }
77
- agent.getBalance().then((bal) => tui.setInfo({ balance: (Number(bal) / 1e9).toFixed(4) })).catch(() => { });
67
+ agent.getBalance().then((bal) => setInfo((prev) => ({ ...prev, balance: (Number(bal) / 1e9).toFixed(4) }))).catch(() => { });
78
68
  if (sp)
79
69
  saveSession(sp, agent.getHistory());
80
- tui.render();
81
70
  });
82
71
  }
72
+ function cleanError(raw) {
73
+ try {
74
+ const obj = JSON.parse(raw);
75
+ if (obj?.error?.type === "overloaded_error")
76
+ return "Claude is overloaded — try again in a moment.";
77
+ if (obj?.error?.message)
78
+ return obj.error.message;
79
+ }
80
+ catch { /* not JSON */ }
81
+ return raw;
82
+ }
83
83
  //# sourceMappingURL=chat.js.map
package/dist/chat.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"chat.js","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chat.js","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"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"}
package/dist/tui.d.ts CHANGED
@@ -1,4 +1,14 @@
1
1
  import type { SoferAgent, SoferConfig } from "@sofer_agent/core";
2
+ import { createSignal } from "solid-js";
3
+ export interface TuiState {
4
+ rows: ReturnType<typeof createSignal<ChatLine[]>>;
5
+ status: ReturnType<typeof createSignal<"idle" | "thinking">>;
6
+ input: ReturnType<typeof createSignal<string>>;
7
+ info: ReturnType<typeof createSignal<TuiInfo>>;
8
+ spinnerFrame: ReturnType<typeof createSignal<number>>;
9
+ turnStartedAt: ReturnType<typeof createSignal<number | null>>;
10
+ activeAbort: ReturnType<typeof createSignal<AbortController | null>>;
11
+ }
2
12
  export interface ChatLine {
3
13
  role: "you" | "agent" | "system" | "error";
4
14
  text: string;
@@ -8,41 +18,6 @@ export interface TuiInfo {
8
18
  lastTurnOut: number;
9
19
  balance?: string;
10
20
  }
11
- export declare class SoferTui {
12
- private readonly agent;
13
- private readonly config;
14
- private state;
15
- private inputBuf;
16
- private cursor;
17
- private columns;
18
- private rows;
19
- private chatW;
20
- private sideW;
21
- private bodyH;
22
- private scrollOff;
23
- private status;
24
- private spinnerFrame;
25
- private spinnerTimer;
26
- private turnStartedAt;
27
- private abortCtrl;
28
- private info;
29
- private resolve;
30
- private onLine;
31
- constructor(agent: SoferAgent, config: SoferConfig);
32
- private measure;
33
- addMessage(role: ChatLine["role"], text: string): void;
34
- setInfo(info: Partial<TuiInfo>): void;
35
- private startSpinner;
36
- private stopSpinner;
37
- run(onLine: (line: string, signal?: AbortSignal) => Promise<void>): Promise<void>;
38
- private shutdown;
39
- private handleInput;
40
- private submit;
41
- private handleResize;
42
- render(): void;
43
- private buildFrame;
44
- private wrapMessage;
45
- private roleLabel;
46
- private buildSidebar;
47
- }
21
+ export declare function createTuiState(): TuiState;
22
+ export declare function runTui(state: TuiState, agent: SoferAgent, config: SoferConfig, onSubmit: (line: string, signal: AbortSignal) => Promise<void>): Promise<void>;
48
23
  //# sourceMappingURL=tui.d.ts.map
package/dist/tui.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAgBjE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,OAAO;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,QAAQ;IAoBjB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IApBzB,OAAO,CAAC,KAAK,CAAkB;IAC/B,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,YAAY,CAA+C;IACnE,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,SAAS,CAAgC;IACjD,OAAO,CAAC,IAAI,CAA8C;IAC1D,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAAwE;gBAGnE,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,WAAW;IAGtC,OAAO,CAAC,OAAO;IAQf,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAKtD,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI;IAIrC,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,WAAW;IASb,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvF,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,WAAW,CA6BjB;YAEY,MAAM;IAgBpB,OAAO,CAAC,YAAY,CAAkD;IAEtE,MAAM,IAAI,IAAI;IAKd,OAAO,CAAC,UAAU;IA8DlB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,YAAY;CA8BrB"}
1
+ {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAqB,YAAY,EAAa,MAAM,UAAU,CAAC;AAKtE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IAClD,MAAM,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC;IAC7D,KAAK,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,IAAI,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/C,YAAY,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;IACtD,aAAa,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAC9D,WAAW,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC,CAAC;CACtE;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,OAAO;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA+LD,wBAAgB,cAAc,IAAI,QAAQ,CAUzC;AAED,wBAAsB,MAAM,CAC1B,KAAK,EAAE,QAAQ,EACf,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAC7D,OAAO,CAAC,IAAI,CAAC,CAYf"}
package/dist/tui.jsx ADDED
@@ -0,0 +1,182 @@
1
+ import { render, useKeyboard, useTerminalDimensions } from "@opentui/solid";
2
+ import { For, createEffect, createSignal, onCleanup } from "solid-js";
3
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
+ const SPINNER_MS = 80;
5
+ function formatElapsed(startedAt) {
6
+ if (!startedAt)
7
+ return "";
8
+ const sec = Math.floor((Date.now() - startedAt) / 1000);
9
+ if (sec < 60)
10
+ return `${sec}s`;
11
+ const m = Math.floor(sec / 60);
12
+ return `${m}m${String(sec % 60).padStart(2, "0")}s`;
13
+ }
14
+ function shortModel(model) {
15
+ const m = model.toLowerCase();
16
+ if (m.includes("sonnet"))
17
+ return "sonnet-4";
18
+ if (m.includes("haiku"))
19
+ return "haiku";
20
+ if (m.includes("opus"))
21
+ return "opus-4";
22
+ return model.length > 14 ? `${model.slice(0, 13)}…` : model;
23
+ }
24
+ function App(props) {
25
+ const { state, agent, config } = props;
26
+ const dims = useTerminalDimensions();
27
+ const [rows] = state.rows;
28
+ const [status] = state.status;
29
+ const [input, setInput] = state.input;
30
+ const [info] = state.info;
31
+ const [spinnerFrame, setSpinnerFrame] = state.spinnerFrame;
32
+ const [turnStartedAt] = state.turnStartedAt;
33
+ const [, setActiveAbort] = state.activeAbort;
34
+ const [activeAbort] = state.activeAbort;
35
+ createEffect(() => {
36
+ if (status() !== "thinking") {
37
+ setSpinnerFrame(0);
38
+ return;
39
+ }
40
+ const id = setInterval(() => setSpinnerFrame((f) => (f + 1) % SPINNER_FRAMES.length), SPINNER_MS);
41
+ onCleanup(() => clearInterval(id));
42
+ });
43
+ useKeyboard((evt) => {
44
+ if (evt.ctrl && evt.name === "c") {
45
+ evt.preventDefault();
46
+ props.onExit();
47
+ return;
48
+ }
49
+ if (evt.name === "escape") {
50
+ const ab = activeAbort();
51
+ if (ab && !ab.signal.aborted)
52
+ ab.abort();
53
+ return;
54
+ }
55
+ if (status() === "thinking")
56
+ return;
57
+ if (evt.name === "return") {
58
+ const text = input().trim();
59
+ if (!text)
60
+ return;
61
+ if (text === "/exit" || text === "/quit") {
62
+ props.onExit();
63
+ return;
64
+ }
65
+ const ctrl = new AbortController();
66
+ setActiveAbort(ctrl);
67
+ state.rows[1](prev => [...prev, { role: "you", text }]);
68
+ setInput("");
69
+ state.status[1]("thinking");
70
+ state.turnStartedAt[1](Date.now());
71
+ props.onSubmit(text, ctrl.signal).finally(() => {
72
+ state.status[1]("idle");
73
+ state.turnStartedAt[1](null);
74
+ setActiveAbort(null);
75
+ });
76
+ return;
77
+ }
78
+ if (evt.name === "backspace" || evt.name === "delete") {
79
+ setInput((p) => p.slice(0, -1));
80
+ return;
81
+ }
82
+ if (evt.sequence && !evt.ctrl && !evt.meta && !evt.option) {
83
+ setInput((p) => p + evt.sequence);
84
+ }
85
+ });
86
+ const sidebarW = Math.min(30, Math.floor(dims().width * 0.22));
87
+ const chatW = dims().width - sidebarW;
88
+ const addr = () => `${agent.address.slice(0, 6)}…${agent.address.slice(-4)}`;
89
+ return (<box flexDirection="row" width={dims().width} height={dims().height}>
90
+ {/* Chat area */}
91
+ <box flexDirection="column" width={chatW} height={dims().height}>
92
+ <scrollbox flexGrow={1} flexShrink={1} stickyScroll stickyStart="bottom" contentOptions={{ flexDirection: "column", paddingLeft: 1, paddingRight: 1, paddingTop: 1, paddingBottom: 1 }}>
93
+ <For each={rows()}>
94
+ {(line) => {
95
+ const color = line.role === "you" ? "#67e8f9"
96
+ : line.role === "agent" ? "#86efac"
97
+ : line.role === "error" ? "#fca5a5"
98
+ : "#9ca3af";
99
+ const label = line.role === "you" ? "you"
100
+ : line.role === "agent" ? agent.name.toLowerCase().slice(0, 5)
101
+ : line.role === "error" ? "err"
102
+ : "sys";
103
+ return (<box flexDirection="row">
104
+ <text fg={color} flexShrink={0}>{` ${label.padEnd(5)} `}</text>
105
+ <text wrapMode="word" flexGrow={1} fg="#e5e7eb">
106
+ {line.text}
107
+ </text>
108
+ </box>);
109
+ }}
110
+ </For>
111
+ </scrollbox>
112
+
113
+ <box flexDirection="row" flexShrink={0} paddingLeft={3} paddingRight={2} marginTop={1}>
114
+ <text fg="#67e8f9" flexGrow={1}>
115
+ {(() => {
116
+ if (status() !== "thinking")
117
+ return " ";
118
+ const frame = SPINNER_FRAMES[spinnerFrame()];
119
+ const elapsed = formatElapsed(turnStartedAt());
120
+ return elapsed
121
+ ? `${frame} thinking… ${elapsed} (esc to interrupt)`
122
+ : `${frame} thinking… (esc to interrupt)`;
123
+ })()}
124
+ </text>
125
+ </box>
126
+
127
+ <box flexDirection="row" flexShrink={0} minHeight={3} maxHeight={8} borderStyle="rounded" borderColor="#374151" paddingLeft={1} paddingRight={1} marginLeft={2} marginRight={2}>
128
+ <text fg="#67e8f9" flexShrink={0}>{"› "}</text>
129
+ <text wrapMode="word" flexGrow={1} fg="#e5e7eb">
130
+ {`${input()}${status() === "idle" ? "▋" : ""}`}
131
+ </text>
132
+ </box>
133
+
134
+ <box flexDirection="row" flexShrink={0} paddingLeft={2} paddingRight={2}>
135
+ <text fg="#9ca3af">
136
+ {[
137
+ addr(),
138
+ config.network,
139
+ shortModel(config.brain.model),
140
+ info().balance ? `${info().balance} SUI` : null,
141
+ `in ${agent.usage.tokens.inputTokens} out ${agent.usage.tokens.outputTokens} $${agent.usage.costUsd.toFixed(4)}`,
142
+ "/exit",
143
+ ].filter(Boolean).join(" · ")}
144
+ </text>
145
+ </box>
146
+ </box>
147
+
148
+ {/* Sidebar */}
149
+ <box flexDirection="column" width={sidebarW} borderStyle="single" borderColor="#374151" paddingLeft={2} paddingRight={1}>
150
+ <text fg="#86efac">{agent.name}</text>
151
+ <text fg="#9ca3af">{addr()}</text>
152
+ <text>{" "}</text>
153
+ <text fg="#6b7280">{"─".repeat(sidebarW - 4)}</text>
154
+ <text fg="#9ca3af">{`network ${config.network}`}</text>
155
+ <text fg="#9ca3af">{`model ${shortModel(config.brain.model)}`}</text>
156
+ {info().balance ? <text fg="#fbbf24">{`balance ${info().balance} SUI`}</text> : null}
157
+ <text>{" "}</text>
158
+ <text fg="#6b7280">{"─".repeat(sidebarW - 4)}</text>
159
+ <text fg="#9ca3af">{`last in ${info().lastTurnIn} out ${info().lastTurnOut}`}</text>
160
+ <text fg="#9ca3af">{`total in ${agent.usage.tokens.inputTokens}`}</text>
161
+ <text fg="#9ca3af">{` out ${agent.usage.tokens.outputTokens}`}</text>
162
+ <text fg="#fbbf24">{` $${agent.usage.costUsd.toFixed(6)}`}</text>
163
+ </box>
164
+ </box>);
165
+ }
166
+ export function createTuiState() {
167
+ return {
168
+ rows: createSignal([]),
169
+ status: createSignal("idle"),
170
+ input: createSignal(""),
171
+ info: createSignal({ lastTurnIn: 0, lastTurnOut: 0 }),
172
+ spinnerFrame: createSignal(0),
173
+ turnStartedAt: createSignal(null),
174
+ activeAbort: createSignal(null),
175
+ };
176
+ }
177
+ export async function runTui(state, agent, config, onSubmit) {
178
+ return new Promise((resolve) => {
179
+ render(() => (<App state={state} agent={agent} config={config} onSubmit={onSubmit} onExit={resolve}/>));
180
+ });
181
+ }
182
+ //# 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,12 @@
1
1
  {
2
2
  "name": "@sofer_agent/cli",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "type": "module",
5
5
  "bin": {
6
- "sofer": "./dist/bin.js"
6
+ "sofer": "./bin/sofer"
7
7
  },
8
8
  "files": [
9
+ "bin",
9
10
  "dist",
10
11
  "src"
11
12
  ],
@@ -26,14 +27,17 @@
26
27
  ],
27
28
  "dependencies": {
28
29
  "@clack/prompts": "^1.5.1",
29
- "@sofer_agent/core": "0.3.3"
30
+ "@opentui/core": "^0.1.97",
31
+ "@opentui/solid": "^0.1.97",
32
+ "@sofer_agent/core": "0.3.3",
33
+ "solid-js": "^1.9.12"
30
34
  },
31
35
  "devDependencies": {
32
36
  "@types/node": "^22.10.2",
33
37
  "typescript": "^5.7.2"
34
38
  },
35
39
  "scripts": {
36
- "build": "tsc -b && chmod +x dist/bin.js",
40
+ "build": "tsc -b && chmod +x bin/sofer",
37
41
  "typecheck": "tsc --noEmit"
38
42
  }
39
43
  }
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 { SoferTui } from "./tui.js";
11
+ import { createTuiState, runTui } from "./tui.jsx";
12
12
 
13
13
  interface SessionMessage {
14
14
  role: string;
@@ -36,15 +36,6 @@ function saveSession(path: string, messages: SessionMessage[]): void {
36
36
  writeFileSync(path, messages.map((m) => JSON.stringify(m)).join("\n") + "\n", "utf8");
37
37
  }
38
38
 
39
- function cleanError(raw: string): string {
40
- try {
41
- const obj = JSON.parse(raw);
42
- if (obj?.error?.type === "overloaded_error") return "Claude is overloaded — try again in a moment.";
43
- if (obj?.error?.message) return obj.error.message;
44
- } catch { /* not JSON */ }
45
- return raw;
46
- }
47
-
48
39
  export async function chatCommand(): Promise<void> {
49
40
  const config = loadConfig();
50
41
  const secrets = loadSecrets();
@@ -63,26 +54,35 @@ export async function chatCommand(): Promise<void> {
63
54
  if (history.length > 0) agent.loadHistory(history);
64
55
  }
65
56
 
66
- const tui = new SoferTui(agent, config);
57
+ const tuiState = createTuiState();
58
+ const [, setRows] = tuiState.rows;
59
+ const [, setInfo] = tuiState.info;
67
60
 
68
- agent.getBalance().then((bal) => tui.setInfo({ balance: (Number(bal) / 1e9).toFixed(4) })).catch(() => {});
61
+ agent.getBalance().then((bal) => setInfo((prev) => ({ ...prev, balance: (Number(bal) / 1e9).toFixed(4) }))).catch(() => {});
69
62
 
70
- await tui.run(async (line, signal) => {
63
+ await runTui(tuiState, agent, config, async (line, signal) => {
71
64
  try {
72
65
  const { text, turnUsage } = await agent.chat(line, signal);
73
- tui.addMessage("agent", text);
74
- tui.setInfo({ lastTurnIn: turnUsage.inputTokens, lastTurnOut: turnUsage.outputTokens });
66
+ setRows((prev) => [...prev, { role: "agent", text }]);
67
+ setInfo((prev) => ({ ...prev, lastTurnIn: turnUsage.inputTokens, lastTurnOut: turnUsage.outputTokens }));
75
68
  } catch (e) {
76
- if (signal?.aborted) {
77
- tui.addMessage("system", "turn interrupted (esc).");
69
+ if (signal.aborted) {
70
+ setRows((prev) => [...prev, { role: "system", text: "turn interrupted." }]);
78
71
  } else {
79
72
  const raw = e instanceof Error ? e.message : String(e);
80
- const clean = cleanError(raw);
81
- tui.addMessage("error", clean);
73
+ setRows((prev) => [...prev, { role: "error", text: cleanError(raw) }]);
82
74
  }
83
75
  }
84
- agent.getBalance().then((bal) => tui.setInfo({ balance: (Number(bal) / 1e9).toFixed(4) })).catch(() => {});
76
+ agent.getBalance().then((bal) => setInfo((prev) => ({ ...prev, balance: (Number(bal) / 1e9).toFixed(4) }))).catch(() => {});
85
77
  if (sp) saveSession(sp, agent.getHistory());
86
- tui.render();
87
78
  });
88
79
  }
80
+
81
+ function cleanError(raw: string): string {
82
+ try {
83
+ const obj = JSON.parse(raw);
84
+ if (obj?.error?.type === "overloaded_error") return "Claude is overloaded — try again in a moment.";
85
+ if (obj?.error?.message) return obj.error.message;
86
+ } catch { /* not JSON */ }
87
+ return raw;
88
+ }
package/src/tui.tsx ADDED
@@ -0,0 +1,247 @@
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
+ }
package/src/tui.ts DELETED
@@ -1,358 +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
- const B = {
12
- tl: "╭", tr: "╮", bl: "╰", br: "╯",
13
- h: "─", v: "│",
14
- lt: "├", rt: "┤", tt: "┬", bt: "┴",
15
- };
16
-
17
- export interface ChatLine {
18
- role: "you" | "agent" | "system" | "error";
19
- text: string;
20
- }
21
-
22
- export interface TuiInfo {
23
- lastTurnIn: number;
24
- lastTurnOut: number;
25
- balance?: string;
26
- }
27
-
28
- export class SoferTui {
29
- private state: ChatLine[] = [];
30
- private inputBuf = "";
31
- private cursor = 0;
32
- private columns = 90;
33
- private rows = 28;
34
- private chatW = 58;
35
- private sideW = 26;
36
- private bodyH = 22;
37
- private scrollOff = Number.MAX_SAFE_INTEGER;
38
- private status: "idle" | "thinking" = "idle";
39
- private spinnerFrame = 0;
40
- private spinnerTimer: ReturnType<typeof setInterval> | null = null;
41
- private turnStartedAt = 0;
42
- private abortCtrl: AbortController | null = null;
43
- private info: TuiInfo = { lastTurnIn: 0, lastTurnOut: 0 };
44
- private resolve: (() => void) | null = null;
45
- private onLine: ((line: string, signal?: AbortSignal) => Promise<void>) | null = null;
46
-
47
- constructor(
48
- private readonly agent: SoferAgent,
49
- private readonly config: SoferConfig,
50
- ) {}
51
-
52
- private measure(): void {
53
- this.columns = Math.max(80, process.stdout.columns ?? 80);
54
- this.rows = Math.max(24, process.stdout.rows ?? 24);
55
- this.chatW = Math.min(120, Math.max(MIN_CHAT_W, this.columns - SIDEBAR_W - 3));
56
- this.sideW = this.columns - this.chatW - 3;
57
- this.bodyH = this.rows - 5;
58
- }
59
-
60
- addMessage(role: ChatLine["role"], text: string): void {
61
- this.state.push({ role, text });
62
- this.scrollOff = Number.MAX_SAFE_INTEGER;
63
- }
64
-
65
- setInfo(info: Partial<TuiInfo>): void {
66
- Object.assign(this.info, info);
67
- }
68
-
69
- private startSpinner(): void {
70
- if (this.spinnerTimer) return;
71
- this.status = "thinking";
72
- this.turnStartedAt = Date.now();
73
- this.spinnerFrame = 0;
74
- this.spinnerTimer = setInterval(() => {
75
- this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
76
- this.render();
77
- }, SPINNER_MS);
78
- }
79
-
80
- private stopSpinner(): void {
81
- this.status = "idle";
82
- this.turnStartedAt = 0;
83
- if (this.spinnerTimer) {
84
- clearInterval(this.spinnerTimer);
85
- this.spinnerTimer = null;
86
- }
87
- }
88
-
89
- async run(onLine: (line: string, signal?: AbortSignal) => Promise<void>): Promise<void> {
90
- this.onLine = onLine;
91
- this.measure();
92
- if (process.stdin.isTTY) process.stdin.setRawMode?.(true);
93
- process.stdin.on("data", this.handleInput);
94
- process.stdout.on("resize", this.handleResize);
95
- process.on("SIGWINCH", this.handleResize);
96
- this.render();
97
- return new Promise<void>((resolve) => { this.resolve = resolve; });
98
- }
99
-
100
- private shutdown(): void {
101
- this.stopSpinner();
102
- process.stdin.setRawMode?.(false);
103
- process.stdin.removeAllListeners("data");
104
- process.stdout.removeAllListeners("resize");
105
- process.removeAllListeners("SIGWINCH");
106
- this.render();
107
- process.stdout.write("\n");
108
- this.resolve?.();
109
- }
110
-
111
- private handleInput = (data: Buffer): void => {
112
- const str = data.toString();
113
- for (const ch of str) {
114
- const code = ch.charCodeAt(0);
115
- if (code === 3) { this.shutdown(); return; }
116
- if (code === 4 && this.inputBuf.length === 0) { this.shutdown(); return; }
117
- if (code === 27) {
118
- if (this.status === "thinking" && this.abortCtrl) { this.abortCtrl.abort(); return; }
119
- this.inputBuf = "";
120
- this.cursor = 0;
121
- this.render();
122
- return;
123
- }
124
- if (this.status === "thinking") continue;
125
- if (code === 13) { this.submit(); return; }
126
- if (code === 127) {
127
- if (this.cursor > 0) {
128
- this.inputBuf = this.inputBuf.slice(0, this.cursor - 1) + this.inputBuf.slice(this.cursor);
129
- this.cursor--;
130
- }
131
- this.render();
132
- return;
133
- }
134
- if (ch >= " ") {
135
- this.inputBuf = this.inputBuf.slice(0, this.cursor) + ch + this.inputBuf.slice(this.cursor);
136
- this.cursor++;
137
- this.render();
138
- }
139
- }
140
- };
141
-
142
- private async submit(): Promise<void> {
143
- const line = this.inputBuf.trim();
144
- this.inputBuf = "";
145
- this.cursor = 0;
146
- if (!line) { this.render(); return; }
147
- if (line === "/exit" || line === "/quit") { this.shutdown(); return; }
148
- this.addMessage("you", line);
149
- this.abortCtrl = new AbortController();
150
- this.startSpinner();
151
- this.render();
152
- await this.onLine?.(line, this.abortCtrl.signal);
153
- this.abortCtrl = null;
154
- this.stopSpinner();
155
- this.render();
156
- }
157
-
158
- private handleResize = (): void => { this.measure(); this.render(); };
159
-
160
- render(): void {
161
- this.measure();
162
- process.stdout.write("\x1b[?25l\x1b[2J\x1b[H" + this.buildFrame() + "\x1b[?25h");
163
- }
164
-
165
- private buildFrame(): string {
166
- const { columns, chatW, sideW, bodyH } = this;
167
- const innerSideW = sideW - 2;
168
- const sep = `${B.lt}${B.h.repeat(chatW)}${B.bt}${B.h.repeat(sideW)}${B.rt}`;
169
- const inputW = columns - 2;
170
-
171
- let buf = `${B.tl}${B.h.repeat(chatW)}${B.tt}${B.h.repeat(sideW)}${B.tr}\n`;
172
-
173
- const wrapped = this.state.flatMap((m) => this.wrapMessage(m, chatW));
174
- const maxScroll = Math.max(0, wrapped.length - bodyH);
175
- if (this.scrollOff > maxScroll) this.scrollOff = maxScroll;
176
- if (this.scrollOff < 0) this.scrollOff = 0;
177
- const visible = wrapped.slice(this.scrollOff, this.scrollOff + bodyH);
178
-
179
- const sidebarLines = this.buildSidebar(innerSideW);
180
- const sbTop = Math.max(0, Math.floor((bodyH - sidebarLines.length) / 2));
181
-
182
- for (let r = 0; r < bodyH; r++) {
183
- buf += B.v;
184
- const line = visible[r];
185
- if (line) buf += padTrunc(line, chatW);
186
- else buf += " ".repeat(chatW);
187
- buf += B.v;
188
- const sbIdx = r - sbTop;
189
- const sbLine = (sbIdx >= 0 && sbIdx < sidebarLines.length) ? sidebarLines[sbIdx]! : "";
190
- buf += ` ${padRight(sbLine, innerSideW)} `;
191
- buf += `${B.v}\n`;
192
- }
193
-
194
- buf += `${sep}\n`;
195
-
196
- if (this.status === "thinking") {
197
- const frame = SPINNER_FRAMES[this.spinnerFrame]!;
198
- const elapsed = this.turnStartedAt ? formatElapsed(Date.now() - this.turnStartedAt) : "";
199
- const spinnerText = `${frame} thinking…${elapsed ? ` ${elapsed}` : ""} (esc to interrupt)`;
200
- const padLen = Math.max(0, inputW - 1 - spinnerText.length);
201
- buf += `${B.v} \x1b[36m${spinnerText}\x1b[0m${" ".repeat(padLen)}${B.v}\n`;
202
- }
203
-
204
- buf += `${B.tl}${B.h.repeat(inputW)}${B.tr}\n`;
205
- const cursorCh = this.status === "idle" ? "\x1b[5m▋\x1b[25m" : "";
206
- const inputMaxW = inputW - 3;
207
- const visibleInput = this.inputBuf.length > inputMaxW
208
- ? this.inputBuf.slice(this.inputBuf.length - inputMaxW)
209
- : this.inputBuf;
210
- const inputPad = inputMaxW - visibleInput.length;
211
- buf += `${B.v} \x1b[36m>\x1b[0m ${visibleInput}${cursorCh}${" ".repeat(Math.max(0, inputPad))} ${B.v}\n`;
212
- buf += `${B.bl}${B.h.repeat(inputW)}${B.br}\n`;
213
-
214
- const totalIn = this.agent.usage.tokens.inputTokens;
215
- const totalOut = this.agent.usage.tokens.outputTokens;
216
- const cost = this.agent.usage.costUsd.toFixed(4);
217
- const ft = `\x1b[90m${this.agent.address.slice(0, 10)}… · in ${totalIn} out ${totalOut} $${cost} /exit\x1b[0m`;
218
- buf += `${padTrunc(ft, columns)}\n`;
219
-
220
- const cursorRow = bodyH + 4 + (this.status === "thinking" ? 1 : 0);
221
- const cursorCol = 4 + Math.min(this.cursor, inputMaxW);
222
- buf += `\x1b[${cursorRow};${cursorCol}H`;
223
-
224
- return buf;
225
- }
226
-
227
- private wrapMessage(msg: ChatLine, width: number): string[] {
228
- const label = this.roleLabel(msg.role);
229
- const bodyWidth = Math.max(10, width - GUTTER.length - LABEL_W - 1);
230
- const lines = wrapText(msg.text, bodyWidth);
231
- return lines.map((l, i) => i === 0 ? `${GUTTER}${label} ${l}` : `${INDENT}${l}`);
232
- }
233
-
234
- private roleLabel(role: ChatLine["role"]): string {
235
- switch (role) {
236
- case "you": return "\x1b[36myou \x1b[0m";
237
- case "agent": return `\x1b[32m${this.agent.name.toLowerCase().padEnd(LABEL_W)}\x1b[0m`;
238
- case "system": return "\x1b[90msys \x1b[0m";
239
- case "error": return "\x1b[31merr \x1b[0m";
240
- }
241
- }
242
-
243
- private buildSidebar(width: number): string[] {
244
- const { agent, config, info } = this;
245
- const addr = `${agent.address.slice(0, 6)}…${agent.address.slice(-4)}`;
246
- const totalIn = agent.usage.tokens.inputTokens;
247
- const totalOut = agent.usage.tokens.outputTokens;
248
- const cost = agent.usage.costUsd.toFixed(6);
249
-
250
- const rows = [
251
- `\x1b[1;36m${agent.name}\x1b[0m`,
252
- `\x1b[90m${addr}\x1b[0m`,
253
- "",
254
- `${B.h.repeat(width)}`,
255
- `\x1b[90mnetwork\x1b[0m ${config.network}`,
256
- `\x1b[90mmodel\x1b[0m ${shortModel(config.brain.model)}`,
257
- ];
258
-
259
- if (info.balance) {
260
- rows.push("", `\x1b[90mbalance\x1b[0m \x1b[33m${info.balance} SUI\x1b[0m`);
261
- }
262
-
263
- rows.push(
264
- "",
265
- `\x1b[90mlast\x1b[0m in ${info.lastTurnIn} out ${info.lastTurnOut}`,
266
- "",
267
- `\x1b[90mtotal\x1b[0m in ${totalIn} out ${totalOut}`,
268
- ` \x1b[33m$${cost}\x1b[0m`,
269
- );
270
-
271
- return rows;
272
- }
273
- }
274
-
275
- function wrapText(text: string, width: number): string[] {
276
- if (!text) return [""];
277
- const lines: string[] = [];
278
- for (const para of text.split("\n")) {
279
- const words = para.split(" ");
280
- let cur = "";
281
- for (const w of words) {
282
- if (w.length > width) {
283
- if (cur) { lines.push(cur); cur = ""; }
284
- for (let i = 0; i < w.length; i += width) lines.push(w.slice(i, i + width));
285
- } else if (cur && cur.length + 1 + w.length > width) {
286
- lines.push(cur);
287
- cur = w;
288
- } else {
289
- cur = cur ? `${cur} ${w}` : w;
290
- }
291
- }
292
- if (cur) lines.push(cur);
293
- }
294
- return lines.length > 0 ? lines : [""];
295
- }
296
-
297
- function stripAnsi(s: string): string {
298
- // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping
299
- return s.replace(/\x1b\[\d*;?\d*m/g, "");
300
- }
301
-
302
- function visibleLen(s: string): number {
303
- // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping
304
- const re = /\x1b\[\d*;?\d*m/g;
305
- let len = 0;
306
- let last = 0;
307
- let m = re.exec(s);
308
- while (m !== null) {
309
- len += m.index - last;
310
- last = m.index + m[0].length;
311
- m = re.exec(s);
312
- }
313
- return len + s.length - last;
314
- }
315
-
316
- function padRight(s: string, w: number): string {
317
- const vl = visibleLen(s);
318
- return vl >= w ? s : s + " ".repeat(w - vl);
319
- }
320
-
321
- function padTrunc(s: string, w: number): string {
322
- const vl = visibleLen(s);
323
- if (vl < w) return s + " ".repeat(w - vl);
324
- if (vl === w) return s;
325
- // Truncate preserving ANSI codes — walk through character by character
326
- let out = "";
327
- let visible = 0;
328
- let i = 0;
329
- while (i < s.length && visible < w) {
330
- if (s[i] === "\x1b" && s[i + 1] === "[") {
331
- // ANSI escape sequence — copy until 'm'
332
- const end = s.indexOf("m", i);
333
- if (end !== -1) { out += s.slice(i, end + 1); i = end + 1; }
334
- else { out += s[i]; i++; }
335
- } else {
336
- out += s[i];
337
- visible++;
338
- i++;
339
- }
340
- }
341
- return out;
342
- }
343
-
344
- function formatElapsed(ms: number): string {
345
- const sec = Math.floor(ms / 1000);
346
- if (sec < 60) return `${sec}s`;
347
- const m = Math.floor(sec / 60);
348
- return `${m}m${String(sec % 60).padStart(2, "0")}s`;
349
- }
350
-
351
- function shortModel(model: string): string {
352
- const m = model.toLowerCase();
353
- if (m.includes("sonnet")) return "Sonnet 4";
354
- if (m.includes("haiku")) return "Haiku";
355
- if (m.includes("opus")) return "Opus 4";
356
- if (m.includes("fable")) return "Fable 5";
357
- return model.length > 14 ? `${model.slice(0, 13)}…` : model;
358
- }