codeep 1.2.86 → 1.2.88

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.
@@ -190,12 +190,17 @@ export async function handleCommand(input, session, onChunk, abortSignal) {
190
190
  return { handled: true, response: `Write access granted for \`${ctx?.name || session.workspaceRoot}\`` };
191
191
  }
192
192
  case 'lang': {
193
+ const validLangs = ['auto', 'en', 'zh', 'es', 'hi', 'ar', 'pt', 'fr', 'de', 'ja', 'ru', 'hr'];
193
194
  if (!args.length) {
194
195
  const current = config.get('language') || 'auto';
195
- return { handled: true, response: `Current language: \`${current}\`. Usage: \`/lang <code>\` (e.g. \`en\`, \`hr\`, \`auto\`)` };
196
+ return { handled: true, response: `Current language: \`${current}\`. Usage: \`/lang <code>\` (${validLangs.join(', ')})` };
196
197
  }
197
- config.set('language', args[0]);
198
- return { handled: true, response: `Language set to \`${args[0]}\`` };
198
+ const lang = args[0].toLowerCase();
199
+ if (!validLangs.includes(lang)) {
200
+ return { handled: true, response: `Invalid language \`${args[0]}\`. Valid: ${validLangs.join(', ')}` };
201
+ }
202
+ config.set('language', lang);
203
+ return { handled: true, response: `Language set to \`${lang}\`` };
199
204
  }
200
205
  // ─── File context ──────────────────────────────────────────────────────────
201
206
  case 'add': {
@@ -211,7 +216,11 @@ export async function handleCommand(input, session, onChunk, abortSignal) {
211
216
  const added = [];
212
217
  const errors = [];
213
218
  for (const filePath of args) {
214
- const fullPath = pathMod.isAbsolute(filePath) ? filePath : pathMod.join(root, filePath);
219
+ const fullPath = pathMod.resolve(root, filePath);
220
+ if (!fullPath.startsWith(root + pathMod.sep) && fullPath !== root) {
221
+ errors.push(`\`${filePath}\`: path outside workspace`);
222
+ continue;
223
+ }
215
224
  const relativePath = pathMod.relative(root, fullPath);
216
225
  try {
217
226
  const stat = await fs.stat(fullPath);
@@ -248,7 +257,9 @@ export async function handleCommand(input, session, onChunk, abortSignal) {
248
257
  const root = session.workspaceRoot;
249
258
  let dropped = 0;
250
259
  for (const filePath of args) {
251
- const fullPath = pathMod.isAbsolute(filePath) ? filePath : pathMod.join(root, filePath);
260
+ const fullPath = pathMod.resolve(root, filePath);
261
+ if (!fullPath.startsWith(root + pathMod.sep) && fullPath !== root)
262
+ continue;
252
263
  if (session.addedFiles.delete(fullPath))
253
264
  dropped++;
254
265
  }
@@ -1,5 +1,7 @@
1
1
  // acp/transport.ts
2
2
  // Newline-delimited JSON-RPC over stdio
3
+ const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
4
+ const REQUEST_TIMEOUT_MS = 30_000; // 30s
3
5
  export class StdioTransport {
4
6
  buffer = '';
5
7
  handler = null;
@@ -13,6 +15,10 @@ export class StdioTransport {
13
15
  }
14
16
  onData(chunk) {
15
17
  this.buffer += chunk;
18
+ if (this.buffer.length > MAX_BUFFER_SIZE) {
19
+ this.buffer = '';
20
+ return;
21
+ }
16
22
  const lines = this.buffer.split('\n');
17
23
  this.buffer = lines.pop() ?? '';
18
24
  for (const line of lines) {
@@ -59,6 +65,11 @@ export class StdioTransport {
59
65
  return new Promise((resolve) => {
60
66
  this.pendingRequests.set(id, resolve);
61
67
  process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
68
+ setTimeout(() => {
69
+ if (this.pendingRequests.delete(id)) {
70
+ resolve(null);
71
+ }
72
+ }, REQUEST_TIMEOUT_MS);
62
73
  });
63
74
  }
64
75
  }
@@ -37,6 +37,7 @@ export declare class App {
37
37
  private scrollOffset;
38
38
  private notification;
39
39
  private notificationTimeout;
40
+ private pendingRender;
40
41
  private spinnerFrame;
41
42
  private spinnerInterval;
42
43
  private isAgentRunning;
@@ -48,6 +49,7 @@ export declare class App {
48
49
  private pasteInfo;
49
50
  private pasteInfoOpen;
50
51
  private codeBlockCounter;
52
+ private messageCache;
51
53
  private helpOpen;
52
54
  private helpScrollIndex;
53
55
  private statusOpen;
@@ -116,13 +118,7 @@ export declare class App {
116
118
  * Add a message
117
119
  */
118
120
  addMessage(message: Message): void;
119
- /**
120
- * Set messages (for loading session)
121
- */
122
121
  setMessages(messages: Message[]): void;
123
- /**
124
- * Clear messages
125
- */
126
122
  clearMessages(): void;
127
123
  /**
128
124
  * Get all messages (for API history)
@@ -336,6 +332,7 @@ export declare class App {
336
332
  /**
337
333
  * Render current screen
338
334
  */
335
+ scheduleRender(): void;
339
336
  render(): void;
340
337
  /**
341
338
  * Render chat screen
@@ -93,6 +93,8 @@ export class App {
93
93
  scrollOffset = 0;
94
94
  notification = '';
95
95
  notificationTimeout = null;
96
+ // Render scheduling
97
+ pendingRender = false;
96
98
  // Spinner animation state
97
99
  spinnerFrame = 0;
98
100
  spinnerInterval = null;
@@ -107,6 +109,8 @@ export class App {
107
109
  pasteInfo = null;
108
110
  pasteInfoOpen = false;
109
111
  codeBlockCounter = 0; // Global code block counter for /copy numbering
112
+ // Message render cache: index → { lines, width, startBlock, blockCount }
113
+ messageCache = [];
110
114
  // Inline help state
111
115
  helpOpen = false;
112
116
  helpScrollIndex = 0;
@@ -214,13 +218,21 @@ export class App {
214
218
  this.screen.init();
215
219
  this.input.start();
216
220
  this.input.onKey((event) => this.handleKey(event));
217
- this.screen.onResize(() => this.render());
218
- this.render();
221
+ this.screen.onResize(() => {
222
+ this.messageCache = new Array(this.messages.length).fill(null);
223
+ this.scheduleRender();
224
+ });
225
+ this.scheduleRender();
219
226
  }
220
227
  /**
221
228
  * Stop the application
222
229
  */
223
230
  stop() {
231
+ this.stopSpinner();
232
+ if (this.notificationTimeout)
233
+ clearTimeout(this.notificationTimeout);
234
+ if (this.introInterval)
235
+ clearInterval(this.introInterval);
224
236
  this.input.stop();
225
237
  this.screen.cleanup();
226
238
  }
@@ -229,24 +241,21 @@ export class App {
229
241
  */
230
242
  addMessage(message) {
231
243
  this.messages.push(message);
244
+ this.messageCache.push(null); // slot za novu poruku
232
245
  this.scrollOffset = 0;
233
- this.render();
246
+ this.scheduleRender();
234
247
  }
235
- /**
236
- * Set messages (for loading session)
237
- */
238
248
  setMessages(messages) {
239
249
  this.messages = messages;
250
+ this.messageCache = new Array(messages.length).fill(null);
240
251
  this.scrollOffset = 0;
241
- this.render();
252
+ this.scheduleRender();
242
253
  }
243
- /**
244
- * Clear messages
245
- */
246
254
  clearMessages() {
247
255
  this.messages = [];
256
+ this.messageCache = [];
248
257
  this.scrollOffset = 0;
249
- this.render();
258
+ this.scheduleRender();
250
259
  }
251
260
  /**
252
261
  * Get all messages (for API history)
@@ -280,7 +289,7 @@ export class App {
280
289
  const visibleLines = height - 12; // Approximate visible area
281
290
  // Set scroll offset to show the target message near the top
282
291
  this.scrollOffset = Math.max(0, totalLines - targetStartLine - Math.floor(visibleLines / 2));
283
- this.render();
292
+ this.scheduleRender();
284
293
  this.notify(`Jumped to message #${messageIndex + 1}`);
285
294
  }
286
295
  /**
@@ -299,14 +308,14 @@ export class App {
299
308
  this.isLoading = false;
300
309
  this.streamingContent = '';
301
310
  this.startSpinner();
302
- this.render();
311
+ this.scheduleRender();
303
312
  }
304
313
  /**
305
314
  * Add streaming chunk
306
315
  */
307
316
  addStreamChunk(chunk) {
308
317
  this.streamingContent += chunk;
309
- this.render();
318
+ this.scheduleRender();
310
319
  }
311
320
  /**
312
321
  * End streaming
@@ -317,11 +326,12 @@ export class App {
317
326
  role: 'assistant',
318
327
  content: this.streamingContent,
319
328
  });
329
+ this.messageCache.push(null);
320
330
  }
321
331
  this.streamingContent = '';
322
332
  this.isStreaming = false;
323
333
  this.stopSpinner();
324
- this.render();
334
+ this.scheduleRender();
325
335
  }
326
336
  /**
327
337
  * Set loading state
@@ -334,7 +344,7 @@ export class App {
334
344
  else {
335
345
  this.stopSpinner();
336
346
  }
337
- this.render();
347
+ this.scheduleRender();
338
348
  }
339
349
  /**
340
350
  * Start spinner animation
@@ -374,7 +384,7 @@ export class App {
374
384
  this.isLoading = false; // Ensure loading is cleared when agent finishes
375
385
  this.stopSpinner();
376
386
  }
377
- this.render();
387
+ this.scheduleRender();
378
388
  }
379
389
  /**
380
390
  * Update agent progress
@@ -384,7 +394,7 @@ export class App {
384
394
  if (action) {
385
395
  this.agentActions.push(action);
386
396
  }
387
- this.render();
397
+ this.scheduleRender();
388
398
  }
389
399
  setAgentMaxIterations(max) {
390
400
  this.agentMaxIterations = max;
@@ -394,11 +404,11 @@ export class App {
394
404
  */
395
405
  setAgentThinking(text) {
396
406
  this.agentThinking = text;
397
- this.render();
407
+ this.scheduleRender();
398
408
  }
399
409
  setAgentWaitingForAI(waiting) {
400
410
  this.agentWaitingForAI = waiting;
401
- this.render();
411
+ this.scheduleRender();
402
412
  }
403
413
  /**
404
414
  * Paste from system clipboard (Ctrl+V)
@@ -429,7 +439,7 @@ export class App {
429
439
  // Small paste - just add to input directly
430
440
  this.editor.insert(text);
431
441
  this.updateAutocomplete();
432
- this.render();
442
+ this.scheduleRender();
433
443
  return;
434
444
  }
435
445
  // Large paste - show info box
@@ -441,7 +451,7 @@ export class App {
441
451
  fullText: text,
442
452
  };
443
453
  this.pasteInfoOpen = true;
444
- this.render();
454
+ this.scheduleRender();
445
455
  }
446
456
  /**
447
457
  * Handle paste info key events
@@ -452,7 +462,7 @@ export class App {
452
462
  this.pasteInfo = null;
453
463
  this.pasteInfoOpen = false;
454
464
  this.notify('Paste cancelled');
455
- this.render();
465
+ this.scheduleRender();
456
466
  return;
457
467
  }
458
468
  if (event.key === 'enter' || event.key === 'y') {
@@ -463,7 +473,7 @@ export class App {
463
473
  }
464
474
  this.pasteInfo = null;
465
475
  this.pasteInfoOpen = false;
466
- this.render();
476
+ this.scheduleRender();
467
477
  return;
468
478
  }
469
479
  if (event.key === 's') {
@@ -472,7 +482,7 @@ export class App {
472
482
  const text = this.pasteInfo.fullText;
473
483
  this.pasteInfo = null;
474
484
  this.pasteInfoOpen = false;
475
- this.render();
485
+ this.scheduleRender();
476
486
  // Submit directly
477
487
  this.addMessage({ role: 'user', content: text });
478
488
  this.setLoading(true);
@@ -489,13 +499,13 @@ export class App {
489
499
  */
490
500
  notify(message, duration = 3000) {
491
501
  this.notification = message;
492
- this.render();
502
+ this.scheduleRender();
493
503
  if (this.notificationTimeout) {
494
504
  clearTimeout(this.notificationTimeout);
495
505
  }
496
506
  this.notificationTimeout = setTimeout(() => {
497
507
  this.notification = '';
498
- this.render();
508
+ this.scheduleRender();
499
509
  }, duration);
500
510
  }
501
511
  /**
@@ -513,7 +523,7 @@ export class App {
513
523
  this.menuIndex = 0;
514
524
  this.menuCallback = (item) => callback(parseInt(item.key, 10));
515
525
  this.menuOpen = true;
516
- this.render();
526
+ this.scheduleRender();
517
527
  }
518
528
  /**
519
529
  * Show settings (inline, below status bar)
@@ -521,7 +531,7 @@ export class App {
521
531
  showSettings() {
522
532
  this.settingsState = { selectedIndex: 0, editing: false, editValue: '' };
523
533
  this.settingsOpen = true;
524
- this.render();
534
+ this.scheduleRender();
525
535
  }
526
536
  /**
527
537
  * Show confirmation dialog
@@ -530,7 +540,7 @@ export class App {
530
540
  this.confirmOptions = options;
531
541
  this.confirmSelection = 'no'; // Default to No for safety
532
542
  this.confirmOpen = true;
533
- this.render();
543
+ this.scheduleRender();
534
544
  }
535
545
  /**
536
546
  * Show permission dialog (inline, below status bar)
@@ -541,7 +551,7 @@ export class App {
541
551
  this.permissionIndex = 0;
542
552
  this.permissionCallback = callback;
543
553
  this.permissionOpen = true;
544
- this.render();
554
+ this.scheduleRender();
545
555
  }
546
556
  /**
547
557
  * Show session picker (inline, below status bar)
@@ -553,7 +563,7 @@ export class App {
553
563
  this.sessionPickerDeleteCallback = deleteCallback || null;
554
564
  this.sessionPickerDeleteMode = false;
555
565
  this.sessionPickerOpen = true;
556
- this.render();
566
+ this.scheduleRender();
557
567
  }
558
568
  /**
559
569
  * Show search screen
@@ -564,7 +574,7 @@ export class App {
564
574
  this.searchIndex = 0;
565
575
  this.searchCallback = callback;
566
576
  this.searchOpen = true;
567
- this.render();
577
+ this.scheduleRender();
568
578
  }
569
579
  /**
570
580
  * Show export screen
@@ -573,7 +583,7 @@ export class App {
573
583
  this.exportIndex = 0;
574
584
  this.exportCallback = callback;
575
585
  this.exportOpen = true;
576
- this.render();
586
+ this.scheduleRender();
577
587
  }
578
588
  /**
579
589
  * Show logout picker
@@ -583,7 +593,7 @@ export class App {
583
593
  this.logoutIndex = 0;
584
594
  this.logoutCallback = callback;
585
595
  this.logoutOpen = true;
586
- this.render();
596
+ this.scheduleRender();
587
597
  }
588
598
  /**
589
599
  * Start intro animation
@@ -598,7 +608,7 @@ export class App {
598
608
  const noiseInterval = setInterval(() => {
599
609
  noiseCount++;
600
610
  this.introProgress = Math.random();
601
- this.render();
611
+ this.scheduleRender();
602
612
  if (noiseCount >= 10) {
603
613
  clearInterval(noiseInterval);
604
614
  // Phase 2: Decryption animation (1500ms)
@@ -608,14 +618,14 @@ export class App {
608
618
  this.introInterval = setInterval(() => {
609
619
  const elapsed = Date.now() - startTime;
610
620
  this.introProgress = Math.min(elapsed / duration, 1);
611
- this.render();
621
+ this.scheduleRender();
612
622
  if (this.introProgress >= 1) {
613
623
  this.finishIntro();
614
624
  }
615
625
  }, 16); // ~60 FPS
616
626
  }
617
627
  }, 50);
618
- this.render();
628
+ this.scheduleRender();
619
629
  }
620
630
  /**
621
631
  * Skip intro animation
@@ -637,7 +647,7 @@ export class App {
637
647
  this.introCallback();
638
648
  this.introCallback = null;
639
649
  }
640
- this.render();
650
+ this.scheduleRender();
641
651
  }
642
652
  /**
643
653
  * Show inline login dialog
@@ -650,7 +660,7 @@ export class App {
650
660
  this.loginError = '';
651
661
  this.loginCallback = callback;
652
662
  this.loginOpen = true;
653
- this.render();
663
+ this.scheduleRender();
654
664
  }
655
665
  /**
656
666
  * Reinitialize screen (after external screen takeover)
@@ -659,7 +669,7 @@ export class App {
659
669
  this.screen.init();
660
670
  this.input.start();
661
671
  this.input.onKey((event) => this.handleKey(event));
662
- this.render();
672
+ this.scheduleRender();
663
673
  }
664
674
  /**
665
675
  * Show inline menu (renders below status bar)
@@ -673,7 +683,7 @@ export class App {
673
683
  // Find current value index
674
684
  const currentIndex = items.findIndex(item => item.key === currentValue);
675
685
  this.menuIndex = currentIndex >= 0 ? currentIndex : 0;
676
- this.render();
686
+ this.scheduleRender();
677
687
  }
678
688
  /**
679
689
  * Handle keyboard input
@@ -760,7 +770,7 @@ export class App {
760
770
  if (event.key === 'escape') {
761
771
  if (this.showAutocomplete) {
762
772
  this.showAutocomplete = false;
763
- this.render();
773
+ this.scheduleRender();
764
774
  return;
765
775
  }
766
776
  // In multiline mode, Escape submits the buffered input
@@ -783,12 +793,12 @@ export class App {
783
793
  if (this.showAutocomplete) {
784
794
  if (event.key === 'up') {
785
795
  this.autocompleteIndex = Math.max(0, this.autocompleteIndex - 1);
786
- this.render();
796
+ this.scheduleRender();
787
797
  return;
788
798
  }
789
799
  if (event.key === 'down') {
790
800
  this.autocompleteIndex = Math.min(this.autocompleteItems.length - 1, this.autocompleteIndex + 1);
791
- this.render();
801
+ this.scheduleRender();
792
802
  return;
793
803
  }
794
804
  if (event.key === 'tab' || event.key === 'enter') {
@@ -797,7 +807,7 @@ export class App {
797
807
  const selected = this.autocompleteItems[this.autocompleteIndex];
798
808
  this.editor.setValue('/' + selected + ' ');
799
809
  this.showAutocomplete = false;
800
- this.render();
810
+ this.scheduleRender();
801
811
  return;
802
812
  }
803
813
  }
@@ -816,69 +826,69 @@ export class App {
816
826
  // Ctrl+A - go to beginning of line
817
827
  if (event.ctrl && event.key === 'a') {
818
828
  this.editor.setCursorPos(0);
819
- this.render();
829
+ this.scheduleRender();
820
830
  return;
821
831
  }
822
832
  // Ctrl+E - go to end of line
823
833
  if (event.ctrl && event.key === 'e') {
824
834
  this.editor.setCursorPos(this.editor.getValue().length);
825
- this.render();
835
+ this.scheduleRender();
826
836
  return;
827
837
  }
828
838
  // Ctrl+U - clear line
829
839
  if (event.ctrl && event.key === 'u') {
830
840
  this.editor.clear();
831
841
  this.showAutocomplete = false;
832
- this.render();
842
+ this.scheduleRender();
833
843
  return;
834
844
  }
835
845
  // Ctrl+W - delete word backward
836
846
  if (event.ctrl && event.key === 'w') {
837
847
  this.editor.deleteWordBackward();
838
848
  this.updateAutocomplete();
839
- this.render();
849
+ this.scheduleRender();
840
850
  return;
841
851
  }
842
852
  // Ctrl+K - delete to end of line
843
853
  if (event.ctrl && event.key === 'k') {
844
854
  this.editor.deleteToEnd();
845
855
  this.updateAutocomplete();
846
- this.render();
856
+ this.scheduleRender();
847
857
  return;
848
858
  }
849
859
  // Page up/down for scrolling chat history
850
860
  if (event.key === 'pageup') {
851
861
  // Scroll up (show older messages)
852
862
  this.scrollOffset += 10;
853
- this.render();
863
+ this.scheduleRender();
854
864
  return;
855
865
  }
856
866
  if (event.key === 'pagedown') {
857
867
  // Scroll down (show newer messages)
858
868
  this.scrollOffset = Math.max(0, this.scrollOffset - 10);
859
- this.render();
869
+ this.scheduleRender();
860
870
  return;
861
871
  }
862
872
  // Arrow up/down can also scroll when input is empty
863
873
  if (event.key === 'up' && !this.editor.getValue() && !this.showAutocomplete) {
864
874
  this.scrollOffset += 3;
865
- this.render();
875
+ this.scheduleRender();
866
876
  return;
867
877
  }
868
878
  if (event.key === 'down' && !this.editor.getValue() && !this.showAutocomplete && this.scrollOffset > 0) {
869
879
  this.scrollOffset = Math.max(0, this.scrollOffset - 3);
870
- this.render();
880
+ this.scheduleRender();
871
881
  return;
872
882
  }
873
883
  // Mouse scroll
874
884
  if (event.key === 'scrollup') {
875
885
  this.scrollOffset += 3;
876
- this.render();
886
+ this.scheduleRender();
877
887
  return;
878
888
  }
879
889
  if (event.key === 'scrolldown') {
880
890
  this.scrollOffset = Math.max(0, this.scrollOffset - 3);
881
- this.render();
891
+ this.scheduleRender();
882
892
  return;
883
893
  }
884
894
  // Ignore other mouse events
@@ -891,13 +901,13 @@ export class App {
891
901
  // Backslash continuation: if line ends with \, add newline instead of submitting
892
902
  if (rawValue.endsWith('\\')) {
893
903
  this.editor.setValue(rawValue.slice(0, -1) + '\n');
894
- this.render();
904
+ this.scheduleRender();
895
905
  return;
896
906
  }
897
907
  // Multiline mode: Enter adds newline, Ctrl+Enter submits
898
908
  if (this.isMultilineMode && !event.ctrl) {
899
909
  this.editor.insert('\n');
900
- this.render();
910
+ this.scheduleRender();
901
911
  return;
902
912
  }
903
913
  this.submitInput();
@@ -912,7 +922,7 @@ export class App {
912
922
  if (this.editor.handleKey(event)) {
913
923
  // Update autocomplete based on input
914
924
  this.updateAutocomplete();
915
- this.render();
925
+ this.scheduleRender();
916
926
  }
917
927
  }
918
928
  /**
@@ -938,7 +948,7 @@ export class App {
938
948
  handleInlineStatusKey(event) {
939
949
  handleInlineStatusKey(event, {
940
950
  close: () => { this.statusOpen = false; },
941
- render: () => this.render(),
951
+ render: () => this.scheduleRender(),
942
952
  });
943
953
  }
944
954
  /**
@@ -949,7 +959,7 @@ export class App {
949
959
  scrollIndex: this.helpScrollIndex,
950
960
  setScrollIndex: (v) => { this.helpScrollIndex = v; },
951
961
  close: () => { this.helpOpen = false; },
952
- render: () => this.render(),
962
+ render: () => this.scheduleRender(),
953
963
  });
954
964
  }
955
965
  /**
@@ -964,7 +974,7 @@ export class App {
964
974
  if (result.notify) {
965
975
  this.notify(result.notify);
966
976
  }
967
- this.render();
977
+ this.scheduleRender();
968
978
  }
969
979
  /**
970
980
  * Handle search screen keys
@@ -985,7 +995,7 @@ export class App {
985
995
  },
986
996
  onRender: () => {
987
997
  this.searchIndex = state.searchIndex;
988
- this.render();
998
+ this.scheduleRender();
989
999
  },
990
1000
  onResult: (messageIndex) => {
991
1001
  if (callback) {
@@ -1007,7 +1017,7 @@ export class App {
1007
1017
  this.exportOpen = state.exportOpen;
1008
1018
  this.exportIndex = state.exportIndex;
1009
1019
  this.exportCallback = state.exportCallback;
1010
- this.render();
1020
+ this.scheduleRender();
1011
1021
  };
1012
1022
  handleExportKeyComponent(event, state, {
1013
1023
  onClose: syncState,
@@ -1038,7 +1048,7 @@ export class App {
1038
1048
  };
1039
1049
  handleLogoutKeyComponent(event, state, {
1040
1050
  onClose: () => { },
1041
- onRender: () => { syncState(); this.render(); },
1051
+ onRender: () => { syncState(); this.scheduleRender(); },
1042
1052
  onSelect: () => { },
1043
1053
  });
1044
1054
  syncState();
@@ -1063,7 +1073,7 @@ export class App {
1063
1073
  if (callback)
1064
1074
  callback(result);
1065
1075
  },
1066
- render: () => this.render(),
1076
+ render: () => this.scheduleRender(),
1067
1077
  });
1068
1078
  }
1069
1079
  /**
@@ -1081,7 +1091,7 @@ export class App {
1081
1091
  if (selected && callback)
1082
1092
  callback(selected);
1083
1093
  },
1084
- render: () => this.render(),
1094
+ render: () => this.scheduleRender(),
1085
1095
  });
1086
1096
  }
1087
1097
  /**
@@ -1098,7 +1108,7 @@ export class App {
1098
1108
  if (callback)
1099
1109
  callback(level);
1100
1110
  },
1101
- render: () => this.render(),
1111
+ render: () => this.scheduleRender(),
1102
1112
  });
1103
1113
  }
1104
1114
  handleInlineSessionPickerKey(event) {
@@ -1123,13 +1133,13 @@ export class App {
1123
1133
  this.sessionPickerDeleteCallback(name);
1124
1134
  },
1125
1135
  notify: (msg) => this.notify(msg),
1126
- render: () => this.render(),
1136
+ render: () => this.scheduleRender(),
1127
1137
  });
1128
1138
  }
1129
1139
  handleInlineConfirmKey(event) {
1130
1140
  if (!this.confirmOptions) {
1131
1141
  this.confirmOpen = false;
1132
- this.render();
1142
+ this.scheduleRender();
1133
1143
  return;
1134
1144
  }
1135
1145
  handleInlineConfirmKey(event, {
@@ -1145,7 +1155,7 @@ export class App {
1145
1155
  else if (options.onCancel)
1146
1156
  options.onCancel();
1147
1157
  },
1148
- render: () => this.render(),
1158
+ render: () => this.scheduleRender(),
1149
1159
  });
1150
1160
  }
1151
1161
  /**
@@ -1181,11 +1191,11 @@ export class App {
1181
1191
  case 'help':
1182
1192
  this.helpOpen = true;
1183
1193
  this.helpScrollIndex = 0;
1184
- this.render();
1194
+ this.scheduleRender();
1185
1195
  break;
1186
1196
  case 'status':
1187
1197
  this.statusOpen = true;
1188
- this.render();
1198
+ this.scheduleRender();
1189
1199
  break;
1190
1200
  case 'clear':
1191
1201
  this.clearMessages();
@@ -1211,6 +1221,15 @@ export class App {
1211
1221
  /**
1212
1222
  * Render current screen
1213
1223
  */
1224
+ scheduleRender() {
1225
+ if (this.pendingRender)
1226
+ return;
1227
+ this.pendingRender = true;
1228
+ setImmediate(() => {
1229
+ this.pendingRender = false;
1230
+ this.render();
1231
+ });
1232
+ }
1214
1233
  render() {
1215
1234
  // Intro animation takes over the whole screen
1216
1235
  if (this.showIntro) {
@@ -1896,10 +1915,9 @@ export class App {
1896
1915
  // Top border: gradient line with gradient title embedded
1897
1916
  const titleInner = ` ${spinner} AGENT `;
1898
1917
  const titlePadLeft = 2;
1899
- const lineChar = PRIMARY_COLOR + '─' + style.reset;
1900
- const lineLeft = lineChar.repeat(titlePadLeft);
1918
+ const lineLeft = PRIMARY_COLOR + '─'.repeat(titlePadLeft) + style.reset;
1901
1919
  const titleColored = PRIMARY_COLOR + style.bold + titleInner + style.reset;
1902
- const lineRight = lineChar.repeat(Math.max(0, width - titlePadLeft - titleInner.length - 1));
1920
+ const lineRight = PRIMARY_COLOR + '─'.repeat(Math.max(0, width - titlePadLeft - titleInner.length - 1)) + style.reset;
1903
1921
  this.screen.write(0, y, lineLeft + titleColored + lineRight);
1904
1922
  y++;
1905
1923
  // Current action line
@@ -2102,11 +2120,24 @@ export class App {
2102
2120
  allLines.push({ text: ' Codeep', style: PRIMARY_COLOR, raw: false });
2103
2121
  allLines.push({ text: '', style: '' });
2104
2122
  }
2105
- for (const msg of this.messages) {
2106
- const msgLines = msg.role === 'welcome'
2107
- ? this.formatWelcomeMessage(msg.content)
2108
- : this.formatMessage(msg.role, msg.content, width);
2109
- allLines.push(...msgLines);
2123
+ for (let i = 0; i < this.messages.length; i++) {
2124
+ const msg = this.messages[i];
2125
+ const cached = this.messageCache[i];
2126
+ if (cached && cached.width === width && cached.startBlock === this.codeBlockCounter) {
2127
+ // Cache hit — preskoči formatiranje
2128
+ this.codeBlockCounter += cached.blockCount;
2129
+ allLines.push(...cached.lines);
2130
+ }
2131
+ else {
2132
+ // Cache miss — formatiraj i spremi
2133
+ const startBlock = this.codeBlockCounter;
2134
+ const msgLines = msg.role === 'welcome'
2135
+ ? this.formatWelcomeMessage(msg.content)
2136
+ : this.formatMessage(msg.role, msg.content, width);
2137
+ const blockCount = this.codeBlockCounter - startBlock;
2138
+ this.messageCache[i] = { lines: msgLines, width, startBlock, blockCount };
2139
+ allLines.push(...msgLines);
2140
+ }
2110
2141
  }
2111
2142
  if (this.isStreaming && this.streamingContent) {
2112
2143
  const streamLines = this.formatMessage('assistant', this.streamingContent + '▊', width);
@@ -2196,7 +2227,7 @@ export class App {
2196
2227
  firstPrefix = ' ';
2197
2228
  }
2198
2229
  else {
2199
- firstPrefix = fg.rgb(100, 140, 200) + '\u25b8 ' + style.reset;
2230
+ firstPrefix = PRIMARY_COLOR + '\u25b8 ' + style.reset;
2200
2231
  }
2201
2232
  const codeBlockRegex = /```([^\n]*)\n([\s\S]*?)```/g;
2202
2233
  let lastIndex = 0;
@@ -146,7 +146,8 @@ export async function executeAgentTask(task, dryRun, ctx) {
146
146
  const fileContext = ctx.formatAddedFilesContext();
147
147
  const enrichedTask = fileContext ? fileContext + task : task;
148
148
  // Show N/M progress in status bar
149
- app.setAgentMaxIterations(config.get('agentMaxIterations'));
149
+ const rawIterations = config.get('agentMaxIterations') || 100;
150
+ app.setAgentMaxIterations(Math.max(5, Math.min(500, rawIterations)));
150
151
  const result = await runAgent(enrichedTask, context, {
151
152
  dryRun,
152
153
  chatHistory: app.getChatHistory(),
@@ -221,31 +222,7 @@ export async function executeAgentTask(task, dryRun, ctx) {
221
222
  const filePath = tool.parameters.path;
222
223
  app.addMessage({ role: 'system', content: `**Delete** \`${filePath}\`` });
223
224
  }
224
- else if (actionType === 'read') {
225
- const filePath = tool.parameters.path || shortTarget;
226
- if (filePath)
227
- app.addMessage({ role: 'system', content: `**Reading** \`${filePath}\`` });
228
- }
229
- else if (actionType === 'search') {
230
- const pattern = tool.parameters.pattern || tool.parameters.query || shortTarget;
231
- if (pattern)
232
- app.addMessage({ role: 'system', content: `**Searching** for \`${pattern}\`` });
233
- }
234
- else if (actionType === 'list') {
235
- const dirPath = tool.parameters.path || shortTarget;
236
- if (dirPath)
237
- app.addMessage({ role: 'system', content: `**Listing** \`${dirPath}\`` });
238
- }
239
- else if (actionType === 'fetch') {
240
- const url = tool.parameters.url || shortTarget;
241
- if (url)
242
- app.addMessage({ role: 'system', content: `**Fetching** \`${url}\`` });
243
- }
244
- else if (actionType === 'command') {
245
- const cmd = tool.parameters.command || shortTarget;
246
- if (cmd)
247
- app.addMessage({ role: 'system', content: `**Running** \`${cmd}\`` });
248
- }
225
+ // read/search/list/fetch/command — setAgentThinking() above is enough, no chat message needed
249
226
  },
250
227
  onToolResult: (result, toolCall) => {
251
228
  const toolName = toolCall.tool.toLowerCase();
@@ -83,8 +83,15 @@ function extractImports(content, ext) {
83
83
  }
84
84
  return imports;
85
85
  }
86
- // Cache for resolveImportPath — avoids redundant disk lookups across import graphs
86
+ // LRU cache for resolveImportPath — bounded to prevent memory growth in long sessions
87
+ const IMPORT_CACHE_MAX = 1000;
87
88
  const importResolutionCache = new Map();
89
+ function importCacheSet(key, value) {
90
+ if (importResolutionCache.size >= IMPORT_CACHE_MAX) {
91
+ importResolutionCache.delete(importResolutionCache.keys().next().value);
92
+ }
93
+ importResolutionCache.set(key, value);
94
+ }
88
95
  /**
89
96
  * Resolve import path to actual file path.
90
97
  * Results are cached by (fromFile, importPath) to avoid O(n²) disk I/O.
@@ -113,7 +120,7 @@ function resolveImportPath(importPath, fromFile, projectRoot) {
113
120
  const resolved = join(fromDir, importPath);
114
121
  result = resolveWithExtensions(resolved, ext);
115
122
  }
116
- importResolutionCache.set(cacheKey, result);
123
+ importCacheSet(cacheKey, result);
117
124
  return result;
118
125
  }
119
126
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "1.2.86",
3
+ "version": "1.2.88",
4
4
  "description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",