@ikyyofc/gemini-cli 1.0.3 → 1.0.5

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/index.js CHANGED
@@ -19,7 +19,8 @@ import {
19
19
  printError, printInfo, printSuccess, printWarning
20
20
  } from "./src/renderer.js";
21
21
  import {
22
- enableBracketedPaste, disableBracketedPaste, setupPaste
22
+ PasteTransform, restorePaste,
23
+ enableBracketedPaste, disableBracketedPaste
23
24
  } from "./src/input.js";
24
25
 
25
26
  // ─────────────────────────────────────────────────────────────────
@@ -54,26 +55,31 @@ function sysInstruction() {
54
55
  }
55
56
 
56
57
  // ─────────────────────────────────────────────────────────────────
57
- // Readline — directly on process.stdin, NO transform stream
58
+ // Readline — stdin PasteTransform readline
59
+ // { end: false } prevents Ctrl+D on stdin from auto-closing readline
58
60
  // ─────────────────────────────────────────────────────────────────
61
+ enableBracketedPaste();
62
+
63
+ const pasteStream = new PasteTransform();
64
+ process.stdin.pipe(pasteStream, { end: false });
65
+
66
+ // Handle Ctrl+D (stdin end) manually
67
+ process.stdin.once("end", () => {
68
+ cleanup();
69
+ process.exit(0);
70
+ });
71
+
59
72
  const rl = readline.createInterface({
60
- input: process.stdin,
73
+ input: pasteStream,
61
74
  output: process.stdout,
62
75
  terminal: true,
63
76
  historySize: 200,
64
77
  crlfDelay: Infinity,
65
78
  });
66
79
 
67
- // Attach paste interception (keypress-based, not Transform-based)
68
- enableBracketedPaste();
69
- setupPaste(rl);
70
-
71
- // Clean up on any exit
72
80
  function cleanup() {
73
81
  disableBracketedPaste();
74
- if (process.stdin.isTTY) {
75
- try { process.stdin.setRawMode(false); } catch {}
76
- }
82
+ try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch {}
77
83
  }
78
84
  process.on("exit", cleanup);
79
85
  process.on("SIGTERM", () => { cleanup(); process.exit(0); });
@@ -96,7 +102,7 @@ function showPrompt() {
96
102
  }
97
103
 
98
104
  // ─────────────────────────────────────────────────────────────────
99
- // File attachment
105
+ // Attach file
100
106
  // ─────────────────────────────────────────────────────────────────
101
107
  function attachFile(fp) {
102
108
  const p = path.resolve(fp.trim().replace(/^['"]|['"]$/g, ""));
@@ -110,8 +116,10 @@ function attachFile(fp) {
110
116
  // ─────────────────────────────────────────────────────────────────
111
117
  // Send message
112
118
  // ─────────────────────────────────────────────────────────────────
113
- async function send(userText) {
114
- if (!userText.trim()) return;
119
+ async function send(rawLine) {
120
+ // Decode \x00 → \n from paste encoding
121
+ const userText = restorePaste(rawLine).trim();
122
+ if (!userText) return;
115
123
 
116
124
  printUser(userText + (pendingFile ? chalk.dim(` [${path.basename(pendingPath)}]`) : ""));
117
125
 
@@ -127,7 +135,10 @@ async function send(userText) {
127
135
  }
128
136
  } else {
129
137
  const { default: ora } = await import("ora");
130
- const sp = ora({ text: "thinking…", spinner: "dots", color: "cyan", prefixText: " ", discardStdin: false }).start();
138
+ const sp = ora({
139
+ text: "thinking…", spinner: "dots", color: "cyan",
140
+ prefixText: " ", discardStdin: false
141
+ }).start();
131
142
  const msgs = [];
132
143
  if (sysInstruction()) msgs.push({ role: "system", content: sysInstruction() });
133
144
  msgs.push(...history, { role: "user", content: userText });
@@ -182,7 +193,7 @@ async function handleCommand(input) {
182
193
  } else if (sub === "install") {
183
194
  const src = tokens.slice(2).join(" ");
184
195
  if (!src) { printError("usage: /ext install <path-or-url>"); return; }
185
- printInfo(`installing…`);
196
+ printInfo("installing…");
186
197
  const r = await installExtension(src);
187
198
  r.error ? printError(r.error) : (printSuccess(`installed: ${r.name}`), reloadAll());
188
199
  } else if (sub === "uninstall") {
@@ -240,7 +251,7 @@ async function handleCommand(input) {
240
251
  history.forEach((m, i) => {
241
252
  const who = m.role === "user" ? chalk.hex("#4A9EFF")("you") : chalk.hex("#4EC9B0")("gemini");
242
253
  const body = String(m.content).replace(/\n/g, " ").slice(0, 90);
243
- console.log(` ${chalk.dim("["+( i+1)+"]")} ${who} ${chalk.dim(body)}`);
254
+ console.log(` ${chalk.dim("["+(i+1)+"]")} ${who} ${chalk.dim(body)}`);
244
255
  });
245
256
  console.log("");
246
257
  break;
@@ -256,8 +267,7 @@ async function handleCommand(input) {
256
267
  printSuccess(`exported → ${arg}`);
257
268
  break;
258
269
 
259
- case "/cwd":
260
- printInfo(process.cwd()); break;
270
+ case "/cwd": printInfo(process.cwd()); break;
261
271
 
262
272
  case "/cd":
263
273
  if (!arg) { printError("usage: /cd <path>"); break; }
@@ -274,10 +284,7 @@ async function handleCommand(input) {
274
284
  break;
275
285
 
276
286
  case "/exit": case "/quit":
277
- cleanup();
278
- printInfo("bye");
279
- process.exit(0);
280
- break;
287
+ cleanup(); console.log(""); process.exit(0); break;
281
288
 
282
289
  default:
283
290
  printError(`unknown command: ${cmd} — /help for list`);
@@ -285,13 +292,12 @@ async function handleCommand(input) {
285
292
  }
286
293
 
287
294
  // ─────────────────────────────────────────────────────────────────
288
- // Main REPL loop
295
+ // Main
289
296
  // ─────────────────────────────────────────────────────────────────
290
297
  async function main() {
291
298
  process.stdout.write("\x1Bc");
292
299
  console.log(renderWelcome(memoryLoaded.length, extensions.length));
293
300
 
294
- // Parse flags
295
301
  const argv = process.argv.slice(2);
296
302
  const positional = [];
297
303
  for (let i = 0; i < argv.length; i++) {
@@ -303,30 +309,30 @@ async function main() {
303
309
  else if (!a.startsWith("--")) { positional.push(a); }
304
310
  }
305
311
 
306
- // One-shot mode
307
312
  if (positional.length > 0) {
308
313
  await send(positional.join(" "));
309
- cleanup();
310
- process.exit(0);
314
+ cleanup(); process.exit(0);
311
315
  }
312
316
 
313
317
  showPrompt();
314
318
 
315
- rl.on("line", async (input) => {
316
- // If already processing, just show prompt again — do NOT block or pause stream
319
+ rl.on("line", async (rawLine) => {
320
+ // Decode paste encoding before doing anything else
321
+ const text = restorePaste(rawLine).trim();
322
+
323
+ if (!text) { showPrompt(); return; }
324
+
325
+ // Don't queue — just warn and re-prompt
317
326
  if (processing) {
318
327
  printWarning("still thinking…");
319
328
  showPrompt();
320
329
  return;
321
330
  }
322
331
 
323
- const text = input.trim();
324
- if (!text) { showPrompt(); return; }
325
-
326
332
  processing = true;
327
333
  try {
328
334
  if (text.startsWith("/")) await handleCommand(text);
329
- else await send(input); // preserve original (may have \n from paste)
335
+ else await send(rawLine); // send rawLine; send() decodes internally
330
336
  } catch (e) {
331
337
  printError(e.message);
332
338
  } finally {
@@ -335,14 +341,9 @@ async function main() {
335
341
  }
336
342
  });
337
343
 
338
- // 'close' fires on Ctrl+D exit gracefully
339
- rl.on("close", () => {
340
- cleanup();
341
- console.log("");
342
- process.exit(0);
343
- });
344
+ // readline 'close' = Ctrl+D via our manual handler, or rl.close() call
345
+ rl.on("close", () => { cleanup(); console.log(""); process.exit(0); });
344
346
 
345
- // SIGINT (Ctrl+C) — cancel current operation or exit
346
347
  rl.on("SIGINT", () => {
347
348
  if (processing) {
348
349
  process.stdout.write("\n");
@@ -350,14 +351,8 @@ async function main() {
350
351
  showPrompt();
351
352
  return;
352
353
  }
353
- // Not processing — treat as exit intent
354
- cleanup();
355
- console.log("");
356
- process.exit(0);
354
+ cleanup(); console.log(""); process.exit(0);
357
355
  });
358
356
  }
359
357
 
360
- main().catch(e => {
361
- console.error(chalk.red("fatal:"), e.message);
362
- process.exit(1);
363
- });
358
+ main().catch(e => { console.error(chalk.red("fatal:"), e.message); process.exit(1); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikyyofc/gemini-cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "AI CLI Agent powered by Gemini — your own terminal assistant",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/src/agent.js CHANGED
@@ -3,7 +3,7 @@ import chalk from "chalk";
3
3
  import ora from "ora";
4
4
  import { callGemini } from "./gemini.js";
5
5
  import { GEMINI_TOOLS, executeTool } from "./tools.js";
6
- import { printAssistant, printError, printWarning } from "./renderer.js";
6
+ import { printAssistant, printError, printWarning, printToolCall, printToolResult } from "./renderer.js";
7
7
 
8
8
  /**
9
9
  * Run the agent loop until the model gives a final text answer (no more tool calls).
@@ -68,15 +68,28 @@ export async function runAgentLoop(userMessage, history, {
68
68
  // ── No tool calls → final answer ────────────────────────
69
69
  if (callParts.length === 0) {
70
70
  const final = textParts.map(p => p.text).join("").trim();
71
+ // Close the tool section if it was opened
72
+ if (iteration > 1) {
73
+ process.stdout.write(
74
+ chalk.hex("#4A9EFF")(" ╰") +
75
+ chalk.hex("#555566")("─".repeat(47)) + "\n"
76
+ );
77
+ }
71
78
  if (final) printAssistant(final);
72
79
  return { finalResponse: final, iterations: iteration };
73
80
  }
74
81
 
75
82
  // ── Execute each tool call ───────────────────────────────
76
- // Append the model's turn (contains functionCall parts) to history
77
83
  messages.push({ role: "model", parts });
78
84
 
79
- // Build the functionResponse turn
85
+ // Print tool section header on first iteration
86
+ if (iteration === 1) {
87
+ process.stdout.write(
88
+ "\n" + chalk.hex("#4A9EFF")(" ╭─ working") +
89
+ chalk.hex("#555566")(" " + "─".repeat(36)) + "\n"
90
+ );
91
+ }
92
+
80
93
  const responseParts = [];
81
94
 
82
95
  for (const part of callParts) {
@@ -84,17 +97,13 @@ export async function runAgentLoop(userMessage, history, {
84
97
  printToolCall(name, args);
85
98
 
86
99
  const result = await executeTool(name, args ?? {}, { autoApprove });
87
- printToolResult(result, name);
100
+ printToolResult(result);
88
101
 
89
102
  responseParts.push({
90
- functionResponse: {
91
- name,
92
- response: result // the entire result object goes here
93
- }
103
+ functionResponse: { name, response: result }
94
104
  });
95
105
  }
96
106
 
97
- // Append user turn with all function responses
98
107
  messages.push({ role: "user", parts: responseParts });
99
108
  }
100
109
 
@@ -102,39 +111,6 @@ export async function runAgentLoop(userMessage, history, {
102
111
  return { finalResponse: null, iterations: iteration };
103
112
  }
104
113
 
105
- // ─────────────────────────────────────────────────────────────────
106
- // Terminal rendering helpers for tool calls
107
- // ─────────────────────────────────────────────────────────────────
108
- function printToolCall(name, args = {}) {
109
- const preview = Object.entries(args)
110
- .map(([k, v]) => {
111
- const s = String(v);
112
- return chalk.dim(k + ":") + (s.length > 60 ? s.slice(0, 60) + "…" : s);
113
- })
114
- .join(" ");
115
-
116
- process.stdout.write(
117
- " " + chalk.dim("run") + " " +
118
- chalk.hex("#569CD6")(name) +
119
- (preview ? chalk.dim(" " + preview) : "") + "\n"
120
- );
121
- }
122
-
123
- function printToolResult(result, name) {
124
- const text = typeof result === "object"
125
- ? (result.result ?? result.error ?? JSON.stringify(result))
126
- : String(result);
127
- const isErr = typeof result === "object" && result.error;
128
- const color = isErr ? chalk.red : chalk.dim;
129
- const lines = text.split("\n").slice(0, 12);
130
- const more = text.split("\n").length > 12
131
- ? chalk.dim(`\n … +${text.split("\n").length - 12} lines`) : "";
132
-
133
- process.stdout.write(
134
- " " + color(lines.join("\n ")) + more + "\n"
135
- );
136
- }
137
-
138
114
  function clearLine() {
139
115
  if (process.stdout.clearLine) { process.stdout.clearLine(0); process.stdout.cursorTo(0); }
140
116
  }
package/src/input.js CHANGED
@@ -1,60 +1,96 @@
1
- // src/input.js — Bracketed paste via keypress interception
1
+ // src/input.js — Bracketed paste via Transform stream (byte-level interception)
2
2
  //
3
- // No Transform stream. We intercept keypress events directly:
4
- // \x1b[200~ → enter paste mode, start buffering lines
5
- // \x1b[201~ → end paste mode, emit full buffer as one line
3
+ // Keypress-based approach is unreliable because keypress events for \x1b[200~
4
+ // may arrive AFTER readline has already fired 'line' events for the pasted lines.
5
+ // Byte-level interception via Transform is the only reliable approach.
6
6
  //
7
- import readline from "readline";
7
+ import { Transform } from "stream";
8
8
 
9
- export function enableBracketedPaste() { process.stdout.write("\x1b[?2004h"); }
10
- export function disableBracketedPaste() { process.stdout.write("\x1b[?2004l"); }
9
+ const PASTE_START = "\x1b[200~";
10
+ const PASTE_END = "\x1b[201~";
11
11
 
12
- /**
13
- * Attach bracketed paste interception to an existing readline interface.
14
- * readline must be created BEFORE calling this.
15
- */
16
- export function setupPaste(rl) {
17
- // Make stdin emit 'keypress' events (readline may have already done this)
18
- readline.emitKeypressEvents(process.stdin, rl);
19
-
20
- let pasting = false;
21
- let pasteBuf = [];
22
-
23
- // Watch for paste start/end escape sequences
24
- const onKeypress = (_str, key) => {
25
- if (!key) return;
26
- const seq = key.sequence ?? "";
27
-
28
- if (seq === "\x1b[200~") {
29
- pasting = true;
30
- pasteBuf = [];
31
- return;
32
- }
12
+ export class PasteTransform extends Transform {
13
+ constructor() {
14
+ super();
15
+ this._buf = ""; // raw accumulator
16
+ this._pasting = false;
17
+ this._pasteBuf = ""; // paste content accumulator
18
+ }
33
19
 
34
- if (seq === "\x1b[201~") {
35
- pasting = false;
36
- const full = pasteBuf.join("\n");
37
- pasteBuf = [];
38
- // Fire the complete paste as one synthetic 'line' event
39
- rl.emit("line", full);
40
- }
41
- };
20
+ _transform(chunk, _enc, cb) {
21
+ this._buf += chunk.toString("utf8");
22
+
23
+ let output = "";
24
+
25
+ while (this._buf.length > 0) {
26
+
27
+ if (!this._pasting) {
28
+ const si = this._buf.indexOf(PASTE_START);
29
+
30
+ if (si === -1) {
31
+ // No paste start found — check for partial escape at tail
32
+ const esc = this._buf.lastIndexOf("\x1b");
33
+ if (esc !== -1 && (this._buf.length - esc) < PASTE_START.length) {
34
+ // Could be partial escape sequence — flush safe part, hold rest
35
+ output += this._buf.slice(0, esc);
36
+ this._buf = this._buf.slice(esc);
37
+ break;
38
+ }
39
+ // No escape at all, pass through
40
+ output += this._buf;
41
+ this._buf = "";
42
+ break;
43
+ }
44
+
45
+ // Found paste start
46
+ output += this._buf.slice(0, si); // pass through text before it
47
+ this._buf = this._buf.slice(si + PASTE_START.length);
48
+ this._pasting = true;
49
+ this._pasteBuf = "";
42
50
 
43
- process.stdin.on("keypress", onKeypress);
51
+ } else {
52
+ // Inside paste — look for end marker
53
+ const ei = this._buf.indexOf(PASTE_END);
44
54
 
45
- // Patch rl.emit to swallow 'line' events while pasting
46
- // (readline fires one per \n inside the paste — we buffer them)
47
- const _emit = rl.emit.bind(rl);
48
- rl.emit = function (event, ...args) {
49
- if (event === "line" && pasting) {
50
- pasteBuf.push(args[0] ?? "");
51
- return false;
55
+ if (ei === -1) {
56
+ // End not here yet buffer everything and wait
57
+ this._pasteBuf += this._buf;
58
+ this._buf = "";
59
+ break;
60
+ }
61
+
62
+ // Found paste end
63
+ this._pasteBuf += this._buf.slice(0, ei);
64
+ this._buf = this._buf.slice(ei + PASTE_END.length);
65
+ this._pasting = false;
66
+
67
+ // Replace newlines with \x00 so readline treats whole paste as ONE line
68
+ const encoded = this._pasteBuf
69
+ .replace(/\r\n/g, "\x00")
70
+ .replace(/\r/g, "\x00")
71
+ .replace(/\n/g, "\x00");
72
+
73
+ output += encoded + "\n"; // \n makes readline fire one 'line' event
74
+ this._pasteBuf = "";
75
+ }
52
76
  }
53
- return _emit(event, ...args);
54
- };
55
77
 
56
- return () => {
57
- process.stdin.off("keypress", onKeypress);
58
- rl.emit = _emit;
59
- };
78
+ if (output) this.push(output);
79
+ cb();
80
+ }
81
+
82
+ _flush(cb) {
83
+ // Flush anything remaining
84
+ if (this._buf) this.push(this._buf);
85
+ if (this._pasteBuf) this.push(this._pasteBuf);
86
+ cb();
87
+ }
60
88
  }
89
+
90
+ /** Decode: \x00 → \n (undo the encoding done above) */
91
+ export function restorePaste(line) {
92
+ return line.replace(/\x00/g, "\n");
93
+ }
94
+
95
+ export function enableBracketedPaste() { process.stdout.write("\x1b[?2004h"); }
96
+ export function disableBracketedPaste() { process.stdout.write("\x1b[?2004l"); }
package/src/renderer.js CHANGED
@@ -1,6 +1,23 @@
1
- // src/renderer.js — Clean, minimal terminal UI
1
+ // src/renderer.js
2
2
  import chalk from "chalk";
3
3
 
4
+ // Accent colors
5
+ const C = {
6
+ blue: "#4A9EFF",
7
+ teal: "#1DB8A0",
8
+ dim: "#555566",
9
+ code: "#CE9178",
10
+ kw: "#569CD6",
11
+ str: "#CE9178",
12
+ comment: "#6A9955",
13
+ num: "#B5CEA8",
14
+ fn: "#DCDCAA",
15
+ green: "#4EC9B0",
16
+ yellow: "#E5C07B",
17
+ red: "#E06C75",
18
+ white: "#CDD6F4",
19
+ };
20
+
4
21
  // ─────────────────────────────────────────────────────────────────
5
22
  // Syntax highlighting
6
23
  // ─────────────────────────────────────────────────────────────────
@@ -9,13 +26,13 @@ const KW = {
9
26
  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,
10
27
  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,
11
28
  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,
12
- sh: /\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|export|echo|local|source)\b/g,
29
+ 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)\b/g,
13
30
  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,
14
31
  };
15
32
  const LANGMAP = {
16
33
  javascript:"js",js:"js",typescript:"ts",ts:"ts",
17
34
  python:"py",py:"py",go:"go",golang:"go",
18
- rust:"rs",rs:"rs",bash:"sh",sh:"sh",shell:"sh",zsh:"sh"
35
+ rust:"rs",rs:"rs",bash:"sh",sh:"sh",shell:"sh",zsh:"sh",fish:"sh",
19
36
  };
20
37
 
21
38
  function highlight(code, lang = "") {
@@ -24,11 +41,11 @@ function highlight(code, lang = "") {
24
41
  const saved = [];
25
42
  const save = s => { const id = `\x00${saved.length}\x00`; saved.push(s); return id; };
26
43
 
27
- r = r.replace(/(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm, m => save(chalk.hex("#6A9955").italic(m)));
28
- r = r.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, m => save(chalk.hex("#CE9178")(m)));
29
- if (KW[l]) r = r.replace(KW[l], m => save(chalk.hex("#569CD6")(m)));
30
- r = r.replace(/\b(\d+\.?\d*)\b/g, m => save(chalk.hex("#B5CEA8")(m)));
31
- r = r.replace(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/g, m => save(chalk.hex("#DCDCAA")(m)));
44
+ r = r.replace(/(\/\/.*$|#.*$|\/\*[\s\S]*?\*\/)/gm, m => save(chalk.hex(C.comment).italic(m)));
45
+ r = r.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, m => save(chalk.hex(C.str)(m)));
46
+ if (KW[l]) r = r.replace(KW[l], m => save(chalk.hex(C.kw).bold(m)));
47
+ r = r.replace(/\b(\d+\.?\d*)\b/g, m => save(chalk.hex(C.num)(m)));
48
+ r = r.replace(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/g, m => save(chalk.hex(C.fn)(m)));
32
49
  return r.replace(/\x00(\d+)\x00/g, (_, i) => saved[parseInt(i)]);
33
50
  }
34
51
 
@@ -38,22 +55,46 @@ function highlight(code, lang = "") {
38
55
  export function renderMarkdown(text) {
39
56
  let r = text;
40
57
 
41
- // Fenced code blocks
58
+ // Fenced code blocks — with line gutter
42
59
  r = r.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
43
- const hl = highlight(code.trimEnd(), lang);
44
- const lbl = lang ? chalk.dim(` ${lang} `) : "";
45
- const top = chalk.dim("┄".repeat(40)) + lbl;
46
- const body = hl.split("\n").map(l => " " + l).join("\n");
47
- return `\n ${top}\n${body}\n ${chalk.dim("┄".repeat(40))}\n`;
60
+ const trimmed = code.trimEnd();
61
+ const lines = trimmed.split("\n");
62
+ const hl = highlight(trimmed, lang);
63
+ const hlLines = hl.split("\n");
64
+ const lbl = lang
65
+ ? chalk.hex(C.blue).bold(` ${lang} `)
66
+ : "";
67
+ const gutterW = String(lines.length).length;
68
+
69
+ const header = chalk.hex(C.dim)(" ╭") +
70
+ chalk.hex(C.dim)("─".repeat(2)) +
71
+ lbl +
72
+ chalk.hex(C.dim)("─".repeat(Math.max(0, 40 - lang.length - 2))) +
73
+ "╮";
74
+ const footer = chalk.hex(C.dim)(" ╰" + "─".repeat(44) + "╯");
75
+ const body = hlLines.map((l, i) => {
76
+ const n = chalk.hex(C.dim)(String(i + 1).padStart(gutterW));
77
+ return chalk.hex(C.dim)(" │ ") + n + chalk.hex(C.dim)(" │ ") + l;
78
+ }).join("\n");
79
+
80
+ return `\n${header}\n${body}\n${footer}\n`;
48
81
  });
49
82
 
50
83
  // Inline code
51
- r = r.replace(/`([^`\n]+)`/g, (_, c) => chalk.hex("#CE9178")(`\`${c}\``));
84
+ r = r.replace(/`([^`\n]+)`/g, (_, c) =>
85
+ chalk.hex(C.code)("‹") + chalk.hex(C.code).bold(c) + chalk.hex(C.code)("›")
86
+ );
52
87
 
53
88
  // Headers
54
- r = r.replace(/^### (.+)$/gm, (_, t) => "\n" + chalk.bold(t));
55
- r = r.replace(/^## (.+)$/gm, (_, t) => "\n" + chalk.bold.underline(t));
56
- r = r.replace(/^# (.+)$/gm, (_, t) => "\n" + chalk.bold.underline(t));
89
+ r = r.replace(/^### (.+)$/gm, (_, t) =>
90
+ "\n" + chalk.hex(C.yellow).bold(" " + t)
91
+ );
92
+ r = r.replace(/^## (.+)$/gm, (_, t) =>
93
+ "\n" + chalk.hex(C.blue).bold(" ›› " + t)
94
+ );
95
+ r = r.replace(/^# (.+)$/gm, (_, t) =>
96
+ "\n" + chalk.hex(C.teal).bold(" ▸ " + t.toUpperCase())
97
+ );
57
98
 
58
99
  // Bold / italic
59
100
  r = r.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => chalk.bold.italic(t));
@@ -61,124 +102,225 @@ export function renderMarkdown(text) {
61
102
  r = r.replace(/\*(.+?)\*/g, (_, t) => chalk.italic(t));
62
103
 
63
104
  // Blockquotes
64
- r = r.replace(/^> (.+)$/gm, (_, t) => chalk.dim("▌ ") + chalk.italic(t));
105
+ r = r.replace(/^> (.+)$/gm, (_, t) =>
106
+ chalk.hex(C.blue)(" ▌ ") + chalk.hex(C.white).italic(t)
107
+ );
65
108
 
66
109
  // Lists
67
- r = r.replace(/^(\s*)[*\-+] (.+)$/gm, (_, i, t) => `${i} · ${t}`);
68
- r = r.replace(/^(\s*)(\d+)\. (.+)$/gm, (_, i, n, t) => `${i} ${chalk.bold(n+".")} ${t}`);
110
+ r = r.replace(/^(\s*)[*\-+] (.+)$/gm, (_, ind, t) =>
111
+ ind + chalk.hex(C.teal)(" ◆ ") + t
112
+ );
113
+ r = r.replace(/^(\s*)(\d+)\. (.+)$/gm, (_, ind, n, t) =>
114
+ ind + chalk.hex(C.blue)(" " + n + ". ") + t
115
+ );
69
116
 
70
117
  // HR
71
- r = r.replace(/^---+$/gm, chalk.dim("─".repeat(48)));
118
+ r = r.replace(/^---+$/gm,
119
+ chalk.hex(C.dim)(" " + "─".repeat(50))
120
+ );
72
121
 
73
122
  return r;
74
123
  }
75
124
 
76
125
  // ─────────────────────────────────────────────────────────────────
77
- // Message printers (clean, no heavy boxing)
126
+ // Message printers
78
127
  // ─────────────────────────────────────────────────────────────────
79
128
  export function printUser(text) {
80
- // Show multi-line messages cleanly
81
129
  const lines = text.split("\n");
82
- const prefix = chalk.hex("#4A9EFF").bold(" you ");
83
- if (lines.length === 1) {
84
- process.stdout.write("\n" + prefix + chalk.white(text) + "\n");
85
- } else {
86
- process.stdout.write("\n" + prefix + chalk.white(lines[0]) + "\n");
87
- lines.slice(1).forEach(l => process.stdout.write(" " + chalk.white(l) + "\n"));
130
+ const isMulti = lines.length > 1;
131
+
132
+ // Single line: compact
133
+ if (!isMulti) {
134
+ process.stdout.write(
135
+ "\n" +
136
+ chalk.hex(C.blue)(" ╸ ") +
137
+ chalk.hex(C.blue).bold("you ") +
138
+ chalk.hex(C.white)(text) +
139
+ "\n"
140
+ );
141
+ return;
88
142
  }
143
+
144
+ // Multi-line (pasted code): show with left bar
145
+ process.stdout.write(
146
+ "\n" +
147
+ chalk.hex(C.blue)(" ╸ ") +
148
+ chalk.hex(C.blue).bold("you") +
149
+ chalk.hex(C.dim)(` (${lines.length} lines)`) +
150
+ "\n"
151
+ );
152
+ lines.forEach(l =>
153
+ process.stdout.write(chalk.hex(C.blue)(" │ ") + chalk.hex(C.white)(l) + "\n")
154
+ );
155
+ process.stdout.write(chalk.hex(C.blue)(" ╹") + "\n");
89
156
  }
90
157
 
91
158
  export function printAssistant(text) {
92
- const label = chalk.hex("#4EC9B0").bold("gemini ");
93
- const lines = renderMarkdown(text).split("\n");
94
- process.stdout.write("\n");
95
- lines.forEach((line, i) => {
96
- const pre = i === 0 ? " " + label : " ";
97
- process.stdout.write(pre + line + "\n");
98
- });
99
- process.stdout.write("\n");
159
+ const rendered = renderMarkdown(text);
160
+ const lines = rendered.split("\n");
161
+
162
+ process.stdout.write(
163
+ "\n" +
164
+ chalk.hex(C.teal)(" ╸ ") +
165
+ chalk.hex(C.teal).bold("gemini") +
166
+ "\n"
167
+ );
168
+
169
+ lines.forEach(l =>
170
+ process.stdout.write(chalk.hex(C.teal)(" │ ") + l + "\n")
171
+ );
172
+
173
+ process.stdout.write(chalk.hex(C.teal)(" ╹") + "\n\n");
100
174
  }
101
175
 
176
+ // ─────────────────────────────────────────────────────────────────
177
+ // Status messages
178
+ // ─────────────────────────────────────────────────────────────────
102
179
  export function printError(msg) {
103
- process.stdout.write(chalk.red(" error ") + chalk.dim(msg) + "\n");
180
+ process.stdout.write(
181
+ "\n" +
182
+ chalk.hex(C.red)(" ✗ ") +
183
+ chalk.hex(C.red).bold("error ") +
184
+ chalk.white(msg) +
185
+ "\n\n"
186
+ );
104
187
  }
105
188
  export function printInfo(msg) {
106
- process.stdout.write(chalk.dim(" info ") + msg + "\n");
189
+ process.stdout.write(
190
+ chalk.hex(C.dim)(" · ") +
191
+ chalk.hex(C.white)(msg) +
192
+ "\n"
193
+ );
107
194
  }
108
195
  export function printSuccess(msg) {
109
- process.stdout.write(chalk.green(" ✓ ") + msg + "\n");
196
+ process.stdout.write(
197
+ chalk.hex(C.teal)(" ✓ ") +
198
+ chalk.hex(C.teal)(msg) +
199
+ "\n"
200
+ );
110
201
  }
111
202
  export function printWarning(msg) {
112
- process.stdout.write(chalk.yellow(" warn ") + msg + "\n");
203
+ process.stdout.write(
204
+ chalk.hex(C.yellow)(" ⚠ ") +
205
+ chalk.hex(C.yellow)(msg) +
206
+ "\n"
207
+ );
113
208
  }
114
209
 
115
210
  // ─────────────────────────────────────────────────────────────────
116
- // Welcome
211
+ // Tool call display (used by agent.js)
212
+ // ─────────────────────────────────────────────────────────────────
213
+ export function printToolCall(name, args = {}) {
214
+ const argStr = Object.entries(args)
215
+ .map(([k, v]) => {
216
+ const s = String(v);
217
+ return chalk.hex(C.dim)(k + ":") + chalk.hex(C.code)(s.length > 55 ? s.slice(0, 55) + "…" : s);
218
+ })
219
+ .join(" ");
220
+
221
+ process.stdout.write(
222
+ chalk.hex(C.blue)(" ├─ ") +
223
+ chalk.hex(C.blue).bold(name) +
224
+ (argStr ? " " + argStr : "") +
225
+ "\n"
226
+ );
227
+ }
228
+
229
+ export function printToolResult(result) {
230
+ const isErr = typeof result === "object" && result.error;
231
+ const text = typeof result === "object"
232
+ ? (result.result ?? result.error ?? JSON.stringify(result))
233
+ : String(result);
234
+ const color = isErr ? chalk.hex(C.red) : chalk.hex(C.dim);
235
+ const lines = text.split("\n");
236
+ const shown = lines.slice(0, 10);
237
+ const extra = lines.length > 10 ? chalk.hex(C.dim)(` +${lines.length - 10} more lines`) : "";
238
+
239
+ shown.forEach(l =>
240
+ process.stdout.write(chalk.hex(C.dim)(" │ ") + color(l) + "\n")
241
+ );
242
+ if (extra) process.stdout.write(" " + extra + "\n");
243
+ }
244
+
245
+ // ─────────────────────────────────────────────────────────────────
246
+ // Welcome screen
117
247
  // ─────────────────────────────────────────────────────────────────
118
248
  export function renderWelcome(memCount = 0, extCount = 0) {
119
249
  const stats = [
120
- memCount ? `${memCount} context file${memCount > 1 ? "s" : ""}` : null,
121
- extCount ? `${extCount} extension${extCount > 1 ? "s" : ""}` : null,
122
- ].filter(Boolean).join(" · ");
250
+ memCount ? `${memCount} context file${memCount > 1 ? "s" : ""}` : null,
251
+ extCount ? `${extCount} extension${extCount > 1 ? "s" : ""}` : null,
252
+ ].filter(Boolean);
253
+
254
+ const sep = chalk.hex(C.blue)(" " + "━".repeat(46));
123
255
 
124
256
  return [
125
257
  "",
126
- chalk.hex("#4EC9B0").bold(" Gemini CLI") +
127
- chalk.dim(" ─ AI Agent ─ native function calling"),
128
- stats ? chalk.dim(" " + stats) : "",
129
- chalk.dim(" /help for commands · paste code freely · /agent to toggle tools"),
258
+ sep,
259
+ chalk.hex(C.teal).bold(" Gemini CLI") +
260
+ chalk.hex(C.dim)(" ─ AI Agent ─ native function calling"),
261
+ sep,
262
+ stats.length
263
+ ? chalk.hex(C.dim)(" ") + chalk.hex(C.white)(stats.join(" · "))
264
+ : "",
265
+ chalk.hex(C.dim)(" /help") +
266
+ chalk.hex(C.dim)(" for commands · ") +
267
+ chalk.hex(C.dim)("/agent") +
268
+ chalk.hex(C.dim)(" to toggle tools"),
130
269
  "",
131
270
  ].join("\n");
132
271
  }
133
272
 
134
273
  // ─────────────────────────────────────────────────────────────────
135
- // Help
274
+ // Help screen
136
275
  // ─────────────────────────────────────────────────────────────────
137
276
  export function renderHelp(customCommands = {}) {
138
- const section = (title, rows) => [
139
- "",
140
- chalk.dim(" " + title),
141
- chalk.dim(" " + "─".repeat(48)),
142
- ...rows.map(([cmd, desc]) =>
143
- " " + chalk.hex("#CE9178")(cmd.padEnd(26)) + chalk.dim(desc)
144
- ),
145
- ].join("\n");
277
+ const sep = chalk.hex(C.dim)(" " + "─".repeat(50));
278
+
279
+ const row = (cmd, desc) =>
280
+ " " +
281
+ chalk.hex(C.blue).bold(cmd.padEnd(28)) +
282
+ chalk.hex(C.dim)(desc);
146
283
 
147
284
  const lines = [
148
- section("COMMANDS", [
149
- ["/agent", "Toggle agent (tools) ↔ chat mode"],
150
- ["/yolo", "Skip all tool confirmations"],
151
- ["/memory show", "Show loaded GEMINI.md files"],
152
- ["/memory reload", "Reload context from disk"],
153
- ["/memory add <text>", "Append to ~/.gemini/GEMINI.md"],
154
- ["/ext list", "List extensions"],
155
- ["/ext install <src>", "Install extension (path or git URL)"],
156
- ["/ext uninstall <n>", "Remove extension"],
157
- ["/ext enable/disable <n>", "Toggle extension"],
158
- ["/ext update <n>", "Pull latest (git)"],
159
- ["/file <path>", "Attach file to next message"],
160
- ["/system <text>", "Set system instruction"],
161
- ["/history", "Show conversation turns"],
162
- ["/export <file>", "Export history to JSON"],
163
- ["/cd <path>", "Change working directory"],
164
- ["/new /clear", "Reset conversation"],
165
- ["/model", "Show model info"],
166
- ["/exit /quit", "Exit"],
167
- ]),
285
+ "",
286
+ chalk.hex(C.teal).bold(" Commands"),
287
+ sep,
288
+ row("/agent", "Toggle agent mode (tools on/off)"),
289
+ row("/yolo", "Skip all tool confirmations"),
290
+ sep,
291
+ row("/memory show", "Show loaded GEMINI.md files"),
292
+ row("/memory reload", "Reload context from disk"),
293
+ row("/memory add <text>", "Append to ~/.gemini/GEMINI.md"),
294
+ sep,
295
+ row("/ext list", "List installed extensions"),
296
+ row("/ext install <src>", "Install from path or git URL"),
297
+ row("/ext uninstall <n>", "Remove extension"),
298
+ row("/ext enable <n>", "Enable extension"),
299
+ row("/ext disable <n>", "Disable extension"),
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 system instruction"),
304
+ row("/history", "Show conversation turns"),
305
+ row("/export <file>", "Export history to JSON"),
306
+ row("/cd <path>", "Change working directory"),
307
+ row("/new /clear", "Reset conversation"),
308
+ row("/model", "Show model & config"),
309
+ row("/exit /quit", "Exit"),
168
310
  ];
169
311
 
170
312
  const cmds = Object.entries(customCommands);
171
313
  if (cmds.length) {
172
- lines.push(section("EXTENSION COMMANDS",
173
- cmds.map(([k, c]) => ["/" + k, c.description ?? ""])
174
- ));
314
+ lines.push("", chalk.hex(C.teal).bold(" Extension Commands"), sep);
315
+ cmds.forEach(([k, c]) => lines.push(row("/" + k, c.description ?? "")));
175
316
  }
176
317
 
177
318
  lines.push(
178
319
  "",
179
- chalk.dim(" Paste multi-line code freely — input is collected until you press Enter."),
180
- chalk.dim(" Ctrl+C cancel · Ctrl+D exit"),
181
- ""
320
+ chalk.hex(C.dim)(" Paste multi-line code freely — collected as one input."),
321
+ chalk.hex(C.dim)(" Ctrl+C cancel/exit · Ctrl+D exit"),
322
+ "",
182
323
  );
324
+
183
325
  return lines.join("\n");
184
326
  }