@ikyyofc/gemini-cli 2.0.9 → 3.0.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/renderer.js +293 -241
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikyyofc/gemini-cli",
3
- "version": "2.0.9",
3
+ "version": "3.0.1",
4
4
  "description": "AI Agent CLI — native function calling · GEMINI.md context · extensions",
5
5
  "type": "module",
6
6
  "bin": { "gemini": "./index.js" },
package/src/renderer.js CHANGED
@@ -1,328 +1,380 @@
1
- // src/renderer.js — Terminal UI
1
+ // src/renderer.js
2
2
  import chalk from "chalk";
3
3
 
4
4
  const C = {
5
- blue: "#4A9EFF",
6
- teal: "#00D4AA",
7
- purple: "#C586FF",
8
- yellow: "#FFD080",
9
- orange: "#FF9060",
10
- red: "#FF5F7E",
11
- green: "#50FA7B",
12
- dim: "#4A4A5E",
13
- dimmer: "#32323E",
14
- muted: "#7A7A9A",
15
- white: "#E8E8F0",
16
- kw: "#79B8FF",
17
- str: "#F0A070",
18
- comment: "#6A9955",
19
- num: "#B5CEA8",
20
- fn: "#FFD080",
5
+ blue: "#4A9EFF", teal: "#00D4AA", purple: "#C586FF",
6
+ yellow: "#FFD080", orange:"#FF9060", red: "#FF5F7E",
7
+ dim: "#4A4A5E", dimmer:"#32323E", muted: "#7A7A9A",
8
+ white: "#E8E8F0", kw: "#79B8FF", str: "#F0A070",
9
+ comment: "#6A9955", num: "#B5CEA8", fn: "#FFD080",
21
10
  };
22
11
 
12
+ const tw = () => Math.min(process.stdout.columns || 72, 84);
13
+ const bw = () => Math.min(tw() - 4, 68);
14
+ const vlen = s => s.replace(/\x1b\[[0-9;]*m/g, "").length;
15
+
16
+ // Word-wrap a PLAIN (no ANSI) string to maxW, returns array of lines
17
+ function wrapPlain(text, maxW) {
18
+ if (!text || text.length <= maxW) return [text || ""];
19
+ const words = text.split(" ");
20
+ const lines = [];
21
+ let cur = "";
22
+ for (const w of words) {
23
+ const candidate = cur ? cur + " " + w : w;
24
+ if (candidate.length <= maxW) { cur = candidate; continue; }
25
+ if (cur) lines.push(cur);
26
+ if (w.length > maxW) {
27
+ // hard-wrap single very long word (e.g. bare URL)
28
+ for (let i = 0; i < w.length; i += maxW) lines.push(w.slice(i, i + maxW));
29
+ cur = "";
30
+ } else { cur = w; }
31
+ }
32
+ if (cur) lines.push(cur);
33
+ return lines.length ? lines : [""];
34
+ }
35
+
23
36
  // ─────────────────────────────────────────────────────────────────
24
- // Syntax highlighting
37
+ // Syntax highlighting — applyToRaw prevents placeholder corruption
25
38
  // ─────────────────────────────────────────────────────────────────
26
39
  const KW = {
27
- js: /\b(const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|new|this|class|extends|import|export|default|async|await|try|catch|finally|throw|typeof|instanceof|of|in|null|undefined|true|false|void|delete|yield|from|static|super)\b/g,
28
- ts: /\b(const|let|var|function|return|if|else|for|while|switch|case|class|extends|import|export|default|async|await|try|catch|type|interface|enum|implements|declare|readonly|abstract|as|keyof|never|any|string|number|boolean|null|undefined|true|false)\b/g,
29
- py: /\b(def|class|return|if|elif|else|for|while|import|from|as|with|try|except|finally|raise|pass|break|continue|and|or|not|in|is|None|True|False|lambda|yield|global|async|await)\b/g,
30
- go: /\b(func|return|if|else|for|range|switch|var|const|type|struct|interface|import|package|defer|go|chan|map|make|new|nil|true|false)\b/g,
31
- sh: /\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|export|echo|local|source|cd|mkdir|rm|cp|mv|sudo|apt|npm|pip|git)\b/g,
32
- rs: /\b(fn|let|mut|return|if|else|for|match|use|mod|pub|struct|enum|impl|trait|type|const|async|await|true|false|None|Some|Ok|Err)\b/g,
33
- json: null,
40
+ js:/\b(const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|new|this|class|extends|import|export|default|async|await|try|catch|finally|throw|typeof|instanceof|of|in|null|undefined|true|false|void|delete|yield|from|static|super)\b/g,
41
+ ts:/\b(const|let|var|function|return|if|else|for|while|switch|case|class|extends|import|export|default|async|await|try|catch|type|interface|enum|implements|declare|readonly|abstract|as|keyof|never|any|string|number|boolean|null|undefined|true|false)\b/g,
42
+ py:/\b(def|class|return|if|elif|else|for|while|import|from|as|with|try|except|finally|raise|pass|break|continue|and|or|not|in|is|None|True|False|lambda|yield|global|async|await)\b/g,
43
+ go:/\b(func|return|if|else|for|range|switch|var|const|type|struct|interface|import|package|defer|go|chan|map|make|new|nil|true|false)\b/g,
44
+ sh:/\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|export|echo|local|source|cd|mkdir|rm|cp|mv|sudo|npm|pip|git)\b/g,
45
+ rs:/\b(fn|let|mut|return|if|else|for|match|use|mod|pub|struct|enum|impl|trait|type|const|async|await|true|false|None|Some|Ok|Err)\b/g,
34
46
  };
35
47
  const LANGMAP = {
36
- javascript:"js", js:"js", typescript:"ts", ts:"ts",
37
- python:"py", py:"py", go:"go", golang:"go",
38
- rust:"rs", rs:"rs", bash:"sh", sh:"sh", shell:"sh", zsh:"sh", fish:"sh",
39
- json:"json", jsonc:"json",
48
+ javascript:"js",js:"js",typescript:"ts",ts:"ts",
49
+ python:"py",py:"py",go:"go",golang:"go",
50
+ rust:"rs",rs:"rs",bash:"sh",sh:"sh",shell:"sh",zsh:"sh",fish:"sh",
40
51
  };
41
52
 
53
+ function applyToRaw(str, re, fn) {
54
+ return str.split(/(\x00\d+\x00)/).map((p, i) => i % 2 === 0 ? p.replace(re, fn) : p).join("");
55
+ }
56
+
42
57
  function highlight(code, lang = "") {
43
58
  const l = LANGMAP[lang.toLowerCase()] || "";
44
- if (!l || l === "json") return code;
45
- let r = code;
59
+ if (!l) return code;
46
60
  const saved = [];
47
61
  const save = s => { const id = `\x00${saved.length}\x00`; saved.push(s); return id; };
48
- r = r.replace(/(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm, m => save(chalk.hex(C.comment).italic(m)));
49
- r = r.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, m => save(chalk.hex(C.str)(m)));
50
- if (KW[l]) r = r.replace(KW[l], m => save(chalk.hex(C.kw).bold(m)));
51
- r = r.replace(/\b(\d+\.?\d*)\b/g, m => save(chalk.hex(C.num)(m)));
52
- r = r.replace(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/g, m => save(chalk.hex(C.fn)(m)));
53
- return r.replace(/\x00(\d+)\x00/g, (_, i) => saved[+i]);
62
+ let r = code;
63
+ r = applyToRaw(r, /(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm, m => save(chalk.hex(C.comment).italic(m)));
64
+ r = applyToRaw(r, /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, m => save(chalk.hex(C.str)(m)));
65
+ if (KW[l]) { KW[l].lastIndex = 0; r = applyToRaw(r, KW[l], m => save(chalk.hex(C.kw).bold(m))); }
66
+ r = applyToRaw(r, /\b(\d+\.?\d*)\b/g, m => save(chalk.hex(C.num)(m)));
67
+ r = applyToRaw(r, /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/g, m => save(chalk.hex(C.fn)(m)));
68
+ return r.replace(/\x00(\d+)\x00/g, (_, i) => saved[+i] ?? "");
54
69
  }
55
70
 
56
71
  // ─────────────────────────────────────────────────────────────────
57
- // Markdown → terminal
72
+ // Markdown → array of display lines (with wrapping baked in)
73
+ // Returns: Array<{ text: string, raw: boolean }>
74
+ // raw=true → ANSI already applied (code blocks), print as-is
75
+ // raw=false → plain text, will receive border + chalk in printBox
58
76
  // ─────────────────────────────────────────────────────────────────
59
- export function renderMarkdown(text) {
60
- let r = text;
61
-
62
- // Code blocks
63
- r = r.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
64
- const trimmed = code.trimEnd();
65
- const lines = trimmed.split("\n");
66
- const hl = highlight(trimmed, lang);
67
- const hlLines = hl.split("\n");
68
- const gw = String(lines.length).length;
69
- const label = lang ? chalk.hex(C.blue).bold(` ${lang} `) + chalk.hex(C.dimmer)("─".repeat(Math.max(0, 36 - lang.length))) : chalk.hex(C.dimmer)("─".repeat(38));
70
-
71
- const top = chalk.hex(C.dim)(" ┌─") + label + chalk.hex(C.dim)("┐");
72
- const bot = chalk.hex(C.dim)(" └" + "─".repeat(40) + "┘");
73
- const body = hlLines.map((l, i) => {
74
- const ln = chalk.hex(C.dimmer)(String(i + 1).padStart(gw));
75
- return chalk.hex(C.dim)("") + chalk.hex(C.dimmer)(" " + ln + " ╎ ") + l;
76
- }).join("\n");
77
-
78
- return `\n${top}\n${body}\n${bot}\n`;
79
- });
80
-
81
- // Inline code
82
- r = r.replace(/`([^`\n]+)`/g, (_, c) =>
83
- chalk.bgHex("#2A2A3E")(chalk.hex(C.orange)(" " + c + " "))
84
- );
85
-
86
- // Headers
87
- r = r.replace(/^### (.+)$/gm, (_, t) => "\n" + chalk.hex(C.yellow).bold(" ◈ " + t));
88
- r = r.replace(/^## (.+)$/gm, (_, t) => "\n" + chalk.hex(C.blue).bold.underline(" " + t));
89
- r = r.replace(/^# (.+)$/gm, (_, t) => "\n" + chalk.hex(C.teal).bold(" ◉ " + t.toUpperCase()));
90
-
91
- // Bold / italic
92
- r = r.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t));
93
- r = r.replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t));
94
- r = r.replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t));
95
-
96
- // Blockquote
97
- r = r.replace(/^> (.+)$/gm, (_, t) =>
98
- chalk.hex(C.purple)(" ▎ ") + chalk.hex(C.muted).italic(t)
99
- );
77
+ export function renderMarkdown(text, contentW) {
78
+ const cw = contentW ?? bw() - 2;
79
+ const out = []; // { text, raw }
80
+
81
+ const pushPlain = (s) => out.push({ text: s, raw: false });
82
+ const pushStyled = (s) => out.push({ text: s, raw: true });
83
+ const pushBlank = () => out.push({ text: "", raw: false });
84
+
85
+ // Normalise line endings
86
+ const src = text.replace(/\r\n/g, "\n").trimEnd();
87
+
88
+ // Split into tokens: code blocks vs everything else
89
+ const segments = src.split(/(```[\s\S]*?```)/g);
90
+
91
+ for (const seg of segments) {
92
+ // ── Fenced code block ──────────────────────────────────────
93
+ if (seg.startsWith("```")) {
94
+ const m = seg.match(/^```(\w*)\n?([\s\S]*?)```$/);
95
+ const lang = m?.[1] ?? "";
96
+ const code = m?.[2]?.trimEnd() ?? seg.slice(3);
97
+ const lines = code.split("\n");
98
+ const hl = highlight(code, lang);
99
+ const hlLines = hl.split("\n");
100
+ const gw = String(lines.length).length;
101
+ const bw2 = Math.min(cw + 2, tw() - 6);
102
+ const lbl = lang ? chalk.hex(C.blue).bold(` ${lang} `) : "";
103
+ const lblLen = lang ? lang.length + 2 : 0;
104
+ const dashes = Math.max(2, bw2 - lblLen - 1);
105
+ const maxCode = bw2 - gw - 4;
106
+
107
+ pushBlank();
108
+ pushStyled(chalk.hex(C.dim)("┌─") + lbl + chalk.hex(C.dimmer)("─".repeat(dashes)) + chalk.hex(C.dim)("┐"));
109
+ hlLines.forEach((hl_line, i) => {
110
+ const ln = chalk.hex(C.dimmer)(String(i + 1).padStart(gw));
111
+ const raw = lines[i] ?? "";
112
+ const disp = raw.length > maxCode ? hl_line.slice(0, maxCode * 3) + chalk.hex(C.dimmer)("…") : hl_line;
113
+ pushStyled(chalk.hex(C.dim)("│") + chalk.hex(C.dimmer)(" " + ln + " ╎ ") + disp);
114
+ });
115
+ pushStyled(chalk.hex(C.dim)("└" + "─".repeat(bw2 + 1) + "┘"));
116
+ pushBlank();
117
+ continue;
118
+ }
119
+
120
+ // ── Text segment — process line by line ───────────────────
121
+ for (const rawLine of seg.split("\n")) {
122
+ const line = rawLine;
123
+
124
+ // Blank line
125
+ if (!line.trim()) { pushBlank(); continue; }
126
+
127
+ // === Header === (non-standard but model uses it)
128
+ if (/^={3,}\s*.+\s*={3,}$/.test(line)) {
129
+ const t = line.replace(/^=+\s*/, "").replace(/\s*=+$/, "");
130
+ pushBlank();
131
+ pushStyled(chalk.hex(C.blue).bold.underline(t));
132
+ pushBlank();
133
+ continue;
134
+ }
135
+
136
+ // ATX headers
137
+ if (/^### /.test(line)) { pushStyled(chalk.hex(C.yellow).bold("◈ " + line.slice(4))); continue; }
138
+ if (/^## /.test(line)) { pushStyled(chalk.hex(C.blue).bold.underline(line.slice(3))); continue; }
139
+ if (/^# /.test(line)) { pushStyled(chalk.hex(C.teal).bold("◉ " + line.slice(2).toUpperCase())); continue; }
140
+
141
+ // HR
142
+ if (/^---+$/.test(line)) { pushStyled(chalk.hex(C.dimmer)("╌".repeat(Math.min(cw, 48)))); continue; }
143
+
144
+ // Blockquote
145
+ if (/^> /.test(line)) {
146
+ const t = line.slice(2);
147
+ wrapPlain(t, cw - 2).forEach((wl, i) =>
148
+ pushStyled(chalk.hex(C.purple)("▎ ") + chalk.hex(C.muted).italic(wl))
149
+ );
150
+ continue;
151
+ }
152
+
153
+ // Unordered list
154
+ const ulM = line.match(/^(\s*)[*\-+] (.+)$/);
155
+ if (ulM) {
156
+ const [, ind, content] = ulM;
157
+ const indW = ind.length;
158
+ const itemW = cw - indW - 4; // "◆ " = 2 chars visible + 2 padding
159
+ const plain = stripInline(content);
160
+ wrapPlain(plain, itemW).forEach((wl, i) => {
161
+ if (i === 0)
162
+ pushStyled(ind + chalk.hex(C.teal)("◆ ") + chalk.hex(C.white)(applyInline(wl)));
163
+ else
164
+ pushStyled(ind + " " + chalk.hex(C.white)(applyInline(wl)));
165
+ });
166
+ continue;
167
+ }
168
+
169
+ // Ordered list
170
+ const olM = line.match(/^(\s*)(\d+)\. (.+)$/);
171
+ if (olM) {
172
+ const [, ind, num, content] = olM;
173
+ const markerW = num.length + 2;
174
+ const itemW = cw - ind.length - markerW;
175
+ const plain = stripInline(content);
176
+ wrapPlain(plain, itemW).forEach((wl, i) => {
177
+ if (i === 0)
178
+ pushStyled(ind + chalk.hex(C.blue)(chalk.bold(num + ".") + " ") + chalk.hex(C.white)(applyInline(wl)));
179
+ else
180
+ pushStyled(ind + " ".repeat(markerW) + chalk.hex(C.white)(applyInline(wl)));
181
+ });
182
+ continue;
183
+ }
184
+
185
+ // Plain paragraph — strip inline markup for wrapping, then re-apply
186
+ const plain = stripInline(line);
187
+ wrapPlain(plain, cw).forEach(wl =>
188
+ pushStyled(chalk.hex(C.white)(applyInline(wl)))
189
+ );
190
+ }
191
+ }
100
192
 
101
- // Lists
102
- r = r.replace(/^(\s*)[*\-+] (.+)$/gm, (_, i, t) => i + chalk.hex(C.teal)(" ◆ ") + chalk.hex(C.white)(t));
103
- r = r.replace(/^(\s*)(\d+)\. (.+)$/gm, (_, i, n, t) => i + chalk.hex(C.blue)(" " + chalk.bold(n + ".") + " ") + chalk.hex(C.white)(t));
193
+ return out;
194
+ }
104
195
 
105
- // HR
106
- r = r.replace(/^---+$/gm, chalk.hex(C.dimmer)(" " + "╌".repeat(48)));
196
+ // Strip inline markdown to get plain text for accurate wrapping
197
+ function stripInline(s) {
198
+ return s
199
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // links → text only
200
+ .replace(/`([^`]+)`/g, "$1")
201
+ .replace(/\*\*\*(.+?)\*\*\*/g, "$1")
202
+ .replace(/\*\*(.+?)\*\*/g, "$1")
203
+ .replace(/\*(.+?)\*/g, "$1")
204
+ .replace(/_(.+?)_/g, "$1");
205
+ }
107
206
 
108
- return r;
207
+ // Apply inline markdown styling to already-wrapped line
208
+ function applyInline(s) {
209
+ return s
210
+ // Links → show "text (url truncated)" — URLs not clickable in terminal
211
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
212
+ const short = url.length > 40 ? url.slice(0, 40) + "…" : url;
213
+ return chalk.hex(C.teal)(text) + chalk.hex(C.dimmer)(" (" + short + ")");
214
+ })
215
+ .replace(/`([^`]+)`/g, (_, c) => chalk.bgHex("#2A2A3E")(chalk.hex(C.orange)(" " + c + " ")))
216
+ .replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t))
217
+ .replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold(t))
218
+ .replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t))
219
+ .replace(/_(.+?)_/g, (_, t) => chalk.italic(t));
109
220
  }
110
221
 
111
222
  // ─────────────────────────────────────────────────────────────────
112
- // User message
223
+ // Box printer — all lines are pre-wrapped, just add border
113
224
  // ─────────────────────────────────────────────────────────────────
114
- export function printUser(text) {
115
- const lines = text.split("\n");
116
- const isMulti = lines.length > 1;
117
- const W = 50;
118
-
119
- if (!isMulti) {
120
- process.stdout.write(
121
- "\n" +
122
- chalk.hex(C.blue)(" ╭─") +
123
- chalk.hex(C.blue).bold(" you ") +
124
- chalk.hex(C.dim)("─".repeat(W - 5)) + "\n" +
125
- chalk.hex(C.blue)(" │ ") + chalk.hex(C.white)(text) + "\n" +
126
- chalk.hex(C.blue)(" ╰" + "─".repeat(W + 2)) + "\n"
127
- );
128
- return;
129
- }
225
+ function printBox(renderedLines, borderHex, label, extra = "") {
226
+ const W = bw();
227
+ const lblLen = vlen(label) + vlen(extra);
228
+ const dashes = Math.max(0, W - lblLen - 1);
130
229
 
131
230
  process.stdout.write(
132
- "\n" +
133
- chalk.hex(C.blue)(" ╭─") +
134
- chalk.hex(C.blue).bold(" you ") +
135
- chalk.hex(C.dim)("─".repeat(W - 5)) +
136
- chalk.hex(C.dim)(` (${lines.length} lines)`) + "\n"
137
- );
138
- lines.forEach(l =>
139
- process.stdout.write(chalk.hex(C.blue)(" │ ") + chalk.hex(C.white)(l) + "\n")
231
+ "\n" + chalk.hex(borderHex)("╭─") + label +
232
+ chalk.hex(C.dim)("".repeat(dashes)) + extra + "\n"
140
233
  );
141
- process.stdout.write(chalk.hex(C.blue)(" ╰" + "─".repeat(W + 2)) + "\n");
234
+
235
+ for (const { text, raw } of renderedLines) {
236
+ // raw lines (code blocks, headers with box-drawing) — print as-is
237
+ // non-raw lines — already plain/styled text, just add border
238
+ process.stdout.write(chalk.hex(borderHex)("│ ") + text + "\n");
239
+ }
240
+
241
+ process.stdout.write(chalk.hex(borderHex)("╰" + "─".repeat(W + 1)) + "\n");
142
242
  }
143
243
 
144
244
  // ─────────────────────────────────────────────────────────────────
145
- // Gemini response
245
+ // Messages
146
246
  // ─────────────────────────────────────────────────────────────────
147
- export function printAssistant(text) {
148
- const W = 50;
149
- const rendered = renderMarkdown(text).trimEnd();
150
- const lines = rendered.split("\n");
247
+ export function printUser(text) {
248
+ const W = bw();
249
+ const inner = W - 2;
250
+ const rawLines = text.split("\n");
251
+ const multi = rawLines.length > 1;
252
+ const label = chalk.hex(C.blue).bold(" you ");
253
+ const extra = multi ? chalk.hex(C.muted)(` (${rawLines.length} lines) `) : "";
254
+
255
+ const lines = multi
256
+ ? rawLines.flatMap(l => wrapPlain(l, inner).map(wl => ({ text: chalk.hex(C.white)(wl), raw: true })))
257
+ : wrapPlain(text, inner).map(wl => ({ text: chalk.hex(C.white)(wl), raw: true }));
258
+
259
+ printBox(lines, C.blue, label, extra);
260
+ }
151
261
 
152
- process.stdout.write(
153
- "\n" +
154
- chalk.hex(C.teal)(" ╭─") +
155
- chalk.hex(C.teal).bold(" gemini ") +
156
- chalk.hex(C.dim)("─".repeat(W - 8)) + "\n"
157
- );
158
- lines.forEach(l =>
159
- process.stdout.write(chalk.hex(C.teal)(" │") + " " + l + "\n")
160
- );
161
- process.stdout.write(
162
- chalk.hex(C.teal)(" ╰" + "─".repeat(W + 2)) + "\n\n"
163
- );
262
+ export function printAssistant(text) {
263
+ const inner = bw() - 2;
264
+ const lines = renderMarkdown(text.trimEnd(), inner);
265
+ printBox(lines, C.teal, chalk.hex(C.teal).bold(" gemini "));
266
+ process.stdout.write("\n");
164
267
  }
165
268
 
166
269
  // ─────────────────────────────────────────────────────────────────
167
- // Agent step block — one block per iteration, fresh each time
270
+ // Agent step blocks
168
271
  // ─────────────────────────────────────────────────────────────────
169
- export function printStepHeader(step, label = "working") {
170
- const tag = chalk.hex(C.yellow).bold(` ${label} `);
171
- const num = step > 1 ? chalk.hex(C.dim)(` step ${step}`) : "";
172
- const trail = chalk.hex(C.dim)("─".repeat(Math.max(2, 40 - label.length)));
272
+ export function printStepHeader(step) {
273
+ const W = bw();
274
+ const label = chalk.hex(C.yellow).bold(" working ");
275
+ const num = step > 1 ? chalk.hex(C.dim)(` step ${step} `) : " ";
276
+ const trail = Math.max(2, W - 9 - vlen(num));
173
277
  process.stdout.write(
174
- "\n" +
175
- chalk.hex(C.yellow)(" ╭─") + tag + trail + num + "\n"
278
+ "\n" + chalk.hex(C.yellow)("╭─") + label +
279
+ chalk.hex(C.dim)("".repeat(trail)) + num + "\n"
176
280
  );
177
281
  }
178
282
 
179
283
  export function printStepFooter() {
180
- process.stdout.write(
181
- chalk.hex(C.yellow)(" ╰" + "─".repeat(50)) + "\n"
182
- );
284
+ process.stdout.write(chalk.hex(C.yellow)("╰" + "─".repeat(bw() + 1)) + "\n");
183
285
  }
184
286
 
185
287
  export function printToolCall(name, args = {}) {
186
- const argStr = Object.entries(args)
187
- .map(([k, v]) => {
188
- const s = String(v).replace(/\n/g, "");
189
- return chalk.hex(C.muted)(k + ":") + chalk.hex(C.orange)(s.length > 50 ? s.slice(0, 50) + "…" : s);
190
- })
191
- .join(" ");
192
-
288
+ const argStr = Object.entries(args).map(([k, v]) => {
289
+ const raw = String(v).replace(/\n/g, "↵");
290
+ const val = raw.length > 42 ? raw.slice(0, 42) + "" : raw;
291
+ return chalk.hex(C.muted)(k + ":") + chalk.hex(C.orange)(val);
292
+ }).join(" ");
193
293
  process.stdout.write(
194
- chalk.hex(C.yellow)(" ├─ ") +
195
- chalk.hex(C.blue).bold(name) +
294
+ chalk.hex(C.yellow)("├─ ") + chalk.hex(C.blue).bold(name) +
196
295
  (argStr ? " " + argStr : "") + "\n"
197
296
  );
198
297
  }
199
298
 
200
299
  export function printToolResult(result) {
300
+ const W = bw() - 5;
201
301
  const isErr = typeof result === "object" && result.error;
202
302
  const text = typeof result === "object"
203
303
  ? (result.result ?? result.error ?? JSON.stringify(result, null, 2))
204
304
  : String(result);
205
305
  const color = isErr ? chalk.hex(C.red) : chalk.hex(C.muted);
206
- const border = isErr ? chalk.hex(C.red) : chalk.hex(C.dimmer);
306
+ const border = chalk.hex(C.dimmer)("│ ");
207
307
  const lines = text.split("\n");
208
- const shown = lines.slice(0, 12);
209
- const extra = lines.length > 12 ? lines.length - 12 : 0;
210
-
308
+ const shown = lines.slice(0, 10);
309
+ const extra = lines.length - 10;
211
310
  shown.forEach(l =>
212
- process.stdout.write(border(" │ ") + color(l) + "\n")
311
+ process.stdout.write(border + color(l.length > W ? l.slice(0, W) + "…" : l) + "\n")
213
312
  );
214
- if (extra > 0)
215
- process.stdout.write(border(" │ ") + chalk.hex(C.dim)(`… +${extra} more lines`) + "\n");
313
+ if (extra > 0) process.stdout.write(border + chalk.hex(C.dim)(`… +${extra} more lines`) + "\n");
216
314
  }
217
315
 
218
316
  // ─────────────────────────────────────────────────────────────────
219
- // Status messages
317
+ // Status
220
318
  // ─────────────────────────────────────────────────────────────────
221
- export function printThinking(step) {
222
- process.stdout.write(
223
- "\n" +
224
- chalk.hex(C.dim)(" ") +
225
- chalk.hex(C.muted)("thinking") +
226
- (step > 1 ? chalk.hex(C.dim)(` · step ${step}`) : "") +
227
- chalk.hex(C.dim)(" …") + "\n"
228
- );
229
- }
230
-
231
- export function printError(msg) {
232
- process.stdout.write(
233
- "\n" +
234
- chalk.hex(C.red)(" ╳ ") +
235
- chalk.hex(C.red).bold("error") +
236
- chalk.hex(C.muted)(" " + msg) + "\n\n"
237
- );
238
- }
239
- export function printInfo(msg) {
240
- process.stdout.write(
241
- chalk.hex(C.dim)(" · ") + chalk.hex(C.white)(msg) + "\n"
242
- );
243
- }
244
- export function printSuccess(msg) {
245
- process.stdout.write(
246
- chalk.hex(C.teal)(" ✓ ") + chalk.hex(C.teal)(msg) + "\n"
247
- );
248
- }
249
- export function printWarning(msg) {
250
- process.stdout.write(
251
- chalk.hex(C.yellow)(" ⚠ ") + chalk.hex(C.yellow)(msg) + "\n"
252
- );
253
- }
319
+ export function printError(msg) { process.stdout.write("\n" + chalk.hex(C.red)("╳ ") + chalk.hex(C.red).bold("error ") + chalk.hex(C.muted)(msg) + "\n\n"); }
320
+ export function printInfo(msg) { process.stdout.write(chalk.hex(C.dim)("· ") + chalk.hex(C.white)(msg) + "\n"); }
321
+ export function printSuccess(msg) { process.stdout.write(chalk.hex(C.teal)("✓ ") + chalk.hex(C.teal)(msg) + "\n"); }
322
+ export function printWarning(msg) { process.stdout.write(chalk.hex(C.yellow)(" ") + chalk.hex(C.yellow)(msg) + "\n"); }
254
323
 
255
324
  // ─────────────────────────────────────────────────────────────────
256
- // Welcome
325
+ // Welcome & Help
257
326
  // ─────────────────────────────────────────────────────────────────
258
327
  export function renderWelcome(memCount = 0, extCount = 0) {
328
+ const W = bw();
259
329
  const stats = [
260
330
  memCount ? `${memCount} context file${memCount > 1 ? "s" : ""}` : null,
261
331
  extCount ? `${extCount} extension${extCount > 1 ? "s" : ""}` : null,
262
332
  ].filter(Boolean).join(" · ");
263
-
264
333
  return [
265
334
  "",
266
- chalk.hex(C.dim)(" ┌─────────────────────────────────────────────────┐"),
267
- chalk.hex(C.dim)(" ") + " " + chalk.hex(C.teal).bold("Gemini") + chalk.hex(C.blue).bold(" CLI") + chalk.hex(C.dim)(" ─ AI Agent ─ native function calling ") + chalk.hex(C.dim)("│"),
268
- chalk.hex(C.dim)(" └─────────────────────────────────────────────────┘"),
269
- stats
270
- ? chalk.hex(C.dim)(" ") + chalk.hex(C.muted)(stats)
271
- : "",
272
- chalk.hex(C.dim)(" ") + chalk.hex(C.dim)("/help") + chalk.hex(C.dim)(" · ") + chalk.hex(C.dim)("/agent") + chalk.hex(C.dim)(" toggle tools · ") + chalk.hex(C.dim)("/yolo") + chalk.hex(C.dim)(" skip confirms"),
335
+ chalk.hex(C.dim)("" + "─".repeat(W + 1) + "┐"),
336
+ chalk.hex(C.dim)(" ") + chalk.hex(C.teal).bold("Gemini") + chalk.hex(C.blue).bold(" CLI") +
337
+ chalk.hex(C.dim)(" ─ AI Agent ─ native function calling") + chalk.hex(C.dim)(" │"),
338
+ chalk.hex(C.dim)("└" + "─".repeat(W + 1) + "┘"),
339
+ stats ? chalk.hex(C.muted)(" " + stats) : "",
340
+ chalk.hex(C.dim)(" /help · /agent toggle · /yolo skip confirms"),
273
341
  "",
274
342
  ].join("\n");
275
343
  }
276
344
 
277
- // ─────────────────────────────────────────────────────────────────
278
- // Help
279
- // ─────────────────────────────────────────────────────────────────
280
345
  export function renderHelp(customCommands = {}) {
281
- const sep = chalk.hex(C.dimmer)(" " + "─".repeat(52));
282
- const row = (cmd, desc) =>
283
- " " + chalk.hex(C.blue).bold(cmd.padEnd(26)) + chalk.hex(C.muted)(desc);
284
-
346
+ const sep = chalk.hex(C.dimmer)(" " + "─".repeat(Math.min(tw() - 4, 52)));
347
+ const row = (cmd, desc) => " " + chalk.hex(C.blue).bold(cmd.padEnd(24)) + chalk.hex(C.muted)(desc);
285
348
  const lines = [
286
- "",
287
- chalk.hex(C.teal).bold(" Commands"),
288
- sep,
289
- row("/agent", "Toggle agent mode ↔ chat mode"),
290
- row("/yolo", "Skip all tool confirmations"),
291
- sep,
292
- row("/memory show", "Show loaded GEMINI.md context files"),
293
- row("/memory reload", "Reload all context from disk"),
294
- row("/memory add <text>", "Append text to ~/.gemini/GEMINI.md"),
295
- sep,
296
- row("/ext list", "List installed extensions"),
297
- row("/ext install <src>", "Install from local path or git URL"),
298
- row("/ext uninstall <n>", "Uninstall extension"),
299
- row("/ext enable/disable", "Toggle extension on/off"),
300
- row("/ext update <n>", "Pull latest from git"),
301
- sep,
302
- row("/file <path>", "Attach file to next message"),
303
- row("/system <text>", "Set session system instruction"),
304
- row("/history", "Show conversation turns"),
305
- row("/export <file.json>", "Export conversation to JSON"),
306
- row("/cd <path>", "Change working directory"),
307
- row("/new /clear", "Reset conversation"),
308
- row("/model", "Show model & config"),
309
- row("/proxy [on|off]", "Proxy rotation status / toggle"),
310
- row("/exit /quit", "Exit"),
311
- sep,
349
+ "", chalk.hex(C.teal).bold(" Commands"), sep,
350
+ row("/agent", "Toggle agent ↔ chat mode"),
351
+ row("/yolo", "Skip all tool confirmations"), sep,
352
+ row("/memory show", "Show loaded GEMINI.md files"),
353
+ row("/memory reload", "Reload context from disk"),
354
+ row("/memory add <text>", "Append to ~/.gemini/GEMINI.md"), sep,
355
+ row("/ext list", "List extensions"),
356
+ row("/ext install <src>", "Install (path or git URL)"),
357
+ row("/ext uninstall <n>", "Uninstall extension"),
358
+ row("/ext enable <n>", "Enable extension"),
359
+ row("/ext disable <n>", "Disable extension"),
360
+ row("/ext update <n>", "Pull latest from git"), sep,
361
+ row("/file <path>", "Attach file to message"),
362
+ row("/system <text>", "Set system instruction"),
363
+ row("/history", "Show conversation turns"),
364
+ row("/export <file>", "Export history to JSON"),
365
+ row("/cd <path>", "Change working directory"),
366
+ row("/new /clear", "Reset conversation"),
367
+ row("/model", "Model & config info"),
368
+ row("/proxy [on|off]", "Proxy rotation status/toggle"),
369
+ row("/exit /quit", "Exit"), sep,
370
+ chalk.hex(C.dim)(" Ctrl+C interrupt · Ctrl+D exit"), "",
312
371
  ];
313
-
314
372
  if (Object.keys(customCommands).length) {
315
- lines.push(chalk.hex(C.purple).bold(" Extension Commands"), sep);
316
- Object.entries(customCommands).forEach(([k, c]) =>
317
- lines.push(row("/" + k, c.description ?? ""))
373
+ lines.splice(-2, 0,
374
+ chalk.hex(C.purple).bold(" Extension Commands"), sep,
375
+ ...Object.entries(customCommands).map(([k, c]) => row("/" + k, c.description ?? "")),
376
+ sep
318
377
  );
319
- lines.push(sep);
320
378
  }
321
-
322
- lines.push(
323
- chalk.hex(C.dim)(" Paste multi-line code freely."),
324
- chalk.hex(C.dim)(" Ctrl+C interrupt · Ctrl+D exit"),
325
- "",
326
- );
327
379
  return lines.join("\n");
328
380
  }