codeep 2.18.1 → 2.19.0

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 (49) hide show
  1. package/README.md +53 -16
  2. package/dist/acp/commands.js +11 -55
  3. package/dist/acp/protocol.d.ts +34 -0
  4. package/dist/acp/server.d.ts +6 -1
  5. package/dist/acp/server.js +97 -2
  6. package/dist/api/index.js +9 -0
  7. package/dist/commands/core/index.d.ts +19 -0
  8. package/dist/commands/core/index.js +28 -0
  9. package/dist/commands/core/keysync.d.ts +2 -0
  10. package/dist/commands/core/keysync.js +34 -0
  11. package/dist/commands/core/telemetry.d.ts +2 -0
  12. package/dist/commands/core/telemetry.js +34 -0
  13. package/dist/config/index.js +2 -2
  14. package/dist/renderer/App.d.ts +9 -48
  15. package/dist/renderer/App.js +113 -338
  16. package/dist/renderer/Screen.d.ts +13 -0
  17. package/dist/renderer/Screen.js +22 -0
  18. package/dist/renderer/commands/registry.js +3 -3
  19. package/dist/renderer/commands.js +19 -51
  20. package/dist/renderer/components/CommandAutocomplete.d.ts +46 -0
  21. package/dist/renderer/components/CommandAutocomplete.js +103 -0
  22. package/dist/renderer/components/HunkPicker.d.ts +48 -0
  23. package/dist/renderer/components/HunkPicker.js +140 -0
  24. package/dist/renderer/components/MentionPicker.d.ts +60 -0
  25. package/dist/renderer/components/MentionPicker.js +111 -0
  26. package/dist/renderer/components/PasteDialog.d.ts +43 -0
  27. package/dist/renderer/components/PasteDialog.js +70 -0
  28. package/dist/renderer/layout.js +1 -0
  29. package/dist/renderer/main.js +15 -39
  30. package/dist/utils/agent.js +121 -26
  31. package/dist/utils/agentChat.d.ts +11 -4
  32. package/dist/utils/agentChat.js +53 -25
  33. package/dist/utils/codeepCloud.d.ts +3 -0
  34. package/dist/utils/codeepCloud.js +62 -7
  35. package/dist/utils/personalities.d.ts +63 -5
  36. package/dist/utils/personalities.js +583 -31
  37. package/dist/utils/shell.d.ts +11 -1
  38. package/dist/utils/shell.js +169 -82
  39. package/dist/utils/ssrfGuard.d.ts +18 -0
  40. package/dist/utils/ssrfGuard.js +83 -0
  41. package/dist/utils/taskPlanner.d.ts +7 -1
  42. package/dist/utils/taskPlanner.js +16 -7
  43. package/dist/utils/toolExecution.d.ts +1 -0
  44. package/dist/utils/toolExecution.js +48 -88
  45. package/dist/utils/tools.d.ts +3 -3
  46. package/dist/utils/tools.js +18 -13
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +1 -1
@@ -30,7 +30,11 @@ import { handleSettingsKey, SETTINGS } from './components/Settings.js';
30
30
  import { renderExportPanel, handleExportKey as handleExportKeyComponent } from './components/Export.js';
31
31
  import { renderLogoutPanel, handleLogoutKey as handleLogoutKeyComponent } from './components/Logout.js';
32
32
  import { renderSearchPanel, handleSearchKey as handleSearchKeyComponent } from './components/Search.js';
33
+ import { createPasteDialogState, handlePasteDialogKey, renderPasteDialog, } from './components/PasteDialog.js';
34
+ import { createMentionPickerState, openMentionPicker, closeMentionPicker, handleMentionPickerKey, applyMentionToBuffer, renderMentionPicker, } from './components/MentionPicker.js';
35
+ import { createCommandAutocompleteState, setCommandAutocomplete, closeCommandAutocomplete, handleCommandAutocompleteKey, commandToBuffer, renderCommandAutocomplete, } from './components/CommandAutocomplete.js';
33
36
  import { formatMessage as formatMessageFn, } from './components/MessageFormatter.js';
37
+ import { createHunkPickerState, handleHunkPickerKey, renderHunkPicker, } from './components/HunkPicker.js';
34
38
  export class App {
35
39
  screen;
36
40
  input;
@@ -64,9 +68,9 @@ export class App {
64
68
  appStartedAt = Date.now();
65
69
  /** Start of the current agent run; unlike app uptime, resets per task. */
66
70
  agentStartedAt = null;
67
- // Paste detection state
68
- pasteInfo = null;
69
- pasteInfoOpen = false;
71
+ // Paste detection state — owned by the extracted components/PasteDialog.ts;
72
+ // App performs the side effects (editor insert / message submit) itself.
73
+ pasteDialog = createPasteDialogState();
70
74
  codeBlockCounter = { current: 0 }; // Global code block counter for /copy numbering
71
75
  // Message render cache: index → { lines, width, startBlock, blockCount }
72
76
  messageCache = [];
@@ -82,29 +86,23 @@ export class App {
82
86
  editValue: '',
83
87
  };
84
88
  // Autocomplete state
85
- showAutocomplete = false;
86
- autocompleteIndex = 0;
87
- autocompleteItems = [];
89
+ // `/command` autocomplete — owned by components/CommandAutocomplete.ts.
90
+ autocomplete = createCommandAutocompleteState();
88
91
  // `@mention` autocomplete state — separate from the `/command` picker
89
92
  // because mentions appear mid-sentence (not just at the start) and
90
93
  // insert a file path (not a slash command). `mentionAtStart` is the
91
94
  // index of the `@` in the editor value, used to replace `@query` with
92
95
  // `@selectedPath` on Tab/Enter.
93
- showMentionAutocomplete = false;
94
- mentionIndex = 0;
95
- mentionItems = [];
96
- mentionAtStart = 0;
97
- /** Project root for resolving `suggestMentions`. Cached per update. */
98
- mentionRoot = '';
96
+ // `@mention` autocomplete — owned by components/MentionPicker.ts; App
97
+ // keeps the editor mutations as side effects (see applyMentionSelection).
98
+ mention = createMentionPickerState();
99
99
  // Inline confirmation dialog state
100
100
  confirmOpen = false;
101
101
  confirmOptions = null;
102
102
  confirmSelection = 'no';
103
- // Inline hunk-picker state (`/apply --interactive`)
104
- hunkPickerOpen = false;
105
- hunkPickerOptions = null;
106
- hunkPickerIndex = 0;
107
- hunkPickerAccepted = [];
103
+ // Inline hunk-picker state (`/apply --interactive`) — owned by the
104
+ // extracted components/HunkPicker.ts module; App only wires it in.
105
+ hunkPicker = createHunkPickerState();
108
106
  // Inline menu state (renders below input/status)
109
107
  menuOpen = false;
110
108
  menuTitle = '';
@@ -423,49 +421,44 @@ export class App {
423
421
  return;
424
422
  }
425
423
  // Large paste - show info box
426
- this.pasteInfo = buildPasteInfo(text);
427
- this.pasteInfoOpen = true;
424
+ this.pasteDialog = { open: true, info: buildPasteInfo(text) };
428
425
  this.scheduleRender();
429
426
  }
430
427
  /**
431
428
  * Handle paste info key events
432
429
  */
433
430
  handlePasteInfoKey(event) {
434
- if (event.key === 'escape' || event.key === 'n') {
435
- // Cancel paste
436
- this.pasteInfo = null;
437
- this.pasteInfoOpen = false;
438
- this.notify('Paste cancelled');
439
- this.scheduleRender();
440
- return;
441
- }
442
- if (event.key === 'enter' || event.key === 'y') {
443
- // Accept paste - add to input
444
- if (this.pasteInfo) {
445
- this.editor.insert(this.pasteInfo.fullText);
446
- this.updateAutocomplete();
447
- }
448
- this.pasteInfo = null;
449
- this.pasteInfoOpen = false;
450
- this.scheduleRender();
451
- return;
452
- }
453
- if (event.key === 's') {
454
- // Submit paste directly as message
455
- if (this.pasteInfo) {
456
- const text = this.pasteInfo.fullText;
457
- this.pasteInfo = null;
458
- this.pasteInfoOpen = false;
431
+ // Delegates to components/PasteDialog.ts — the pure handler returns the
432
+ // next state plus an action; App performs the side effects itself
433
+ // (editor insert / message submit / notification).
434
+ const { state, action } = handlePasteDialogKey(this.pasteDialog, event);
435
+ this.pasteDialog = state;
436
+ switch (action.type) {
437
+ case 'cancel':
438
+ this.notify('Paste cancelled');
459
439
  this.scheduleRender();
460
- // Submit directly
461
- this.addMessage({ role: 'user', content: text });
440
+ return;
441
+ case 'add-to-input':
442
+ if (action.text) {
443
+ this.editor.insert(action.text);
444
+ this.updateAutocomplete();
445
+ }
446
+ this.scheduleRender();
447
+ return;
448
+ case 'send-directly': {
449
+ if (!action.text)
450
+ return;
451
+ this.scheduleRender();
452
+ this.addMessage({ role: 'user', content: action.text });
462
453
  this.setLoading(true);
463
- this.options.onSubmit(text).catch(err => {
454
+ this.options.onSubmit(action.text).catch(err => {
464
455
  this.notify(`Error: ${err.message}`);
465
456
  this.setLoading(false);
466
457
  });
458
+ return;
467
459
  }
468
- return;
460
+ case 'none':
461
+ return;
469
462
  }
470
463
  }
471
464
  /**
@@ -532,6 +525,7 @@ export class App {
532
525
  showConfirm(options) {
533
526
  this.confirmOptions = options;
534
527
  this.confirmSelection = 'no'; // Default to No for safety
528
+ this.screen.invalidate();
535
529
  this.confirmOpen = true;
536
530
  this.scheduleRender();
537
531
  }
@@ -540,10 +534,7 @@ export class App {
540
534
  * The caller passes pre-built items + an `onComplete` callback.
541
535
  */
542
536
  showHunkPicker(options) {
543
- this.hunkPickerOptions = options;
544
- this.hunkPickerIndex = 0;
545
- this.hunkPickerAccepted = [];
546
- this.hunkPickerOpen = true;
537
+ this.hunkPicker = { open: true, options, index: 0, accepted: [] };
547
538
  this.scheduleRender();
548
539
  }
549
540
  /**
@@ -566,6 +557,11 @@ export class App {
566
557
  this.sessionPickerCallback = callback;
567
558
  this.sessionPickerDeleteCallback = deleteCallback || null;
568
559
  this.sessionPickerDeleteMode = false;
560
+ // Repaint from scratch on the way IN as well as out. The overlay draws
561
+ // below the managed area and scrolls the terminal, so anything already
562
+ // stale underneath it stays visible for as long as the picker is up —
563
+ // clearing only on close left the artifact on screen the whole time.
564
+ this.screen.invalidate();
569
565
  this.sessionPickerOpen = true;
570
566
  this.scheduleRender();
571
567
  }
@@ -730,7 +726,7 @@ export class App {
730
726
  handleChatKey(event) {
731
727
  // Dispatch to whichever inline panel currently owns focus.
732
728
  switch (activePanel({
733
- pasteInfoOpen: this.pasteInfoOpen,
729
+ pasteInfoOpen: this.pasteDialog.open,
734
730
  permissionOpen: this.permissionOpen,
735
731
  sessionPickerOpen: this.sessionPickerOpen,
736
732
  confirmOpen: this.confirmOpen,
@@ -742,8 +738,8 @@ export class App {
742
738
  logoutOpen: this.logoutOpen,
743
739
  loginOpen: this.loginOpen,
744
740
  menuOpen: this.menuOpen,
745
- showAutocomplete: this.showAutocomplete,
746
- hunkPickerOpen: this.hunkPickerOpen,
741
+ showAutocomplete: this.autocomplete.open,
742
+ hunkPickerOpen: this.hunkPicker.open,
747
743
  })) {
748
744
  case 'pasteInfo':
749
745
  this.handlePasteInfoKey(event);
@@ -792,13 +788,13 @@ export class App {
792
788
  }
793
789
  // Escape to cancel streaming/loading/agent or close autocomplete
794
790
  if (event.key === 'escape') {
795
- if (this.showMentionAutocomplete) {
796
- this.showMentionAutocomplete = false;
791
+ if (this.mention.open) {
792
+ this.mention = closeMentionPicker(this.mention);
797
793
  this.scheduleRender();
798
794
  return;
799
795
  }
800
- if (this.showAutocomplete) {
801
- this.showAutocomplete = false;
796
+ if (this.autocomplete.open) {
797
+ this.autocomplete = closeCommandAutocomplete(this.autocomplete);
802
798
  this.scheduleRender();
803
799
  return;
804
800
  }
@@ -819,49 +815,35 @@ export class App {
819
815
  return;
820
816
  }
821
817
  // Handle autocomplete navigation (`/command` picker)
822
- if (this.showAutocomplete) {
823
- if (event.key === 'up') {
824
- this.autocompleteIndex = Math.max(0, this.autocompleteIndex - 1);
818
+ if (this.autocomplete.open) {
819
+ const { state, action } = handleCommandAutocompleteKey(this.autocomplete, event);
820
+ this.autocomplete = state;
821
+ if (action.type === 'navigate') {
825
822
  this.scheduleRender();
826
823
  return;
827
824
  }
828
- if (event.key === 'down') {
829
- this.autocompleteIndex = Math.min(this.autocompleteItems.length - 1, this.autocompleteIndex + 1);
825
+ if (action.type === 'select') {
826
+ this.editor.setValue(commandToBuffer(action.command));
827
+ this.autocomplete = closeCommandAutocomplete(this.autocomplete);
830
828
  this.scheduleRender();
831
829
  return;
832
830
  }
833
- if (event.key === 'tab' || event.key === 'enter') {
834
- // Select autocomplete item
835
- if (this.autocompleteItems.length > 0) {
836
- const selected = this.autocompleteItems[this.autocompleteIndex];
837
- this.editor.setValue('/' + selected + ' ');
838
- this.showAutocomplete = false;
839
- this.scheduleRender();
840
- return;
841
- }
842
- }
843
831
  }
844
832
  // Handle `@mention` autocomplete navigation
845
- if (this.showMentionAutocomplete) {
846
- if (event.key === 'up') {
847
- this.mentionIndex = Math.max(0, this.mentionIndex - 1);
833
+ if (this.mention.open) {
834
+ const { state, action } = handleMentionPickerKey(this.mention, event);
835
+ this.mention = state;
836
+ if (action.type === 'navigate') {
848
837
  this.scheduleRender();
849
838
  return;
850
839
  }
851
- if (event.key === 'down') {
852
- this.mentionIndex = Math.min(this.mentionItems.length - 1, this.mentionIndex + 1);
840
+ if (action.type === 'select') {
841
+ // Replace `@query` with `@selectedPath` in the editor.
842
+ this.applyMentionSelection(action.suggestion, action.atStart);
843
+ this.mention = closeMentionPicker(this.mention);
853
844
  this.scheduleRender();
854
845
  return;
855
846
  }
856
- if (event.key === 'tab') {
857
- // Replace `@query` with `@selectedPath` in the editor.
858
- if (this.mentionItems.length > 0) {
859
- this.applyMentionSelection();
860
- this.showMentionAutocomplete = false;
861
- this.scheduleRender();
862
- return;
863
- }
864
- }
865
847
  }
866
848
  // Ctrl+L to clear
867
849
  if (event.ctrl && event.key === 'l') {
@@ -889,7 +871,7 @@ export class App {
889
871
  // Ctrl+U - clear line
890
872
  if (event.ctrl && event.key === 'u') {
891
873
  this.editor.clear();
892
- this.showAutocomplete = false;
874
+ this.autocomplete = closeCommandAutocomplete(this.autocomplete);
893
875
  this.scheduleRender();
894
876
  return;
895
877
  }
@@ -945,7 +927,7 @@ export class App {
945
927
  }
946
928
  // While agent/loading/streaming: allow typing and Enter to interrupt+reply
947
929
  if (this.isAgentRunning || this.isLoading || this.isStreaming) {
948
- if (event.key === 'enter' && !this.showAutocomplete) {
930
+ if (event.key === 'enter' && !this.autocomplete.open) {
949
931
  const text = this.editor.getValue().trim();
950
932
  if (text) {
951
933
  if (this.isStreaming) {
@@ -966,7 +948,7 @@ export class App {
966
948
  return;
967
949
  }
968
950
  // Enter to submit (only if not in autocomplete)
969
- if (event.key === 'enter' && !this.isLoading && !this.isStreaming && !this.showAutocomplete) {
951
+ if (event.key === 'enter' && !this.isLoading && !this.isStreaming && !this.autocomplete.open) {
970
952
  const rawValue = this.editor.getValue();
971
953
  // Backslash continuation: if line ends with \, add newline instead of submitting
972
954
  if (rawValue.endsWith('\\')) {
@@ -1005,28 +987,20 @@ export class App {
1005
987
  // cursor is in the command-name segment (no space yet).
1006
988
  const result = filterCommands(value, App.COMMANDS);
1007
989
  if (result === null) {
1008
- this.showAutocomplete = false;
1009
- this.autocompleteItems = [];
990
+ this.autocomplete = closeCommandAutocomplete(this.autocomplete);
1010
991
  }
1011
992
  else {
1012
- this.autocompleteItems = result.items;
1013
- this.showAutocomplete = result.items.length > 0;
1014
- this.autocompleteIndex = result.index;
993
+ this.autocomplete = setCommandAutocomplete(this.autocomplete, result.items, result.index);
1015
994
  }
1016
995
  // `@mention` picker — detect an in-progress mention at the cursor.
1017
996
  // Independent of the `/` picker so the two never compete.
1018
997
  const mention = detectMentionQuery(value, cursorPos);
1019
998
  if (mention) {
1020
999
  const root = this.options.getProjectRoot?.() ?? process.cwd();
1021
- this.mentionRoot = root;
1022
- this.mentionAtStart = mention.atStart;
1023
- this.mentionItems = suggestMentions({ root, query: mention.query, limit: 10 });
1024
- this.showMentionAutocomplete = this.mentionItems.length > 0;
1025
- this.mentionIndex = 0;
1000
+ this.mention = openMentionPicker(this.mention, suggestMentions({ root, query: mention.query, limit: 10 }), mention.atStart, root);
1026
1001
  }
1027
1002
  else {
1028
- this.showMentionAutocomplete = false;
1029
- this.mentionItems = [];
1003
+ this.mention = closeMentionPicker(this.mention);
1030
1004
  }
1031
1005
  }
1032
1006
  /**
@@ -1035,23 +1009,16 @@ export class App {
1035
1009
  * positions the cursor right after the inserted path so the user can
1036
1010
  * keep typing the rest of the message.
1037
1011
  */
1038
- applyMentionSelection() {
1039
- if (this.mentionItems.length === 0)
1040
- return;
1041
- const selected = this.mentionItems[this.mentionIndex];
1012
+ applyMentionSelection(selected, atStart) {
1042
1013
  const value = this.editor.getValue();
1043
1014
  const cursor = this.editor.getCursorPos();
1044
- if (this.mentionAtStart >= value.length)
1015
+ if (atStart >= value.length)
1045
1016
  return;
1046
- // `mentionAtStart` is the index OF the `@`, so this slice EXCLUDES it —
1047
- // re-add the sigil or the completed path is no longer a mention and the
1048
- // file never gets attached.
1049
- const before = value.slice(0, this.mentionAtStart) + '@';
1050
- const after = value.slice(cursor);
1051
- const next = before + selected.insertPath + ' ' + after;
1052
- this.editor.setValue(next);
1053
- const newCursor = (before + selected.insertPath + ' ').length;
1054
- this.editor.setCursorPos(newCursor);
1017
+ // Buffer math (including the `@`-sigil re-add) lives in the component —
1018
+ // see applyMentionToBuffer for why the sigil matters.
1019
+ const next = applyMentionToBuffer(value, cursor, atStart, selected);
1020
+ this.editor.setValue(next.value);
1021
+ this.editor.setCursorPos(next.cursor);
1055
1022
  // The picker may still have matches for the new prefix — refresh.
1056
1023
  this.updateAutocomplete();
1057
1024
  }
@@ -1239,6 +1206,9 @@ export class App {
1239
1206
  setDeleteMode: (v) => { this.sessionPickerDeleteMode = v; },
1240
1207
  close: (sessionName) => {
1241
1208
  const callback = this.sessionPickerCallback;
1209
+ // The picker drew below the managed area and scrolled the terminal, so
1210
+ // the shadow buffer no longer matches what is on screen.
1211
+ this.screen.invalidate();
1242
1212
  this.sessionPickerOpen = false;
1243
1213
  this.sessionPickerCallback = null;
1244
1214
  this.sessionPickerDeleteMode = false;
@@ -1265,6 +1235,7 @@ export class App {
1265
1235
  setSelection: (v) => { this.confirmSelection = v; },
1266
1236
  close: (result) => {
1267
1237
  const options = this.confirmOptions;
1238
+ this.screen.invalidate();
1268
1239
  this.confirmOptions = null;
1269
1240
  this.confirmOpen = false;
1270
1241
  if (result === 'yes')
@@ -1286,75 +1257,10 @@ export class App {
1286
1257
  * ↑ / ↓ navigate (preview only — no decision)
1287
1258
  */
1288
1259
  handleHunkPickerKey(event) {
1289
- const opts = this.hunkPickerOptions;
1290
- if (!opts) {
1291
- this.hunkPickerOpen = false;
1292
- this.scheduleRender();
1293
- return;
1294
- }
1295
- const finish = () => {
1296
- const accepted = this.hunkPickerAccepted;
1297
- const cb = opts.onComplete;
1298
- this.hunkPickerOptions = null;
1299
- this.hunkPickerOpen = false;
1300
- this.hunkPickerAccepted = [];
1301
- this.hunkPickerIndex = 0;
1302
- cb(accepted);
1303
- this.scheduleRender();
1304
- };
1305
- const advance = () => {
1306
- if (this.hunkPickerIndex >= opts.items.length - 1) {
1307
- finish();
1308
- }
1309
- else {
1310
- this.hunkPickerIndex++;
1311
- this.scheduleRender();
1312
- }
1313
- };
1314
- const acceptCurrent = () => {
1315
- const item = opts.items[this.hunkPickerIndex];
1316
- if (item) {
1317
- this.hunkPickerAccepted.push({ path: item.path, hunkIndex: item.hunkIndex });
1318
- }
1319
- advance();
1320
- };
1321
- switch (event.key) {
1322
- case 'y':
1323
- case 'enter':
1324
- case 'right':
1325
- acceptCurrent();
1326
- return;
1327
- case 'n':
1328
- case 'left':
1329
- advance();
1330
- return;
1331
- case 'a':
1332
- // Accept current + all remaining.
1333
- for (let i = this.hunkPickerIndex; i < opts.items.length; i++) {
1334
- const item = opts.items[i];
1335
- this.hunkPickerAccepted.push({ path: item.path, hunkIndex: item.hunkIndex });
1336
- }
1337
- finish();
1338
- return;
1339
- case 'q':
1340
- case 'escape':
1341
- finish();
1342
- return;
1343
- case 'up':
1344
- if (this.hunkPickerIndex > 0) {
1345
- this.hunkPickerIndex--;
1346
- this.scheduleRender();
1347
- }
1348
- return;
1349
- case 'down':
1350
- if (this.hunkPickerIndex < opts.items.length - 1) {
1351
- this.hunkPickerIndex++;
1352
- this.scheduleRender();
1353
- }
1354
- return;
1355
- default:
1356
- return;
1357
- }
1260
+ // Delegates to components/HunkPicker.ts — the pure handler returns the
1261
+ // next state (and fires onComplete exactly once); App only stores it.
1262
+ this.hunkPicker = handleHunkPickerKey(this.hunkPicker, event);
1263
+ this.scheduleRender();
1358
1264
  }
1359
1265
  /**
1360
1266
  * Submit the current input buffer (used by Enter and Escape-in-multiline)
@@ -1365,7 +1271,7 @@ export class App {
1365
1271
  return;
1366
1272
  this.editor.addToHistory(value);
1367
1273
  this.editor.clear();
1368
- this.showAutocomplete = false;
1274
+ this.autocomplete = closeCommandAutocomplete(this.autocomplete);
1369
1275
  if (value.startsWith('/')) {
1370
1276
  this.handleCommand(value);
1371
1277
  }
@@ -1458,11 +1364,11 @@ export class App {
1458
1364
  // If menu or settings is open, reserve space for it at bottom
1459
1365
  const panelHeight = bottomPanelHeight({
1460
1366
  height,
1461
- pasteInfoOpen: this.pasteInfoOpen,
1462
- pasteInfoPreviewLines: this.pasteInfo ? this.pasteInfo.preview.split('\n').length : 0,
1367
+ pasteInfoOpen: this.pasteDialog.open,
1368
+ pasteInfoPreviewLines: this.pasteDialog.info ? this.pasteDialog.info.preview.split('\n').length : 0,
1463
1369
  isAgentRunning: this.isAgentRunning,
1464
1370
  confirmOpen: this.confirmOpen && !!this.confirmOptions,
1465
- hunkPickerOpen: this.hunkPickerOpen && !!this.hunkPickerOptions,
1371
+ hunkPickerOpen: this.hunkPicker.open && !!this.hunkPicker.options,
1466
1372
  permissionOpen: this.permissionOpen,
1467
1373
  sessionPickerOpen: this.sessionPickerOpen,
1468
1374
  sessionPickerItemCount: this.sessionPickerItems.length,
@@ -1481,10 +1387,10 @@ export class App {
1481
1387
  menuItemCount: this.menuItems.length,
1482
1388
  settingsOpen: this.settingsOpen,
1483
1389
  settingsCount: SETTINGS.length,
1484
- showAutocomplete: this.showAutocomplete,
1485
- autocompleteItemCount: this.autocompleteItems.length,
1486
- mentionPickerOpen: this.showMentionAutocomplete,
1487
- mentionItemCount: this.mentionItems.length,
1390
+ showAutocomplete: this.autocomplete.open,
1391
+ autocompleteItemCount: this.autocomplete.items.length,
1392
+ mentionPickerOpen: this.mention.open,
1393
+ mentionItemCount: this.mention.items.length,
1488
1394
  });
1489
1395
  const layout = chatLayout(height, panelHeight);
1490
1396
  const mainHeight = layout.mainHeight;
@@ -1556,14 +1462,14 @@ export class App {
1556
1462
  this.renderInlineConfirm(statusLine + 1, width);
1557
1463
  }
1558
1464
  // Inline hunk picker renders BELOW status bar
1559
- if (this.hunkPickerOpen && this.hunkPickerOptions) {
1465
+ if (this.hunkPicker.open && this.hunkPicker.options) {
1560
1466
  this.renderInlineHunkPicker(statusLine + 1, width);
1561
1467
  }
1562
1468
  // Inline autocomplete renders BELOW status bar
1563
- if (this.showAutocomplete && this.autocompleteItems.length > 0 && !this.menuOpen && !this.settingsOpen && !this.helpOpen && !this.confirmOpen && !this.permissionOpen && !this.sessionPickerOpen) {
1469
+ if (this.autocomplete.open && this.autocomplete.items.length > 0 && !this.menuOpen && !this.settingsOpen && !this.helpOpen && !this.confirmOpen && !this.permissionOpen && !this.sessionPickerOpen) {
1564
1470
  this.renderInlineAutocomplete(statusLine + 1, width);
1565
1471
  }
1566
- else if (this.showMentionAutocomplete && this.mentionItems.length > 0 && !this.menuOpen && !this.settingsOpen && !this.helpOpen && !this.confirmOpen && !this.permissionOpen && !this.sessionPickerOpen) {
1472
+ else if (this.mention.open && this.mention.items.length > 0 && !this.menuOpen && !this.settingsOpen && !this.helpOpen && !this.confirmOpen && !this.permissionOpen && !this.sessionPickerOpen) {
1567
1473
  this.renderInlineMentionPicker(statusLine + 1, width);
1568
1474
  }
1569
1475
  // Inline permission renders BELOW status bar
@@ -1579,7 +1485,7 @@ export class App {
1579
1485
  this.renderInlineAgentProgress(statusLine + 1, width);
1580
1486
  }
1581
1487
  // Inline paste info renders BELOW status bar
1582
- if (this.pasteInfoOpen && this.pasteInfo) {
1488
+ if (this.pasteDialog.open && this.pasteDialog.info) {
1583
1489
  this.renderInlinePasteInfo(statusLine + 1, width);
1584
1490
  }
1585
1491
  this.screen.render();
@@ -1587,11 +1493,11 @@ export class App {
1587
1493
  shouldRenderAgentTimeline(width, height) {
1588
1494
  if (!this.isAgentRunning || width < 92 || height < 26)
1589
1495
  return false;
1590
- return !(this.pasteInfoOpen ||
1496
+ return !(this.pasteDialog.open ||
1591
1497
  this.permissionOpen ||
1592
1498
  this.sessionPickerOpen ||
1593
1499
  this.confirmOpen ||
1594
- this.hunkPickerOpen ||
1500
+ this.hunkPicker.open ||
1595
1501
  this.statusOpen ||
1596
1502
  this.helpOpen ||
1597
1503
  this.settingsOpen ||
@@ -1600,8 +1506,8 @@ export class App {
1600
1506
  this.logoutOpen ||
1601
1507
  this.loginOpen ||
1602
1508
  this.menuOpen ||
1603
- this.showAutocomplete ||
1604
- this.showMentionAutocomplete);
1509
+ this.autocomplete.open ||
1510
+ this.mention.open);
1605
1511
  }
1606
1512
  renderPersistentHeader(width) {
1607
1513
  const status = this.options.getStatus();
@@ -1893,52 +1799,8 @@ export class App {
1893
1799
  // Footer
1894
1800
  this.screen.writeLine(y, '←/→ select • y/n quick • Enter confirm • Esc cancel', fg.gray);
1895
1801
  }
1896
- /**
1897
- * Render inline hunk picker (`/apply --interactive`).
1898
- * Shows the current hunk's diff + the y/n/a/q key legend.
1899
- */
1900
1802
  renderInlineHunkPicker(startY, width) {
1901
- const opts = this.hunkPickerOptions;
1902
- if (!opts)
1903
- return;
1904
- const item = opts.items[this.hunkPickerIndex];
1905
- let y = startY;
1906
- this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
1907
- // Title + progress
1908
- const progress = opts.items.length > 0
1909
- ? ` (${this.hunkPickerIndex + 1}/${opts.items.length})`
1910
- : '';
1911
- this.screen.writeLine(y++, `${opts.title}${progress}`, PRIMARY_COLOR + style.bold);
1912
- if (!item) {
1913
- this.screen.writeLine(y++, 'No hunks to review.', fg.gray);
1914
- this.screen.writeLine(y, 'Press any key to close.', fg.gray);
1915
- return;
1916
- }
1917
- // File path + hunk header
1918
- this.screen.writeLine(y++, `File: ${item.path}`, fg.cyan);
1919
- this.screen.writeLine(y++, `Hunk: ${item.header}`, fg.gray);
1920
- // Diff lines (capped to available vertical space; show up to 12)
1921
- const maxDiffLines = 12;
1922
- const lines = item.lines.slice(0, maxDiffLines);
1923
- for (const line of lines) {
1924
- const prefix = line.charAt(0);
1925
- let color = fg.white;
1926
- if (prefix === '+')
1927
- color = fg.green;
1928
- else if (prefix === '-')
1929
- color = fg.red;
1930
- else if (prefix === '@')
1931
- color = fg.cyan;
1932
- // Truncate long lines to terminal width.
1933
- const truncated = line.length > width - 2 ? line.slice(0, width - 5) + '...' : line;
1934
- this.screen.writeLine(y++, ` ${truncated}`, color);
1935
- }
1936
- if (item.lines.length > maxDiffLines) {
1937
- this.screen.writeLine(y++, ` … (${item.lines.length - maxDiffLines} more lines)`, fg.gray);
1938
- }
1939
- y++;
1940
- // Key legend
1941
- this.screen.writeLine(y, 'y/Enter accept • n skip • a accept all • q/Esc quit • ↑/↓ navigate', fg.gray);
1803
+ renderHunkPicker(this.screen, this.hunkPicker, startY, width);
1942
1804
  }
1943
1805
  /**
1944
1806
  * Render input line
@@ -1961,7 +1823,7 @@ export class App {
1961
1823
  return;
1962
1824
  }
1963
1825
  // Paste info open
1964
- if (this.pasteInfoOpen) {
1826
+ if (this.pasteDialog.open) {
1965
1827
  this.screen.write(0, y, '> ', fg.gray);
1966
1828
  this.screen.write(2, y, 'Confirm paste action below...', fg.yellow);
1967
1829
  this.screen.showCursor(false);
@@ -2268,38 +2130,7 @@ export class App {
2268
2130
  * Render inline autocomplete below status bar
2269
2131
  */
2270
2132
  renderInlineAutocomplete(startY, width) {
2271
- const items = this.autocompleteItems;
2272
- const maxVisible = Math.min(items.length, 8);
2273
- let y = startY;
2274
- // Separator line
2275
- this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
2276
- // Title
2277
- this.screen.writeLine(y++, 'Commands', PRIMARY_COLOR + style.bold);
2278
- // Items with descriptions
2279
- const visibleStart = Math.max(0, this.autocompleteIndex - maxVisible + 1);
2280
- const visibleItems = items.slice(visibleStart, visibleStart + maxVisible);
2281
- for (let i = 0; i < visibleItems.length; i++) {
2282
- const item = visibleItems[i];
2283
- const actualIndex = visibleStart + i;
2284
- const isSelected = actualIndex === this.autocompleteIndex;
2285
- const desc = COMMAND_DESCRIPTIONS[item] || '';
2286
- const prefix = isSelected ? '► ' : ' ';
2287
- const cmdText = ('/' + item).padEnd(18);
2288
- if (isSelected) {
2289
- this.screen.write(0, y, prefix, PRIMARY_COLOR);
2290
- this.screen.write(prefix.length, y, cmdText, PRIMARY_COLOR + style.bold);
2291
- this.screen.write(prefix.length + cmdText.length, y, desc, fg.white);
2292
- }
2293
- else {
2294
- this.screen.write(0, y, prefix, '');
2295
- this.screen.write(prefix.length, y, cmdText, fg.green);
2296
- this.screen.write(prefix.length + cmdText.length, y, desc, fg.gray);
2297
- }
2298
- y++;
2299
- }
2300
- // Footer
2301
- const scrollInfo = items.length > maxVisible ? ` (${visibleStart + 1}-${visibleStart + visibleItems.length}/${items.length})` : '';
2302
- this.screen.writeLine(y, `↑↓ navigate • Tab/Enter select • Esc cancel${scrollInfo}`, fg.gray);
2133
+ renderCommandAutocomplete(this.screen, this.autocomplete, startY, COMMAND_DESCRIPTIONS);
2303
2134
  }
2304
2135
  /**
2305
2136
  * Render inline `@mention` file picker below the status bar.
@@ -2309,37 +2140,7 @@ export class App {
2309
2140
  * the description, and a `@` prefix instead of `/`.
2310
2141
  */
2311
2142
  renderInlineMentionPicker(startY, width) {
2312
- const items = this.mentionItems;
2313
- const maxVisible = Math.min(items.length, 8);
2314
- let y = startY;
2315
- // Separator line
2316
- this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
2317
- // Title
2318
- this.screen.writeLine(y++, 'Add file to context (@mention)', PRIMARY_COLOR + style.bold);
2319
- // Items: `path` + directory detail
2320
- const visibleStart = Math.max(0, this.mentionIndex - maxVisible + 1);
2321
- const visibleItems = items.slice(visibleStart, visibleStart + maxVisible);
2322
- for (let i = 0; i < visibleItems.length; i++) {
2323
- const item = visibleItems[i];
2324
- const actualIndex = visibleStart + i;
2325
- const isSelected = actualIndex === this.mentionIndex;
2326
- const prefix = isSelected ? '► ' : ' ';
2327
- const pathText = ('@' + item.label).padEnd(40);
2328
- if (isSelected) {
2329
- this.screen.write(0, y, prefix, PRIMARY_COLOR);
2330
- this.screen.write(prefix.length, y, pathText, PRIMARY_COLOR + style.bold);
2331
- this.screen.write(prefix.length + pathText.length, y, item.detail, fg.white);
2332
- }
2333
- else {
2334
- this.screen.write(0, y, prefix, '');
2335
- this.screen.write(prefix.length, y, pathText, fg.cyan);
2336
- this.screen.write(prefix.length + pathText.length, y, item.detail, fg.gray);
2337
- }
2338
- y++;
2339
- }
2340
- // Footer
2341
- const scrollInfo = items.length > maxVisible ? ` (${visibleStart + 1}-${visibleStart + visibleItems.length}/${items.length})` : '';
2342
- this.screen.writeLine(y, `↑↓ navigate • Tab select • Esc cancel${scrollInfo}`, fg.gray);
2143
+ renderMentionPicker(this.screen, this.mention, startY);
2343
2144
  }
2344
2145
  /**
2345
2146
  * Render inline permission dialog
@@ -2451,33 +2252,7 @@ export class App {
2451
2252
  * Render inline paste info below status bar
2452
2253
  */
2453
2254
  renderInlinePasteInfo(startY, width) {
2454
- if (!this.pasteInfo)
2455
- return;
2456
- let y = startY;
2457
- // Separator line
2458
- this.screen.horizontalLine(y++, '─', PRIMARY_COLOR);
2459
- // Title with stats
2460
- this.screen.write(0, y, 'Paste Detected ', PRIMARY_COLOR + style.bold);
2461
- this.screen.write(15, y, `(${this.pasteInfo.chars} chars, ${this.pasteInfo.lines} lines)`, fg.cyan);
2462
- y++;
2463
- // Preview box
2464
- y++;
2465
- const previewLines = this.pasteInfo.preview.split('\n').slice(0, 5);
2466
- for (const line of previewLines) {
2467
- const displayLine = line.length > width - 4 ? line.slice(0, width - 7) + '...' : line;
2468
- this.screen.writeLine(y++, ' ' + displayLine, fg.gray);
2469
- }
2470
- if (this.pasteInfo.lines > 5) {
2471
- this.screen.writeLine(y++, ` ... (${this.pasteInfo.lines - 5} more lines)`, fg.gray);
2472
- }
2473
- y++;
2474
- // Options
2475
- this.screen.write(0, y, '[Y/Enter] ', fg.green);
2476
- this.screen.write(10, y, 'Add to input', fg.white);
2477
- this.screen.write(25, y, '[S] ', fg.yellow);
2478
- this.screen.write(29, y, 'Send directly', fg.white);
2479
- this.screen.write(45, y, '[N/Esc] ', fg.red);
2480
- this.screen.write(53, y, 'Cancel', fg.white);
2255
+ renderPasteDialog(this.screen, this.pasteDialog, startY, width);
2481
2256
  }
2482
2257
  /**
2483
2258
  * Render inline agent progress below status bar (LiveCodeStream style)