@botconnector/bccli 0.4.0-beta.3 → 0.4.0-beta.30

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.
@@ -3,9 +3,11 @@ import {
3
3
  BUILTIN_AGENTS,
4
4
  ignorePatterns,
5
5
  instructionPaths,
6
+ normalizeGlobPath,
6
7
  parseFrontmatter,
7
- toolLabel
8
- } from "./chunk-OSWQPSA4.js";
8
+ toolLabel,
9
+ validateGlobPattern
10
+ } from "./chunk-22CEHQVR.js";
9
11
  import {
10
12
  nextMode
11
13
  } from "./chunk-ELML4KVQ.js";
@@ -13,24 +15,35 @@ import {
13
15
  hasKey,
14
16
  providerName,
15
17
  writeGlobalConfig
16
- } from "./chunk-U7DLMVPZ.js";
18
+ } from "./chunk-K5JNR3HL.js";
19
+ import {
20
+ REASONING_LEVELS,
21
+ assertReasoningSupported,
22
+ parseReasoningLevel,
23
+ supportedReasoningLevels
24
+ } from "./chunk-SN3ODJZN.js";
17
25
  import {
18
26
  Session
19
27
  } from "./chunk-SJH3YLW4.js";
20
28
  import {
21
29
  CATALOG,
22
30
  fillTemplate
23
- } from "./chunk-UESQNU4A.js";
31
+ } from "./chunk-BEXQSEBI.js";
24
32
  import {
25
33
  addGlobalServer,
26
34
  globalMcpPath,
27
35
  readMcpFile,
28
36
  removeGlobalServer
29
- } from "./chunk-NGLKANMD.js";
37
+ } from "./chunk-44MXWCFN.js";
30
38
  import {
31
39
  PRESETS,
32
- removeCredential
33
- } from "./chunk-RZUJYAQV.js";
40
+ getLanguage,
41
+ locale,
42
+ parseLang,
43
+ removeCredential,
44
+ setLanguage,
45
+ t
46
+ } from "./chunk-EO7IVVX4.js";
34
47
 
35
48
  // src/ui/index.tsx
36
49
  import { render } from "ink";
@@ -39,36 +52,38 @@ import { render } from "ink";
39
52
  import { execFile as execFile2 } from "child_process";
40
53
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
41
54
  import { homedir } from "os";
42
- import { dirname, join, relative, resolve } from "path";
55
+ import { dirname, join as join2, relative, resolve } from "path";
43
56
  import { Box as Box13, Static, Text as Text15, useApp, useInput as useInput9 } from "ink";
44
57
  import { useCallback, useEffect as useEffect2, useRef, useState as useState8 } from "react";
45
58
 
46
59
  // src/commands.ts
47
60
  var SLASH_COMMANDS = [
48
- { name: "help", description: "daftar perintah dan pintasan" },
49
- { name: "model", description: "ganti model" },
50
- { name: "provider", description: "tambah/pilih provider AI" },
51
- { name: "mcp", description: "pasang/kelola server MCP" },
52
- { name: "new", description: "sesi baru (sesi lama tetap tersimpan)" },
53
- { name: "resume", description: "lanjutkan sesi lain di folder ini" },
54
- { name: "session", description: "info sesi ini" },
55
- { name: "status", description: "versi, model, mode izin, MCP, konteks" },
56
- { name: "permissions", description: "lihat/cabut izin" },
57
- { name: "undo", description: "batalkan edit file giliran terakhir" },
58
- { name: "diff", description: "git diff project" },
59
- { name: "copy", description: "salin jawaban terakhir" },
60
- { name: "export", description: "simpan percakapan ke markdown" },
61
- { name: "memory", description: "instruksi project (/memory <teks>, /memory global <teks>)" },
62
- { name: "init", description: "buat/perbarui AGENTS.md untuk project ini" },
63
- { name: "agents", description: "daftar subagent" },
64
- { name: "skills", description: "daftar skill dan perintah custom" },
65
- { name: "doctor", description: "cek kesehatan instalasi" },
66
- { name: "login", description: "simpan API key provider aktif" },
67
- { name: "logout", description: "hapus API key provider aktif" },
68
- { name: "clear", description: "mulai percakapan baru" },
69
- { name: "compact", description: "ringkas percakapan" },
70
- { name: "cost", description: "pemakaian token sesi ini" },
71
- { name: "exit", description: "keluar" }
61
+ { name: "help", description: "list of commands and shortcuts" },
62
+ { name: "model", description: "change model" },
63
+ { name: "reasoning", description: "set reasoning: auto/off/low/medium/high/max" },
64
+ { name: "provider", description: "add/choose an AI provider" },
65
+ { name: "mcp", description: "install/manage MCP servers" },
66
+ { name: "new", description: "new session (the old one stays saved)" },
67
+ { name: "resume", description: "continue another session in this folder" },
68
+ { name: "session", description: "info about this session" },
69
+ { name: "status", description: "version, model, permission mode, MCP, context" },
70
+ { name: "permissions", description: "view/revoke permissions" },
71
+ { name: "undo", description: "undo the file edits of the last turn" },
72
+ { name: "diff", description: "git diff of the project" },
73
+ { name: "copy", description: "copy the last answer" },
74
+ { name: "export", description: "save the conversation as markdown" },
75
+ { name: "memory", description: "project instructions (/memory <text>, /memory global <text>)" },
76
+ { name: "init", description: "create/update AGENTS.md for this project" },
77
+ { name: "agents", description: "list subagents" },
78
+ { name: "skills", description: "list skills and custom commands" },
79
+ { name: "doctor", description: "check installation health" },
80
+ { name: "login", description: "save the API key of the current provider" },
81
+ { name: "logout", description: "delete the API key of the current provider" },
82
+ { name: "language", description: "set the interface language: en/id" },
83
+ { name: "clear", description: "start a new conversation" },
84
+ { name: "compact", description: "summarize the conversation" },
85
+ { name: "cost", description: "token usage of this session" },
86
+ { name: "exit", description: "quit" }
72
87
  ];
73
88
  function parseSlash(text) {
74
89
  if (!text.startsWith("/")) return void 0;
@@ -102,8 +117,8 @@ function git(cwd, args, bin = "git") {
102
117
  }
103
118
  async function gitDiff(cwd, maxLines = 400, bin = "git") {
104
119
  const inside = await git(cwd, ["rev-parse", "--is-inside-work-tree"], bin);
105
- if (inside.missing) return "git tidak terpasang atau tidak ada di PATH, jadi /diff tidak bisa dipakai.";
106
- if (!inside.ok || inside.out.trim() !== "true") return "Folder ini bukan repository git, jadi tidak ada diff.";
120
+ if (inside.missing) return t("git is not installed or not on PATH, so /diff cannot be used.");
121
+ if (!inside.ok || inside.out.trim() !== "true") return t("This folder is not a git repository, so there is no diff.");
107
122
  const base = (await git(cwd, ["rev-parse", "--verify", "-q", "HEAD"])).ok ? "HEAD" : "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
108
123
  const [stat, diff, untracked] = await Promise.all([
109
124
  git(cwd, ["diff", "--stat", base]),
@@ -111,23 +126,23 @@ async function gitDiff(cwd, maxLines = 400, bin = "git") {
111
126
  git(cwd, ["ls-files", "--others", "--exclude-standard"])
112
127
  ]);
113
128
  const fresh = untracked.out.split("\n").filter(Boolean);
114
- if (!diff.out.trim() && !fresh.length) return "Tidak ada perubahan yang belum di-commit.";
129
+ if (!diff.out.trim() && !fresh.length) return t("No uncommitted changes.");
115
130
  const lines = diff.out.trimEnd().split("\n");
116
131
  const more = lines.length > maxLines ? `
117
- \u2026 ${lines.length - maxLines} baris lagi` : "";
118
- return [stat.out.trim(), lines.slice(0, maxLines).join("\n") + more, fresh.length ? `File baru (belum di-track): ${fresh.join(", ")}` : ""].filter(Boolean).join("\n\n");
132
+ ${t("\u2026 {n} more lines", { n: lines.length - maxLines })}` : "";
133
+ return [stat.out.trim(), lines.slice(0, maxLines).join("\n") + more, fresh.length ? t("New files (untracked): {files}", { files: fresh.join(", ") }) : ""].filter(Boolean).join("\n\n");
119
134
  }
120
135
 
121
136
  // src/slash/doctor.ts
122
137
  async function doctorText(input, deps) {
123
138
  const major = Number(deps.nodeVersion.replace(/^v/, "").split(".")[0]);
124
- const lines = [major >= 22 ? `\u2713 Node ${deps.nodeVersion}` : `\u2717 Node ${deps.nodeVersion} (butuh \u2265 22)`];
125
- for (const p of input.providers) lines.push(p.ready ? `\u2713 ${p.name}` : `\u25CB ${p.name} (belum ada key)`);
139
+ const lines = [major >= 22 ? `\u2713 Node ${deps.nodeVersion}` : t("\u2717 Node {version} (needs \u2265 22)", { version: deps.nodeVersion })];
140
+ for (const p of input.providers) lines.push(p.ready ? `\u2713 ${p.name}` : t("\u25CB {name} (no key yet)", { name: p.name }));
126
141
  const cmds = ["rg", "git", "npx", "uvx"];
127
142
  const [connection, found] = await Promise.all([
128
143
  deps.listModels().then(
129
- (models) => `\u2713 Koneksi ${input.activeProvider} (${models.length} model)`,
130
- (error) => `\u2717 Koneksi ${input.activeProvider}: ${error.message}`
144
+ (models) => t("\u2713 Connection {provider} ({n} models)", { provider: input.activeProvider, n: models.length }),
145
+ (error) => t("\u2717 Connection {provider}: {error}", { provider: input.activeProvider, error: error.message })
131
146
  ),
132
147
  Promise.all(cmds.map(async (cmd) => deps.which(cmd)))
133
148
  ]);
@@ -158,31 +173,31 @@ function statusText(s) {
158
173
  return [
159
174
  `BCCLI ${s.version}`,
160
175
  `Model: ${s.modelRef} (${s.providerLabel})`,
161
- `Mode izin: ${s.mode}`,
162
- `Folder: ${s.cwd}`,
163
- `MCP: ${ready} aktif, ${errors} error`,
164
- `Token sesi: ${k(s.usage.inputTokens)} masuk \xB7 ${k(s.usage.outputTokens)} keluar`,
165
- `Konteks: \xB1${Math.round(s.lastInputTokens / 128e3 * 100)}% dari 128k`
176
+ t("Permission mode: {mode}", { mode: s.mode }),
177
+ t("Folder: {cwd}", { cwd: s.cwd }),
178
+ t("MCP: {ready} active, {errors} errors", { ready, errors }),
179
+ t("Session tokens: {input} in \xB7 {output} out", { input: k(s.usage.inputTokens), output: k(s.usage.outputTokens) }),
180
+ t("Context: ~{pct}% of 128k", { pct: Math.round(s.lastInputTokens / 128e3 * 100) })
166
181
  ].join("\n");
167
182
  }
168
183
  function agentsText(defs) {
169
- return defs.map((d) => `${d.name} \u2014 ${d.description} \xB7 alat: ${d.tools ? d.tools.join(", ") : "semua"} \xB7 model: ${d.model ?? "ikut utama"}`).join("\n");
184
+ return defs.map((d) => t("{name} \u2014 {description} \xB7 tools: {tools} \xB7 model: {model}", { name: d.name, description: d.description, tools: d.tools ? d.tools.join(", ") : t("all"), model: d.model ?? t("same as main") })).join("\n");
170
185
  }
171
186
  function skillsText(skills, commands) {
172
- const s = skills.length ? skills.map((x) => `${x.name} \u2014 ${x.description.slice(0, 80)} (${x.dir})`).join("\n") : "(tidak ada skill)";
173
- const c = commands.length ? commands.map((x) => `/${x.name} \u2014 ${x.description ?? ""}`).join("\n") : "(tidak ada perintah custom)";
174
- return `Skill:
187
+ const s = skills.length ? skills.map((x) => `${x.name} \u2014 ${x.description.slice(0, 80)} (${x.dir})`).join("\n") : t("(no skills)");
188
+ const c = commands.length ? commands.map((x) => `/${x.name} \u2014 ${x.description ?? ""}`).join("\n") : t("(no custom commands)");
189
+ return `${t("Skills")}:
175
190
  ${s}
176
191
 
177
- Perintah custom:
192
+ ${t("Custom commands")}:
178
193
  ${c}`;
179
194
  }
180
195
  function sessionText(s) {
181
196
  return [
182
- `Mulai: ${s.started.toLocaleString("id-ID")}`,
183
- `${s.messages} pesan \xB7 ${k(s.usage.inputTokens)} token masuk \xB7 ${k(s.usage.outputTokens)} keluar`,
197
+ t("Started: {when}", { when: s.started.toLocaleString(locale()) }),
198
+ t("{messages} messages \xB7 {input} tokens in \xB7 {output} out", { messages: s.messages, input: k(s.usage.inputTokens), output: k(s.usage.outputTokens) }),
184
199
  `Model: ${s.modelRef}`,
185
- `File: ${s.file}`
200
+ t("File: {file}", { file: s.file })
186
201
  ].join("\n");
187
202
  }
188
203
 
@@ -202,7 +217,308 @@ function parseMemoryArgs(args) {
202
217
  }
203
218
 
204
219
  // src/ui/Markdown.tsx
205
- import { Box, Text } from "ink";
220
+ import { Box, Text, useStdout } from "ink";
221
+
222
+ // src/ui/table.ts
223
+ var WIDE = [
224
+ [4352, 4447],
225
+ [8986, 8987],
226
+ [9193, 9196],
227
+ [9200, 9200],
228
+ [9203, 9203],
229
+ [9725, 9726],
230
+ [9748, 9749],
231
+ [9800, 9811],
232
+ [9855, 9855],
233
+ [9875, 9875],
234
+ [9889, 9889],
235
+ [9898, 9899],
236
+ [9917, 9918],
237
+ [9924, 9925],
238
+ [9934, 9934],
239
+ [9940, 9940],
240
+ [9962, 9962],
241
+ [9970, 9971],
242
+ [9973, 9973],
243
+ [9978, 9978],
244
+ [9981, 9981],
245
+ [9989, 9989],
246
+ [9994, 9995],
247
+ [10024, 10024],
248
+ [10060, 10060],
249
+ [10062, 10062],
250
+ [10067, 10069],
251
+ [10071, 10071],
252
+ [10133, 10135],
253
+ [10160, 10160],
254
+ [10175, 10175],
255
+ [11035, 11036],
256
+ [11088, 11088],
257
+ [11093, 11093],
258
+ [11904, 42191],
259
+ [44032, 55203],
260
+ [63744, 64255],
261
+ [65072, 65135],
262
+ [65280, 65376],
263
+ [65504, 65510],
264
+ [127744, 128591],
265
+ [128640, 128767],
266
+ [129280, 129791],
267
+ [131072, 262141]
268
+ ];
269
+ var ZERO = [
270
+ [768, 879],
271
+ [6832, 6911],
272
+ [7616, 7679],
273
+ [8203, 8207],
274
+ [8400, 8447],
275
+ [65024, 65039],
276
+ [65279, 65279],
277
+ [917760, 917999]
278
+ ];
279
+ var inRanges = (cp, ranges) => ranges.some(([a, b]) => cp >= a && cp <= b);
280
+ function charWidth(cp) {
281
+ if (cp < 32 || cp >= 127 && cp < 160) return 0;
282
+ if (cp < 768) return 1;
283
+ if (inRanges(cp, ZERO)) return 0;
284
+ return inRanges(cp, WIDE) ? 2 : 1;
285
+ }
286
+ function displayWidth(text) {
287
+ let width = 0;
288
+ for (const ch of text) width += charWidth(ch.codePointAt(0));
289
+ return width;
290
+ }
291
+ var segsWidth = (segs) => segs.reduce((sum, s) => sum + displayWidth(s.text), 0);
292
+ var INLINE = /(`[^`]+`|\*\*[^*]+\*\*|~~[^~]+~~|!?\[[^\]\n]+\]\([^)\s]+\)|(?<![\w*])\*(?![\s*])[^*\n]+?(?<![\s*])\*(?![\w*])|(?<!\w)_(?![\s_])[^_\n]+?(?<![\s_])_(?!\w))/g;
293
+ function parseInline(text) {
294
+ const segs = [];
295
+ for (const part of text.split(INLINE)) {
296
+ if (part === "") continue;
297
+ if (part.length > 2 && part.startsWith("`") && part.endsWith("`")) segs.push({ text: part.slice(1, -1), style: "code" });
298
+ else if (part.length > 4 && part.startsWith("**") && part.endsWith("**")) segs.push({ text: part.slice(2, -2), style: "bold" });
299
+ else if (part.length > 4 && part.startsWith("~~") && part.endsWith("~~")) segs.push({ text: part.slice(2, -2), style: "strike" });
300
+ else if (/^!?\[[^\]\n]+\]\([^)\s]+\)$/.test(part)) {
301
+ const m = /^(!?)\[([^\]]+)\]\(([^)\s]+)\)$/.exec(part);
302
+ const [, image, label, url] = m;
303
+ segs.push({ text: label, style: image ? "plain" : "link" });
304
+ if (label !== url && !url.startsWith("#")) segs.push({ text: ` (${url})`, style: "dim" });
305
+ } else if (part.length > 2 && (part.startsWith("*") && part.endsWith("*") || part.startsWith("_") && part.endsWith("_"))) {
306
+ segs.push({ text: part.slice(1, -1), style: "italic" });
307
+ } else segs.push({ text: part, style: "plain" });
308
+ }
309
+ return segs;
310
+ }
311
+ function splitRow(line) {
312
+ let s = line.trim();
313
+ if (s.startsWith("|")) s = s.slice(1);
314
+ if (s.endsWith("|") && !s.endsWith("\\|")) s = s.slice(0, -1);
315
+ const cells = [];
316
+ let current = "";
317
+ for (let i = 0; i < s.length; i++) {
318
+ if (s[i] === "\\" && s[i + 1] === "|") {
319
+ current += "|";
320
+ i++;
321
+ } else if (s[i] === "|") {
322
+ cells.push(current);
323
+ current = "";
324
+ } else current += s[i];
325
+ }
326
+ cells.push(current);
327
+ return cells.map((c) => c.trim());
328
+ }
329
+ var SEPARATOR = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
330
+ var isSeparator = (line) => line.includes("|") && SEPARATOR.test(line);
331
+ var cell = (raw) => parseInline(raw.replace(/<br\s*\/?>/gi, "\n"));
332
+ function parseTable(lines, start) {
333
+ if (start + 1 >= lines.length || !lines[start].includes("|") || !isSeparator(lines[start + 1])) return void 0;
334
+ const header = splitRow(lines[start]);
335
+ const sep = splitRow(lines[start + 1]);
336
+ if (sep.length !== header.length) return void 0;
337
+ const body = [];
338
+ let end = start + 2;
339
+ while (end < lines.length && lines[end].trim() && lines[end].includes("|")) body.push(splitRow(lines[end++]));
340
+ const columns = Math.max(header.length, ...body.map((r) => r.length));
341
+ const pad2 = (row, fill) => [...row, ...Array.from({ length: columns - row.length }, () => fill)];
342
+ const align = pad2(
343
+ sep.map((s) => s.startsWith(":") && s.endsWith(":") ? "center" : s.endsWith(":") ? "right" : "left"),
344
+ "left"
345
+ );
346
+ return { table: { header: pad2(header, "").map(cell), align, rows: body.map((r) => pad2(r, "").map(cell)) }, end };
347
+ }
348
+ function tokenize(segs) {
349
+ const tokens = [];
350
+ let word = [];
351
+ const endWord = () => {
352
+ if (word.length) tokens.push(word);
353
+ word = [];
354
+ };
355
+ for (const seg of segs) {
356
+ for (const piece of seg.text.match(/\n|[^\S\n]+|[^\s]+/g) ?? []) {
357
+ if (piece === "\n") {
358
+ endWord();
359
+ tokens.push("br");
360
+ } else if (/^\s+$/.test(piece)) endWord();
361
+ else word.push({ text: piece, style: seg.style });
362
+ }
363
+ }
364
+ endWord();
365
+ return tokens;
366
+ }
367
+ function splitWord(word, width) {
368
+ const pieces = [];
369
+ let current = [];
370
+ let used = 0;
371
+ for (const seg of word) {
372
+ let text = "";
373
+ for (const ch of seg.text) {
374
+ const w = charWidth(ch.codePointAt(0));
375
+ if (used + w > width && used > 0) {
376
+ if (text) current.push({ text, style: seg.style });
377
+ pieces.push(current);
378
+ current = [];
379
+ text = "";
380
+ used = 0;
381
+ }
382
+ text += ch;
383
+ used += w;
384
+ }
385
+ if (text) current.push({ text, style: seg.style });
386
+ }
387
+ if (current.length) pieces.push(current);
388
+ return pieces;
389
+ }
390
+ function wrap(tokens, width) {
391
+ const lines = [];
392
+ let line = [];
393
+ let used = 0;
394
+ const flush = () => {
395
+ lines.push(line);
396
+ line = [];
397
+ used = 0;
398
+ };
399
+ for (const token of tokens) {
400
+ if (token === "br") {
401
+ flush();
402
+ continue;
403
+ }
404
+ const w = segsWidth(token);
405
+ if (w > width) {
406
+ if (used > 0) flush();
407
+ const pieces = splitWord(token, width);
408
+ for (const piece of pieces.slice(0, -1)) {
409
+ line = piece;
410
+ flush();
411
+ }
412
+ line = pieces.at(-1) ?? [];
413
+ used = segsWidth(line);
414
+ } else if (used === 0) {
415
+ line = [...token];
416
+ used = w;
417
+ } else if (used + 1 + w <= width) {
418
+ line.push({ text: " ", style: "plain" }, ...token);
419
+ used += 1 + w;
420
+ } else {
421
+ flush();
422
+ line = [...token];
423
+ used = w;
424
+ }
425
+ }
426
+ flush();
427
+ return lines;
428
+ }
429
+ var SEP = " \u2502 ";
430
+ var RULE_JOIN = "\u2500\u253C\u2500";
431
+ var MAX_MIN_WIDTH = 12;
432
+ var dim = (text) => ({ text, style: "dim" });
433
+ function rtrim(line) {
434
+ const out = [...line];
435
+ while (out.length) {
436
+ const last = out[out.length - 1];
437
+ const text = last.text.trimEnd();
438
+ if (text) {
439
+ out[out.length - 1] = { ...last, text };
440
+ break;
441
+ }
442
+ out.pop();
443
+ }
444
+ return out;
445
+ }
446
+ var bold = (segs) => segs.map((s) => s.style === "plain" ? { ...s, style: "bold" } : s);
447
+ function pad(segs, width, align, last) {
448
+ const space = Math.max(0, width - segsWidth(segs));
449
+ const left = align === "right" ? space : align === "center" ? Math.floor(space / 2) : 0;
450
+ const right = last ? 0 : space - left;
451
+ return [...left ? [{ text: " ".repeat(left), style: "plain" }] : [], ...segs, ...right ? [{ text: " ".repeat(right), style: "plain" }] : []];
452
+ }
453
+ function columnWidths(table, available) {
454
+ const columns = table.align.length;
455
+ const all = [table.header, ...table.rows];
456
+ const natural = [];
457
+ const minimum = [];
458
+ for (let c = 0; c < columns; c++) {
459
+ let widest = 1;
460
+ let longestWord = 1;
461
+ for (const row of all) {
462
+ const tokens = tokenize(row[c]);
463
+ for (const line of wrap(tokens, Number.POSITIVE_INFINITY)) widest = Math.max(widest, segsWidth(line));
464
+ for (const token of tokens) if (token !== "br") longestWord = Math.max(longestWord, segsWidth(token));
465
+ }
466
+ natural.push(widest);
467
+ minimum.push(Math.min(longestWord, MAX_MIN_WIDTH, widest));
468
+ }
469
+ if (minimum.reduce((a, b) => a + b, 0) > available) return void 0;
470
+ const widths = [...natural];
471
+ let total = widths.reduce((a, b) => a + b, 0);
472
+ while (total > available) {
473
+ let pick = -1;
474
+ for (let c = 0; c < columns; c++) if (widths[c] > minimum[c] && (pick < 0 || widths[c] > widths[pick])) pick = c;
475
+ if (pick < 0) return void 0;
476
+ widths[pick]--;
477
+ total--;
478
+ }
479
+ return widths;
480
+ }
481
+ function stacked(table, width) {
482
+ const lines = [];
483
+ table.rows.forEach((row, r) => {
484
+ if (r > 0) lines.push([dim("\u2500".repeat(Math.min(width, 40)))]);
485
+ row.forEach((value, c) => {
486
+ const label = [...bold(table.header[c]), { text: ": ", style: "plain" }, ...value];
487
+ lines.push(...wrap(tokenize(label), width));
488
+ });
489
+ });
490
+ return lines.length ? lines : [bold(table.header.flat())];
491
+ }
492
+ function layoutTable(table, width) {
493
+ const columns = table.align.length;
494
+ const available = Math.max(columns, width - (columns - 1) * SEP.length);
495
+ const widths = columnWidths(table, available);
496
+ if (!widths) return stacked(table, Math.max(10, width));
497
+ const renderRow = (cells, header) => {
498
+ const wrapped = cells.map((segs, c) => wrap(tokenize(header ? bold(segs) : segs), widths[c]));
499
+ const height = Math.max(...wrapped.map((w) => w.length));
500
+ const lines = [];
501
+ for (let l = 0; l < height; l++) {
502
+ const line = [];
503
+ wrapped.forEach((w, c) => {
504
+ if (c > 0) line.push(dim(SEP));
505
+ line.push(...pad(w[l] ?? [], widths[c], table.align[c], c === columns - 1));
506
+ });
507
+ lines.push(rtrim(line));
508
+ }
509
+ return { lines, height };
510
+ };
511
+ const rule = [dim(widths.map((w) => "\u2500".repeat(w)).join(RULE_JOIN))];
512
+ const head = renderRow(table.header, true);
513
+ const body = table.rows.map((r) => renderRow(r, false));
514
+ const wrappedRows = body.some((b) => b.height > 1);
515
+ const out = [...head.lines, rule];
516
+ body.forEach((b, i) => {
517
+ if (wrappedRows && i > 0) out.push(rule);
518
+ out.push(...b.lines);
519
+ });
520
+ return out;
521
+ }
206
522
 
207
523
  // src/ui/theme.ts
208
524
  function color(name) {
@@ -211,58 +527,136 @@ function color(name) {
211
527
  var ACCENT = "green";
212
528
 
213
529
  // src/ui/Markdown.tsx
214
- import { jsx } from "react/jsx-runtime";
215
- function inline(line) {
216
- return line.split(/(\*\*[^*]+\*\*|`[^`]+`)/g).map((part, i) => {
217
- if (part.length > 4 && part.startsWith("**") && part.endsWith("**")) {
218
- return /* @__PURE__ */ jsx(Text, { bold: true, children: part.slice(2, -2) }, i);
219
- }
220
- if (part.length > 2 && part.startsWith("`") && part.endsWith("`")) {
221
- return /* @__PURE__ */ jsx(Text, { color: color("cyan"), children: part.slice(1, -1) }, i);
222
- }
223
- return part;
224
- });
530
+ import { jsx, jsxs } from "react/jsx-runtime";
531
+ function segment(seg, key) {
532
+ switch (seg.style) {
533
+ case "bold":
534
+ return /* @__PURE__ */ jsx(Text, { bold: true, children: seg.text }, key);
535
+ case "italic":
536
+ return /* @__PURE__ */ jsx(Text, { italic: true, children: seg.text }, key);
537
+ case "strike":
538
+ return /* @__PURE__ */ jsx(Text, { strikethrough: true, children: seg.text }, key);
539
+ case "link":
540
+ return /* @__PURE__ */ jsx(Text, { underline: true, color: color("cyan"), children: seg.text }, key);
541
+ case "code":
542
+ return /* @__PURE__ */ jsx(Text, { color: color("cyan"), children: seg.text }, key);
543
+ case "dim":
544
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: seg.text }, key);
545
+ default:
546
+ return seg.text;
547
+ }
225
548
  }
226
- function Markdown({ text }) {
227
- let inCode = false;
549
+ var inline = (line) => parseInline(line).map(segment);
550
+ var HEADING = /^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
551
+ var RULE = /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/;
552
+ var QUOTE = /^\s{0,3}((?:>\s?)+)(.*)$/;
553
+ var BULLET = /^(\s*)[-*+]\s+(.*)$/;
554
+ var NUMBERED = /^(\s*)(\d{1,9})[.)]\s+(.*)$/;
555
+ var BULLETS = ["\u2022", "\u25E6", "\u25AA"];
556
+ var bulletFor = (spaces) => BULLETS[spaces === 0 ? 0 : spaces <= 4 ? 1 : 2];
557
+ function hanging(key, marker, body, width, padding = 0) {
558
+ const textWidth = Math.max(10, width - padding - displayWidth(marker) - 1);
559
+ return /* @__PURE__ */ jsxs(Box, { paddingLeft: padding, children: [
560
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, marginRight: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: marker }) }),
561
+ /* @__PURE__ */ jsx(Box, { width: textWidth, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { children: body }) })
562
+ ] }, key);
563
+ }
564
+ function Markdown({ text, indent = 2 }) {
565
+ const { stdout } = useStdout();
566
+ const width = Math.max(20, (stdout?.columns ?? 80) - indent - 1);
567
+ const lines = text.split("\n");
228
568
  const rows = [];
229
- text.split("\n").forEach((line, i) => {
569
+ let inCode = false;
570
+ for (let i = 0; i < lines.length; i++) {
571
+ const line = lines[i];
230
572
  if (line.trimStart().startsWith("```")) {
231
573
  inCode = !inCode;
232
- return;
574
+ continue;
233
575
  }
234
- rows.push(
235
- inCode ? /* @__PURE__ */ jsx(Text, { color: color("cyan"), children: ` ${line}` }, i) : /* @__PURE__ */ jsx(Text, { children: line ? inline(line) : " " }, i)
236
- );
237
- });
576
+ if (inCode) {
577
+ rows.push(
578
+ /* @__PURE__ */ jsx(Text, { color: color("cyan"), children: ` ${line}` }, i)
579
+ );
580
+ continue;
581
+ }
582
+ const parsed = parseTable(lines, i);
583
+ if (parsed) {
584
+ layoutTable(parsed.table, width).forEach((tableLine, n) => {
585
+ rows.push(/* @__PURE__ */ jsx(Text, { children: tableLine.length ? tableLine.map(segment) : " " }, `${i}-${n}`));
586
+ });
587
+ i = parsed.end - 1;
588
+ continue;
589
+ }
590
+ const heading = HEADING.exec(line);
591
+ if (heading) {
592
+ const level = heading[1].length;
593
+ rows.push(
594
+ /* @__PURE__ */ jsx(Text, { bold: true, underline: level === 1, color: level <= 2 ? color(ACCENT) : void 0, children: parseInline(heading[2]).map((s) => s.text).join("") }, i)
595
+ );
596
+ continue;
597
+ }
598
+ if (RULE.test(line)) {
599
+ rows.push(
600
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(Math.min(width, 80)) }, i)
601
+ );
602
+ continue;
603
+ }
604
+ const quote = QUOTE.exec(line);
605
+ if (quote) {
606
+ const depth = quote[1].replace(/\s/g, "").length;
607
+ rows.push(hanging(i, "\u258E".repeat(depth), /* @__PURE__ */ jsx(Text, { italic: true, children: inline(quote[2]) }), width));
608
+ continue;
609
+ }
610
+ const bullet = BULLET.exec(line);
611
+ if (bullet) {
612
+ const padding = Math.min(bullet[1].replace(/\t/g, " ").length, 16);
613
+ const task = /^\[([ xX])\]\s+(.*)$/.exec(bullet[2]);
614
+ rows.push(
615
+ task ? hanging(i, task[1] === " " ? "\u2610" : "\u2611", inline(task[2]), width, padding) : hanging(i, bulletFor(padding), inline(bullet[2]), width, padding)
616
+ );
617
+ continue;
618
+ }
619
+ const numbered = NUMBERED.exec(line);
620
+ if (numbered) {
621
+ const padding = Math.min(numbered[1].replace(/\t/g, " ").length, 16);
622
+ rows.push(hanging(i, `${numbered[2]}.`, inline(numbered[3]), width, padding));
623
+ continue;
624
+ }
625
+ rows.push(/* @__PURE__ */ jsx(Text, { children: line ? inline(line) : " " }, i));
626
+ }
238
627
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: rows });
239
628
  }
240
629
 
241
630
  // src/ui/LinePrompt.tsx
242
631
  import { Box as Box2, Text as Text2, useInput } from "ink";
243
632
  import { useState } from "react";
244
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
633
+
634
+ // src/ui/focusReport.ts
635
+ var isFocusReport = (input) => input === "[I" || input === "[O";
636
+
637
+ // src/ui/LinePrompt.tsx
638
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
245
639
  function LinePrompt({ label, mask, onSubmit, onCancel }) {
246
640
  const [value, setValue] = useState("");
247
641
  useInput((input, key) => {
248
642
  if (key.return) onSubmit(value.trim());
249
643
  else if (key.escape) onCancel();
250
644
  else if (key.backspace || key.delete) setValue((v) => v.slice(0, -1));
251
- else if (!key.ctrl && !key.meta && !key.tab && !key.upArrow && !key.downArrow) setValue((v) => v + input.replace(/[\r\n]/g, ""));
645
+ else if (!key.ctrl && !key.meta && !key.tab && !key.upArrow && !key.downArrow && !isFocusReport(input)) setValue((v) => v + input.replace(/[\r\n]/g, ""));
252
646
  });
253
647
  const shown = mask ? "\u2022".repeat(value.length) : value;
254
- return /* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: /* @__PURE__ */ jsxs(Text2, { children: [
648
+ return /* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: /* @__PURE__ */ jsxs2(Text2, { children: [
255
649
  `${label}: `,
256
650
  shown,
257
651
  /* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " }),
258
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " enter simpan \xB7 esc batal" })
652
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: t(" enter save \xB7 esc cancel") })
259
653
  ] }) });
260
654
  }
261
655
 
262
656
  // src/ui/ListPicker.tsx
263
657
  import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
264
658
  import { useState as useState2 } from "react";
265
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
659
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
266
660
  var VISIBLE = 12;
267
661
  function ListPicker({ title, items, onPick }) {
268
662
  const [filter, setFilter] = useState2("");
@@ -272,31 +666,31 @@ function ListPicker({ title, items, onPick }) {
272
666
  useInput2((input, key) => {
273
667
  if (key.upArrow) setIndex(Math.max(0, cursor - 1));
274
668
  else if (key.downArrow) setIndex(Math.min(shown.length - 1, cursor + 1));
275
- else if (key.return) onPick(shown[cursor]?.id);
669
+ else if (key.return && !shown[cursor]?.disabled) onPick(shown[cursor]?.id);
276
670
  else if (key.escape) onPick(void 0);
277
671
  else if (key.backspace || key.delete) {
278
672
  setFilter((f) => f.slice(0, -1));
279
673
  setIndex(0);
280
- } else if (!key.ctrl && !key.meta && !key.tab && input) {
674
+ } else if (!key.ctrl && !key.meta && !key.tab && input && !isFocusReport(input)) {
281
675
  setFilter((f) => f + input);
282
676
  setIndex(0);
283
677
  }
284
678
  });
285
679
  const start = Math.max(0, Math.min(cursor - 5, shown.length - VISIBLE));
286
- return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
287
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: `${title} (\u2191\u2193 enter \xB7 ketik untuk menyaring${filter ? `: ${filter}` : ""} \xB7 esc batal)` }),
288
- shown.slice(start, start + VISIBLE).map((item, i) => /* @__PURE__ */ jsxs2(Text3, { color: start + i === cursor ? color("green") : void 0, children: [
680
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
681
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: t("{title} (\u2191\u2193 enter \xB7 type to filter{filter} \xB7 esc cancel)", { title, filter: filter ? `: ${filter}` : "" }) }),
682
+ shown.slice(start, start + VISIBLE).map((item, i) => /* @__PURE__ */ jsxs3(Text3, { color: start + i === cursor && !item.disabled ? color("green") : void 0, dimColor: item.disabled, children: [
289
683
  `${start + i === cursor ? "\u203A" : " "} ${item.label}`,
290
684
  item.hint ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: ` ${item.hint}` }) : null
291
685
  ] }, item.id)),
292
- shown.length ? null : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Tidak ada yang cocok." })
686
+ shown.length ? null : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: t("No matches.") })
293
687
  ] });
294
688
  }
295
689
 
296
690
  // src/ui/ModelPicker.tsx
297
691
  import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
298
692
  import { useState as useState3 } from "react";
299
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
693
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
300
694
  var VISIBLE2 = 14;
301
695
  function ModelPicker({ groups, current, onPick }) {
302
696
  const [filter, setFilter] = useState3("");
@@ -322,18 +716,18 @@ function ModelPicker({ groups, current, onPick }) {
322
716
  else if (key.backspace || key.delete) {
323
717
  setFilter((f) => f.slice(0, -1));
324
718
  setIndex(0);
325
- } else if (!key.ctrl && !key.meta && !key.tab && input) {
719
+ } else if (!key.ctrl && !key.meta && !key.tab && input && !isFocusReport(input)) {
326
720
  setFilter((f) => f + input);
327
721
  setIndex(0);
328
722
  }
329
723
  });
330
724
  const start = Math.max(0, Math.min(cursor - 5, rows.length - VISIBLE2));
331
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
332
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: `Pilih model (ketik untuk menyaring${filter ? `: ${filter}` : ""} \xB7 \u2191\u2193 enter \xB7 esc batal)` }),
725
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
726
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: t("Choose a model (type to filter{filter} \xB7 \u2191\u2193 enter \xB7 esc cancel)", { filter: filter ? `: ${filter}` : "" }) }),
333
727
  rows.slice(start, start + VISIBLE2).map(
334
728
  (row, i) => row.kind === "header" ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `\u2500\u2500 ${row.text} ` }, `h${start + i}`) : /* @__PURE__ */ jsx4(Text4, { color: start + i === cursor ? color("green") : void 0, children: `${start + i === cursor ? "\u203A" : " "} ${row.label}${row.ref === current ? " (aktif)" : ""}` }, row.ref)
335
729
  ),
336
- !selectable.length ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Tidak ada model yang cocok." }) : null,
730
+ !selectable.length ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: t("No matching models.") }) : null,
337
731
  groups.filter((g) => g.error).map((g) => /* @__PURE__ */ jsx4(Text4, { color: color("yellow"), children: `${g.providerName} \u2014 ${g.error}` }, `e-${g.providerId}`))
338
732
  ] });
339
733
  }
@@ -341,7 +735,7 @@ function ModelPicker({ groups, current, onPick }) {
341
735
  // src/ui/ProviderMenu.tsx
342
736
  import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
343
737
  import { useState as useState4 } from "react";
344
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
738
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
345
739
  function ProviderMenu({ entries, onPick }) {
346
740
  const rows = [...entries.map((e) => e.id), "custom"];
347
741
  const [index, setIndex] = useState4(0);
@@ -351,9 +745,9 @@ function ProviderMenu({ entries, onPick }) {
351
745
  else if (key.return) onPick(rows[index]);
352
746
  else if (key.escape) onPick(void 0);
353
747
  });
354
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
355
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Provider (\u2191\u2193 enter, esc batal) \xB7 \u2713 siap \xB7 \u25CB butuh API key" }),
356
- entries.map((e, i) => /* @__PURE__ */ jsxs4(Text5, { color: i === index ? color("green") : void 0, children: [
748
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
749
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: t("Provider (\u2191\u2193 enter, esc cancel) \xB7 \u2713 ready \xB7 \u25CB needs API key") }),
750
+ entries.map((e, i) => /* @__PURE__ */ jsxs5(Text5, { color: i === index ? color("green") : void 0, children: [
357
751
  `${i === index ? "\u203A" : " "} ${e.ready ? "\u2713" : "\u25CB"} ${e.name}`,
358
752
  /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` ${e.baseURL}` })
359
753
  ] }, e.id)),
@@ -376,8 +770,8 @@ function DiffView({ diff }) {
376
770
  }
377
771
 
378
772
  // src/ui/PermissionPrompt.tsx
379
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
380
- var VERB = { edit: "edit", write: "tulis", bash: "jalankan", fetch: "ambil" };
773
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
774
+ var VERB = { edit: "edit", write: "write", bash: "run", fetch: "fetch" };
381
775
  function PermissionPrompt({
382
776
  request,
383
777
  onAnswer
@@ -390,16 +784,16 @@ function PermissionPrompt({
390
784
  else if (k2 === "n" || key.escape) onAnswer("no");
391
785
  });
392
786
  const rules = request.sessionRules ?? [];
393
- const sessionLabel = !rules.length ? null : request.kind === "edit" ? "ya untuk semua edit di project ini, sesi ini" : `ya sesi ini untuk ${rules.join(", ")}`;
394
- return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", borderStyle: "round", borderColor: color("yellow"), paddingX: 1, children: [
395
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: `${request.agent ? `[${request.agent}] ` : ""}Izinkan ${VERB[request.tool] ?? request.tool} ${request.target}?` }),
787
+ const sessionLabel = !rules.length ? null : request.kind === "edit" ? t("yes for all edits in this project, this session") : t("yes this session for {rules}", { rules: rules.join(", ") });
788
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", borderStyle: "round", borderColor: color("yellow"), paddingX: 1, children: [
789
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: `${request.agent ? `[${request.agent}] ` : ""}${t("Allow {verb} {target}?", { verb: t(VERB[request.tool] ?? request.tool), target: request.target })}` }),
396
790
  request.preview ? /* @__PURE__ */ jsx7(DiffView, { diff: request.preview }) : null,
397
- /* @__PURE__ */ jsxs5(Text7, { children: [
398
- /* @__PURE__ */ jsx7(Text7, { color: color("green"), children: "[y] ya" }),
791
+ /* @__PURE__ */ jsxs6(Text7, { children: [
792
+ /* @__PURE__ */ jsx7(Text7, { color: color("green"), children: t("[y] yes") }),
399
793
  " ",
400
794
  sessionLabel ? /* @__PURE__ */ jsx7(Text7, { color: color("cyan"), children: `[a] ${sessionLabel} ` }) : null,
401
- /* @__PURE__ */ jsx7(Text7, { color: color("yellow"), children: "[s] ya semua " }),
402
- /* @__PURE__ */ jsx7(Text7, { color: color("red"), children: "[n] tidak" })
795
+ /* @__PURE__ */ jsx7(Text7, { color: color("yellow"), children: t("[s] yes to all ") }),
796
+ /* @__PURE__ */ jsx7(Text7, { color: color("red"), children: t("[n] no") })
403
797
  ] })
404
798
  ] });
405
799
  }
@@ -409,20 +803,31 @@ import { Box as Box8, Text as Text8, useInput as useInput6 } from "ink";
409
803
  import { useState as useState5 } from "react";
410
804
 
411
805
  // src/ui/complete.ts
412
- import fg from "fast-glob";
806
+ import { globSync } from "fs";
807
+ import { statSync } from "fs";
808
+ import { join } from "path";
413
809
  function completeFile(value, cwd) {
414
810
  const match = /(^|\s)@([^\s]*)$/.exec(value);
415
811
  if (!match) return value;
416
812
  const prefix = match[2];
417
813
  const escaped = prefix.replace(/([*?[\]{}()!])/g, "\\$1");
418
- const found = fg.sync(`${escaped}*`, { cwd, ignore: ignorePatterns(cwd), onlyFiles: false, markDirectories: true, dot: false }).sort();
814
+ const pattern = `${escaped}*`;
815
+ if (validateGlobPattern(pattern)) return value;
816
+ const found = globSync(pattern, { cwd, exclude: ignorePatterns(cwd) }).map((entry2) => {
817
+ const normalized = normalizeGlobPath(entry2);
818
+ try {
819
+ return statSync(join(cwd, entry2)).isDirectory() ? `${normalized}/` : normalized;
820
+ } catch {
821
+ return normalized;
822
+ }
823
+ }).sort();
419
824
  if (!found.length) return value;
420
825
  const pick = found[0];
421
826
  return `${value.slice(0, value.length - prefix.length)}${pick}${pick.endsWith("/") ? "" : " "}`;
422
827
  }
423
828
 
424
829
  // src/ui/PromptInput.tsx
425
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
830
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
426
831
  function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
427
832
  const [value, setValue] = useState5("");
428
833
  const [historyIndex, setHistoryIndex] = useState5(null);
@@ -491,18 +896,18 @@ function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
491
896
  edit((v) => completeFile(v, cwd));
492
897
  return;
493
898
  }
494
- if (key.ctrl || key.meta || key.escape || key.tab || key.leftArrow || key.rightArrow) return;
899
+ if (key.ctrl || key.meta || key.escape || key.tab || key.leftArrow || key.rightArrow || isFocusReport(input)) return;
495
900
  edit((v) => v + input.replace(/\r/g, "\n"));
496
901
  },
497
902
  { isActive: !disabled }
498
903
  );
499
- return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", children: [
500
- /* @__PURE__ */ jsx8(Box8, { borderStyle: "round", borderColor: color("gray"), paddingX: 1, children: /* @__PURE__ */ jsxs6(Text8, { children: [
904
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
905
+ /* @__PURE__ */ jsx8(Box8, { borderStyle: "round", borderColor: color("gray"), paddingX: 1, children: /* @__PURE__ */ jsxs7(Text8, { children: [
501
906
  /* @__PURE__ */ jsx8(Text8, { color: color("green"), children: "> " }),
502
907
  value,
503
908
  disabled ? "" : /* @__PURE__ */ jsx8(Text8, { inverse: true, children: " " })
504
909
  ] }) }),
505
- suggestions.map((s, i) => /* @__PURE__ */ jsx8(Text8, { color: i === highlighted ? color("green") : void 0, dimColor: i !== highlighted, children: `${i === highlighted ? "\u203A" : " "} /${s.name} ${s.description}` }, s.name))
910
+ suggestions.map((s, i) => /* @__PURE__ */ jsx8(Text8, { color: i === highlighted ? color("green") : void 0, dimColor: i !== highlighted, children: `${i === highlighted ? "\u203A" : " "} /${s.name} ${t(s.description)}` }, s.name))
506
911
  ] });
507
912
  }
508
913
 
@@ -514,11 +919,11 @@ var FRAMES = ["\xB7", "\u2722", "\u2733", "\u2736", "\u273B", "\u273D"];
514
919
  function Spinner({ label, startedAt }) {
515
920
  const [tick, setTick] = useState6(0);
516
921
  useEffect(() => {
517
- const id = setInterval(() => setTick((t) => t + 1), 120);
922
+ const id = setInterval(() => setTick((t2) => t2 + 1), 120);
518
923
  return () => clearInterval(id);
519
924
  }, []);
520
925
  const seconds = Math.floor((Date.now() - startedAt) / 1e3);
521
- return /* @__PURE__ */ jsx9(Text9, { color: color(ACCENT), children: `${FRAMES[tick % FRAMES.length]} ${label}\u2026 (${seconds} dtk \xB7 esc batal)` });
926
+ return /* @__PURE__ */ jsx9(Text9, { color: color(ACCENT), children: `${FRAMES[tick % FRAMES.length]} ${t("{label}\u2026 ({seconds}s \xB7 esc to cancel)", { label, seconds })}` });
522
927
  }
523
928
 
524
929
  // src/ui/StatusBar.tsx
@@ -533,15 +938,15 @@ var LABEL = {
533
938
  function formatTokens(n) {
534
939
  return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
535
940
  }
536
- function StatusBar({ mode, tokens, busy, model }) {
941
+ function StatusBar({ mode, tokens, busy, model, reasoning = "auto" }) {
537
942
  const danger = mode === "allowAll";
538
- return /* @__PURE__ */ jsx10(Text10, { color: danger ? color("red") : void 0, dimColor: !danger, children: ` ${LABEL[mode]}${model ? ` \xB7 ${model}` : ""} \xB7 shift+tab ganti mode \xB7 ${formatTokens(tokens)} token \xB7 ${busy ? "esc batal" : "/ perintah"}` });
943
+ return /* @__PURE__ */ jsx10(Text10, { color: danger ? color("red") : void 0, dimColor: !danger, children: ` ${LABEL[mode]}${model ? ` \xB7 ${model}` : ""} \xB7 reasoning: ${reasoning} \xB7 ${t("shift+tab switch mode")} \xB7 ${t("{n} tokens", { n: formatTokens(tokens) })} \xB7 ${busy ? t("esc to cancel") : t("/ for commands")}` });
539
944
  }
540
945
 
541
946
  // src/ui/McpMenu.tsx
542
947
  import { Box as Box9, Text as Text11, useInput as useInput7 } from "ink";
543
948
  import { useState as useState7 } from "react";
544
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
949
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
545
950
  var MARK = { ready: "\u2713", error: "\u2717", starting: "\u2026", off: "\u25CB" };
546
951
  function McpMenu({ items, onPick }) {
547
952
  const [index, setIndex] = useState7(0);
@@ -551,9 +956,9 @@ function McpMenu({ items, onPick }) {
551
956
  else if (key.return) onPick(items[index]?.name);
552
957
  else if (key.escape) onPick(void 0);
553
958
  });
554
- return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
555
- /* @__PURE__ */ jsx11(Text11, { bold: true, children: "Server MCP (enter: pasang / hapus \xB7 esc tutup) \xB7 \u2713 aktif \u25CB belum \u2717 error" }),
556
- items.map((item, i) => /* @__PURE__ */ jsxs7(Text11, { color: i === index ? color("green") : void 0, children: [
959
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
960
+ /* @__PURE__ */ jsx11(Text11, { bold: true, children: t("MCP servers (enter: install / remove \xB7 esc close) \xB7 \u2713 active \u25CB not installed \u2717 error") }),
961
+ items.map((item, i) => /* @__PURE__ */ jsxs8(Text11, { color: i === index ? color("green") : void 0, children: [
557
962
  `${i === index ? "\u203A" : " "} ${MARK[item.status]} ${item.name.padEnd(12)}`,
558
963
  /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: item.status === "error" ? ` ${item.error ?? ""}` : ` ${item.description}` })
559
964
  ] }, item.name))
@@ -562,7 +967,7 @@ function McpMenu({ items, onPick }) {
562
967
 
563
968
  // src/ui/PlanApproval.tsx
564
969
  import { Box as Box10, Text as Text12, useInput as useInput8 } from "ink";
565
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
970
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
566
971
  function PlanApproval({ plan, onAnswer }) {
567
972
  useInput8((input, key) => {
568
973
  const k2 = input.toLowerCase();
@@ -570,15 +975,15 @@ function PlanApproval({ plan, onAnswer }) {
570
975
  else if (k2 === "y") onAnswer("default");
571
976
  else if (k2 === "n" || key.escape) onAnswer("no");
572
977
  });
573
- return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
574
- /* @__PURE__ */ jsx12(Text12, { bold: true, children: "Rencana dari agent" }),
575
- /* @__PURE__ */ jsx12(Markdown, { text: plan }),
576
- /* @__PURE__ */ jsxs8(Text12, { children: [
577
- /* @__PURE__ */ jsx12(Text12, { color: color("green"), children: "[a] ya, edit otomatis" }),
978
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: [
979
+ /* @__PURE__ */ jsx12(Text12, { bold: true, children: t("Plan from the agent") }),
980
+ /* @__PURE__ */ jsx12(Markdown, { text: plan, indent: 5 }),
981
+ /* @__PURE__ */ jsxs9(Text12, { children: [
982
+ /* @__PURE__ */ jsx12(Text12, { color: color("green"), children: t("[a] yes, auto-edit") }),
578
983
  " ",
579
- /* @__PURE__ */ jsx12(Text12, { color: color("cyan"), children: "[y] ya, tanya tiap langkah" }),
984
+ /* @__PURE__ */ jsx12(Text12, { color: color("cyan"), children: t("[y] yes, ask at each step") }),
580
985
  " ",
581
- /* @__PURE__ */ jsx12(Text12, { color: color("red"), children: "[n] belum, perbaiki dulu" })
986
+ /* @__PURE__ */ jsx12(Text12, { color: color("red"), children: t("[n] not yet, revise first") })
582
987
  ] })
583
988
  ] });
584
989
  }
@@ -588,34 +993,34 @@ import { Box as Box11, Text as Text13 } from "ink";
588
993
  import { jsx as jsx13 } from "react/jsx-runtime";
589
994
  var MARK2 = { completed: "\u2611", in_progress: "\u25FC", pending: "\u2610" };
590
995
  function TodoList({ items }) {
591
- if (!items.length || items.every((t) => t.status === "completed")) return null;
592
- return /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: items.map((t, i) => /* @__PURE__ */ jsx13(Text13, { dimColor: t.status === "completed", color: t.status === "in_progress" ? color("yellow") : void 0, children: `${MARK2[t.status]} ${t.content}` }, i)) });
996
+ if (!items.length || items.every((t2) => t2.status === "completed")) return null;
997
+ return /* @__PURE__ */ jsx13(Box11, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: items.map((t2, i) => /* @__PURE__ */ jsx13(Text13, { dimColor: t2.status === "completed", color: t2.status === "in_progress" ? color("yellow") : void 0, children: `${MARK2[t2.status]} ${t2.content}` }, i)) });
593
998
  }
594
999
 
595
1000
  // src/ui/ToolBlock.tsx
596
1001
  import { Box as Box12, Text as Text14 } from "ink";
597
- import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1002
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
598
1003
  var PREVIEW_LINES = 5;
599
1004
  function ToolBlock({ tool, target, output, display, isError, done, expanded, sub }) {
600
1005
  const isTask = tool === "task";
601
1006
  const isDiff = !!display && /^\s*\d+ [+\- ] /.test(display);
602
1007
  const lines = (output ?? "").split("\n");
603
1008
  const shown = expanded ? lines : lines.slice(0, PREVIEW_LINES);
604
- return /* @__PURE__ */ jsxs9(Box12, { flexDirection: "column", marginLeft: 2, children: [
605
- /* @__PURE__ */ jsxs9(Text14, { children: [
1009
+ return /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", marginLeft: 2, children: [
1010
+ /* @__PURE__ */ jsxs10(Text14, { children: [
606
1011
  /* @__PURE__ */ jsx14(Text14, { color: isError ? color("red") : done ? color("green") : color("yellow"), children: "\u23BF " }),
607
1012
  /* @__PURE__ */ jsx14(Text14, { bold: true, children: toolLabel(tool) }),
608
1013
  /* @__PURE__ */ jsx14(Text14, { children: ` ${target}` }),
609
1014
  display && !isDiff && !isTask ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: ` (${display})` }) : null
610
1015
  ] }),
611
- sub?.length ? /* @__PURE__ */ jsxs9(Box12, { flexDirection: "column", marginLeft: 3, children: [
1016
+ sub?.length ? /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", marginLeft: 3, children: [
612
1017
  sub.slice(-6).map((s) => /* @__PURE__ */ jsx14(Text14, { dimColor: s.done && !s.isError, color: s.isError ? color("red") : void 0, children: `\u23BF ${toolLabel(s.tool)} ${s.target}` }, s.id)),
613
- sub.length > 6 ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: `\u2026 ${sub.length - 6} langkah sebelumnya` }) : null
1018
+ sub.length > 6 ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: t("\u2026 {n} earlier steps", { n: sub.length - 6 }) }) : null
614
1019
  ] }) : null,
615
- isTask && done ? /* @__PURE__ */ jsx14(Box12, { marginLeft: 3, children: /* @__PURE__ */ jsx14(Text14, { color: isError ? color("red") : color("green"), children: `${isError ? "\u2717 gagal" : "\u2713 selesai"}${display ? ` \xB7 ${display}` : ""}` }) }) : null,
616
- isDiff ? /* @__PURE__ */ jsx14(Box12, { marginLeft: 3, children: /* @__PURE__ */ jsx14(DiffView, { diff: display }) }) : output && (isError || tool === "bash") && !(isTask && !isError) ? /* @__PURE__ */ jsxs9(Box12, { flexDirection: "column", marginLeft: 3, children: [
1020
+ isTask && done ? /* @__PURE__ */ jsx14(Box12, { marginLeft: 3, children: /* @__PURE__ */ jsx14(Text14, { color: isError ? color("red") : color("green"), children: `${isError ? t("\u2717 failed") : t("\u2713 done")}${display ? ` \xB7 ${display}` : ""}` }) }) : null,
1021
+ isDiff ? /* @__PURE__ */ jsx14(Box12, { marginLeft: 3, children: /* @__PURE__ */ jsx14(DiffView, { diff: display }) }) : output && (isError || tool === "bash") && !(isTask && !isError) ? /* @__PURE__ */ jsxs10(Box12, { flexDirection: "column", marginLeft: 3, children: [
617
1022
  shown.map((line, i) => /* @__PURE__ */ jsx14(Text14, { dimColor: !isError, color: isError ? color("red") : void 0, children: line }, i)),
618
- lines.length > shown.length ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: `\u2026 ${lines.length - shown.length} baris lagi (ctrl+o)` }) : null
1023
+ lines.length > shown.length ? /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: t("\u2026 {n} more lines (ctrl+o)", { n: lines.length - shown.length }) }) : null
619
1024
  ] }) : null
620
1025
  ] });
621
1026
  }
@@ -624,73 +1029,105 @@ function ToolBlock({ tool, target, output, display, isError, done, expanded, sub
624
1029
  var nextId = 0;
625
1030
  var entry = (e) => ({ ...e, id: nextId++ });
626
1031
  var NOTICES = {
627
- compacted: ["Percakapan diringkas agar muat di konteks model.", "info"],
628
- stepLimit: ['Batas 50 langkah tercapai. Ketik "lanjut" untuk meneruskan.', "warn"],
629
- aborted: ["Dibatalkan.", "warn"]
1032
+ compacted: ["Conversation summarized to fit the model context.", "info"],
1033
+ stepLimit: ['Step limit of 50 reached. Type "continue" to keep going.', "warn"],
1034
+ aborted: ["Cancelled.", "warn"]
630
1035
  };
631
- function endTurn(t) {
632
- return { done: [...t.done, ...t.live], live: [] };
1036
+ var isEmptyCont = (e) => e.kind === "assistant" && e.cont && !e.text;
1037
+ function endTurn(t2) {
1038
+ return { done: [...t2.done, ...t2.live.filter((e) => !isEmptyCont(e))], live: [] };
633
1039
  }
634
- function applyEvent(t, event) {
1040
+ var TOOL_MARKUP = /<\/?tool_call>|<\/?arg_(?:key|value)>|Tool call list|^\s*\d+\.\s*Tool:|`\s*=\s*`/;
1041
+ function finishedCut(text) {
1042
+ let inFence = false;
1043
+ let pos = 0;
1044
+ let cut = -1;
1045
+ const lines = text.split("\n");
1046
+ for (const line of lines.slice(0, -1)) {
1047
+ if (TOOL_MARKUP.test(line)) break;
1048
+ if (line.trimStart().startsWith("```")) inFence = !inFence;
1049
+ const tableRow = !inFence && !line.trimStart().startsWith("```") && line.includes("|") && line.trim() !== "";
1050
+ pos += line.length + 1;
1051
+ if (!inFence && !tableRow) cut = pos - 1;
1052
+ }
1053
+ return cut;
1054
+ }
1055
+ function flushLines(t2) {
1056
+ const last = t2.live.at(-1);
1057
+ if (last?.kind !== "assistant") return t2;
1058
+ const cut = finishedCut(last.text);
1059
+ if (cut < 0) return t2;
1060
+ const chunk = last.text.slice(0, cut);
1061
+ if (!last.cont && !chunk.trim()) return t2;
1062
+ const rest = entry({ kind: "assistant", text: last.text.slice(cut + 1), cont: true, flushed: (last.flushed ?? 0) + cut + 1 });
1063
+ return { done: [...t2.done, ...t2.live.slice(0, -1).filter((e) => !isEmptyCont(e)), { ...last, text: chunk || " " }], live: [rest] };
1064
+ }
1065
+ function applyEvent(t2, event) {
635
1066
  switch (event.type) {
636
1067
  case "text": {
637
- const last = t.live.at(-1);
638
- if (last?.kind === "assistant") return { ...t, live: [...t.live.slice(0, -1), { ...last, text: last.text + event.delta }] };
639
- return { ...t, live: [...t.live, entry({ kind: "assistant", text: event.delta })] };
1068
+ const last = t2.live.at(-1);
1069
+ if (last?.kind === "assistant") return flushLines({ ...t2, live: [...t2.live.slice(0, -1), { ...last, text: last.text + event.delta }] });
1070
+ return flushLines({ ...t2, live: [...t2.live, entry({ kind: "assistant", text: event.delta })] });
1071
+ }
1072
+ case "thinking": {
1073
+ const last = t2.live.at(-1);
1074
+ if (last?.kind === "thinking") return { ...t2, live: [...t2.live.slice(0, -1), { ...last, text: last.text + event.delta }] };
1075
+ return { ...t2, live: [...t2.live, entry({ kind: "thinking", text: event.delta })] };
640
1076
  }
641
1077
  case "textReplace": {
642
- const last = t.live.at(-1);
643
- if (last?.kind !== "assistant") return t;
644
- return { ...t, live: [...t.live.slice(0, -1), { ...last, text: event.text }] };
1078
+ const last = t2.live.at(-1);
1079
+ if (last?.kind !== "assistant") return t2;
1080
+ return { ...t2, live: [...t2.live.slice(0, -1), { ...last, text: event.text.slice(last.flushed ?? 0) }] };
645
1081
  }
646
1082
  case "toolStart": {
647
- const running = t.live.some((e) => e.kind === "tool" && !e.done);
1083
+ const running = t2.live.some((e) => e.kind === "tool" && !e.done);
648
1084
  const tool = entry({ kind: "tool", callId: event.id, tool: event.tool, target: event.target, done: false });
649
- return running ? { ...t, live: [...t.live, tool] } : { done: [...t.done, ...t.live], live: [tool] };
1085
+ return running ? { ...t2, live: [...t2.live, tool] } : { done: [...t2.done, ...t2.live.filter((e) => !isEmptyCont(e))], live: [tool] };
650
1086
  }
651
1087
  case "toolEnd": {
652
- const live = t.live.map(
1088
+ const live = t2.live.map(
653
1089
  (e) => e.kind === "tool" && e.callId === event.id ? { ...e, output: event.output, display: event.display, isError: event.isError, done: true } : e
654
1090
  );
655
1091
  const firstRunning = live.findIndex((e) => e.kind === "tool" && !e.done);
656
1092
  const cut = firstRunning === -1 ? live.length : firstRunning;
657
- return { done: [...t.done, ...live.slice(0, cut)], live: live.slice(cut) };
1093
+ return { done: [...t2.done, ...live.slice(0, cut)], live: live.slice(cut) };
658
1094
  }
659
1095
  case "subagent": {
660
1096
  const inner = event.event;
661
- if (inner.type !== "toolStart" && inner.type !== "toolEnd") return t;
1097
+ if (inner.type !== "toolStart" && inner.type !== "toolEnd") return t2;
662
1098
  const update = (e) => {
663
1099
  if (e.kind !== "tool" || e.callId !== event.parentId) return e;
664
1100
  const sub = e.sub ?? [];
665
1101
  if (inner.type === "toolStart") return { ...e, sub: [...sub, { id: inner.id, tool: inner.tool, target: inner.target, done: false }] };
666
1102
  return { ...e, sub: sub.map((s) => s.id === inner.id ? { ...s, done: true, isError: inner.isError } : s) };
667
1103
  };
668
- return { ...t, live: t.live.map(update) };
1104
+ return { ...t2, live: t2.live.map(update) };
669
1105
  }
670
1106
  case "error":
671
- return { ...t, live: [...t.live, entry({ kind: "notice", text: `Error: ${event.message}`, tone: "error" })] };
1107
+ return { ...t2, live: [...t2.live, entry({ kind: "notice", text: `Error: ${event.message}`, tone: "error" })] };
672
1108
  default: {
673
1109
  const notice = NOTICES[event.type];
674
- return notice ? { ...t, live: [...t.live, entry({ kind: "notice", text: notice[0], tone: notice[1] })] } : t;
1110
+ return notice ? { ...t2, live: [...t2.live, entry({ kind: "notice", text: t(notice[0]), tone: notice[1] })] } : t2;
675
1111
  }
676
1112
  }
677
1113
  }
678
1114
 
679
1115
  // src/ui/App.tsx
680
- import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
1116
+ import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
681
1117
  function helpText(runtime) {
682
1118
  const custom = runtime.commands.filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name));
683
1119
  return [
684
- ...SLASH_COMMANDS.map((c) => `/${c.name.padEnd(12)} ${c.description}`),
685
- ...custom.length ? ["", "Perintah custom:", ...custom.map((c) => `/${c.name.padEnd(8)} ${c.description ?? ""}`)] : [],
686
- ...runtime.skills.length ? ["", "Skill:", ...runtime.skills.map((s) => `/${s.name.padEnd(8)} ${s.description.slice(0, 60)}`)] : [],
1120
+ ...SLASH_COMMANDS.map((c) => `/${c.name.padEnd(12)} ${t(c.description)}`),
1121
+ ...custom.length ? ["", t("Custom commands:"), ...custom.map((c) => `/${c.name.padEnd(8)} ${c.description ?? ""}`)] : [],
1122
+ ...runtime.skills.length ? ["", t("Skills:"), ...runtime.skills.map((s) => `/${s.name.padEnd(8)} ${s.description.slice(0, 60)}`)] : [],
687
1123
  "",
688
- "shift+tab ganti mode izin esc batalkan giliran",
689
- "ctrl+o output alat terakhir lengkap \\ + enter baris baru",
690
- "@file + tab lengkapi nama file \u2191\u2193 riwayat input"
1124
+ t("shift+tab switch permission mode esc cancel the turn"),
1125
+ t("ctrl+o full output of the last tool \\ + enter new line"),
1126
+ t("ctrl+t show/hide the model\u2019s thinking"),
1127
+ t("@file + tab complete file names \u2191\u2193 input history")
691
1128
  ].join("\n");
692
1129
  }
693
- var INIT_PROMPT = "Pelajari project ini (struktur folder, file package/build, perintah test dan lint, konvensi kode yang terlihat). Lalu buat AGENTS.md di root project, atau perbarui bila sudah ada, berisi: cara build, test, dan lint; struktur singkat; konvensi penting. Ringkas dan faktual, hanya yang benar-benar ada di project.";
1130
+ var INIT_PROMPT = "Study this project (folder structure, package/build files, test and lint commands, visible code conventions). Then create AGENTS.md in the project root, or update it if it already exists, covering: how to build, test, and lint; a short structure overview; important conventions. Keep it concise and factual, only what actually exists in the project.";
694
1131
  var labelFor = (runtime, ref) => `${ref.slice(ref.indexOf("/") + 1)} \xB7 ${runtime.providerLabel()}`;
695
1132
  function App({ runtime, initialPrompt, version }) {
696
1133
  const { exit } = useApp();
@@ -698,6 +1135,7 @@ function App({ runtime, initialPrompt, version }) {
698
1135
  const [busy, setBusy] = useState8(false);
699
1136
  const [startedAt, setStartedAt] = useState8(0);
700
1137
  const [mode, setMode] = useState8(runtime.agent.permissions.mode);
1138
+ const [reasoning, setReasoning] = useState8(runtime.reasoning);
701
1139
  const [tokens, setTokens] = useState8(runtime.agent.totalUsage.inputTokens + runtime.agent.totalUsage.outputTokens);
702
1140
  const [asks, setAsks] = useState8([]);
703
1141
  const pending = asks[0] ?? null;
@@ -713,15 +1151,17 @@ function App({ runtime, initialPrompt, version }) {
713
1151
  const [mcpMenu, setMcpMenu] = useState8(null);
714
1152
  const [listPicker, setListPicker] = useState8(null);
715
1153
  const [planAsk, setPlanAsk] = useState8(null);
1154
+ const [queued, setQueued] = useState8([]);
1155
+ const [showThinking, setShowThinking] = useState8(false);
716
1156
  useEffect2(() => runtime.todos.subscribe(setTodos), [runtime]);
717
1157
  const extraCommands = [
718
- ...runtime.commands.filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name.toLowerCase())).map((c) => ({ name: c.name.toLowerCase(), description: c.description ?? "perintah custom" })),
719
- ...runtime.skills.filter((s) => !runtime.commands.some((c) => c.name.toLowerCase() === s.name.toLowerCase())).filter((s) => !SLASH_COMMANDS.some((b) => b.name === s.name.toLowerCase())).map((s) => ({ name: s.name, description: `skill \xB7 ${s.description.slice(0, 50)}` }))
1158
+ ...runtime.commands.filter((c) => !SLASH_COMMANDS.some((b) => b.name === c.name.toLowerCase())).map((c) => ({ name: c.name.toLowerCase(), description: c.description ?? t("custom command") })),
1159
+ ...runtime.skills.filter((s) => !runtime.commands.some((c) => c.name.toLowerCase() === s.name.toLowerCase())).filter((s) => !SLASH_COMMANDS.some((b) => b.name === s.name.toLowerCase())).map((s) => ({ name: s.name, description: t("skill \xB7 {description}", { description: s.description.slice(0, 50) }) }))
720
1160
  ];
721
1161
  const lastTool = useRef(null);
722
1162
  const toolTargets = useRef(/* @__PURE__ */ new Map());
723
1163
  const notice = useCallback((text, tone = "info") => {
724
- setTranscript((t) => ({ ...t, done: [...t.done, entry({ kind: "notice", text, tone })] }));
1164
+ setTranscript((t2) => ({ ...t2, done: [...t2.done, entry({ kind: "notice", text, tone })] }));
725
1165
  }, []);
726
1166
  const onEvent = useCallback((event) => {
727
1167
  if (event.type === "toolStart") toolTargets.current.set(event.id, event.target);
@@ -729,7 +1169,7 @@ function App({ runtime, initialPrompt, version }) {
729
1169
  lastTool.current = { tool: event.tool, target: toolTargets.current.get(event.id) ?? "", output: event.output };
730
1170
  }
731
1171
  if (event.type === "usage") setTokens(event.inputTokens + event.outputTokens);
732
- setTranscript((t) => applyEvent(t, event));
1172
+ setTranscript((t2) => applyEvent(t2, event));
733
1173
  }, []);
734
1174
  useEffect2(() => {
735
1175
  runtime.agent.onEvent = onEvent;
@@ -738,7 +1178,7 @@ function App({ runtime, initialPrompt, version }) {
738
1178
  }, [runtime, onEvent]);
739
1179
  const runTurn = useCallback(
740
1180
  async (text) => {
741
- setTranscript((t) => ({ ...t, done: [...t.done, entry({ kind: "user", text })] }));
1181
+ setTranscript((t2) => ({ ...t2, done: [...t2.done, entry({ kind: "user", text })] }));
742
1182
  setBusy(true);
743
1183
  setStartedAt(Date.now());
744
1184
  controller.current = new AbortController();
@@ -761,11 +1201,11 @@ function App({ runtime, initialPrompt, version }) {
761
1201
  case "clear":
762
1202
  runtime.agent.clear();
763
1203
  setTokens(0);
764
- notice("Percakapan dikosongkan.");
1204
+ notice(t("Conversation cleared."));
765
1205
  return;
766
1206
  case "cost": {
767
1207
  const u = runtime.agent.totalUsage;
768
- notice(`${u.inputTokens.toLocaleString("id-ID")} token masuk \xB7 ${u.outputTokens.toLocaleString("id-ID")} token keluar`);
1208
+ notice(t("{input} tokens in \xB7 {output} tokens out", { input: u.inputTokens.toLocaleString(locale()), output: u.outputTokens.toLocaleString(locale()) }));
769
1209
  return;
770
1210
  }
771
1211
  case "compact": {
@@ -775,19 +1215,54 @@ function App({ runtime, initialPrompt, version }) {
775
1215
  try {
776
1216
  await runtime.agent.compact(controller.current.signal);
777
1217
  } catch (error) {
778
- notice(`Gagal meringkas: ${error.message}`, "error");
1218
+ notice(t("Could not summarize: {error}", { error: error.message }), "error");
779
1219
  }
780
1220
  controller.current = null;
781
1221
  setBusy(false);
782
1222
  return;
783
1223
  }
1224
+ case "reasoning": {
1225
+ const apply = (value) => {
1226
+ try {
1227
+ const level = parseReasoningLevel(value, "/reasoning");
1228
+ const providerId2 = runtime.modelRef.slice(0, runtime.modelRef.indexOf("/"));
1229
+ assertReasoningSupported(providerId2, level);
1230
+ runtime.setReasoning(level);
1231
+ writeGlobalConfig({ reasoning: level }, runtime.env);
1232
+ setReasoning(level);
1233
+ notice(t("Reasoning: {level} (saved as default)", { level }));
1234
+ } catch (error) {
1235
+ notice(error.message, "error");
1236
+ }
1237
+ };
1238
+ if (args) {
1239
+ apply(args);
1240
+ return;
1241
+ }
1242
+ const providerId = runtime.modelRef.slice(0, runtime.modelRef.indexOf("/"));
1243
+ const supported = supportedReasoningLevels(providerId);
1244
+ setListPicker({
1245
+ title: "Reasoning",
1246
+ items: REASONING_LEVELS.map((level) => ({
1247
+ id: level,
1248
+ label: level === "auto" ? "Auto (model default)" : level.charAt(0).toUpperCase() + level.slice(1),
1249
+ disabled: !supported.includes(level),
1250
+ hint: supported.includes(level) ? void 0 : t("not supported by this provider")
1251
+ })),
1252
+ onPick: (id) => {
1253
+ setListPicker(null);
1254
+ if (id) apply(id);
1255
+ }
1256
+ });
1257
+ return;
1258
+ }
784
1259
  case "model": {
785
1260
  if (args) {
786
1261
  try {
787
1262
  runtime.setModel(args);
788
1263
  writeGlobalConfig({ model: args }, runtime.env);
789
1264
  setModelLabel(labelFor(runtime, args));
790
- notice(`Model: ${args} (tersimpan sebagai default)`);
1265
+ notice(t("Model: {model} (saved as default)", { model: args }));
791
1266
  } catch (error) {
792
1267
  notice(error.message, "error");
793
1268
  }
@@ -796,7 +1271,7 @@ function App({ runtime, initialPrompt, version }) {
796
1271
  try {
797
1272
  setPicker(await runtime.listModels());
798
1273
  } catch (error) {
799
- notice(`Tidak bisa mengambil daftar model: ${error.message}`, "error");
1274
+ notice(t("Cannot fetch the model list: {error}", { error: error.message }), "error");
800
1275
  }
801
1276
  return;
802
1277
  }
@@ -804,7 +1279,7 @@ function App({ runtime, initialPrompt, version }) {
804
1279
  const installed = readMcpFile(globalMcpPath(runtime.home));
805
1280
  const states = runtime.mcp.states();
806
1281
  const items = [
807
- ...CATALOG.map((c) => ({ name: c.name, description: c.description, installed: !!installed[c.name] })),
1282
+ ...CATALOG.map((c) => ({ name: c.name, description: t(c.description), installed: !!installed[c.name] })),
808
1283
  ...Object.keys(installed).filter((n) => !CATALOG.some((c) => c.name === n)).map((n) => ({ name: n, description: "(custom)", installed: true }))
809
1284
  ].map((i) => {
810
1285
  const s = states.find((st) => st.name === i.name);
@@ -816,7 +1291,7 @@ function App({ runtime, initialPrompt, version }) {
816
1291
  case "new":
817
1292
  runtime.newSession();
818
1293
  setTokens(0);
819
- notice("Sesi baru dimulai. Sesi sebelumnya tetap tersimpan (/resume).");
1294
+ notice(t("New session started. The previous session stays saved (/resume)."));
820
1295
  return;
821
1296
  case "session":
822
1297
  notice(
@@ -832,15 +1307,15 @@ function App({ runtime, initialPrompt, version }) {
832
1307
  case "resume": {
833
1308
  const others = Session.list(runtime.home, runtime.cwd).filter((s) => s.session.file !== runtime.session.file);
834
1309
  if (!others.length) {
835
- notice("Belum ada sesi lain di folder ini.");
1310
+ notice(t("No other sessions in this folder yet."));
836
1311
  return;
837
1312
  }
838
1313
  setListPicker({
839
- title: "Lanjutkan sesi",
1314
+ title: t("Continue a session"),
840
1315
  items: others.map((s) => ({
841
1316
  id: s.session.file,
842
- label: `${s.mtime.toLocaleString("id-ID")} ${s.preview || "(kosong)"}`,
843
- hint: `${s.session.load().length} pesan`
1317
+ label: `${s.mtime.toLocaleString(locale())} ${s.preview || t("(empty)")}`,
1318
+ hint: t("{n} messages", { n: s.session.load().length })
844
1319
  })),
845
1320
  onPick: (id) => {
846
1321
  setListPicker(null);
@@ -848,12 +1323,23 @@ function App({ runtime, initialPrompt, version }) {
848
1323
  if (!chosen) return;
849
1324
  runtime.resume(chosen.session);
850
1325
  setTokens(0);
851
- const tail = runtime.agent.messages.filter((m) => (m.role === "user" || m.role === "assistant") && m.content).slice(-3);
852
- notice(
853
- [`Melanjutkan sesi (${runtime.agent.messages.length} pesan):`, ...tail.map((m) => `${m.role === "user" ? ">" : "\u25CF"} ${String(m.content).slice(0, 200)}`)].join(
854
- "\n"
855
- )
1326
+ const chat = runtime.agent.messages.filter(
1327
+ (m) => (m.role === "user" || m.role === "assistant") && typeof m.content === "string" && !!m.content.trim()
856
1328
  );
1329
+ setTranscript({
1330
+ done: [
1331
+ entry({ kind: "header" }),
1332
+ entry({
1333
+ kind: "notice",
1334
+ text: t("Resuming the session ({messages} messages saved \xB7 {chat} user/assistant chats shown).", { messages: runtime.agent.messages.length, chat: chat.length }),
1335
+ tone: "info"
1336
+ }),
1337
+ ...chat.map(
1338
+ (m) => entry(m.role === "user" ? { kind: "user", text: m.content } : { kind: "assistant", text: m.content })
1339
+ )
1340
+ ],
1341
+ live: []
1342
+ });
857
1343
  }
858
1344
  });
859
1345
  return;
@@ -879,7 +1365,7 @@ function App({ runtime, initialPrompt, version }) {
879
1365
  notice(skillsText(runtime.skills, runtime.commands));
880
1366
  return;
881
1367
  case "doctor": {
882
- notice("Memeriksa\u2026");
1368
+ notice(t("Checking\u2026"));
883
1369
  const c = runtime.config;
884
1370
  notice(
885
1371
  await doctorText(
@@ -900,20 +1386,20 @@ function App({ runtime, initialPrompt, version }) {
900
1386
  case "permissions": {
901
1387
  const rules = runtime.agent.permissions.list();
902
1388
  if (!rules.length) {
903
- notice("Belum ada izin tersimpan. Izin dari jawaban [a] dan dari config akan muncul di sini.");
1389
+ notice(t("No saved permissions yet. Permissions from [a] answers and from the config will appear here."));
904
1390
  return;
905
1391
  }
906
1392
  setListPicker({
907
- title: "Izin aktif (enter: cabut izin sesi)",
908
- items: rules.map((r) => ({ id: r.rule, label: r.rule, hint: r.source === "config" ? "config" : "sesi ini" })),
1393
+ title: t("Active permissions (enter: revoke a session permission)"),
1394
+ items: rules.map((r) => ({ id: r.rule, label: r.rule, hint: r.source === "config" ? "config" : t("this session") })),
909
1395
  onPick: (id) => {
910
1396
  setListPicker(null);
911
1397
  const rule = rules.find((r) => r.rule === id);
912
1398
  if (!rule) return;
913
- if (rule.source === "config") notice(`${rule.rule} berasal dari config/flag; hapus dari ~/.bccli/config.json untuk mencabutnya.`, "warn");
1399
+ if (rule.source === "config") notice(t("{rule} comes from the config/flag; remove it from ~/.bccli/config.json to revoke it.", { rule: rule.rule }), "warn");
914
1400
  else {
915
1401
  runtime.agent.permissions.revoke(rule.rule);
916
- notice(`Izin ${rule.rule} dicabut.`);
1402
+ notice(t("Permission {rule} revoked.", { rule: rule.rule }));
917
1403
  }
918
1404
  }
919
1405
  });
@@ -922,32 +1408,51 @@ function App({ runtime, initialPrompt, version }) {
922
1408
  case "memory": {
923
1409
  if (!args) {
924
1410
  const files = instructionFiles(runtime.cwd, runtime.home);
925
- notice(files.length ? files.map((f) => `${f.path} (${f.lines} baris)`).join("\n") : "Belum ada AGENTS.md / BCCLI.md. Tambah dengan /memory <teks>.");
1411
+ notice(files.length ? files.map((f) => t("{path} ({lines} lines)", { path: f.path, lines: f.lines })).join("\n") : t("No AGENTS.md / BCCLI.md yet. Add one with /memory <text>."));
926
1412
  return;
927
1413
  }
928
1414
  const { global, text } = parseMemoryArgs(args);
929
1415
  if (!text) {
930
- notice("Tulis teksnya: /memory global <teks>.", "warn");
1416
+ notice(t("Write the text: /memory global <text>."), "warn");
931
1417
  return;
932
1418
  }
933
- const file = global ? join(runtime.home, "BCCLI.md") : join(runtime.cwd, "AGENTS.md");
1419
+ const file = global ? join2(runtime.home, "BCCLI.md") : join2(runtime.cwd, "AGENTS.md");
934
1420
  try {
935
1421
  mkdirSync(dirname(file), { recursive: true });
936
1422
  appendMemory(file, text);
937
1423
  } catch (error) {
938
- notice(`Gagal menulis ${file}: ${error.message}`, "error");
1424
+ notice(t("Could not write {file}: {error}", { file, error: error.message }), "error");
939
1425
  return;
940
1426
  }
941
1427
  runtime.rebuildSystemPrompt();
942
- notice(`Ditambahkan ke ${file}.`);
1428
+ notice(t("Added to {file}.", { file }));
943
1429
  return;
944
1430
  }
945
1431
  case "init":
946
1432
  void runTurn(INIT_PROMPT);
947
1433
  return;
1434
+ case "language": {
1435
+ if (!args) {
1436
+ notice(t("Language: {lang}. Change it with /language en or /language id.", { lang: getLanguage() }));
1437
+ return;
1438
+ }
1439
+ const wanted = parseLang(args);
1440
+ if (!wanted) {
1441
+ notice(t('Unknown language "{lang}". Use en or id.', { lang: args }), "warn");
1442
+ return;
1443
+ }
1444
+ setLanguage(wanted);
1445
+ try {
1446
+ writeGlobalConfig({ language: wanted }, runtime.env);
1447
+ } catch (error) {
1448
+ notice(t("Could not save the language: {error}", { error: error.message }), "error");
1449
+ }
1450
+ notice(t("Language set to {lang} (saved as default).", { lang: wanted }));
1451
+ return;
1452
+ }
948
1453
  case "login": {
949
1454
  const id = runtime.modelRef.slice(0, runtime.modelRef.indexOf("/"));
950
- const key = await ask(`API key untuk ${runtime.providerLabel()}`, true);
1455
+ const key = await ask(t("API key for {provider}", { provider: runtime.providerLabel() }), true);
951
1456
  if (!key) return;
952
1457
  const result = await runtime.addProviderKey(id, key);
953
1458
  if (!result.ok) {
@@ -957,10 +1462,10 @@ function App({ runtime, initialPrompt, version }) {
957
1462
  runtime.setModel(runtime.modelRef);
958
1463
  const envName = runtime.config.providers[id]?.apiKeyEnv;
959
1464
  if (envName && runtime.env[envName]) {
960
- notice(`Key ${runtime.providerLabel()} tersimpan, tapi ${envName} di environment lebih diutamakan dan tetap dipakai.`, "warn");
1465
+ notice(t("Key for {provider} saved, but {env} in the environment takes precedence and is still used.", { provider: runtime.providerLabel(), env: envName }), "warn");
961
1466
  return;
962
1467
  }
963
- notice(`Key ${runtime.providerLabel()} tersimpan \xB7 ${result.models} model.`);
1468
+ notice(t("Key for {provider} saved \xB7 {n} models.", { provider: runtime.providerLabel(), n: result.models }));
964
1469
  return;
965
1470
  }
966
1471
  case "logout": {
@@ -969,13 +1474,13 @@ function App({ runtime, initialPrompt, version }) {
969
1474
  const envName = runtime.config.providers[id]?.apiKeyEnv;
970
1475
  if (envName && runtime.env[envName]) {
971
1476
  notice(
972
- `${removed ? "Key tersimpan dihapus, tapi" : "Tidak ada key tersimpan;"} ${envName} masih ada di environment dan tetap dipakai.`,
1477
+ removed ? t("Stored key deleted, but {env} is still set in the environment and is still used.", { env: envName }) : t("No stored key; {env} is still set in the environment and is still used.", { env: envName }),
973
1478
  "warn"
974
1479
  );
975
1480
  return;
976
1481
  }
977
1482
  if (!removed) {
978
- notice(`Tidak ada key tersimpan untuk ${runtime.providerLabel()}.`);
1483
+ notice(t("No stored key for {provider}.", { provider: runtime.providerLabel() }));
979
1484
  return;
980
1485
  }
981
1486
  try {
@@ -986,7 +1491,7 @@ function App({ runtime, initialPrompt, version }) {
986
1491
  };
987
1492
  runtime.agent.provider = { chat: fail, listModels: fail };
988
1493
  }
989
- notice(`Key ${runtime.providerLabel()} dihapus. Pakai /login atau /provider untuk menambah lagi.`);
1494
+ notice(t("Key for {provider} deleted. Use /login or /provider to add one again.", { provider: runtime.providerLabel() }));
990
1495
  return;
991
1496
  }
992
1497
  case "diff":
@@ -995,17 +1500,17 @@ function App({ runtime, initialPrompt, version }) {
995
1500
  case "undo": {
996
1501
  const result = await runtime.checkpoints.undo();
997
1502
  if (!result) {
998
- notice("Tidak ada edit file yang bisa dibatalkan.");
1503
+ notice(t("No file edits to undo."));
999
1504
  return;
1000
1505
  }
1001
1506
  const rel = (p) => relative(runtime.cwd, p) || p;
1002
1507
  notice(
1003
1508
  [
1004
- result.failed.length ? `Gagal dikembalikan: ${result.failed.map(rel).join(", ")}` : "",
1005
- result.restored.length ? `Dikembalikan: ${result.restored.map(rel).join(", ")}` : "",
1006
- result.deleted.length ? `Dihapus (file baru): ${result.deleted.map(rel).join(", ")}` : "",
1007
- result.skipped.length ? `Terlalu besar untuk disimpan, tidak diubah: ${result.skipped.map(rel).join(", ")}` : "",
1008
- "Catatan: perubahan lewat perintah bash tidak bisa di-undo."
1509
+ result.failed.length ? t("Could not restore: {files}", { files: result.failed.map(rel).join(", ") }) : "",
1510
+ result.restored.length ? t("Restored: {files}", { files: result.restored.map(rel).join(", ") }) : "",
1511
+ result.deleted.length ? t("Deleted (new files): {files}", { files: result.deleted.map(rel).join(", ") }) : "",
1512
+ result.skipped.length ? t("Too large to back up, left unchanged: {files}", { files: result.skipped.map(rel).join(", ") }) : "",
1513
+ t("Note: changes made through bash commands cannot be undone.")
1009
1514
  ].filter(Boolean).join("\n")
1010
1515
  );
1011
1516
  return;
@@ -1013,33 +1518,33 @@ function App({ runtime, initialPrompt, version }) {
1013
1518
  case "copy": {
1014
1519
  const last = runtime.agent.messages.findLast((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim());
1015
1520
  if (!last || typeof last.content !== "string") {
1016
- notice("Belum ada jawaban untuk disalin.");
1521
+ notice(t("No answer to copy yet."));
1017
1522
  return;
1018
1523
  }
1019
1524
  process.stdout.write(osc52(last.content, !!process.env.TMUX));
1020
- const file = join(runtime.home, "last-answer.md");
1525
+ const file = join2(runtime.home, "last-answer.md");
1021
1526
  mkdirSync(runtime.home, { recursive: true });
1022
1527
  writeFileSync(file, last.content);
1023
- notice(`Disalin ke clipboard (bila terminal mendukung OSC 52) dan disimpan di ${file}.`);
1528
+ notice(t("Copied to the clipboard (if the terminal supports OSC 52) and saved to {file}.", { file }));
1024
1529
  return;
1025
1530
  }
1026
1531
  case "export": {
1027
1532
  const name2 = args || `bccli-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[:T]/g, "-")}.md`;
1028
1533
  const file = resolve(runtime.cwd, name2);
1029
1534
  if (existsSync2(file)) {
1030
- const answer2 = await ask(`${file} sudah ada. Timpa? (y/N)`);
1535
+ const answer2 = await ask(t("{file} already exists. Overwrite? (y/N)", { file }));
1031
1536
  if (!answer2 || !/^y(a|es)?$/i.test(answer2.trim())) {
1032
- notice("Ekspor dibatalkan.");
1537
+ notice(t("Export cancelled."));
1033
1538
  return;
1034
1539
  }
1035
1540
  }
1036
1541
  try {
1037
- writeFileSync(file, exportMarkdown(runtime.agent.messages, `Sesi BCCLI ${runtime.startedAt.toLocaleString("id-ID")}`));
1542
+ writeFileSync(file, exportMarkdown(runtime.agent.messages, t("BCCLI session {when}", { when: runtime.startedAt.toLocaleString(locale()) })));
1038
1543
  } catch (error) {
1039
- notice(`Gagal menulis ${file}: ${error.message}`, "error");
1544
+ notice(t("Could not write {file}: {error}", { file, error: error.message }), "error");
1040
1545
  return;
1041
1546
  }
1042
- notice(`Percakapan disimpan di ${file}.`);
1547
+ notice(t("Conversation saved to {file}.", { file }));
1043
1548
  return;
1044
1549
  }
1045
1550
  case "provider": {
@@ -1050,7 +1555,7 @@ function App({ runtime, initialPrompt, version }) {
1050
1555
  return;
1051
1556
  }
1052
1557
  default:
1053
- notice(`Perintah tidak dikenal: /${name}. Ketik /help.`, "warn");
1558
+ notice(t("Unknown command: /{name}. Type /help.", { name }), "warn");
1054
1559
  }
1055
1560
  },
1056
1561
  [runtime, notice, exit, version, runTurn, ask]
@@ -1076,7 +1581,7 @@ function App({ runtime, initialPrompt, version }) {
1076
1581
  try {
1077
1582
  body = parseFrontmatter(readFileSync2(skill.file, "utf8")).body;
1078
1583
  } catch (error) {
1079
- notice(`Skill ${skill.name} tidak bisa dibaca: ${error.code ?? error.message}`, "error");
1584
+ notice(t("Skill {name} cannot be read: {error}", { name: skill.name, error: error.code ?? error.message }), "error");
1080
1585
  return;
1081
1586
  }
1082
1587
  void runTurn(`${body}
@@ -1086,6 +1591,19 @@ ARGUMENTS: ${slash.args}`);
1086
1591
  },
1087
1592
  [runSlash, runTurn, runtime, notice]
1088
1593
  );
1594
+ const onPrompt = useCallback(
1595
+ (text) => {
1596
+ if (busy) setQueued((q) => [...q, text]);
1597
+ else submit(text);
1598
+ },
1599
+ [busy, submit]
1600
+ );
1601
+ useEffect2(() => {
1602
+ if (busy || pending || planAsk || !queued.length) return;
1603
+ const [next, ...rest] = queued;
1604
+ setQueued(rest);
1605
+ submit(next);
1606
+ }, [busy, pending, planAsk, queued, submit]);
1089
1607
  const initialSent = useRef(false);
1090
1608
  useEffect2(() => {
1091
1609
  if (initialPrompt && !initialSent.current) {
@@ -1100,10 +1618,30 @@ ARGUMENTS: ${slash.args}`);
1100
1618
  setMode(next);
1101
1619
  } else if (key.escape && busy && !pending) {
1102
1620
  controller.current?.abort();
1621
+ if (queued.length) {
1622
+ setQueued([]);
1623
+ notice(t("{n} queued messages cancelled.", { n: queued.length }), "warn");
1624
+ }
1625
+ } else if (key.ctrl && input === "t") {
1626
+ const next = !showThinking;
1627
+ const toggled = next ? t("Thinking shown (ctrl+t to hide).") : t("Thinking hidden (ctrl+t to show).");
1628
+ setShowThinking(next);
1629
+ setTranscript((t2) => {
1630
+ const last = [...t2.done, ...t2.live].findLast((e) => e.kind === "thinking");
1631
+ const reprint = next && last?.kind === "thinking" && t2.done.includes(last);
1632
+ return {
1633
+ ...t2,
1634
+ done: [
1635
+ ...t2.done,
1636
+ ...reprint ? [entry({ kind: "thinking", text: last.text, open: true })] : [],
1637
+ entry({ kind: "notice", text: toggled, tone: "info" })
1638
+ ]
1639
+ };
1640
+ });
1103
1641
  } else if (key.ctrl && input === "o" && lastTool.current) {
1104
- const t = lastTool.current;
1105
- notice(`\u23BF ${t.tool} ${t.target}
1106
- ${t.output}`);
1642
+ const t2 = lastTool.current;
1643
+ notice(`\u23BF ${t2.tool} ${t2.target}
1644
+ ${t2.output}`);
1107
1645
  }
1108
1646
  });
1109
1647
  const pickMcp = async (name) => {
@@ -1113,24 +1651,24 @@ ${t.output}`);
1113
1651
  if (item.installed) {
1114
1652
  removeGlobalServer(runtime.home, item.name);
1115
1653
  await runtime.mcp.remove(item.name);
1116
- notice(`MCP ${item.name} dihapus.`);
1654
+ notice(t("MCP {name} removed.", { name: item.name }));
1117
1655
  return;
1118
1656
  }
1119
1657
  const entry2 = CATALOG.find((c) => c.name === item.name);
1120
1658
  if (!entry2) return;
1121
1659
  const values = {};
1122
1660
  for (const input of entry2.inputs ?? []) {
1123
- const v = await ask(input.label, !!input.secret);
1661
+ const v = await ask(t(input.label), !!input.secret);
1124
1662
  if (!v) return;
1125
1663
  values[input.key] = v;
1126
1664
  }
1127
1665
  const config = fillTemplate(entry2.config, values);
1128
1666
  addGlobalServer(runtime.home, entry2.name, config);
1129
- notice(`MCP ${entry2.name} dipasang, menghubungkan\u2026`);
1667
+ notice(t("MCP {name} installed, connecting\u2026", { name: entry2.name }));
1130
1668
  await runtime.mcp.add({ name: entry2.name, config, source: "global" });
1131
1669
  const state = runtime.mcp.states().find((s) => s.name === entry2.name);
1132
- if (state?.status === "ready") notice(`MCP ${entry2.name} aktif \xB7 ${state.tools} alat.`);
1133
- else notice(`MCP ${entry2.name} gagal: ${state?.error ?? "tidak diketahui"}`, "error");
1670
+ if (state?.status === "ready") notice(t("MCP {name} active \xB7 {n} tools.", { name: entry2.name, n: state.tools }));
1671
+ else notice(t("MCP {name} failed: {error}", { name: entry2.name, error: state?.error ?? t("unknown") }), "error");
1134
1672
  };
1135
1673
  const pickProvider = async (id) => {
1136
1674
  setProviderMenu(null);
@@ -1147,7 +1685,7 @@ ${t.output}`);
1147
1685
  const c = runtime.config;
1148
1686
  const keyless = !c.providers[target].apiKeyEnv && PRESETS.some((p) => p.id === target);
1149
1687
  if (!hasKey(c, target, runtime.env) || id === "custom") {
1150
- const key = keyless ? "" : await ask(`API key untuk ${providerName(c, target)}`, true);
1688
+ const key = keyless ? "" : await ask(t("API key for {provider}", { provider: providerName(c, target) }), true);
1151
1689
  if (key === void 0) return;
1152
1690
  const result = await runtime.addProviderKey(target, key);
1153
1691
  if (!result.ok) {
@@ -1167,7 +1705,7 @@ ${t.output}`);
1167
1705
  const renderEntry = (e) => {
1168
1706
  switch (e.kind) {
1169
1707
  case "header":
1170
- return /* @__PURE__ */ jsx15(Box13, { marginBottom: 1, children: /* @__PURE__ */ jsxs10(Text15, { children: [
1708
+ return /* @__PURE__ */ jsx15(Box13, { marginBottom: 1, children: /* @__PURE__ */ jsxs11(Text15, { children: [
1171
1709
  /* @__PURE__ */ jsx15(Text15, { color: color(ACCENT), bold: true, children: " \u273B " }),
1172
1710
  /* @__PURE__ */ jsx15(Text15, { bold: true, children: `BCCLI ${version}` }),
1173
1711
  /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: ` \xB7 ${runtime.modelRef} (${runtime.providerLabel()}) \xB7 ${cwd}` })
@@ -1175,21 +1713,36 @@ ${t.output}`);
1175
1713
  case "user":
1176
1714
  return /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: `> ${e.text}` }) }, e.id);
1177
1715
  case "assistant":
1716
+ if (e.cont) {
1717
+ return e.text ? /* @__PURE__ */ jsx15(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsx15(Markdown, { text: e.text }) }, e.id) : null;
1718
+ }
1178
1719
  if (!e.text.trim()) return null;
1179
- return /* @__PURE__ */ jsxs10(Box13, { marginTop: 1, children: [
1720
+ return /* @__PURE__ */ jsxs11(Box13, { marginTop: 1, children: [
1180
1721
  /* @__PURE__ */ jsx15(Text15, { color: color(ACCENT), children: "\u25CF " }),
1181
1722
  /* @__PURE__ */ jsx15(Markdown, { text: e.text.replace(/^\s+/, "") })
1182
1723
  ] }, e.id);
1724
+ case "thinking": {
1725
+ const text = e.text.trim();
1726
+ if (!text) return null;
1727
+ if (!(e.open ?? showThinking)) {
1728
+ const lines = text.split("\n").length;
1729
+ return /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: t("\u273B Thinking \xB7 {n} lines \xB7 ctrl+t to show", { n: lines }) }) }, e.id);
1730
+ }
1731
+ return /* @__PURE__ */ jsxs11(Box13, { marginTop: 1, flexDirection: "column", children: [
1732
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: t("\u273B Thinking (ctrl+t to hide)") }),
1733
+ /* @__PURE__ */ jsx15(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, italic: true, children: text }) })
1734
+ ] }, e.id);
1735
+ }
1183
1736
  case "tool":
1184
1737
  return /* @__PURE__ */ jsx15(ToolBlock, { tool: e.tool, target: e.target, output: e.output, display: e.display, isError: e.isError, done: e.done, sub: e.sub }, e.id);
1185
1738
  case "notice":
1186
1739
  return /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text15, { color: e.tone === "error" ? color("red") : e.tone === "warn" ? color("yellow") : void 0, dimColor: e.tone === "info", children: e.text }) }, e.id);
1187
1740
  }
1188
1741
  };
1189
- return /* @__PURE__ */ jsxs10(Box13, { flexDirection: "column", children: [
1742
+ return /* @__PURE__ */ jsxs11(Box13, { flexDirection: "column", children: [
1190
1743
  /* @__PURE__ */ jsx15(Static, { items: transcript.done, children: renderEntry }),
1191
1744
  transcript.live.map(renderEntry),
1192
- busy && !pending && !planAsk ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Spinner, { label: "Berpikir", startedAt }) }) : null,
1745
+ busy && !pending && !planAsk ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Spinner, { label: t("Thinking"), startedAt }) }) : null,
1193
1746
  pending ? /* @__PURE__ */ jsx15(PermissionPrompt, { request: pending.request, onAnswer: answer }, pending.id) : null,
1194
1747
  picker ? /* @__PURE__ */ jsx15(
1195
1748
  ModelPicker,
@@ -1232,8 +1785,9 @@ ${t.output}`);
1232
1785
  }
1233
1786
  ) : null,
1234
1787
  /* @__PURE__ */ jsx15(TodoList, { items: todos }),
1235
- /* @__PURE__ */ jsx15(PromptInput, { disabled: busy || !!pending || !!picker || !!providerMenu || !!prompt || !!planAsk || !!mcpMenu || !!listPicker, history, cwd: runtime.cwd, onSubmit: submit, extraCommands }),
1236
- /* @__PURE__ */ jsx15(StatusBar, { mode, tokens, busy, model: modelLabel })
1788
+ queued.map((q, i) => /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: t(" \u23F3 queued: {q}", { q }) }, `${i}-${q}`)),
1789
+ /* @__PURE__ */ jsx15(PromptInput, { disabled: !!pending || !!picker || !!providerMenu || !!prompt || !!planAsk || !!mcpMenu || !!listPicker, history, cwd: runtime.cwd, onSubmit: onPrompt, extraCommands }),
1790
+ /* @__PURE__ */ jsx15(StatusBar, { mode, tokens, busy, model: modelLabel, reasoning })
1237
1791
  ] });
1238
1792
  }
1239
1793
 
@@ -1243,27 +1797,27 @@ async function startInteractive(rt, opts) {
1243
1797
  if (opts.resume) {
1244
1798
  const { Session: Session2 } = await import("./session-7UB77CEG.js");
1245
1799
  const list = Session2.list(rt.home, rt.cwd).slice(0, 20);
1246
- if (!list.length) console.log("Belum ada sesi di folder ini; memulai sesi baru.");
1800
+ if (!list.length) console.log(t("No sessions in this folder yet; starting a new one."));
1247
1801
  else {
1248
1802
  list.forEach((s, i) => {
1249
- console.log(`${String(i + 1).padStart(2)}. ${s.mtime.toLocaleString("id-ID")} ${s.preview}`);
1803
+ console.log(`${String(i + 1).padStart(2)}. ${s.mtime.toLocaleString(locale())} ${s.preview}`);
1250
1804
  });
1251
1805
  const { createInterface } = await import("readline/promises");
1252
1806
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1253
- const answer = Number(await rl.question("Nomor sesi (enter = baru): "));
1807
+ const answer = Number(await rl.question(t("Session number (enter = new): ")));
1254
1808
  rl.close();
1255
1809
  const chosen = list[answer - 1];
1256
1810
  if (chosen) rt.resume(chosen.session);
1257
1811
  }
1258
1812
  }
1259
- const { serversToStart, setProjectTrust } = await import("./config-NPSYFAFD.js");
1813
+ const { serversToStart, setProjectTrust } = await import("./config-PBEZ3IGD.js");
1260
1814
  const plan = serversToStart(rt.home, rt.cwd);
1261
1815
  if (plan.needTrust.length) {
1262
1816
  const { createInterface } = await import("readline/promises");
1263
1817
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1264
1818
  for (const s of plan.needTrust) {
1265
1819
  const what = s.config.type === "http" ? s.config.url : [s.config.command, ...s.config.args ?? []].join(" ");
1266
- const answer = (await rl.question(`Repo ini ingin menjalankan server MCP "${s.name}" (${what}). Izinkan? [y/N] `)).trim().toLowerCase();
1820
+ const answer = (await rl.question(t('This repo wants to run the MCP server "{name}" ({what}). Allow? [y/N] ', { name: s.name, what }))).trim().toLowerCase();
1267
1821
  setProjectTrust(rt.home, rt.cwd, s.name, answer === "y", s.config);
1268
1822
  if (answer === "y") plan.start.push(s);
1269
1823
  }