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

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-NWVFCTTO.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-AOVIQXXO.js";
19
+ import {
20
+ REASONING_LEVELS,
21
+ assertReasoningSupported,
22
+ parseReasoningLevel,
23
+ supportedReasoningLevels
24
+ } from "./chunk-UJH2JT6H.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-GEG6QT5H.js";
24
32
  import {
25
33
  addGlobalServer,
26
34
  globalMcpPath,
27
35
  readMcpFile,
28
36
  removeGlobalServer
29
- } from "./chunk-NGLKANMD.js";
37
+ } from "./chunk-CDUPYHNL.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-IJGTX7RC.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,80 +1029,114 @@ 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();
697
1134
  const [transcript, setTranscript] = useState8(() => ({ done: [entry({ kind: "header" })], live: [] }));
698
1135
  const [busy, setBusy] = useState8(false);
1136
+ const [cancelling, setCancelling] = useState8(false);
699
1137
  const [startedAt, setStartedAt] = useState8(0);
700
1138
  const [mode, setMode] = useState8(runtime.agent.permissions.mode);
1139
+ const [reasoning, setReasoning] = useState8(runtime.reasoning);
701
1140
  const [tokens, setTokens] = useState8(runtime.agent.totalUsage.inputTokens + runtime.agent.totalUsage.outputTokens);
702
1141
  const [asks, setAsks] = useState8([]);
703
1142
  const pending = asks[0] ?? null;
@@ -713,15 +1152,17 @@ function App({ runtime, initialPrompt, version }) {
713
1152
  const [mcpMenu, setMcpMenu] = useState8(null);
714
1153
  const [listPicker, setListPicker] = useState8(null);
715
1154
  const [planAsk, setPlanAsk] = useState8(null);
1155
+ const [queued, setQueued] = useState8([]);
1156
+ const [showThinking, setShowThinking] = useState8(false);
716
1157
  useEffect2(() => runtime.todos.subscribe(setTodos), [runtime]);
717
1158
  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)}` }))
1159
+ ...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") })),
1160
+ ...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
1161
  ];
721
1162
  const lastTool = useRef(null);
722
1163
  const toolTargets = useRef(/* @__PURE__ */ new Map());
723
1164
  const notice = useCallback((text, tone = "info") => {
724
- setTranscript((t) => ({ ...t, done: [...t.done, entry({ kind: "notice", text, tone })] }));
1165
+ setTranscript((t2) => ({ ...t2, done: [...t2.done, entry({ kind: "notice", text, tone })] }));
725
1166
  }, []);
726
1167
  const onEvent = useCallback((event) => {
727
1168
  if (event.type === "toolStart") toolTargets.current.set(event.id, event.target);
@@ -729,7 +1170,7 @@ function App({ runtime, initialPrompt, version }) {
729
1170
  lastTool.current = { tool: event.tool, target: toolTargets.current.get(event.id) ?? "", output: event.output };
730
1171
  }
731
1172
  if (event.type === "usage") setTokens(event.inputTokens + event.outputTokens);
732
- setTranscript((t) => applyEvent(t, event));
1173
+ setTranscript((t2) => applyEvent(t2, event));
733
1174
  }, []);
734
1175
  useEffect2(() => {
735
1176
  runtime.agent.onEvent = onEvent;
@@ -738,7 +1179,7 @@ function App({ runtime, initialPrompt, version }) {
738
1179
  }, [runtime, onEvent]);
739
1180
  const runTurn = useCallback(
740
1181
  async (text) => {
741
- setTranscript((t) => ({ ...t, done: [...t.done, entry({ kind: "user", text })] }));
1182
+ setTranscript((t2) => ({ ...t2, done: [...t2.done, entry({ kind: "user", text })] }));
742
1183
  setBusy(true);
743
1184
  setStartedAt(Date.now());
744
1185
  controller.current = new AbortController();
@@ -746,6 +1187,7 @@ function App({ runtime, initialPrompt, version }) {
746
1187
  controller.current = null;
747
1188
  setTranscript(endTurn);
748
1189
  setBusy(false);
1190
+ setCancelling(false);
749
1191
  },
750
1192
  [runtime]
751
1193
  );
@@ -761,11 +1203,11 @@ function App({ runtime, initialPrompt, version }) {
761
1203
  case "clear":
762
1204
  runtime.agent.clear();
763
1205
  setTokens(0);
764
- notice("Percakapan dikosongkan.");
1206
+ notice(t("Conversation cleared."));
765
1207
  return;
766
1208
  case "cost": {
767
1209
  const u = runtime.agent.totalUsage;
768
- notice(`${u.inputTokens.toLocaleString("id-ID")} token masuk \xB7 ${u.outputTokens.toLocaleString("id-ID")} token keluar`);
1210
+ notice(t("{input} tokens in \xB7 {output} tokens out", { input: u.inputTokens.toLocaleString(locale()), output: u.outputTokens.toLocaleString(locale()) }));
769
1211
  return;
770
1212
  }
771
1213
  case "compact": {
@@ -775,10 +1217,46 @@ function App({ runtime, initialPrompt, version }) {
775
1217
  try {
776
1218
  await runtime.agent.compact(controller.current.signal);
777
1219
  } catch (error) {
778
- notice(`Gagal meringkas: ${error.message}`, "error");
1220
+ notice(t("Could not summarize: {error}", { error: error.message }), "error");
779
1221
  }
780
1222
  controller.current = null;
781
1223
  setBusy(false);
1224
+ setCancelling(false);
1225
+ return;
1226
+ }
1227
+ case "reasoning": {
1228
+ const apply = (value) => {
1229
+ try {
1230
+ const level = parseReasoningLevel(value, "/reasoning");
1231
+ const providerId2 = runtime.modelRef.slice(0, runtime.modelRef.indexOf("/"));
1232
+ assertReasoningSupported(providerId2, level);
1233
+ runtime.setReasoning(level);
1234
+ writeGlobalConfig({ reasoning: level }, runtime.env);
1235
+ setReasoning(level);
1236
+ notice(t("Reasoning: {level} (saved as default)", { level }));
1237
+ } catch (error) {
1238
+ notice(error.message, "error");
1239
+ }
1240
+ };
1241
+ if (args) {
1242
+ apply(args);
1243
+ return;
1244
+ }
1245
+ const providerId = runtime.modelRef.slice(0, runtime.modelRef.indexOf("/"));
1246
+ const supported = supportedReasoningLevels(providerId);
1247
+ setListPicker({
1248
+ title: "Reasoning",
1249
+ items: REASONING_LEVELS.map((level) => ({
1250
+ id: level,
1251
+ label: level === "auto" ? "Auto (model default)" : level.charAt(0).toUpperCase() + level.slice(1),
1252
+ disabled: !supported.includes(level),
1253
+ hint: supported.includes(level) ? void 0 : t("not supported by this provider")
1254
+ })),
1255
+ onPick: (id) => {
1256
+ setListPicker(null);
1257
+ if (id) apply(id);
1258
+ }
1259
+ });
782
1260
  return;
783
1261
  }
784
1262
  case "model": {
@@ -787,7 +1265,7 @@ function App({ runtime, initialPrompt, version }) {
787
1265
  runtime.setModel(args);
788
1266
  writeGlobalConfig({ model: args }, runtime.env);
789
1267
  setModelLabel(labelFor(runtime, args));
790
- notice(`Model: ${args} (tersimpan sebagai default)`);
1268
+ notice(t("Model: {model} (saved as default)", { model: args }));
791
1269
  } catch (error) {
792
1270
  notice(error.message, "error");
793
1271
  }
@@ -796,7 +1274,7 @@ function App({ runtime, initialPrompt, version }) {
796
1274
  try {
797
1275
  setPicker(await runtime.listModels());
798
1276
  } catch (error) {
799
- notice(`Tidak bisa mengambil daftar model: ${error.message}`, "error");
1277
+ notice(t("Cannot fetch the model list: {error}", { error: error.message }), "error");
800
1278
  }
801
1279
  return;
802
1280
  }
@@ -804,7 +1282,7 @@ function App({ runtime, initialPrompt, version }) {
804
1282
  const installed = readMcpFile(globalMcpPath(runtime.home));
805
1283
  const states = runtime.mcp.states();
806
1284
  const items = [
807
- ...CATALOG.map((c) => ({ name: c.name, description: c.description, installed: !!installed[c.name] })),
1285
+ ...CATALOG.map((c) => ({ name: c.name, description: t(c.description), installed: !!installed[c.name] })),
808
1286
  ...Object.keys(installed).filter((n) => !CATALOG.some((c) => c.name === n)).map((n) => ({ name: n, description: "(custom)", installed: true }))
809
1287
  ].map((i) => {
810
1288
  const s = states.find((st) => st.name === i.name);
@@ -816,7 +1294,7 @@ function App({ runtime, initialPrompt, version }) {
816
1294
  case "new":
817
1295
  runtime.newSession();
818
1296
  setTokens(0);
819
- notice("Sesi baru dimulai. Sesi sebelumnya tetap tersimpan (/resume).");
1297
+ notice(t("New session started. The previous session stays saved (/resume)."));
820
1298
  return;
821
1299
  case "session":
822
1300
  notice(
@@ -832,15 +1310,15 @@ function App({ runtime, initialPrompt, version }) {
832
1310
  case "resume": {
833
1311
  const others = Session.list(runtime.home, runtime.cwd).filter((s) => s.session.file !== runtime.session.file);
834
1312
  if (!others.length) {
835
- notice("Belum ada sesi lain di folder ini.");
1313
+ notice(t("No other sessions in this folder yet."));
836
1314
  return;
837
1315
  }
838
1316
  setListPicker({
839
- title: "Lanjutkan sesi",
1317
+ title: t("Continue a session"),
840
1318
  items: others.map((s) => ({
841
1319
  id: s.session.file,
842
- label: `${s.mtime.toLocaleString("id-ID")} ${s.preview || "(kosong)"}`,
843
- hint: `${s.session.load().length} pesan`
1320
+ label: `${s.mtime.toLocaleString(locale())} ${s.preview || t("(empty)")}`,
1321
+ hint: t("{n} messages", { n: s.session.load().length })
844
1322
  })),
845
1323
  onPick: (id) => {
846
1324
  setListPicker(null);
@@ -848,12 +1326,23 @@ function App({ runtime, initialPrompt, version }) {
848
1326
  if (!chosen) return;
849
1327
  runtime.resume(chosen.session);
850
1328
  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
- )
1329
+ const chat = runtime.agent.messages.filter(
1330
+ (m) => (m.role === "user" || m.role === "assistant") && typeof m.content === "string" && !!m.content.trim()
856
1331
  );
1332
+ setTranscript({
1333
+ done: [
1334
+ entry({ kind: "header" }),
1335
+ entry({
1336
+ kind: "notice",
1337
+ text: t("Resuming the session ({messages} messages saved \xB7 {chat} user/assistant chats shown).", { messages: runtime.agent.messages.length, chat: chat.length }),
1338
+ tone: "info"
1339
+ }),
1340
+ ...chat.map(
1341
+ (m) => entry(m.role === "user" ? { kind: "user", text: m.content } : { kind: "assistant", text: m.content })
1342
+ )
1343
+ ],
1344
+ live: []
1345
+ });
857
1346
  }
858
1347
  });
859
1348
  return;
@@ -879,7 +1368,7 @@ function App({ runtime, initialPrompt, version }) {
879
1368
  notice(skillsText(runtime.skills, runtime.commands));
880
1369
  return;
881
1370
  case "doctor": {
882
- notice("Memeriksa\u2026");
1371
+ notice(t("Checking\u2026"));
883
1372
  const c = runtime.config;
884
1373
  notice(
885
1374
  await doctorText(
@@ -900,20 +1389,20 @@ function App({ runtime, initialPrompt, version }) {
900
1389
  case "permissions": {
901
1390
  const rules = runtime.agent.permissions.list();
902
1391
  if (!rules.length) {
903
- notice("Belum ada izin tersimpan. Izin dari jawaban [a] dan dari config akan muncul di sini.");
1392
+ notice(t("No saved permissions yet. Permissions from [a] answers and from the config will appear here."));
904
1393
  return;
905
1394
  }
906
1395
  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" })),
1396
+ title: t("Active permissions (enter: revoke a session permission)"),
1397
+ items: rules.map((r) => ({ id: r.rule, label: r.rule, hint: r.source === "config" ? "config" : t("this session") })),
909
1398
  onPick: (id) => {
910
1399
  setListPicker(null);
911
1400
  const rule = rules.find((r) => r.rule === id);
912
1401
  if (!rule) return;
913
- if (rule.source === "config") notice(`${rule.rule} berasal dari config/flag; hapus dari ~/.bccli/config.json untuk mencabutnya.`, "warn");
1402
+ 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
1403
  else {
915
1404
  runtime.agent.permissions.revoke(rule.rule);
916
- notice(`Izin ${rule.rule} dicabut.`);
1405
+ notice(t("Permission {rule} revoked.", { rule: rule.rule }));
917
1406
  }
918
1407
  }
919
1408
  });
@@ -922,32 +1411,51 @@ function App({ runtime, initialPrompt, version }) {
922
1411
  case "memory": {
923
1412
  if (!args) {
924
1413
  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>.");
1414
+ 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
1415
  return;
927
1416
  }
928
1417
  const { global, text } = parseMemoryArgs(args);
929
1418
  if (!text) {
930
- notice("Tulis teksnya: /memory global <teks>.", "warn");
1419
+ notice(t("Write the text: /memory global <text>."), "warn");
931
1420
  return;
932
1421
  }
933
- const file = global ? join(runtime.home, "BCCLI.md") : join(runtime.cwd, "AGENTS.md");
1422
+ const file = global ? join2(runtime.home, "BCCLI.md") : join2(runtime.cwd, "AGENTS.md");
934
1423
  try {
935
1424
  mkdirSync(dirname(file), { recursive: true });
936
1425
  appendMemory(file, text);
937
1426
  } catch (error) {
938
- notice(`Gagal menulis ${file}: ${error.message}`, "error");
1427
+ notice(t("Could not write {file}: {error}", { file, error: error.message }), "error");
939
1428
  return;
940
1429
  }
941
1430
  runtime.rebuildSystemPrompt();
942
- notice(`Ditambahkan ke ${file}.`);
1431
+ notice(t("Added to {file}.", { file }));
943
1432
  return;
944
1433
  }
945
1434
  case "init":
946
1435
  void runTurn(INIT_PROMPT);
947
1436
  return;
1437
+ case "language": {
1438
+ if (!args) {
1439
+ notice(t("Language: {lang}. Change it with /language en or /language id.", { lang: getLanguage() }));
1440
+ return;
1441
+ }
1442
+ const wanted = parseLang(args);
1443
+ if (!wanted) {
1444
+ notice(t('Unknown language "{lang}". Use en or id.', { lang: args }), "warn");
1445
+ return;
1446
+ }
1447
+ setLanguage(wanted);
1448
+ try {
1449
+ writeGlobalConfig({ language: wanted }, runtime.env);
1450
+ } catch (error) {
1451
+ notice(t("Could not save the language: {error}", { error: error.message }), "error");
1452
+ }
1453
+ notice(t("Language set to {lang} (saved as default).", { lang: wanted }));
1454
+ return;
1455
+ }
948
1456
  case "login": {
949
1457
  const id = runtime.modelRef.slice(0, runtime.modelRef.indexOf("/"));
950
- const key = await ask(`API key untuk ${runtime.providerLabel()}`, true);
1458
+ const key = await ask(t("API key for {provider}", { provider: runtime.providerLabel() }), true);
951
1459
  if (!key) return;
952
1460
  const result = await runtime.addProviderKey(id, key);
953
1461
  if (!result.ok) {
@@ -957,10 +1465,10 @@ function App({ runtime, initialPrompt, version }) {
957
1465
  runtime.setModel(runtime.modelRef);
958
1466
  const envName = runtime.config.providers[id]?.apiKeyEnv;
959
1467
  if (envName && runtime.env[envName]) {
960
- notice(`Key ${runtime.providerLabel()} tersimpan, tapi ${envName} di environment lebih diutamakan dan tetap dipakai.`, "warn");
1468
+ notice(t("Key for {provider} saved, but {env} in the environment takes precedence and is still used.", { provider: runtime.providerLabel(), env: envName }), "warn");
961
1469
  return;
962
1470
  }
963
- notice(`Key ${runtime.providerLabel()} tersimpan \xB7 ${result.models} model.`);
1471
+ notice(t("Key for {provider} saved \xB7 {n} models.", { provider: runtime.providerLabel(), n: result.models }));
964
1472
  return;
965
1473
  }
966
1474
  case "logout": {
@@ -969,13 +1477,13 @@ function App({ runtime, initialPrompt, version }) {
969
1477
  const envName = runtime.config.providers[id]?.apiKeyEnv;
970
1478
  if (envName && runtime.env[envName]) {
971
1479
  notice(
972
- `${removed ? "Key tersimpan dihapus, tapi" : "Tidak ada key tersimpan;"} ${envName} masih ada di environment dan tetap dipakai.`,
1480
+ 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
1481
  "warn"
974
1482
  );
975
1483
  return;
976
1484
  }
977
1485
  if (!removed) {
978
- notice(`Tidak ada key tersimpan untuk ${runtime.providerLabel()}.`);
1486
+ notice(t("No stored key for {provider}.", { provider: runtime.providerLabel() }));
979
1487
  return;
980
1488
  }
981
1489
  try {
@@ -986,7 +1494,7 @@ function App({ runtime, initialPrompt, version }) {
986
1494
  };
987
1495
  runtime.agent.provider = { chat: fail, listModels: fail };
988
1496
  }
989
- notice(`Key ${runtime.providerLabel()} dihapus. Pakai /login atau /provider untuk menambah lagi.`);
1497
+ notice(t("Key for {provider} deleted. Use /login or /provider to add one again.", { provider: runtime.providerLabel() }));
990
1498
  return;
991
1499
  }
992
1500
  case "diff":
@@ -995,17 +1503,17 @@ function App({ runtime, initialPrompt, version }) {
995
1503
  case "undo": {
996
1504
  const result = await runtime.checkpoints.undo();
997
1505
  if (!result) {
998
- notice("Tidak ada edit file yang bisa dibatalkan.");
1506
+ notice(t("No file edits to undo."));
999
1507
  return;
1000
1508
  }
1001
1509
  const rel = (p) => relative(runtime.cwd, p) || p;
1002
1510
  notice(
1003
1511
  [
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."
1512
+ result.failed.length ? t("Could not restore: {files}", { files: result.failed.map(rel).join(", ") }) : "",
1513
+ result.restored.length ? t("Restored: {files}", { files: result.restored.map(rel).join(", ") }) : "",
1514
+ result.deleted.length ? t("Deleted (new files): {files}", { files: result.deleted.map(rel).join(", ") }) : "",
1515
+ result.skipped.length ? t("Too large to back up, left unchanged: {files}", { files: result.skipped.map(rel).join(", ") }) : "",
1516
+ t("Note: changes made through bash commands cannot be undone.")
1009
1517
  ].filter(Boolean).join("\n")
1010
1518
  );
1011
1519
  return;
@@ -1013,33 +1521,33 @@ function App({ runtime, initialPrompt, version }) {
1013
1521
  case "copy": {
1014
1522
  const last = runtime.agent.messages.findLast((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim());
1015
1523
  if (!last || typeof last.content !== "string") {
1016
- notice("Belum ada jawaban untuk disalin.");
1524
+ notice(t("No answer to copy yet."));
1017
1525
  return;
1018
1526
  }
1019
1527
  process.stdout.write(osc52(last.content, !!process.env.TMUX));
1020
- const file = join(runtime.home, "last-answer.md");
1528
+ const file = join2(runtime.home, "last-answer.md");
1021
1529
  mkdirSync(runtime.home, { recursive: true });
1022
1530
  writeFileSync(file, last.content);
1023
- notice(`Disalin ke clipboard (bila terminal mendukung OSC 52) dan disimpan di ${file}.`);
1531
+ notice(t("Copied to the clipboard (if the terminal supports OSC 52) and saved to {file}.", { file }));
1024
1532
  return;
1025
1533
  }
1026
1534
  case "export": {
1027
1535
  const name2 = args || `bccli-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace(/[:T]/g, "-")}.md`;
1028
1536
  const file = resolve(runtime.cwd, name2);
1029
1537
  if (existsSync2(file)) {
1030
- const answer2 = await ask(`${file} sudah ada. Timpa? (y/N)`);
1538
+ const answer2 = await ask(t("{file} already exists. Overwrite? (y/N)", { file }));
1031
1539
  if (!answer2 || !/^y(a|es)?$/i.test(answer2.trim())) {
1032
- notice("Ekspor dibatalkan.");
1540
+ notice(t("Export cancelled."));
1033
1541
  return;
1034
1542
  }
1035
1543
  }
1036
1544
  try {
1037
- writeFileSync(file, exportMarkdown(runtime.agent.messages, `Sesi BCCLI ${runtime.startedAt.toLocaleString("id-ID")}`));
1545
+ writeFileSync(file, exportMarkdown(runtime.agent.messages, t("BCCLI session {when}", { when: runtime.startedAt.toLocaleString(locale()) })));
1038
1546
  } catch (error) {
1039
- notice(`Gagal menulis ${file}: ${error.message}`, "error");
1547
+ notice(t("Could not write {file}: {error}", { file, error: error.message }), "error");
1040
1548
  return;
1041
1549
  }
1042
- notice(`Percakapan disimpan di ${file}.`);
1550
+ notice(t("Conversation saved to {file}.", { file }));
1043
1551
  return;
1044
1552
  }
1045
1553
  case "provider": {
@@ -1050,7 +1558,7 @@ function App({ runtime, initialPrompt, version }) {
1050
1558
  return;
1051
1559
  }
1052
1560
  default:
1053
- notice(`Perintah tidak dikenal: /${name}. Ketik /help.`, "warn");
1561
+ notice(t("Unknown command: /{name}. Type /help.", { name }), "warn");
1054
1562
  }
1055
1563
  },
1056
1564
  [runtime, notice, exit, version, runTurn, ask]
@@ -1076,7 +1584,7 @@ function App({ runtime, initialPrompt, version }) {
1076
1584
  try {
1077
1585
  body = parseFrontmatter(readFileSync2(skill.file, "utf8")).body;
1078
1586
  } catch (error) {
1079
- notice(`Skill ${skill.name} tidak bisa dibaca: ${error.code ?? error.message}`, "error");
1587
+ notice(t("Skill {name} cannot be read: {error}", { name: skill.name, error: error.code ?? error.message }), "error");
1080
1588
  return;
1081
1589
  }
1082
1590
  void runTurn(`${body}
@@ -1086,6 +1594,19 @@ ARGUMENTS: ${slash.args}`);
1086
1594
  },
1087
1595
  [runSlash, runTurn, runtime, notice]
1088
1596
  );
1597
+ const onPrompt = useCallback(
1598
+ (text) => {
1599
+ if (busy) setQueued((q) => [...q, text]);
1600
+ else submit(text);
1601
+ },
1602
+ [busy, submit]
1603
+ );
1604
+ useEffect2(() => {
1605
+ if (busy || pending || planAsk || !queued.length) return;
1606
+ const [next, ...rest] = queued;
1607
+ setQueued(rest);
1608
+ submit(next);
1609
+ }, [busy, pending, planAsk, queued, submit]);
1089
1610
  const initialSent = useRef(false);
1090
1611
  useEffect2(() => {
1091
1612
  if (initialPrompt && !initialSent.current) {
@@ -1100,10 +1621,31 @@ ARGUMENTS: ${slash.args}`);
1100
1621
  setMode(next);
1101
1622
  } else if (key.escape && busy && !pending) {
1102
1623
  controller.current?.abort();
1624
+ setCancelling(true);
1625
+ if (queued.length) {
1626
+ setQueued([]);
1627
+ notice(t("{n} queued messages cancelled.", { n: queued.length }), "warn");
1628
+ }
1629
+ } else if (key.ctrl && input === "t") {
1630
+ const next = !showThinking;
1631
+ const toggled = next ? t("Thinking shown (ctrl+t to hide).") : t("Thinking hidden (ctrl+t to show).");
1632
+ setShowThinking(next);
1633
+ setTranscript((t2) => {
1634
+ const last = [...t2.done, ...t2.live].findLast((e) => e.kind === "thinking");
1635
+ const reprint = next && last?.kind === "thinking" && t2.done.includes(last);
1636
+ return {
1637
+ ...t2,
1638
+ done: [
1639
+ ...t2.done,
1640
+ ...reprint ? [entry({ kind: "thinking", text: last.text, open: true })] : [],
1641
+ entry({ kind: "notice", text: toggled, tone: "info" })
1642
+ ]
1643
+ };
1644
+ });
1103
1645
  } else if (key.ctrl && input === "o" && lastTool.current) {
1104
- const t = lastTool.current;
1105
- notice(`\u23BF ${t.tool} ${t.target}
1106
- ${t.output}`);
1646
+ const t2 = lastTool.current;
1647
+ notice(`\u23BF ${t2.tool} ${t2.target}
1648
+ ${t2.output}`);
1107
1649
  }
1108
1650
  });
1109
1651
  const pickMcp = async (name) => {
@@ -1113,24 +1655,24 @@ ${t.output}`);
1113
1655
  if (item.installed) {
1114
1656
  removeGlobalServer(runtime.home, item.name);
1115
1657
  await runtime.mcp.remove(item.name);
1116
- notice(`MCP ${item.name} dihapus.`);
1658
+ notice(t("MCP {name} removed.", { name: item.name }));
1117
1659
  return;
1118
1660
  }
1119
1661
  const entry2 = CATALOG.find((c) => c.name === item.name);
1120
1662
  if (!entry2) return;
1121
1663
  const values = {};
1122
1664
  for (const input of entry2.inputs ?? []) {
1123
- const v = await ask(input.label, !!input.secret);
1665
+ const v = await ask(t(input.label), !!input.secret);
1124
1666
  if (!v) return;
1125
1667
  values[input.key] = v;
1126
1668
  }
1127
1669
  const config = fillTemplate(entry2.config, values);
1128
1670
  addGlobalServer(runtime.home, entry2.name, config);
1129
- notice(`MCP ${entry2.name} dipasang, menghubungkan\u2026`);
1671
+ notice(t("MCP {name} installed, connecting\u2026", { name: entry2.name }));
1130
1672
  await runtime.mcp.add({ name: entry2.name, config, source: "global" });
1131
1673
  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");
1674
+ if (state?.status === "ready") notice(t("MCP {name} active \xB7 {n} tools.", { name: entry2.name, n: state.tools }));
1675
+ else notice(t("MCP {name} failed: {error}", { name: entry2.name, error: state?.error ?? t("unknown") }), "error");
1134
1676
  };
1135
1677
  const pickProvider = async (id) => {
1136
1678
  setProviderMenu(null);
@@ -1147,7 +1689,7 @@ ${t.output}`);
1147
1689
  const c = runtime.config;
1148
1690
  const keyless = !c.providers[target].apiKeyEnv && PRESETS.some((p) => p.id === target);
1149
1691
  if (!hasKey(c, target, runtime.env) || id === "custom") {
1150
- const key = keyless ? "" : await ask(`API key untuk ${providerName(c, target)}`, true);
1692
+ const key = keyless ? "" : await ask(t("API key for {provider}", { provider: providerName(c, target) }), true);
1151
1693
  if (key === void 0) return;
1152
1694
  const result = await runtime.addProviderKey(target, key);
1153
1695
  if (!result.ok) {
@@ -1167,7 +1709,7 @@ ${t.output}`);
1167
1709
  const renderEntry = (e) => {
1168
1710
  switch (e.kind) {
1169
1711
  case "header":
1170
- return /* @__PURE__ */ jsx15(Box13, { marginBottom: 1, children: /* @__PURE__ */ jsxs10(Text15, { children: [
1712
+ return /* @__PURE__ */ jsx15(Box13, { marginBottom: 1, children: /* @__PURE__ */ jsxs11(Text15, { children: [
1171
1713
  /* @__PURE__ */ jsx15(Text15, { color: color(ACCENT), bold: true, children: " \u273B " }),
1172
1714
  /* @__PURE__ */ jsx15(Text15, { bold: true, children: `BCCLI ${version}` }),
1173
1715
  /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: ` \xB7 ${runtime.modelRef} (${runtime.providerLabel()}) \xB7 ${cwd}` })
@@ -1175,21 +1717,36 @@ ${t.output}`);
1175
1717
  case "user":
1176
1718
  return /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: `> ${e.text}` }) }, e.id);
1177
1719
  case "assistant":
1720
+ if (e.cont) {
1721
+ return e.text ? /* @__PURE__ */ jsx15(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsx15(Markdown, { text: e.text }) }, e.id) : null;
1722
+ }
1178
1723
  if (!e.text.trim()) return null;
1179
- return /* @__PURE__ */ jsxs10(Box13, { marginTop: 1, children: [
1724
+ return /* @__PURE__ */ jsxs11(Box13, { marginTop: 1, children: [
1180
1725
  /* @__PURE__ */ jsx15(Text15, { color: color(ACCENT), children: "\u25CF " }),
1181
1726
  /* @__PURE__ */ jsx15(Markdown, { text: e.text.replace(/^\s+/, "") })
1182
1727
  ] }, e.id);
1728
+ case "thinking": {
1729
+ const text = e.text.trim();
1730
+ if (!text) return null;
1731
+ if (!(e.open ?? showThinking)) {
1732
+ const lines = text.split("\n").length;
1733
+ 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);
1734
+ }
1735
+ return /* @__PURE__ */ jsxs11(Box13, { marginTop: 1, flexDirection: "column", children: [
1736
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: t("\u273B Thinking (ctrl+t to hide)") }),
1737
+ /* @__PURE__ */ jsx15(Box13, { paddingLeft: 2, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, italic: true, children: text }) })
1738
+ ] }, e.id);
1739
+ }
1183
1740
  case "tool":
1184
1741
  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
1742
  case "notice":
1186
1743
  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
1744
  }
1188
1745
  };
1189
- return /* @__PURE__ */ jsxs10(Box13, { flexDirection: "column", children: [
1746
+ return /* @__PURE__ */ jsxs11(Box13, { flexDirection: "column", children: [
1190
1747
  /* @__PURE__ */ jsx15(Static, { items: transcript.done, children: renderEntry }),
1191
1748
  transcript.live.map(renderEntry),
1192
- busy && !pending && !planAsk ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Spinner, { label: "Berpikir", startedAt }) }) : null,
1749
+ busy && !pending && !planAsk ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Spinner, { label: cancelling ? t("Cancelling") : t("Thinking"), startedAt }) }) : null,
1193
1750
  pending ? /* @__PURE__ */ jsx15(PermissionPrompt, { request: pending.request, onAnswer: answer }, pending.id) : null,
1194
1751
  picker ? /* @__PURE__ */ jsx15(
1195
1752
  ModelPicker,
@@ -1232,8 +1789,9 @@ ${t.output}`);
1232
1789
  }
1233
1790
  ) : null,
1234
1791
  /* @__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 })
1792
+ queued.map((q, i) => /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: t(" \u23F3 queued: {q}", { q }) }, `${i}-${q}`)),
1793
+ /* @__PURE__ */ jsx15(PromptInput, { disabled: !!pending || !!picker || !!providerMenu || !!prompt || !!planAsk || !!mcpMenu || !!listPicker, history, cwd: runtime.cwd, onSubmit: onPrompt, extraCommands }),
1794
+ /* @__PURE__ */ jsx15(StatusBar, { mode, tokens, busy, model: modelLabel, reasoning })
1237
1795
  ] });
1238
1796
  }
1239
1797
 
@@ -1243,27 +1801,27 @@ async function startInteractive(rt, opts) {
1243
1801
  if (opts.resume) {
1244
1802
  const { Session: Session2 } = await import("./session-7UB77CEG.js");
1245
1803
  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.");
1804
+ if (!list.length) console.log(t("No sessions in this folder yet; starting a new one."));
1247
1805
  else {
1248
1806
  list.forEach((s, i) => {
1249
- console.log(`${String(i + 1).padStart(2)}. ${s.mtime.toLocaleString("id-ID")} ${s.preview}`);
1807
+ console.log(`${String(i + 1).padStart(2)}. ${s.mtime.toLocaleString(locale())} ${s.preview}`);
1250
1808
  });
1251
1809
  const { createInterface } = await import("readline/promises");
1252
1810
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1253
- const answer = Number(await rl.question("Nomor sesi (enter = baru): "));
1811
+ const answer = Number(await rl.question(t("Session number (enter = new): ")));
1254
1812
  rl.close();
1255
1813
  const chosen = list[answer - 1];
1256
1814
  if (chosen) rt.resume(chosen.session);
1257
1815
  }
1258
1816
  }
1259
- const { serversToStart, setProjectTrust } = await import("./config-NPSYFAFD.js");
1817
+ const { serversToStart, setProjectTrust } = await import("./config-DMF4AO7E.js");
1260
1818
  const plan = serversToStart(rt.home, rt.cwd);
1261
1819
  if (plan.needTrust.length) {
1262
1820
  const { createInterface } = await import("readline/promises");
1263
1821
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1264
1822
  for (const s of plan.needTrust) {
1265
1823
  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();
1824
+ 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
1825
  setProjectTrust(rt.home, rt.cwd, s.name, answer === "y", s.config);
1268
1826
  if (answer === "y") plan.start.push(s);
1269
1827
  }