@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 +44 -49
- package/package.json +1 -1
- package/src/agent.js +18 -42
- package/src/input.js +86 -50
- package/src/renderer.js +227 -85
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
|
-
|
|
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 —
|
|
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:
|
|
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
|
-
//
|
|
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(
|
|
114
|
-
|
|
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({
|
|
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(
|
|
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("["+(
|
|
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
|
|
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 (
|
|
316
|
-
//
|
|
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(
|
|
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'
|
|
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
|
-
|
|
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
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
|
-
//
|
|
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
|
|
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
|
|
1
|
+
// src/input.js — Bracketed paste via Transform stream (byte-level interception)
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
7
|
+
import { Transform } from "stream";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const PASTE_START = "\x1b[200~";
|
|
10
|
+
const PASTE_END = "\x1b[201~";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
51
|
+
} else {
|
|
52
|
+
// Inside paste — look for end marker
|
|
53
|
+
const ei = this._buf.indexOf(PASTE_END);
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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,
|
|
28
|
-
r = r.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, m => save(chalk.hex(
|
|
29
|
-
if (KW[l]) r = r.replace(KW[l], m => save(chalk.hex(
|
|
30
|
-
r = r.replace(/\b(\d+\.?\d*)\b/g, m => save(chalk.hex(
|
|
31
|
-
r = r.replace(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/g, m => save(chalk.hex(
|
|
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
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
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) =>
|
|
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) =>
|
|
55
|
-
|
|
56
|
-
|
|
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) =>
|
|
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,
|
|
68
|
-
|
|
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,
|
|
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
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
process.stdout.write(
|
|
87
|
-
|
|
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
|
|
93
|
-
const lines
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
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
|
|
121
|
-
extCount
|
|
122
|
-
].filter(Boolean)
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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(
|
|
173
|
-
|
|
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 —
|
|
180
|
-
chalk.dim(" Ctrl+C
|
|
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
|
}
|