sarangai-cli 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +371 -30
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ var require_package = __commonJS({
35
35
  "package.json"(exports2, module2) {
36
36
  module2.exports = {
37
37
  name: "sarangai-cli",
38
- version: "1.0.11",
38
+ version: "1.0.13",
39
39
  description: "CLI resmi SarangAI \u2014 Gateway multi-model AI langsung dari terminal Anda",
40
40
  main: "dist/index.js",
41
41
  bin: {
@@ -96,6 +96,8 @@ var import_chalk = __toESM(require("chalk"));
96
96
  var import_readline = __toESM(require("readline"));
97
97
  var import_child_process = require("child_process");
98
98
  var import_crypto = __toESM(require("crypto"));
99
+ var import_fs2 = __toESM(require("fs"));
100
+ var import_path2 = __toESM(require("path"));
99
101
  var import_clipboardy = __toESM(require("clipboardy"));
100
102
 
101
103
  // src/config.ts
@@ -124,6 +126,14 @@ function saveConfig(newConfig) {
124
126
 
125
127
  // src/index.ts
126
128
  var program = new import_commander.Command();
129
+ var SARANG_BANNER = [
130
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557",
131
+ "\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551",
132
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551",
133
+ "\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551",
134
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551",
135
+ "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D"
136
+ ];
127
137
  var borderActive = import_chalk.default.hex("#a855f7");
128
138
  var borderMuted = import_chalk.default.hex("#334155");
129
139
  var violetText = import_chalk.default.hex("#c084fc");
@@ -345,6 +355,15 @@ var timerInterval = null;
345
355
  var currentTaskInput = "";
346
356
  var cfg;
347
357
  var baseUrl;
358
+ var respQuery = "";
359
+ var respText = "";
360
+ var respError;
361
+ var respStreaming = false;
362
+ var respScroll = null;
363
+ var respTotalLines = 0;
364
+ var conversationHistory = [];
365
+ var sessionScreen = false;
366
+ var respLastNotes = [];
348
367
  function cleanupAndExit() {
349
368
  if (timerInterval) clearInterval(timerInterval);
350
369
  process.stdout.write("\x1B[?1049l\x1B[?25h\n");
@@ -358,8 +377,10 @@ function triggerCopyLink() {
358
377
  function leaveWorkspace() {
359
378
  inWorkspace = false;
360
379
  isExecuting = false;
380
+ sessionScreen = false;
361
381
  if (timerInterval) clearInterval(timerInterval);
362
382
  currentTaskInput = "";
383
+ conversationHistory = [];
363
384
  process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
364
385
  drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
365
386
  }
@@ -378,13 +399,261 @@ function enterWorkspace() {
378
399
  }
379
400
  }, 1e3);
380
401
  }
402
+ function visibleLength(s) {
403
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
404
+ }
405
+ function wrapWithStyles(input, width) {
406
+ const out = [];
407
+ let inCode = false;
408
+ const text = input.replace(/\r\n?/g, "\n");
409
+ for (const raw of text.split("\n")) {
410
+ const trimmed = raw.trimStart();
411
+ let style = "plain";
412
+ if (trimmed.startsWith("```")) {
413
+ inCode = !inCode;
414
+ style = "code";
415
+ } else if (inCode) {
416
+ style = "code";
417
+ } else if (trimmed.startsWith("#")) {
418
+ style = "heading";
419
+ }
420
+ if (raw === "") {
421
+ out.push({ text: "", style });
422
+ continue;
423
+ }
424
+ let remaining = raw;
425
+ while (remaining.length > width) {
426
+ let cut = remaining.lastIndexOf(" ", width);
427
+ if (cut < Math.floor(width * 0.5)) cut = width;
428
+ out.push({ text: remaining.slice(0, cut), style });
429
+ remaining = remaining.slice(cut).replace(/^ /, "");
430
+ }
431
+ out.push({ text: remaining, style });
432
+ }
433
+ return out;
434
+ }
435
+ function styleBodyLine(l) {
436
+ if (l.style === "heading") return import_chalk.default.bold.hex("#c084fc")(l.text);
437
+ if (l.style === "code") return import_chalk.default.hex("#94a3b8")(l.text);
438
+ if (l.style === "error") return import_chalk.default.red(l.text);
439
+ return import_chalk.default.hex("#e2e8f0")(l.text);
440
+ }
441
+ var EXT_BY_LANG = {
442
+ python: "py",
443
+ py: "py",
444
+ javascript: "js",
445
+ js: "js",
446
+ typescript: "ts",
447
+ ts: "ts",
448
+ tsx: "tsx",
449
+ jsx: "jsx",
450
+ go: "go",
451
+ rust: "rs",
452
+ rs: "rs",
453
+ java: "java",
454
+ c: "c",
455
+ cpp: "cpp",
456
+ "c++": "cpp",
457
+ csharp: "cs",
458
+ "c#": "cs",
459
+ cs: "cs",
460
+ php: "php",
461
+ ruby: "rb",
462
+ rb: "rb",
463
+ swift: "swift",
464
+ kotlin: "kt",
465
+ kt: "kt",
466
+ bash: "sh",
467
+ sh: "sh",
468
+ shell: "sh",
469
+ zsh: "sh",
470
+ powershell: "ps1",
471
+ ps1: "ps1",
472
+ html: "html",
473
+ css: "css",
474
+ scss: "scss",
475
+ sass: "sass",
476
+ less: "less",
477
+ sql: "sql",
478
+ json: "json",
479
+ yaml: "yaml",
480
+ yml: "yml",
481
+ toml: "toml",
482
+ xml: "xml",
483
+ markdown: "md",
484
+ md: "md",
485
+ dockerfile: "",
486
+ makefile: "",
487
+ ini: "ini",
488
+ env: "",
489
+ txt: "txt",
490
+ text: "txt",
491
+ csv: "csv"
492
+ };
493
+ function extractFilesFromMarkdown(text) {
494
+ const files = [];
495
+ const fence = /^[ \t]*```[ \t]*([A-Za-z0-9_#+.-]*)(?::[ \t]*([^\n`]+))?[^\n]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
496
+ let m;
497
+ while ((m = fence.exec(text)) !== null) {
498
+ const lang = (m[1] || "").toLowerCase().trim();
499
+ const fname = (m[2] || "").trim().replace(/^["']|["']$/g, "");
500
+ if (!fname) continue;
501
+ files.push({ filename: fname, content: m[3], lang: lang || "text" });
502
+ }
503
+ return files;
504
+ }
505
+ function sanitizeFilename(name) {
506
+ const base = import_path2.default.basename(name.trim());
507
+ if (!base || base === "." || base === "..") return null;
508
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(base)) return null;
509
+ if (base.includes("..")) return null;
510
+ return base;
511
+ }
512
+ function defaultFilename(lang) {
513
+ if (lang === "dockerfile") return "Dockerfile";
514
+ if (lang === "makefile") return "Makefile";
515
+ const ext = EXT_BY_LANG[lang];
516
+ return ext === void 0 ? null : `snippet.${ext || "txt"}`;
517
+ }
518
+ function extractAllFiles(text) {
519
+ const extracted = extractFilesFromMarkdown(text);
520
+ if (extracted.length) return extracted;
521
+ const fence2 = /^[ \t]*```[ \t]*([A-Za-z0-9_#+.-]+)[ \t]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
522
+ let best = null;
523
+ let m;
524
+ while ((m = fence2.exec(text)) !== null) {
525
+ const lang = (m[1] || "").toLowerCase();
526
+ if (!EXT_BY_LANG.hasOwnProperty(lang) || lang === "text") continue;
527
+ const content = m[2];
528
+ if (content.trim().split("\n").length < 2) continue;
529
+ if (!best || content.length > best.content.length) {
530
+ best = { filename: defaultFilename(lang) || "snippet.txt", content, lang };
531
+ }
532
+ }
533
+ return best ? [best] : [];
534
+ }
535
+ function writeExtractedFiles(files) {
536
+ const notes = [];
537
+ for (const f of files) {
538
+ const safe = sanitizeFilename(f.filename);
539
+ if (!safe) {
540
+ notes.push(import_chalk.default.red(`\u2715 Skip nama file tidak valid: ${f.filename}`));
541
+ continue;
542
+ }
543
+ try {
544
+ const target = import_path2.default.join(process.cwd(), safe);
545
+ import_fs2.default.writeFileSync(target, f.content, "utf-8");
546
+ const bytes = Buffer.byteLength(f.content, "utf-8");
547
+ notes.push(import_chalk.default.green(`\u2714 File created: ${safe}`) + import_chalk.default.gray(` (${bytes} bytes)`));
548
+ } catch (err) {
549
+ notes.push(import_chalk.default.red(`\u2715 Gagal menulis ${safe}: ${err.message}`));
550
+ }
551
+ }
552
+ return notes;
553
+ }
554
+ var AGENT_SYSTEM_PROMPT = [
555
+ "Anda adalah asisten coding agent SarangAI yang berjalan di terminal pengguna.",
556
+ `Direktori kerja aktif: ${process.cwd()}.`,
557
+ "Jika permintaan pengguna melibatkan pembuatan atau perubahan file, SELALU output kode dalam fenced code block dengan format ````bahasa:nama-file` pada baris pembuka, contoh: ```python:binary_search.py",
558
+ "Gunakan SATU file per code block dan jangan menambah teks setelah baris penutup ``` milik block tersebut.",
559
+ "Untuk file konfigurasi tanpa ekstensi gunakan ```dockerfile:Dockerfile atau ```makefile:Makefile.",
560
+ "Jelaskan secara singkat sebelum/sesudah code block, tanpa membungkus nama file di backtick atau bold."
561
+ ].join("\n");
562
+ var lastTurn = { query: "", text: "", streaming: false, notes: [] };
563
+ var activeAbort = null;
564
+ function drawSessionScreen(turn, opts = { inputMode: "idle" }) {
565
+ const boxWidth = 72;
566
+ const termRows = process.stdout.rows || 30;
567
+ const pad = getCenterPad(boxWidth);
568
+ const innerW = boxWidth - 2;
569
+ const contentW = innerW - 4;
570
+ const lines = [];
571
+ SARANG_BANNER.forEach((l) => lines.push(pad + import_chalk.default.white.bold(l)));
572
+ lines.push("");
573
+ const mm = Math.floor(sessionTimeLeft / 60);
574
+ const ss = sessionTimeLeft % 60;
575
+ lines.push(pad + import_chalk.default.gray(`${conversationHistory.length} pesan \u2022 ${activeModel.name} \u2022 ${mm}m ${ss < 10 ? "0" : ""}${ss}s left`));
576
+ lines.push("");
577
+ const qLabel = turn.query.length > 64 ? turn.query.slice(0, 61) + "\u2026" : turn.query;
578
+ lines.push(pad + import_chalk.default.bold.hex("#c084fc")(`\u203A Task: ${qLabel}`));
579
+ const headerRows = lines.length + 1;
580
+ const footerRows = opts.inputMode === "typing" ? 7 : 3;
581
+ const viewRows = Math.max(4, termRows - headerRows - footerRows);
582
+ const body = turn.error ? wrapWithStyles(turn.error, contentW).map((l) => ({ ...l, style: "error" })) : wrapWithStyles(turn.text, contentW);
583
+ respTotalLines = body.length;
584
+ const visible = turn.streaming ? body.slice(0, viewRows) : body.slice(Math.max(0, body.length - viewRows));
585
+ lines.push(pad + import_chalk.default.gray("\u250C" + "\u2500".repeat(innerW) + "\u2510"));
586
+ for (const l of visible) {
587
+ const styled = styleBodyLine(l);
588
+ const fill = Math.max(0, contentW - visibleLength(styled));
589
+ lines.push(pad + import_chalk.default.gray("\u2502 ") + styled + " ".repeat(fill) + import_chalk.default.gray(" \u2502"));
590
+ }
591
+ for (let i = visible.length; i < viewRows; i++) {
592
+ lines.push(pad + import_chalk.default.gray("\u2502 ") + " ".repeat(contentW) + import_chalk.default.gray(" \u2502"));
593
+ }
594
+ lines.push(pad + import_chalk.default.gray("\u2514" + "\u2500".repeat(innerW) + "\u2518"));
595
+ for (const note of turn.notes) lines.push(pad + " " + note);
596
+ if (opts.inputMode === "typing") {
597
+ lines.push("");
598
+ const textContent = currentTaskInput ? currentTaskInput : import_chalk.default.gray("Ketik follow-up\u2026 (/new = sesi baru, exit = keluar)");
599
+ const visibleLen = currentTaskInput ? currentTaskInput.length : 55;
600
+ const paddingRight = Math.max(0, innerW - 2 - visibleLen);
601
+ lines.push(pad + import_chalk.default.gray("\u250C" + "\u2500".repeat(innerW) + "\u2510"));
602
+ const inputRowNumber = lines.length + 1;
603
+ lines.push(pad + import_chalk.default.gray(`\u2502 ${textContent}${" ".repeat(paddingRight)}\u2502`));
604
+ lines.push(pad + import_chalk.default.gray("\u2514" + "\u2500".repeat(innerW) + "\u2518"));
605
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
606
+ const cursorCol = pad.length + 5 + currentTaskInput.length;
607
+ process.stdout.write(`\x1B[${inputRowNumber};${cursorCol}H\x1B[?25h`);
608
+ } else {
609
+ lines.push("");
610
+ const status = turn.streaming ? import_chalk.default.gray(`[Streaming \u2022 ${activeModel.name} \u2022 ${respTotalLines} baris] Esc = batalkan`) : turn.error ? import_chalk.default.red("\u2715 Error") + import_chalk.default.gray(" \u2022 tekan tombol untuk lanjut") : import_chalk.default.green("\u2714 Selesai") + import_chalk.default.gray(` \u2022 ${respTotalLines} baris \u2022 ketik follow-up di bawah \u2022 Esc = menu`);
611
+ lines.push(pad + status);
612
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
613
+ }
614
+ }
615
+ function enterSessionInput() {
616
+ currentTaskInput = "";
617
+ sessionScreen = true;
618
+ drawSessionScreen(lastTurn, { inputMode: "typing" });
619
+ }
620
+ function redrawInputScreen() {
621
+ if (sessionScreen) drawSessionScreen(lastTurn, { inputMode: "typing" });
622
+ else drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
623
+ }
624
+ function waitSessionKeypress() {
625
+ process.stdin.once("keypress", (str, key) => {
626
+ if (key?.ctrl && key?.name === "c") {
627
+ cleanupAndExit();
628
+ return;
629
+ }
630
+ isExecuting = false;
631
+ if (key?.name === "escape") {
632
+ leaveWorkspace();
633
+ return;
634
+ }
635
+ enterSessionInput();
636
+ });
637
+ }
381
638
  async function runPromptStream(query, isRetry = false) {
382
639
  isExecuting = true;
640
+ respStreaming = true;
641
+ respQuery = query;
642
+ respText = "";
643
+ respError = void 0;
644
+ respScroll = null;
645
+ activeAbort = new AbortController();
383
646
  process.stdout.write("\x1B[?25l");
384
- const pad = getCenterPad(72);
385
- console.log("\n\n" + pad + import_chalk.default.bold.hex("#c084fc")(`\u203A Task: ${query}`));
386
- console.log(pad + import_chalk.default.gray(`[Generating with ${activeModel.name}...]
387
- `));
647
+ lastTurn = { query, text: "", streaming: true, notes: [] };
648
+ drawSessionScreen(lastTurn);
649
+ let lastPaint = 0;
650
+ const repaint = (streaming, force = false) => {
651
+ const now = Date.now();
652
+ if (!force && now - lastPaint < 80) return;
653
+ lastPaint = now;
654
+ lastTurn = { query, text: respText, streaming, error: respError, notes: lastTurn.notes };
655
+ drawSessionScreen(lastTurn);
656
+ };
388
657
  try {
389
658
  const res = await fetch(`${baseUrl}/api/gateway/v1/chat/completions`, {
390
659
  method: "POST",
@@ -392,9 +661,13 @@ async function runPromptStream(query, isRetry = false) {
392
661
  "Content-Type": "application/json",
393
662
  Authorization: `Bearer ${cfg.apiKey}`
394
663
  },
664
+ signal: activeAbort.signal,
395
665
  body: JSON.stringify({
396
666
  model: activeModel.id,
397
- messages: [{ role: "user", content: query }],
667
+ messages: [
668
+ { role: "system", content: AGENT_SYSTEM_PROMPT },
669
+ ...conversationHistory
670
+ ],
398
671
  stream: true
399
672
  })
400
673
  });
@@ -406,11 +679,11 @@ async function runPromptStream(query, isRetry = false) {
406
679
  if (msg) detail = typeof msg === "string" ? msg : JSON.stringify(msg);
407
680
  } catch {
408
681
  }
409
- console.log(pad + import_chalk.default.red(`
410
- Error (${res.status}): ${detail}
411
- `));
682
+ respError = `(${res.status}) ${detail}`;
683
+ repaint(false, true);
412
684
  if (res.status === 401 && !isRetry) {
413
- console.log(pad + import_chalk.default.yellow("API key tidak valid atau sudah di-revoke. Memulai ulang otorisasi...\n"));
685
+ respError = "API key tidak valid atau sudah di-revoke. Memulai ulang otorisasi...";
686
+ repaint(false, true);
414
687
  try {
415
688
  saveConfig({ apiKey: void 0 });
416
689
  } catch {
@@ -422,42 +695,58 @@ Error (${res.status}): ${detail}
422
695
  userMeta = await fetchUserMeta(baseUrl, cfg.apiKey || "");
423
696
  return runPromptStream(query, true);
424
697
  }
425
- console.log(pad + import_chalk.default.red("Otorisasi ulang dibatalkan.\n"));
698
+ respError = "Otorisasi ulang dibatalkan.";
699
+ repaint(false, true);
426
700
  }
427
701
  } else {
428
702
  const reader = res.body?.getReader();
429
703
  const decoder = new TextDecoder();
430
704
  if (reader) {
705
+ let sseBuffer = "";
431
706
  while (true) {
432
707
  const { done, value } = await reader.read();
433
708
  if (done) break;
434
- const chunk = decoder.decode(value);
435
- const linesArr = chunk.split("\n");
436
- for (const l of linesArr) {
709
+ sseBuffer += decoder.decode(value, { stream: true });
710
+ const nlIdx = sseBuffer.lastIndexOf("\n");
711
+ if (nlIdx === -1) continue;
712
+ const complete = sseBuffer.slice(0, nlIdx);
713
+ sseBuffer = sseBuffer.slice(nlIdx + 1);
714
+ for (const l of complete.split("\n")) {
437
715
  if (l.startsWith("data: ") && l !== "data: [DONE]") {
438
716
  try {
439
717
  const p = JSON.parse(l.slice(6));
440
718
  const delta = p.choices?.[0]?.delta?.content;
441
- if (delta) process.stdout.write(delta);
719
+ if (delta) respText += delta;
442
720
  } catch {
443
721
  }
444
722
  }
445
723
  }
724
+ repaint(true);
446
725
  }
447
- console.log("\n");
726
+ repaint(false, true);
448
727
  }
449
728
  }
450
729
  } catch (err) {
451
- console.log(pad + import_chalk.default.red(`
452
- Error: ${err.message}
453
- `));
730
+ if (err?.name !== "AbortError") {
731
+ respError = err.message;
732
+ repaint(false, true);
733
+ } else {
734
+ respText += "\n[ dibatalkan oleh pengguna ]";
735
+ }
454
736
  }
455
- console.log(pad + import_chalk.default.gray("\nTekan sembarang tombol untuk kembali ke workspace..."));
456
- process.stdin.once("keypress", () => {
457
- isExecuting = false;
458
- currentTaskInput = "";
459
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
460
- });
737
+ let notes = [];
738
+ if (respText && !respError) {
739
+ notes = writeExtractedFiles(extractAllFiles(respText));
740
+ }
741
+ respStreaming = false;
742
+ lastTurn = { query, text: respText, streaming: false, error: respError, notes };
743
+ drawSessionScreen(lastTurn);
744
+ if (respError && !isRetry) {
745
+ conversationHistory.push({ role: "assistant", content: respText || "(error)" });
746
+ } else if (respText) {
747
+ conversationHistory.push({ role: "assistant", content: respText });
748
+ }
749
+ waitSessionKeypress();
461
750
  }
462
751
  async function handleAutoAuth(baseUrl2) {
463
752
  const sessionCode = "SA-" + import_crypto.default.randomBytes(4).toString("hex").toUpperCase();
@@ -515,8 +804,10 @@ async function startInteractiveSession() {
515
804
  drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
516
805
  process.stdout.on("resize", () => {
517
806
  if (inWorkspace) {
518
- if (!isExecuting) {
519
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
807
+ if (isExecuting) {
808
+ drawSessionScreen(lastTurn);
809
+ } else {
810
+ redrawInputScreen();
520
811
  }
521
812
  } else {
522
813
  drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
@@ -528,7 +819,50 @@ async function startInteractiveSession() {
528
819
  return;
529
820
  }
530
821
  if (inWorkspace) {
531
- if (isExecuting) return;
822
+ if (isExecuting) {
823
+ if (key.name === "escape" && activeAbort) {
824
+ activeAbort.abort();
825
+ }
826
+ return;
827
+ }
828
+ if (sessionScreen) {
829
+ if (key.name === "escape") {
830
+ leaveWorkspace();
831
+ return;
832
+ }
833
+ if (key.name === "return") {
834
+ const query = currentTaskInput.trim();
835
+ if (!query) return;
836
+ if (query === "/exit" || query === "exit" || query === "quit" || query === ":q") {
837
+ leaveWorkspace();
838
+ return;
839
+ }
840
+ if (query === "/model" || query === "/back") {
841
+ leaveWorkspace();
842
+ return;
843
+ }
844
+ if (query === "/new" || query === "/reset") {
845
+ conversationHistory = [];
846
+ respLastNotes = [];
847
+ enterWorkspace();
848
+ return;
849
+ }
850
+ conversationHistory.push({ role: "user", content: query });
851
+ runPromptStream(query);
852
+ return;
853
+ }
854
+ if (key.name === "backspace") {
855
+ currentTaskInput = currentTaskInput.slice(0, -1);
856
+ redrawInputScreen();
857
+ return;
858
+ }
859
+ if (str && !key.ctrl && !key.meta && !str.startsWith("\x1B")) {
860
+ currentTaskInput += str;
861
+ redrawInputScreen();
862
+ return;
863
+ }
864
+ return;
865
+ }
532
866
  if (key.name === "escape") {
533
867
  leaveWorkspace();
534
868
  return;
@@ -536,14 +870,21 @@ async function startInteractiveSession() {
536
870
  if (key.name === "return") {
537
871
  const query = currentTaskInput.trim();
538
872
  if (!query) return;
539
- if (query === "/exit" || query === "exit" || query === ":q") {
540
- cleanupAndExit();
873
+ if (query === "/exit" || query === "exit" || query === "quit" || query === ":q") {
874
+ leaveWorkspace();
541
875
  return;
542
876
  }
543
877
  if (query === "/model" || query === "/back") {
544
878
  leaveWorkspace();
545
879
  return;
546
880
  }
881
+ if (query === "/new" || query === "/reset") {
882
+ conversationHistory = [];
883
+ respLastNotes = [];
884
+ enterWorkspace();
885
+ return;
886
+ }
887
+ conversationHistory.push({ role: "user", content: query });
547
888
  runPromptStream(query);
548
889
  return;
549
890
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sarangai-cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "CLI resmi SarangAI — Gateway multi-model AI langsung dari terminal Anda",
5
5
  "main": "dist/index.js",
6
6
  "bin": {