aether-code 0.6.1 → 0.8.0

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/src/render.js CHANGED
@@ -1,198 +1,58 @@
1
- // ANSI helpers — no chalk dependency. Designed to feel like Claude Code:
2
- // bordered banner, indented tool calls, structured tool-result rendering.
3
-
4
- const isTty = process.stdout.isTTY;
5
- const noColor = !!process.env.NO_COLOR || !isTty;
6
-
7
- function wrap(open, close) {
8
- return (s) => (noColor ? String(s) : `\x1b[${open}m${s}\x1b[${close}m`);
9
- }
10
-
11
- export const c = {
12
- bold: wrap(1, 22),
13
- dim: wrap(2, 22),
14
- italic: wrap(3, 23),
15
- red: wrap(31, 39),
16
- green: wrap(32, 39),
17
- yellow: wrap(33, 39),
18
- blue: wrap(34, 39),
19
- magenta: wrap(35, 39),
20
- cyan: wrap(36, 39),
21
- gray: wrap(90, 39),
22
- white: wrap(37, 39),
23
- };
24
-
25
- const W = () => Math.min(process.stdout.columns || 80, 90);
26
-
27
- export function hr() {
28
- return c.gray("─".repeat(W()));
29
- }
30
-
31
- export function divider() {
32
- return hr();
33
- }
34
-
35
- /* ─── Banner — Claude-Code-style box ─── */
36
-
37
- export function banner({ version, model, mode, balance, cwd }) {
38
- const w = Math.min(W(), 78);
39
- const top = c.gray("" + "─".repeat(w - 2) + "╮");
40
- const bot = c.gray("╰" + "─".repeat(w - 2) + "╯");
41
- const pad = (line) => {
42
- // Strip ANSI for length calc
43
- const visible = line.replace(/\x1b\[[0-9;]*m/g, "");
44
- const space = Math.max(0, w - 2 - visible.length - 2);
45
- return c.gray("│ ") + line + " ".repeat(space) + c.gray(" │");
46
- };
47
- const blank = pad("");
48
- const title = pad(`${c.magenta(c.bold("✻ aether-code"))} ${c.gray("v" + version)}`);
49
- const sub = pad(`${c.gray(model + " · uncensored · " + mode + (balance != null ? " · " + balance.toLocaleString() + " credits" : ""))}`);
50
- const cwdLine = pad(c.gray("📁 " + truncatePath(cwd, w - 6)));
51
- const tip = pad(c.gray("Type ") + c.cyan("/help") + c.gray(" for shortcuts. ") + c.cyan("/exit") + c.gray(" or Ctrl+C twice to quit."));
52
- return [top, blank, title, sub, cwdLine, blank, tip, bot].join("\n");
53
- }
54
-
55
- function truncatePath(p, max) {
56
- if (!p) return "";
57
- if (p.length <= max) return p;
58
- return "…" + p.slice(-(max - 1));
59
- }
60
-
61
- /* ─── Turn / cost line ─── */
62
-
63
- export function turnLine(n) {
64
- return c.dim("─── turn " + n + " " + "─".repeat(Math.max(0, W() - 12 - String(n).length)));
65
- }
66
-
67
- export function costLine({ creditsCharged, inputTokens, outputTokens, finishReason }) {
68
- const parts = [
69
- `${creditsCharged} cr`,
70
- `${inputTokens}→${outputTokens} tok`,
71
- finishReason ? `finish: ${finishReason}` : null,
72
- ].filter(Boolean);
73
- return " " + c.dim(parts.join(" · "));
74
- }
75
-
76
- /* ─── Assistant text ─── */
77
-
78
- export function assistantPrefix() {
79
- return c.magenta("● ") + c.bold(c.magenta("Aether")) + c.gray(":");
80
- }
81
-
82
- /* ─── Tool calls — friendly labels instead of raw JSON ─── */
83
-
84
- export function toolAnnounce(name, args) {
85
- const label = TOOL_LABELS[name] ? TOOL_LABELS[name](args) : `${name}(${jsonPreview(args)})`;
86
- return c.cyan(" ⚡ ") + c.bold(c.cyan(name)) + c.gray(" ") + c.gray(label);
87
- }
88
-
89
- const TOOL_LABELS = {
90
- read_file: (a) => `→ ${a.path || "?"}`,
91
- list_dir: (a) => `→ ${a.path || "."}`,
92
- search_files: (a) => `→ ${a.pattern || "?"} in ${a.path || "."}`,
93
- write_file: (a) => `→ ${a.path || "?"} ${c.gray("(" + (a.content?.length || 0) + " bytes)")}`,
94
- edit_file: (a) => `→ ${a.path || "?"}`,
95
- run_shell: (a) => `${c.yellow("$")} ${a.command || "?"}` + (a.cwd && a.cwd !== "." ? c.gray(` (cwd: ${a.cwd})`) : ""),
96
- };
97
-
98
- function jsonPreview(obj) {
99
- const s = JSON.stringify(obj);
100
- return s.length > 80 ? s.slice(0, 77) + "..." : s;
101
- }
102
-
103
- /* ─── Tool results — structured render per tool ─── */
104
-
105
- export function toolResultLine(name, result) {
106
- const icon = result.ok ? c.green(" ✓") : c.red(" ✗");
107
- const summary = summarizeResult(name, result);
108
- return `${icon} ${c.dim(summary)}`;
109
- }
110
-
111
- function summarizeResult(name, result) {
112
- if (!result.ok) {
113
- // Try to extract a useful one-liner from error JSON
114
- let txt = result.output || "";
115
- try {
116
- const j = JSON.parse(txt);
117
- if (j.exit_code !== undefined) {
118
- // Shell error
119
- const stderr = (j.stderr || "").trim().split("\n")[0] || "(no stderr)";
120
- return `exit ${j.exit_code} — ${stderr}`;
121
- }
122
- } catch { /* not JSON */ }
123
- const firstLine = txt.split("\n")[0];
124
- return firstLine.length > 100 ? firstLine.slice(0, 97) + "..." : firstLine;
125
- }
126
- // Success — short summary by tool type
127
- const out = result.output || "";
128
- if (name === "read_file") {
129
- const lines = out.split("\n").length;
130
- return `read ${out.length} bytes (${lines} line${lines === 1 ? "" : "s"})`;
131
- }
132
- if (name === "list_dir") {
133
- try {
134
- const items = JSON.parse(out);
135
- return `${items.length} entr${items.length === 1 ? "y" : "ies"}`;
136
- } catch {
137
- return "ok";
138
- }
139
- }
140
- if (name === "search_files") {
141
- try {
142
- const matches = JSON.parse(out);
143
- return `${matches.length} match${matches.length === 1 ? "" : "es"}`;
144
- } catch {
145
- return "ok";
146
- }
147
- }
148
- if (name === "write_file" || name === "edit_file") {
149
- return out.split("\n")[0];
150
- }
151
- if (name === "run_shell") {
152
- try {
153
- const j = JSON.parse(out);
154
- const stdoutLines = (j.stdout || "").split("\n").filter(Boolean).length;
155
- return `exit ${j.exit_code} · ${stdoutLines} stdout line${stdoutLines === 1 ? "" : "s"}`;
156
- } catch {
157
- return "ok";
158
- }
159
- }
160
- return out.split("\n")[0].slice(0, 80);
161
- }
162
-
163
- /* ─── Status line at end of turn ─── */
164
-
165
- export function statusLine({ sessionCredits, sessionIn, sessionOut, balance, history, mode }) {
166
- const parts = [];
167
- parts.push(`session: ${sessionCredits} cr · ${sessionIn}→${sessionOut} tok`);
168
- if (balance != null) parts.push(`balance: ${balance.toLocaleString()}`);
169
- if (history > 0) parts.push(`history: ${history} msg${history === 1 ? "" : "s"}`);
170
- parts.push(c.cyan(mode));
171
- return c.dim(parts.join(" · "));
172
- }
173
-
174
- /* ─── Errors ─── */
175
-
176
- export function errorLine(msg) {
177
- return `${c.red(c.bold("✗ Error:"))} ${msg}`;
178
- }
179
-
180
- export function warnLine(msg) {
181
- return `${c.yellow("⚠ ")} ${msg}`;
182
- }
183
-
184
- export function note(msg) {
185
- return c.dim(msg);
186
- }
187
-
188
- /* ─── Confirmation prompt rendering ─── */
189
-
190
- export function confirmPrompt(label) {
191
- return c.yellow(" ? ") + c.bold(label) + c.gray(" [y/N] ");
192
- }
193
-
194
- /* ─── Spinner / pending indicator (no animation, just text) ─── */
195
-
196
- export function pending(msg) {
197
- return c.gray(" ⋯ " + msg);
198
- }
1
+ // ANSI helpers — no chalk dependency.
2
+
3
+ const isTty = process.stdout.isTTY;
4
+ const noColor = !!process.env.NO_COLOR || !isTty;
5
+
6
+ function wrap(open, close) {
7
+ return (s) => (noColor ? String(s) : `\x1b[${open}m${s}\x1b[${close}m`);
8
+ }
9
+
10
+ export const c = {
11
+ bold: wrap(1, 22),
12
+ dim: wrap(2, 22),
13
+ red: wrap(31, 39),
14
+ green: wrap(32, 39),
15
+ yellow: wrap(33, 39),
16
+ blue: wrap(34, 39),
17
+ magenta: wrap(35, 39),
18
+ cyan: wrap(36, 39),
19
+ gray: wrap(90, 39),
20
+ };
21
+
22
+ export function divider() {
23
+ const w = process.stdout.columns || 60;
24
+ return c.gray("─".repeat(Math.min(60, w)));
25
+ }
26
+
27
+ export function turn(n) {
28
+ return c.gray(`turn ${n}`);
29
+ }
30
+
31
+ export function toolHeader(name, args) {
32
+ // Format args compactly. If any value is huge, truncate it.
33
+ const compact = JSON.stringify(args);
34
+ const trimmed = compact.length > 120 ? compact.slice(0, 117) + "..." : compact;
35
+ return `${c.cyan(c.bold(name))}${c.gray("(")}${c.gray(trimmed)}${c.gray(")")}`;
36
+ }
37
+
38
+ export function toolResult(text, ok = true) {
39
+ const prefix = ok ? c.green("") : c.red(" ✗ ");
40
+ // First line bold-ish, then dim continuation
41
+ const lines = text.split("\n");
42
+ const head = lines[0].slice(0, 200);
43
+ const rest = lines.slice(1, 6).join("\n").slice(0, 600);
44
+ return `${prefix}${head}${rest ? "\n" + c.dim(rest) : ""}`;
45
+ }
46
+
47
+ export function assistant(text) {
48
+ // Indent each line for visual separation from tool calls
49
+ return text.split("\n").map((l) => ` ${l}`).join("\n");
50
+ }
51
+
52
+ export function errorLine(msg) {
53
+ return `${c.red(c.bold("Error:"))} ${msg}`;
54
+ }
55
+
56
+ export function note(msg) {
57
+ return c.dim(msg);
58
+ }