@taj-special/dravix-code 1.1.28 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/repl.js CHANGED
@@ -289,7 +289,7 @@ function highlightCode(line, lang) {
289
289
  function applyInline(text) {
290
290
  return text
291
291
  .replace(/\*\*(.+?)\*\*|__(.+?)__/g, (_, a, b) => chalk.bold(a ?? b))
292
- .replace(/`([^`\n]+)`/g, (_, c) => chalk.hex('#fbbf24')(c))
292
+ .replace(/`([^`\n]+)`/g, (_, c) => '`' + chalk.hex('#fbbf24')(c) + '`')
293
293
  .replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, (_, t) => chalk.italic(t));
294
294
  }
295
295
  class MarkdownRenderer {
@@ -409,30 +409,40 @@ class MarkdownRenderer {
409
409
  this.tableRows = [];
410
410
  if (rows.length === 0)
411
411
  return '';
412
- const cols = Math.max(...rows.map(r => r.length));
412
+ // Filter out separator rows (--- | --- | ---)
413
+ const dataRows = rows.filter(r => !r.every(c => /^:?-+:?$/.test(c.trim())));
414
+ if (dataRows.length === 0)
415
+ return '';
416
+ const cols = Math.max(...dataRows.map(r => r.length));
413
417
  const termW = process.stdout.columns ?? 80;
414
418
  // max usable width per column (distribute evenly, min 6)
415
419
  const maxColW = Math.max(6, Math.floor((termW - 4 - (cols + 1)) / cols));
416
420
  const widths = Array(cols).fill(0);
417
- for (const row of rows) {
421
+ for (const row of dataRows) {
418
422
  for (let i = 0; i < cols; i++) {
419
- const cellLen = Math.min((row[i] ?? '').length, maxColW);
423
+ // Strip markdown formatting for width calculation
424
+ const rawCell = (row[i] ?? '').replace(/\*\*/g, '').replace(/`/g, '').trim();
425
+ const cellLen = Math.min(rawCell.length, maxColW);
420
426
  widths[i] = Math.max(widths[i], cellLen + 2);
421
427
  }
422
428
  }
423
429
  const bar = (l, m, r) => colors.dim(' ' + l + widths.map(w => '─'.repeat(w)).join(m) + r);
424
430
  let out = '\n' + bar('┌', '┬', '┐') + '\n';
425
- for (let ri = 0; ri < rows.length; ri++) {
426
- const row = rows[ri];
431
+ for (let ri = 0; ri < dataRows.length; ri++) {
432
+ const row = dataRows[ri];
427
433
  const cells = widths.map((w, i) => {
428
- const raw = row[i] ?? '';
429
- const cell = raw.length > w - 2 ? raw.slice(0, w - 3) + '…' : raw;
434
+ const raw = (row[i] ?? '').trim();
435
+ // Strip markdown for display but keep formatting
436
+ const plain = raw.replace(/\*\*/g, '').replace(/`/g, '');
437
+ const cell = plain.length > w - 2 ? plain.slice(0, w - 3) + '…' : plain;
430
438
  const pad = w - cell.length - 1;
439
+ // Apply formatting to the cell content
440
+ const formatted = applyInline(raw);
431
441
  const text = ri === 0 ? chalk.bold.white(cell) : colors.ai(cell);
432
442
  return ' ' + text + ' '.repeat(Math.max(pad, 0));
433
443
  });
434
444
  out += colors.dim(' │') + cells.join(colors.dim('│')) + colors.dim('│') + '\n';
435
- if (ri === 0 && rows.length > 1)
445
+ if (ri === 0 && dataRows.length > 1)
436
446
  out += bar('├', '┼', '┤') + '\n';
437
447
  }
438
448
  out += bar('└', '┴', '┘') + '\n';
@@ -462,14 +472,14 @@ class MarkdownRenderer {
462
472
  const capped = Math.min(inner, maxInner);
463
473
  const dashAfterLang = Math.max(0, capped - lang.length - 2);
464
474
  const top = lang
465
- ? colors.dim(' ╭─ ') + chalk.hex('#818cf8')(lang) + colors.dim(' ' + '─'.repeat(dashAfterLang))
475
+ ? colors.dim(' ╭─ ') + colors.primary(lang) + colors.dim(' ' + '─'.repeat(dashAfterLang))
466
476
  : colors.dim(' ╭' + '─'.repeat(capped + 2));
467
477
  const bot = colors.dim(' ╰' + '─'.repeat(capped + 2));
468
478
  const canHighlight = lang && !['text', 'txt', 'plain', 'output', 'log'].includes(lang.toLowerCase());
469
479
  let out = '\n' + top + '\n';
470
480
  for (const l of lines) {
471
481
  const display = l.length > capped ? l.slice(0, capped - 1) + '…' : l;
472
- const colorized = canHighlight ? highlightCode(display, lang) : chalk.hex('#e2e8f0')(display);
482
+ const colorized = canHighlight ? highlightCode(display, lang) : colors.text(display);
473
483
  out += colors.dim(' │ ') + colorized + '\n';
474
484
  }
475
485
  out += bot + '\n';
@@ -612,15 +622,15 @@ function printDiffPreview(op, cwd) {
612
622
  if (adds === 0 && removes === 0)
613
623
  return 0;
614
624
  const statStr = [
615
- removes > 0 ? chalk.hex('#f87171').bold(`-${removes}`) : '',
616
- adds > 0 ? chalk.hex('#34d399').bold(`+${adds}`) : '',
625
+ removes > 0 ? colors.error.bold(`-${removes}`) : '',
626
+ adds > 0 ? colors.success.bold(`+${adds}`) : '',
617
627
  ].filter(Boolean).join(' ');
618
628
  const writeLine = (s) => { process.stdout.write(s + '\n'); printed++; };
619
629
  process.stdout.write('\n');
620
630
  printed++;
621
- writeLine(chalk.hex('#fbbf24')(' ~') +
622
- chalk.hex('#94a3b8')(' ') +
623
- chalk.hex('#e2e8f0')(op.path ?? '') +
631
+ writeLine(colors.warn(' ~') +
632
+ colors.subtext(' ') +
633
+ colors.text(op.path ?? '') +
624
634
  (statStr ? ' ' + statStr : ''));
625
635
  const CONTEXT = 2;
626
636
  const show = new Uint8Array(diff.length);
@@ -633,15 +643,15 @@ function printDiffPreview(op, cwd) {
633
643
  const renderLine = (line) => {
634
644
  const num = String(line.lineNo).padStart(5, ' ');
635
645
  if (line.kind === 'remove') {
636
- const content = ` ${num} ${chalk.hex('#f87171')('-')} ${clip(line.text)}`;
637
- writeLine(' ' + chalk.hex('#7f1d1d')('▌') + chalk.bgHex('#1c0a0a')(content.padEnd(cols - 3)));
646
+ const content = ` ${num} ${colors.error('-')} ${clip(line.text)}`;
647
+ writeLine(' ' + colors.error('▌') + chalk.bgHex('#1c0a0a')(content.padEnd(cols - 3)));
638
648
  }
639
649
  else if (line.kind === 'add') {
640
- const content = ` ${num} ${chalk.hex('#34d399')('+')} ${clip(line.text)}`;
641
- writeLine(' ' + chalk.hex('#065f46')('▌') + chalk.bgHex('#021a0e')(content.padEnd(cols - 3)));
650
+ const content = ` ${num} ${colors.success('+')} ${clip(line.text)}`;
651
+ writeLine(' ' + colors.success('▌') + chalk.bgHex('#021a0e')(content.padEnd(cols - 3)));
642
652
  }
643
653
  else {
644
- writeLine(chalk.hex('#374151')(` ${num} ${clip(line.text)}`));
654
+ writeLine(colors.dim(` ${num} ${clip(line.text)}`));
645
655
  }
646
656
  };
647
657
  const hunks = [];
@@ -665,7 +675,7 @@ function printDiffPreview(op, cwd) {
665
675
  if (shown >= MAX_LINES)
666
676
  break;
667
677
  if (h > 0) {
668
- writeLine(chalk.hex('#374151')(' ╌╌╌'));
678
+ writeLine(colors.dim(' ╌╌╌'));
669
679
  }
670
680
  const hunk = hunks[h];
671
681
  const before = [];
@@ -708,7 +718,7 @@ function printDiffPreview(op, cwd) {
708
718
  }
709
719
  const total = diff.filter(d => d.kind !== 'context').length;
710
720
  if (total > MAX_LINES) {
711
- writeLine(chalk.hex('#4b5563')(` … ${total - MAX_LINES} more lines`));
721
+ writeLine(colors.muted(` … ${total - MAX_LINES} more lines`));
712
722
  }
713
723
  }
714
724
  catch { /* ignore */ }
@@ -734,7 +744,6 @@ async function askPermission(label, key, alwaysAllowed, noAlways, diffShown, pre
734
744
  drawn = true;
735
745
  let lines;
736
746
  if (diffShown) {
737
- // Diff already shown above — just show options, no redundant box
738
747
  lines = [
739
748
  '',
740
749
  ...optLabels.map((lbl, i) => i === sel
@@ -846,25 +855,22 @@ async function readLine(prompt, cwd) {
846
855
  let suffix = '';
847
856
  let ctrlCCount = 0;
848
857
  let ctrlCTimer = null;
849
- // @ file-completion state
850
858
  let atMode = false;
851
859
  let atQuery = '';
852
860
  let atSel = 0;
853
- let atBoxLines = 0; // total picker box lines on screen (header + items + footer)
861
+ let atBoxLines = 0;
854
862
  let allFiles = [];
855
- // / slash-command picker state
856
863
  let slashMode = false;
857
864
  let slashQuery = '';
858
865
  let slashSel = 0;
859
866
  let slashBoxLines = 0;
860
867
  let slashDrawn = false;
861
- // Tab path-autocomplete state
862
868
  let tabMode = false;
863
869
  let tabQuery = '';
864
870
  let tabSel = 0;
865
871
  let tabBoxLines = 0;
866
872
  let tabDrawn = false;
867
- let tabPreWord = ''; // prefix text before the tab-completed token
873
+ let tabPreWord = '';
868
874
  function onResize() {
869
875
  const newCols = process.stdout.columns ?? 80;
870
876
  const oldSepRows = Math.ceil(sepVisualLen / newCols);
@@ -979,9 +985,9 @@ async function readLine(prompt, cwd) {
979
985
  atDrawn = true;
980
986
  }
981
987
  atBoxLines = 0;
982
- const inputLine = colors.primary(' › ') + chalk.white(prefix) +
983
- (pasteBlock !== null ? colors.muted(`[paste: ${pasteCount} lines]`) + chalk.white(suffix) : '') +
984
- chalk.hex('#818cf8')('@') + chalk.white(atQuery);
988
+ const inputLine = colors.primary(' › ') + colors.text(prefix) +
989
+ (pasteBlock !== null ? colors.muted(`[paste: ${pasteCount} lines]`) + colors.text(suffix) : '') +
990
+ colors.primary('@') + colors.text(atQuery);
985
991
  process.stdout.write(inputLine + '\r\n' + SEP + '\r\n');
986
992
  const DW = Math.min(Math.max((process.stdout.columns ?? 80) - 6, 44), 72);
987
993
  const IW = DW - 6;
@@ -1009,8 +1015,8 @@ async function readLine(prompt, cwd) {
1009
1015
  const pad = ' '.repeat(Math.max(IW - clip.length, 0));
1010
1016
  const name = sel && isDir ? colors.primary.bold(clip) + pad
1011
1017
  : sel ? colors.primary.bold(clip) + pad
1012
- : isDir ? chalk.hex('#38bdf8')(clip) + pad
1013
- : chalk.hex('#94a3b8')(clip + pad);
1018
+ : isDir ? chalk.hex('#74b9ff')(clip) + pad
1019
+ : colors.subtext(clip + pad);
1014
1020
  process.stdout.write(colors.dim(' │') + ` ${mark} ` + name + colors.dim('│') + '\n');
1015
1021
  atBoxLines++;
1016
1022
  }
@@ -1048,7 +1054,7 @@ async function readLine(prompt, cwd) {
1048
1054
  slashDrawn = true;
1049
1055
  }
1050
1056
  slashBoxLines = 0;
1051
- const inputLine = colors.primary(' › ') + colors.primary('/') + chalk.white(slashQuery);
1057
+ const inputLine = colors.primary(' › ') + colors.primary('/') + colors.text(slashQuery);
1052
1058
  process.stdout.write(inputLine + '\r\n' + SEP + '\r\n');
1053
1059
  const DW = Math.min(Math.max((process.stdout.columns ?? 80) - 6, 44), 72);
1054
1060
  const nameW = 10;
@@ -1074,8 +1080,8 @@ async function readLine(prompt, cwd) {
1074
1080
  const nameStr = cmd.name.padEnd(nameW);
1075
1081
  const descStr = cmd.desc.length > descW ? cmd.desc.slice(0, descW - 1) + '…' : cmd.desc;
1076
1082
  const descPad = ' '.repeat(Math.max(descW - descStr.length, 0));
1077
- const namePaint = sel ? colors.primary.bold(nameStr) : chalk.hex('#818cf8')(nameStr);
1078
- const descPaint = sel ? colors.muted(descStr) : chalk.hex('#4b5563')(descStr);
1083
+ const namePaint = sel ? colors.primary.bold(nameStr) : colors.primary(nameStr);
1084
+ const descPaint = sel ? colors.muted(descStr) : colors.subtext(descStr);
1079
1085
  process.stdout.write(colors.dim(' │') + ` ${mark} ` + namePaint + ' ' + descPaint + descPad + colors.dim('│') + '\n');
1080
1086
  slashBoxLines++;
1081
1087
  }
@@ -1113,8 +1119,8 @@ async function readLine(prompt, cwd) {
1113
1119
  tabDrawn = true;
1114
1120
  }
1115
1121
  tabBoxLines = 0;
1116
- const TC = chalk.hex('#34d399');
1117
- const inputLine = colors.primary(' › ') + chalk.white(tabPreWord) + TC(tabQuery);
1122
+ const TC = colors.success;
1123
+ const inputLine = colors.primary(' › ') + colors.text(tabPreWord) + TC(tabQuery);
1118
1124
  process.stdout.write(inputLine + '\r\n' + SEP + '\r\n');
1119
1125
  const DW = Math.min(Math.max((process.stdout.columns ?? 80) - 6, 44), 72);
1120
1126
  const IW = DW - 6;
@@ -1141,8 +1147,8 @@ async function readLine(prompt, cwd) {
1141
1147
  const clip = f.length > IW ? f.slice(0, IW - 1) + '…' : f;
1142
1148
  const pad = ' '.repeat(Math.max(IW - clip.length, 0));
1143
1149
  const name = sel ? TC.bold(clip) + pad
1144
- : isDir ? chalk.hex('#38bdf8')(clip) + pad
1145
- : chalk.hex('#94a3b8')(clip + pad);
1150
+ : isDir ? chalk.hex('#74b9ff')(clip) + pad
1151
+ : colors.subtext(clip + pad);
1146
1152
  process.stdout.write(colors.dim(' │') + ` ${mark} ` + name + colors.dim('│') + '\n');
1147
1153
  tabBoxLines++;
1148
1154
  }
@@ -1178,11 +1184,11 @@ async function readLine(prompt, cwd) {
1178
1184
  displayPre = '…' + displayPre.slice(-(maxLen - 1));
1179
1185
  let inp;
1180
1186
  if (pasteBlock !== null) {
1181
- inp = colors.primary(' › ') + chalk.white(displayPre) +
1182
- colors.muted(`[paste: ${pasteCount} lines]`) + chalk.white(suffix);
1187
+ inp = colors.primary(' › ') + colors.text(displayPre) +
1188
+ colors.muted(`[paste: ${pasteCount} lines]`) + colors.text(suffix);
1183
1189
  }
1184
1190
  else {
1185
- inp = colors.primary(' › ') + chalk.white(displayPre);
1191
+ inp = colors.primary(' › ') + colors.text(displayPre);
1186
1192
  }
1187
1193
  process.stdout.write('\r\x1b[K' + inp);
1188
1194
  process.stdout.write('\r\n' + SEP + '\x1b[1A\r' + inp);
@@ -2120,12 +2126,63 @@ export async function startRepl(cwd) {
2120
2126
  // Rules appended to every system prompt to enforce safe file-operation behavior
2121
2127
  const SAFE_FILE_RULES = `
2122
2128
 
2123
- ## CRITICAL RULES — Follow these on every response
2129
+ ## CORE RULES
2130
+
2131
+ ### READ FIRST, THEN ANSWER
2132
+ When the user asks about their code:
2133
+ 1. Find the relevant file(s)
2134
+ 2. Read them silently
2135
+ 3. Give a direct, accurate answer based on what you found
2136
+ Never guess or fabricate information.
2137
+
2138
+ ### BE ACCURATE
2139
+ - Only report what you actually see in the code
2140
+ - If you are unsure, say so — do not fabricate answers
2141
+ - Never add features or items that do not exist in the code
2142
+
2143
+ ### RESPOND IN THE SAME LANGUAGE
2144
+ - Respond in the same language the user writes in
2145
+ - If user writes in English → respond in English
2146
+ - If user writes in Russian → respond in Russian
2147
+ - And so on for any language
2148
+
2149
+ ### FORMAT ANSWERS CLEARLY
2150
+ - Use bullet points or numbered lists for multiple items
2151
+ - Put each item on its own line
2152
+ - Keep answers concise — 3-10 lines maximum
2153
+ - Do not dump entire file contents unless asked
2154
+ - Do not add unnecessary commentary
2155
+
2156
+ ### WORK SILENTLY
2157
+ - Do not say "I will read the file" — just read it and answer
2158
+ - Do not narrate your thought process
2159
+ - Do not say "I already answered your question"
2160
+ - Give your answer ONCE and STOP
2161
+
2162
+ ### WHEN CREATING/EDITING FILES
2163
+ - write_file = CREATE new files only
2164
+ - edit_file = MODIFY existing (read first, then find/replace)
2165
+ - Before editing: ALWAYS read_file first
2166
+ - Complete the ENTIRE task in ONE response
2167
+ - Use tags, NOT code blocks
2168
+
2169
+ ### WHEN CREATING/EDITING FILES
2170
+ - write_file = CREATE new files only
2171
+ - edit_file = MODIFY existing (read first, then find/replace)
2172
+ - Before editing: ALWAYS read_file first
2173
+ - Multiple edits: order from BOTTOM to TOP
2174
+ - Complete the ENTIRE task in ONE response
2175
+ - Use tags, NOT code blocks
2176
+
2177
+ ### When the user asks you to do something:
2178
+ 1. Read the relevant files first
2179
+ 2. Do the work using <write_file> / <edit_file> tags
2180
+ 3. Summarize what you did in 1-2 sentences
2124
2181
 
2125
2182
  ### Act decisively
2126
2183
  - Use judgment and execute immediately when intent is clear.
2127
2184
  - Do NOT ask unnecessary clarifying questions — just do the task.
2128
- - Make reasonable assumptions and proceed. State the assumption briefly if needed.
2185
+ - Make reasonable assumptions and proceed.
2129
2186
 
2130
2187
  ### Creating files
2131
2188
  - NEVER show code in code blocks when the user asks to create files or a project.
@@ -2139,9 +2196,39 @@ export async function startRepl(cwd) {
2139
2196
  - Before editing any file: use <read_file> to see its current content first.
2140
2197
  - NEVER overwrite an existing file with write_file — it destroys user code.
2141
2198
 
2199
+ ### How to use edit_file correctly
2200
+ 1. ALWAYS read_file first to see the current content
2201
+ 2. Copy the EXACT text from the file into <find> — character by character
2202
+ 3. The <find> text must match EXACTLY (including whitespace, indentation)
2203
+ 4. If edit fails, the system will show you the actual content — use it to retry
2204
+ 5. Do NOT put line numbers in <find> (e.g., "42 │ code" is WRONG)
2205
+ 6. Do NOT modify the text in <find> — copy it EXACTLY from the file
2206
+
2207
+ ### Searching and navigating
2208
+ - Use <glob_files pattern="**/*.ts" /> to find files by extension
2209
+ - Use <search_code pattern="functionName" /> to find code
2210
+ - Use <search_code pattern="regex.*pattern" regex="true" /> for regex search
2211
+ - Use <search_code pattern="error" include="*.ts" /> to search specific file types
2212
+ - Use <search_code pattern="TODO" context="2" /> to show context around matches
2213
+ - Read files before editing them — always!
2214
+
2215
+ ### Multi-file tasks
2216
+ - Plan all files FIRST (list them in your response)
2217
+ - Create/edit files in LOGICAL ORDER (dependencies first)
2218
+ - Use write_file for ALL new files
2219
+ - Use edit_file for ALL modifications
2220
+ - Run commands LAST (after all files are created)
2221
+
2222
+ ### Error recovery
2223
+ - If an edit_file fails, read the file again and retry with the actual content
2224
+ - If a run_command fails, read the error message and fix the issue
2225
+ - Never give up — keep trying until the task is complete
2226
+
2142
2227
  ### Scope
2143
2228
  - Only touch files the user explicitly mentioned or that are clearly required by the task.
2144
- - Do NOT restructure, rename, or "improve" anything that wasn't asked.`;
2229
+ - Do NOT restructure, rename, or "improve" anything that wasn't asked.
2230
+ - When creating a new project, create ALL necessary files (not just some).
2231
+ - When fixing a bug, fix the ROOT CAUSE, not just the symptom.`;
2145
2232
  const buildSystemMsg = (dir) => ({
2146
2233
  role: 'system',
2147
2234
  content: SYSTEM_PROMPT + SAFE_FILE_RULES + '\n\nProject context:\n' + buildContext(dir),
@@ -2160,6 +2247,10 @@ export async function startRepl(cwd) {
2160
2247
  let pendingOutputTokens = 0;
2161
2248
  let pendingUserTokens = 0;
2162
2249
  let hasPendingReport = false;
2250
+ // Auto-retry state
2251
+ const MAX_AUTO_RETRIES = 3;
2252
+ let autoRetryCount = 0;
2253
+ let lastFailedOp = null;
2163
2254
  process.stdin.once('end', () => {
2164
2255
  console.log(colors.muted('\n Goodbye!\n'));
2165
2256
  process.exit(0);
@@ -2312,6 +2403,8 @@ export async function startRepl(cwd) {
2312
2403
  if (!skipInput) {
2313
2404
  readFileTurnCount = 0; // reset loop counter for each new user message
2314
2405
  forcedEditMode = false; // reset forced edit mode for each new user message
2406
+ autoRetryCount = 0; // reset auto-retry counter for each new user message
2407
+ lastFailedOp = null;
2315
2408
  // ── Refresh project context (files may have changed since last turn) ──
2316
2409
  history[0] = buildSystemMsg(activeCwd);
2317
2410
  // ── Clear stale operational messages left from previous auto-continue turns ──
@@ -2470,7 +2563,7 @@ export async function startRepl(cwd) {
2470
2563
  }
2471
2564
  catch { /* ignore */ }
2472
2565
  }
2473
- const previewLines = (op.type === 'write' || op.type === 'edit') ? printDiffPreview(op, activeCwd) : 0;
2566
+ const previewLines = (!alwaysAllowed.has(key) && (op.type === 'write' || op.type === 'edit')) ? printDiffPreview(op, activeCwd) : 0;
2474
2567
  const allowed = await askPermission(label, key, alwaysAllowed, op.type === 'run', previewLines > 0, previewLines);
2475
2568
  executedInlineOps.add(inlineOpFingerprint(op));
2476
2569
  if (!allowed) {
@@ -2506,8 +2599,7 @@ export async function startRepl(cwd) {
2506
2599
  const cur = fs.readFileSync(fp, 'utf-8');
2507
2600
  const curLines = cur.split('\n');
2508
2601
  const nLines = curLines.length;
2509
- const isDeleteOp = !op.replace || op.replace.trim() === '';
2510
- // Auto-search for what AI was trying to find — inject exact block
2602
+ // Smart error recovery: find approximate location and provide fix instructions
2511
2603
  const findLines = (op.find ?? '').split('\n').map(l => l.replace(/^\s*\d+\s*[│|:]\s?/, '').trim()).filter(Boolean);
2512
2604
  const searchTerm = findLines[0]?.slice(0, 60) ?? '';
2513
2605
  let foundAt = -1;
@@ -2520,27 +2612,51 @@ export async function startRepl(cwd) {
2520
2612
  }
2521
2613
  }
2522
2614
  if (foundAt >= 0) {
2523
- // Found the approximate location — inject surrounding block
2524
- const start = Math.max(0, foundAt - 2);
2525
- const end = Math.min(curLines.length - 1, foundAt + 100);
2615
+ // Found approximate location — show surrounding context
2616
+ const start = Math.max(0, foundAt - 3);
2617
+ const end = Math.min(curLines.length - 1, foundAt + 50);
2526
2618
  const block = curLines.slice(start, end + 1).join('\n');
2527
- errMsg += `\n\n[FOUND near line ${foundAt + 1} in ${op.path}]:\n${block}\n`;
2528
- errMsg += isDeleteOp
2529
- ? `\nDELETE FIX: Copy the EXACT full container (from its opening <div>/<section> tag through the matching closing tag) from above. Put it in <edit_file path="${op.path}"><find>EXACT_TEXT</find><replace></replace>. Text must match CHARACTER FOR CHARACTER.`
2530
- : `\nEDIT FIX: Copy the EXACT text you want to change from above. Use <edit_file path="${op.path}"><find>EXACT_TEXT</find><replace>NEW_TEXT</replace>.`;
2619
+ errMsg += `\n\n[FILE CONTENT ${op.path} lines ${start + 1}-${end + 1}]:\n${block}`;
2620
+ errMsg += `\n\nFIX: The edit failed. Copy the EXACT text from the file above into <find>. Do NOT include line numbers.`;
2531
2621
  }
2532
2622
  else if (nLines <= 400) {
2533
- errMsg += `\n\nFull content of ${op.path} (${nLines} lines):\n${cur}\n\nFIX: Copy the EXACT text from above into <find>. Character-for-character, no changes.`;
2623
+ // File is small show full content
2624
+ errMsg += `\n\n[FILE CONTENT — ${op.path} (${nLines} lines)]:\n${cur}`;
2625
+ errMsg += `\n\nFIX: Copy the EXACT text from the file above into <find>.`;
2534
2626
  }
2535
2627
  else {
2536
- const safe = searchTerm.replace(/['"<>{}()\[\]]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 35);
2537
- errMsg += `\n\nFIX STEPS:\n1. <search_code pattern="${safe}" path="${op.path}"/>\n2. <read_file path="${op.path}" lines="N-M"/> (use line numbers from search)\n3. Copy EXACT text into <find> — no modifications, no line numbers`;
2628
+ // File is large show first 100 lines and instruct to search
2629
+ const preview = curLines.slice(0, 100).join('\n');
2630
+ errMsg += `\n\n[FILE CONTENT — ${op.path} (first 100 of ${nLines} lines)]:\n${preview}`;
2631
+ errMsg += `\n\nFIX: Use <read_file path="${op.path}" lines="N-M"/> to read the correct section, then copy EXACT text into <find>.`;
2538
2632
  }
2633
+ // Auto-retry: inject file content and force continuation
2634
+ history.push({
2635
+ role: 'system',
2636
+ content: `[AUTO-RETRY] The edit failed. Here is the current file content:\n\n${nLines <= 200 ? cur : curLines.slice(0, 200).join('\n')}\n\nRetry the edit using the EXACT content above. Copy text CHARACTER FOR CHARACTER into <find>.`,
2637
+ });
2638
+ readFileContinue = true;
2539
2639
  }
2540
2640
  }
2541
2641
  catch { /* ignore */ }
2542
2642
  }
2543
2643
  inlineFileOpErrors.push(errMsg);
2644
+ // Auto-retry logic: if edit_file fails, automatically retry with updated context
2645
+ if (op.type === 'edit' && autoRetryCount < MAX_AUTO_RETRIES) {
2646
+ autoRetryCount++;
2647
+ lastFailedOp = op.path ?? null;
2648
+ }
2649
+ else if (autoRetryCount >= MAX_AUTO_RETRIES) {
2650
+ // Reset retry counter after max retries — prevent infinite loop
2651
+ autoRetryCount = 0;
2652
+ lastFailedOp = null;
2653
+ // Tell AI to stop retrying this specific edit
2654
+ history.push({
2655
+ role: 'system',
2656
+ content: `[STOP] Edit failed ${MAX_AUTO_RETRIES} times. Do NOT retry this specific edit. Move on to other tasks or ask the user for help.`,
2657
+ });
2658
+ readFileContinue = true;
2659
+ }
2544
2660
  }
2545
2661
  else if ((opResult.type === 'modified' || opResult.type === 'created') && op.path &&
2546
2662
  (op.type === 'edit' || op.type === 'write')) {
@@ -2737,9 +2853,11 @@ export async function startRepl(cwd) {
2737
2853
  return;
2738
2854
  }
2739
2855
  const normalized = normalizeResponse(fullResponse);
2740
- lastAIResponse = normalized;
2741
- if (normalized.trim()) {
2742
- history.push({ role: 'assistant', content: normalized });
2856
+ // Clean up the response — remove trailing newlines
2857
+ const stripped = normalized.replace(/\n+$/, '').trim();
2858
+ lastAIResponse = stripped;
2859
+ if (stripped.trim()) {
2860
+ history.push({ role: 'assistant', content: stripped });
2743
2861
  }
2744
2862
  if (conversationTitle === null) {
2745
2863
  const _msgs = history.slice(1).filter(m => m.role === 'user' || m.role === 'assistant');
@@ -2777,16 +2895,38 @@ export async function startRepl(cwd) {
2777
2895
  history.push({ role: 'system', content: inlineFileOpErrors.join('\n\n') + '\n\nFix the above errors and retry the failed operations.' });
2778
2896
  readFileContinue = true;
2779
2897
  }
2780
- const allOps = parseOps(normalized);
2898
+ const allOps = parseOps(fullResponse);
2781
2899
  // ── Detect incomplete plan: AI described steps but didn't output tags ──
2782
2900
  if (!readFileContinue && !streamCancelled) {
2783
2901
  const hasAnyReadOp = allOps.some(op => op.type === 'read_file' || op.type === 'read_folder' || op.type === 'search_code');
2902
+ const hasAnyWriteOp = allOps.some(op => op.type === 'write' || op.type === 'edit' || op.type === 'delete' || op.type === 'mkdir' || op.type === 'run');
2784
2903
  const writeOps = allOps.filter(op => op.type !== 'read_file' && op.type !== 'read_folder' && op.type !== 'search_code').length;
2785
- const plannedSteps = (normalized.match(/^\s*[2-9]\.\s+\S/mg) ?? []).length;
2786
- const hasCodeFence = normalized.includes('```');
2787
- const hasStepList = (normalized.match(/^\s*\d+[.)]\s+\S/mg) ?? []).length >= 2;
2904
+ const plannedSteps = (stripped.match(/^\s*[2-9]\.\s+\S/mg) ?? []).length;
2905
+ const hasCodeFence = stripped.includes('```');
2906
+ const hasStepList = (stripped.match(/^\s*\d+[.)]\s+\S/mg) ?? []).length >= 2;
2907
+ // Case 0: AI only output read/search ops with NO substantial text answer — force continuation
2908
+ // BUT: if AI wrote a substantial answer along with read ops, it already answered
2909
+ const textLength = stripped.replace(/<[^>]+>/g, '').trim().length;
2910
+ // Also check if the answer contains useful content (bullet points, lists, specific info)
2911
+ const hasUsefulContent = /[•\-\*]\s|^\d+\.\s|\b\d+\s+(забон|function|file|language|item)/m.test(stripped);
2912
+ // Check for loop: if same answer was given before, STOP
2913
+ const isDuplicate = lastAIResponse && stripped === lastAIResponse;
2914
+ // Check if answer contains specific info (numbers, names, etc.)
2915
+ const hasSpecificInfo = /\d+\s+(забон|function|file|language|item|line|row)|`\w+`/i.test(stripped);
2916
+ // Check if AI already gave a complete answer (more than 3 lines or contains list items)
2917
+ const hasCompleteAnswer = stripped.split('\n').length > 3 || /\n\s*[-•*]\s/.test(stripped) || /\n\s*\d+\.\s/.test(stripped);
2918
+ // Check if this is a repeat of recent answers (compare first 50 chars)
2919
+ const recentAnswerPrefix = lastAIResponse.slice(0, 50);
2920
+ const currentPrefix = stripped.slice(0, 50);
2921
+ const isSimilarToRecent = recentAnswerPrefix.length > 10 && currentPrefix === recentAnswerPrefix;
2922
+ if (hasAnyReadOp && !hasAnyWriteOp && readFileTurnCount < 3 && textLength < 20 && !hasUsefulContent && !isDuplicate && !hasSpecificInfo && !hasCompleteAnswer && !isSimilarToRecent) {
2923
+ // AI read files but didn't produce any answer — force it to answer
2924
+ const readPrompt = `[FILE CONTENTS LOADED] The files have been read and their contents are now in your context above.\n\nThe user's question was: "${lastUserLine.slice(0, 300)}"\n\nANSWER THE QUESTION NOW based on what you read. Do NOT say "let me read" — you already read the files. Give a DIRECT, SPECIFIC answer. Do NOT repeat yourself.`;
2925
+ history.push({ role: 'system', content: readPrompt });
2926
+ readFileContinue = true;
2927
+ }
2788
2928
  // Case 1: AI listed numbered steps (2+) but produced zero file ops
2789
- if (plannedSteps >= 1 && writeOps === 0 && !hasAnyReadOp) {
2929
+ else if (plannedSteps >= 1 && writeOps === 0 && !hasAnyReadOp) {
2790
2930
  history.push({ role: 'system', content: 'CRITICAL ERROR: You described steps but produced NO file operation tags. You MUST output <write_file> or <edit_file> tags to create/modify actual files. Do NOT show code in ``` blocks — use <write_file path="filename"> tags instead. Output all the tags NOW. No explanation.' });
2791
2931
  readFileContinue = true;
2792
2932
  // Case 2: AI showed code blocks + numbered list but no tags — regardless of language
@@ -2849,11 +2989,11 @@ export async function startRepl(cwd) {
2849
2989
  return;
2850
2990
  }
2851
2991
  const cols = process.stdout.columns ?? 80;
2852
- const boxW = Math.min(Math.max(cols - 2, 52), 100);
2853
- const BC = chalk.hex('#4338ca');
2854
- const FC = chalk.hex('#e2e8f0');
2855
- const RIGHT_W = 22;
2856
- const LABEL_W = Math.max(boxW - 11 - RIGHT_W, 8);
2992
+ const boxW = Math.min(Math.max(cols - 4, 50), 80);
2993
+ const BC = colors.dim;
2994
+ const FC = colors.text;
2995
+ const RIGHT_W = 10;
2996
+ const LABEL_W = Math.max(boxW - 11 - RIGHT_W, 10);
2857
2997
  const results = [];
2858
2998
  for (const op of validOps) {
2859
2999
  let spinLabel = '';
@@ -2890,7 +3030,6 @@ export async function startRepl(cwd) {
2890
3030
  const lastLine = (res.output ?? '').split('\n').filter(Boolean).pop() ?? '';
2891
3031
  rightCol = chalk.hex('#6b7280')(lastLine.startsWith('Total:') ? lastLine.slice(7).trim() : `${ms}ms`);
2892
3032
  displayLabel = `"${op.pattern}"`;
2893
- // Auto-read section around first match so AI has exact text without another round-trip
2894
3033
  const firstMatchLine = (res.output ?? '').match(/^([^\n:]+):(\d+):/m);
2895
3034
  if (firstMatchLine) {
2896
3035
  const matchFile = firstMatchLine[1].trim();
@@ -2919,7 +3058,6 @@ export async function startRepl(cwd) {
2919
3058
  ctxLabel = `[File content — ${res.message ?? op.path}]${lineTag}:\n${res.output ?? ''}`;
2920
3059
  rightCol = chalk.hex('#6b7280')(op.lines ? `lines ${op.lines}` : `${ms}ms`);
2921
3060
  displayLabel = op.lines ? `${op.path ?? ''} :${op.lines}` : (op.path ?? '');
2922
- // Auto-continue paginated reads: if file has more pages, queue next page automatically
2923
3061
  const pageMatch = (res.output ?? '').match(/\.\.\. \((\d+) more lines — next page: lines (\d+)-(\d+)\)/);
2924
3062
  if (pageMatch && op.path) {
2925
3063
  const [, , nextStart, nextEnd] = pageMatch;
@@ -2927,7 +3065,6 @@ export async function startRepl(cwd) {
2927
3065
  role: 'system',
2928
3066
  content: `[Auto-reading next page of ${op.path}...]`,
2929
3067
  });
2930
- // Queue the next page read by injecting it as a system op
2931
3068
  validOps.push({ type: 'read_file', path: op.path, lines: `${nextStart}-${nextEnd}` });
2932
3069
  }
2933
3070
  }
@@ -3006,7 +3143,7 @@ export async function startRepl(cwd) {
3006
3143
  continue;
3007
3144
  }
3008
3145
  }
3009
- const previewLines2 = (op.type === 'write' || op.type === 'edit') ? printDiffPreview(op, activeCwd) : 0;
3146
+ const previewLines2 = (!alwaysAllowed.has(key) && (op.type === 'write' || op.type === 'edit')) ? printDiffPreview(op, activeCwd) : 0;
3010
3147
  const allowed = await askPermission(label, key, alwaysAllowed, op.type === 'run', previewLines2 > 0, previewLines2);
3011
3148
  if (!allowed) {
3012
3149
  if (op.path)
@@ -3024,7 +3161,7 @@ export async function startRepl(cwd) {
3024
3161
  stageSpinFrame = 0;
3025
3162
  stageSpinInterval = setInterval(() => {
3026
3163
  const f = SPINNER_FRAMES[stageSpinFrame++ % SPINNER_FRAMES.length];
3027
- process.stdout.write(`\r\x1b[K ${colors.muted(f)} ${chalk.hex('#94a3b8')(stageSpinMsg)}`);
3164
+ process.stdout.write(`\r\x1b[K ${colors.muted(f)} ${colors.subtext(stageSpinMsg)}`);
3028
3165
  }, 80);
3029
3166
  }
3030
3167
  });
@@ -3093,6 +3230,19 @@ export async function startRepl(cwd) {
3093
3230
  ? `[Command failed — ${r.cmd}]:\n${r.output}`
3094
3231
  : `[Command output — ${r.cmd}]:\n${r.output}`).join('\n\n');
3095
3232
  history.push({ role: 'system', content });
3233
+ // Auto-retry for failed commands
3234
+ const failedCmds = runOutputs.filter(r => r.isError);
3235
+ if (failedCmds.length > 0 && autoRetryCount < MAX_AUTO_RETRIES) {
3236
+ autoRetryCount++;
3237
+ const retryHint = `[Auto-retry ${autoRetryCount}/${MAX_AUTO_RETRIES}] ` +
3238
+ `Previous command(s) failed. Analyze the error and try again with a fix. ` +
3239
+ `Common fixes: check file paths, install missing dependencies, verify syntax.`;
3240
+ history.push({ role: 'system', content: retryHint });
3241
+ readFileContinue = true;
3242
+ }
3243
+ else if (autoRetryCount >= MAX_AUTO_RETRIES) {
3244
+ autoRetryCount = 0;
3245
+ }
3096
3246
  // Only auto-continue if there is real output to react to, or an error to explain
3097
3247
  const needsReaction = runOutputs.some(r => r.isError || r.output !== NO_OUTPUT_MARKER);
3098
3248
  if (needsReaction)