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.
- package/README.md +53 -16
- package/dist/acp/commands.js +11 -55
- package/dist/acp/protocol.d.ts +34 -0
- package/dist/acp/server.d.ts +6 -1
- package/dist/acp/server.js +97 -2
- package/dist/api/index.js +9 -0
- package/dist/commands/core/index.d.ts +19 -0
- package/dist/commands/core/index.js +28 -0
- package/dist/commands/core/keysync.d.ts +2 -0
- package/dist/commands/core/keysync.js +34 -0
- package/dist/commands/core/telemetry.d.ts +2 -0
- package/dist/commands/core/telemetry.js +34 -0
- package/dist/config/index.js +2 -2
- package/dist/renderer/App.d.ts +9 -48
- package/dist/renderer/App.js +113 -338
- package/dist/renderer/Screen.d.ts +13 -0
- package/dist/renderer/Screen.js +22 -0
- package/dist/renderer/commands/registry.js +3 -3
- package/dist/renderer/commands.js +19 -51
- package/dist/renderer/components/CommandAutocomplete.d.ts +46 -0
- package/dist/renderer/components/CommandAutocomplete.js +103 -0
- package/dist/renderer/components/HunkPicker.d.ts +48 -0
- package/dist/renderer/components/HunkPicker.js +140 -0
- package/dist/renderer/components/MentionPicker.d.ts +60 -0
- package/dist/renderer/components/MentionPicker.js +111 -0
- package/dist/renderer/components/PasteDialog.d.ts +43 -0
- package/dist/renderer/components/PasteDialog.js +70 -0
- package/dist/renderer/layout.js +1 -0
- package/dist/renderer/main.js +15 -39
- package/dist/utils/agent.js +121 -26
- package/dist/utils/agentChat.d.ts +11 -4
- package/dist/utils/agentChat.js +53 -25
- package/dist/utils/codeepCloud.d.ts +3 -0
- package/dist/utils/codeepCloud.js +62 -7
- package/dist/utils/personalities.d.ts +63 -5
- package/dist/utils/personalities.js +583 -31
- package/dist/utils/shell.d.ts +11 -1
- package/dist/utils/shell.js +169 -82
- package/dist/utils/ssrfGuard.d.ts +18 -0
- package/dist/utils/ssrfGuard.js +83 -0
- package/dist/utils/taskPlanner.d.ts +7 -1
- package/dist/utils/taskPlanner.js +16 -7
- package/dist/utils/toolExecution.d.ts +1 -0
- package/dist/utils/toolExecution.js +48 -88
- package/dist/utils/tools.d.ts +3 -3
- package/dist/utils/tools.js +18 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/renderer/App.js
CHANGED
|
@@ -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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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.
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
746
|
-
hunkPickerOpen: this.
|
|
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.
|
|
796
|
-
this.
|
|
791
|
+
if (this.mention.open) {
|
|
792
|
+
this.mention = closeMentionPicker(this.mention);
|
|
797
793
|
this.scheduleRender();
|
|
798
794
|
return;
|
|
799
795
|
}
|
|
800
|
-
if (this.
|
|
801
|
-
this.
|
|
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.
|
|
823
|
-
|
|
824
|
-
|
|
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 (
|
|
829
|
-
this.
|
|
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.
|
|
846
|
-
|
|
847
|
-
|
|
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 (
|
|
852
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1009
|
-
this.autocompleteItems = [];
|
|
990
|
+
this.autocomplete = closeCommandAutocomplete(this.autocomplete);
|
|
1010
991
|
}
|
|
1011
992
|
else {
|
|
1012
|
-
this.
|
|
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.
|
|
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.
|
|
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 (
|
|
1015
|
+
if (atStart >= value.length)
|
|
1045
1016
|
return;
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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.
|
|
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.
|
|
1462
|
-
pasteInfoPreviewLines: this.
|
|
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.
|
|
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.
|
|
1485
|
-
autocompleteItemCount: this.
|
|
1486
|
-
mentionPickerOpen: this.
|
|
1487
|
-
mentionItemCount: this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1496
|
+
return !(this.pasteDialog.open ||
|
|
1591
1497
|
this.permissionOpen ||
|
|
1592
1498
|
this.sessionPickerOpen ||
|
|
1593
1499
|
this.confirmOpen ||
|
|
1594
|
-
this.
|
|
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.
|
|
1604
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|