@jvittechs/jai1-cli 0.1.100 ā 0.1.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +551 -122
- package/dist/cli.js.map +1 -1
- package/dist/web-chat/app.js +490 -5
- package/dist/web-chat/style.css +227 -0
- package/package.json +2 -1
package/dist/web-chat/app.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
const MAX_STORAGE_SIZE = 4.5 * 1024 * 1024;
|
|
20
20
|
const MAX_CONTEXT_TOKENS = 175000; // 175K token limit
|
|
21
|
+
const MAX_MENTIONED_FILES = 5; // Maximum files that can be mentioned
|
|
21
22
|
|
|
22
23
|
// ============================================
|
|
23
24
|
// Agent Definitions
|
|
@@ -350,6 +351,11 @@ When reviewing code:
|
|
|
350
351
|
currentConversationId: null,
|
|
351
352
|
isStreaming: false,
|
|
352
353
|
modelStats: null, // { date, models: { [modelId]: { limit, used, remaining } } }
|
|
354
|
+
// File mention state
|
|
355
|
+
mentionedFiles: [], // Array of { path, name, size, language }
|
|
356
|
+
fileSearchResults: [], // Autocomplete results
|
|
357
|
+
isFileSearching: false, // Autocomplete loading state
|
|
358
|
+
fileServiceInfo: null, // { workingDir, maxFiles }
|
|
353
359
|
};
|
|
354
360
|
|
|
355
361
|
// ============================================
|
|
@@ -620,6 +626,31 @@ When reviewing code:
|
|
|
620
626
|
reader.releaseLock();
|
|
621
627
|
}
|
|
622
628
|
},
|
|
629
|
+
|
|
630
|
+
// === File API Methods ===
|
|
631
|
+
|
|
632
|
+
async getFileServiceInfo() {
|
|
633
|
+
const response = await this.request('/api/files/info');
|
|
634
|
+
return response.json();
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
async searchFiles(query, limit = 10) {
|
|
638
|
+
const response = await this.request(`/api/files/search?q=${encodeURIComponent(query)}&limit=${limit}`);
|
|
639
|
+
return response.json();
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
async readFile(filePath) {
|
|
643
|
+
const response = await this.request(`/api/files/read?path=${encodeURIComponent(filePath)}`);
|
|
644
|
+
return response.json();
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
async readFiles(paths) {
|
|
648
|
+
const response = await this.request('/api/files/batch', {
|
|
649
|
+
method: 'POST',
|
|
650
|
+
body: JSON.stringify({ paths }),
|
|
651
|
+
});
|
|
652
|
+
return response.json();
|
|
653
|
+
},
|
|
623
654
|
};
|
|
624
655
|
|
|
625
656
|
class APIError extends Error {
|
|
@@ -719,6 +750,431 @@ When reviewing code:
|
|
|
719
750
|
},
|
|
720
751
|
};
|
|
721
752
|
|
|
753
|
+
// ============================================
|
|
754
|
+
// File Mention System
|
|
755
|
+
// ============================================
|
|
756
|
+
const FileMention = {
|
|
757
|
+
autocompleteEl: null,
|
|
758
|
+
debounceTimer: null,
|
|
759
|
+
selectedIndex: -1,
|
|
760
|
+
mentionStartPos: -1,
|
|
761
|
+
lastQuery: '',
|
|
762
|
+
|
|
763
|
+
init() {
|
|
764
|
+
// Create autocomplete dropdown
|
|
765
|
+
this.createAutocompleteElement();
|
|
766
|
+
// Create mentioned files container
|
|
767
|
+
this.createMentionedFilesContainer();
|
|
768
|
+
// Load file service info
|
|
769
|
+
this.loadFileServiceInfo();
|
|
770
|
+
},
|
|
771
|
+
|
|
772
|
+
createAutocompleteElement() {
|
|
773
|
+
this.autocompleteEl = document.createElement('div');
|
|
774
|
+
this.autocompleteEl.className = 'file-autocomplete hidden';
|
|
775
|
+
this.autocompleteEl.innerHTML = `
|
|
776
|
+
<div class="file-autocomplete-header">
|
|
777
|
+
<span class="autocomplete-icon">š</span>
|
|
778
|
+
<span class="autocomplete-title">Files</span>
|
|
779
|
+
<span class="autocomplete-hint">āā to navigate, Enter to select, Esc to close</span>
|
|
780
|
+
</div>
|
|
781
|
+
<div class="file-autocomplete-list"></div>
|
|
782
|
+
`;
|
|
783
|
+
// Insert before input container
|
|
784
|
+
const inputArea = document.querySelector('.input-area');
|
|
785
|
+
if (inputArea) {
|
|
786
|
+
inputArea.insertBefore(this.autocompleteEl, inputArea.querySelector('.input-container'));
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
|
|
790
|
+
createMentionedFilesContainer() {
|
|
791
|
+
const container = document.createElement('div');
|
|
792
|
+
container.id = 'mentioned-files';
|
|
793
|
+
container.className = 'mentioned-files hidden';
|
|
794
|
+
// Insert before input selectors
|
|
795
|
+
const inputArea = document.querySelector('.input-area');
|
|
796
|
+
const selectors = inputArea?.querySelector('.input-selectors');
|
|
797
|
+
if (inputArea && selectors) {
|
|
798
|
+
inputArea.insertBefore(container, selectors);
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
|
|
802
|
+
async loadFileServiceInfo() {
|
|
803
|
+
try {
|
|
804
|
+
const response = await API.getFileServiceInfo();
|
|
805
|
+
if (response.success && response.data) {
|
|
806
|
+
state.fileServiceInfo = response.data;
|
|
807
|
+
}
|
|
808
|
+
} catch (error) {
|
|
809
|
+
console.error('Failed to load file service info:', error);
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
|
|
813
|
+
// Detect @ mention in textarea
|
|
814
|
+
detectMention(textarea) {
|
|
815
|
+
const text = textarea.value;
|
|
816
|
+
const cursorPos = textarea.selectionStart;
|
|
817
|
+
|
|
818
|
+
// Find @ before cursor
|
|
819
|
+
let atPos = -1;
|
|
820
|
+
for (let i = cursorPos - 1; i >= 0; i--) {
|
|
821
|
+
const char = text[i];
|
|
822
|
+
if (char === '@') {
|
|
823
|
+
atPos = i;
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
// Stop at whitespace or newline (not a mention)
|
|
827
|
+
if (char === ' ' || char === '\n' || char === '\t') {
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (atPos === -1) {
|
|
833
|
+
this.hideAutocomplete();
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Extract query after @
|
|
838
|
+
const query = text.substring(atPos + 1, cursorPos);
|
|
839
|
+
this.mentionStartPos = atPos;
|
|
840
|
+
return query;
|
|
841
|
+
},
|
|
842
|
+
|
|
843
|
+
async handleInput(textarea) {
|
|
844
|
+
const query = this.detectMention(textarea);
|
|
845
|
+
|
|
846
|
+
if (query === null) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Debounce search
|
|
851
|
+
clearTimeout(this.debounceTimer);
|
|
852
|
+
this.debounceTimer = setTimeout(async () => {
|
|
853
|
+
if (query.length >= 1) {
|
|
854
|
+
await this.searchFiles(query);
|
|
855
|
+
} else {
|
|
856
|
+
this.hideAutocomplete();
|
|
857
|
+
}
|
|
858
|
+
}, 150);
|
|
859
|
+
},
|
|
860
|
+
|
|
861
|
+
async searchFiles(query) {
|
|
862
|
+
if (query === this.lastQuery) return;
|
|
863
|
+
this.lastQuery = query;
|
|
864
|
+
|
|
865
|
+
state.isFileSearching = true;
|
|
866
|
+
this.selectedIndex = 0; // Reset selection for new search
|
|
867
|
+
this.showAutocomplete();
|
|
868
|
+
this.renderLoading();
|
|
869
|
+
|
|
870
|
+
try {
|
|
871
|
+
const response = await API.searchFiles(query, 10);
|
|
872
|
+
if (response.success && response.data) {
|
|
873
|
+
state.fileSearchResults = response.data.files || [];
|
|
874
|
+
this.selectedIndex = state.fileSearchResults.length > 0 ? 0 : -1;
|
|
875
|
+
this.renderResults();
|
|
876
|
+
}
|
|
877
|
+
} catch (error) {
|
|
878
|
+
console.error('File search error:', error);
|
|
879
|
+
this.renderError('Failed to search files');
|
|
880
|
+
} finally {
|
|
881
|
+
state.isFileSearching = false;
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
|
|
885
|
+
showAutocomplete() {
|
|
886
|
+
if (this.autocompleteEl) {
|
|
887
|
+
this.autocompleteEl.classList.remove('hidden');
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
|
|
891
|
+
hideAutocomplete() {
|
|
892
|
+
if (this.autocompleteEl) {
|
|
893
|
+
this.autocompleteEl.classList.add('hidden');
|
|
894
|
+
}
|
|
895
|
+
this.selectedIndex = -1;
|
|
896
|
+
this.mentionStartPos = -1;
|
|
897
|
+
this.lastQuery = '';
|
|
898
|
+
state.fileSearchResults = [];
|
|
899
|
+
},
|
|
900
|
+
|
|
901
|
+
renderLoading() {
|
|
902
|
+
const list = this.autocompleteEl?.querySelector('.file-autocomplete-list');
|
|
903
|
+
if (list) {
|
|
904
|
+
list.innerHTML = `
|
|
905
|
+
<div class="file-autocomplete-loading">
|
|
906
|
+
<span class="loading-spinner">ā</span>
|
|
907
|
+
<span>Searching files...</span>
|
|
908
|
+
</div>
|
|
909
|
+
`;
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
|
|
913
|
+
renderError(message) {
|
|
914
|
+
const list = this.autocompleteEl?.querySelector('.file-autocomplete-list');
|
|
915
|
+
if (list) {
|
|
916
|
+
list.innerHTML = `
|
|
917
|
+
<div class="file-autocomplete-error">
|
|
918
|
+
<span>ā </span>
|
|
919
|
+
<span>${escapeHtml(message)}</span>
|
|
920
|
+
</div>
|
|
921
|
+
`;
|
|
922
|
+
}
|
|
923
|
+
},
|
|
924
|
+
|
|
925
|
+
renderResults() {
|
|
926
|
+
const list = this.autocompleteEl?.querySelector('.file-autocomplete-list');
|
|
927
|
+
if (!list) return;
|
|
928
|
+
|
|
929
|
+
if (state.fileSearchResults.length === 0) {
|
|
930
|
+
list.innerHTML = `
|
|
931
|
+
<div class="file-autocomplete-empty">
|
|
932
|
+
<span>No files found</span>
|
|
933
|
+
</div>
|
|
934
|
+
`;
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
list.innerHTML = '';
|
|
939
|
+
state.fileSearchResults.forEach((file, index) => {
|
|
940
|
+
const item = document.createElement('div');
|
|
941
|
+
item.className = 'file-autocomplete-item' + (index === this.selectedIndex ? ' selected' : '');
|
|
942
|
+
item.dataset.index = index;
|
|
943
|
+
item.innerHTML = `
|
|
944
|
+
<span class="file-icon">${this.getFileIcon(file.extension)}</span>
|
|
945
|
+
<span class="file-path">${escapeHtml(file.path)}</span>
|
|
946
|
+
<span class="file-size">${this.formatFileSize(file.size)}</span>
|
|
947
|
+
`;
|
|
948
|
+
item.addEventListener('click', () => this.selectFile(index));
|
|
949
|
+
item.addEventListener('mouseenter', () => {
|
|
950
|
+
this.selectedIndex = index;
|
|
951
|
+
this.updateSelection();
|
|
952
|
+
});
|
|
953
|
+
list.appendChild(item);
|
|
954
|
+
});
|
|
955
|
+
},
|
|
956
|
+
|
|
957
|
+
getFileIcon(extension) {
|
|
958
|
+
const iconMap = {
|
|
959
|
+
'ts': 'š', 'tsx': 'š', 'js': 'š', 'jsx': 'š',
|
|
960
|
+
'vue': 'š', 'php': 'š', 'py': 'š',
|
|
961
|
+
'json': 'š', 'md': 'š', 'css': 'šØ', 'scss': 'šØ',
|
|
962
|
+
'html': 'š', 'sql': 'šļø', 'sh': 'āļø',
|
|
963
|
+
};
|
|
964
|
+
return iconMap[extension] || 'š';
|
|
965
|
+
},
|
|
966
|
+
|
|
967
|
+
formatFileSize(bytes) {
|
|
968
|
+
if (bytes < 1024) return bytes + 'B';
|
|
969
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB';
|
|
970
|
+
return (bytes / (1024 * 1024)).toFixed(1) + 'MB';
|
|
971
|
+
},
|
|
972
|
+
|
|
973
|
+
updateSelection() {
|
|
974
|
+
const items = this.autocompleteEl?.querySelectorAll('.file-autocomplete-item');
|
|
975
|
+
items?.forEach((item, index) => {
|
|
976
|
+
item.classList.toggle('selected', index === this.selectedIndex);
|
|
977
|
+
});
|
|
978
|
+
},
|
|
979
|
+
|
|
980
|
+
handleKeydown(e, textarea) {
|
|
981
|
+
if (this.autocompleteEl?.classList.contains('hidden')) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// No results to navigate
|
|
986
|
+
if (state.fileSearchResults.length === 0) {
|
|
987
|
+
if (e.key === 'Escape') {
|
|
988
|
+
e.preventDefault();
|
|
989
|
+
this.hideAutocomplete();
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
switch (e.key) {
|
|
996
|
+
case 'ArrowDown':
|
|
997
|
+
e.preventDefault();
|
|
998
|
+
if (this.selectedIndex < 0) {
|
|
999
|
+
this.selectedIndex = 0;
|
|
1000
|
+
} else {
|
|
1001
|
+
this.selectedIndex = Math.min(this.selectedIndex + 1, state.fileSearchResults.length - 1);
|
|
1002
|
+
}
|
|
1003
|
+
this.updateSelection();
|
|
1004
|
+
this.scrollToSelected();
|
|
1005
|
+
return true;
|
|
1006
|
+
|
|
1007
|
+
case 'ArrowUp':
|
|
1008
|
+
e.preventDefault();
|
|
1009
|
+
if (this.selectedIndex < 0) {
|
|
1010
|
+
this.selectedIndex = state.fileSearchResults.length - 1;
|
|
1011
|
+
} else if (this.selectedIndex > 0) {
|
|
1012
|
+
this.selectedIndex--;
|
|
1013
|
+
}
|
|
1014
|
+
this.updateSelection();
|
|
1015
|
+
this.scrollToSelected();
|
|
1016
|
+
return true;
|
|
1017
|
+
|
|
1018
|
+
case 'Enter':
|
|
1019
|
+
if (this.selectedIndex >= 0) {
|
|
1020
|
+
e.preventDefault();
|
|
1021
|
+
this.selectFile(this.selectedIndex, textarea);
|
|
1022
|
+
return true;
|
|
1023
|
+
} else if (state.fileSearchResults.length > 0) {
|
|
1024
|
+
// Select first item if none selected
|
|
1025
|
+
e.preventDefault();
|
|
1026
|
+
this.selectFile(0, textarea);
|
|
1027
|
+
return true;
|
|
1028
|
+
}
|
|
1029
|
+
break;
|
|
1030
|
+
|
|
1031
|
+
case 'Escape':
|
|
1032
|
+
e.preventDefault();
|
|
1033
|
+
this.hideAutocomplete();
|
|
1034
|
+
return true;
|
|
1035
|
+
|
|
1036
|
+
case 'Tab':
|
|
1037
|
+
if (state.fileSearchResults.length > 0) {
|
|
1038
|
+
e.preventDefault();
|
|
1039
|
+
this.selectFile(Math.max(this.selectedIndex, 0), textarea);
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return false;
|
|
1046
|
+
},
|
|
1047
|
+
|
|
1048
|
+
scrollToSelected() {
|
|
1049
|
+
const list = this.autocompleteEl?.querySelector('.file-autocomplete-list');
|
|
1050
|
+
const selectedItem = list?.querySelector('.file-autocomplete-item.selected');
|
|
1051
|
+
if (selectedItem && list) {
|
|
1052
|
+
selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
|
|
1056
|
+
selectFile(index, textarea = elements.messageInput) {
|
|
1057
|
+
const file = state.fileSearchResults[index];
|
|
1058
|
+
if (!file) return;
|
|
1059
|
+
|
|
1060
|
+
// Check if already mentioned
|
|
1061
|
+
if (state.mentionedFiles.some(f => f.path === file.path)) {
|
|
1062
|
+
this.hideAutocomplete();
|
|
1063
|
+
// Remove the @query from textarea
|
|
1064
|
+
this.removeMentionText(textarea);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Check max files limit
|
|
1069
|
+
if (state.mentionedFiles.length >= MAX_MENTIONED_FILES) {
|
|
1070
|
+
alert(`Maximum ${MAX_MENTIONED_FILES} files can be mentioned per message`);
|
|
1071
|
+
this.hideAutocomplete();
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Add to mentioned files
|
|
1076
|
+
state.mentionedFiles.push(file);
|
|
1077
|
+
this.renderMentionedFiles();
|
|
1078
|
+
|
|
1079
|
+
// Remove @query from textarea
|
|
1080
|
+
this.removeMentionText(textarea);
|
|
1081
|
+
|
|
1082
|
+
this.hideAutocomplete();
|
|
1083
|
+
textarea.focus();
|
|
1084
|
+
},
|
|
1085
|
+
|
|
1086
|
+
removeMentionText(textarea) {
|
|
1087
|
+
if (this.mentionStartPos >= 0) {
|
|
1088
|
+
const text = textarea.value;
|
|
1089
|
+
const cursorPos = textarea.selectionStart;
|
|
1090
|
+
const newText = text.substring(0, this.mentionStartPos) + text.substring(cursorPos);
|
|
1091
|
+
textarea.value = newText;
|
|
1092
|
+
textarea.selectionStart = textarea.selectionEnd = this.mentionStartPos;
|
|
1093
|
+
// Trigger input event
|
|
1094
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1097
|
+
|
|
1098
|
+
renderMentionedFiles() {
|
|
1099
|
+
const container = document.getElementById('mentioned-files');
|
|
1100
|
+
if (!container) return;
|
|
1101
|
+
|
|
1102
|
+
if (state.mentionedFiles.length === 0) {
|
|
1103
|
+
container.classList.add('hidden');
|
|
1104
|
+
container.innerHTML = '';
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
container.classList.remove('hidden');
|
|
1109
|
+
container.innerHTML = `
|
|
1110
|
+
<div class="mentioned-files-header">
|
|
1111
|
+
<span class="mentioned-icon">š</span>
|
|
1112
|
+
<span class="mentioned-title">Attached Files (${state.mentionedFiles.length}/${MAX_MENTIONED_FILES})</span>
|
|
1113
|
+
</div>
|
|
1114
|
+
<div class="mentioned-files-list">
|
|
1115
|
+
${state.mentionedFiles.map((file, index) => `
|
|
1116
|
+
<div class="mentioned-file-badge" data-index="${index}">
|
|
1117
|
+
<span class="badge-icon">${this.getFileIcon(file.extension)}</span>
|
|
1118
|
+
<span class="badge-path">${escapeHtml(file.path)}</span>
|
|
1119
|
+
<button class="badge-remove" title="Remove file">Ć</button>
|
|
1120
|
+
</div>
|
|
1121
|
+
`).join('')}
|
|
1122
|
+
</div>
|
|
1123
|
+
`;
|
|
1124
|
+
|
|
1125
|
+
// Add remove handlers
|
|
1126
|
+
container.querySelectorAll('.badge-remove').forEach(btn => {
|
|
1127
|
+
btn.addEventListener('click', (e) => {
|
|
1128
|
+
const index = parseInt(e.target.closest('.mentioned-file-badge').dataset.index);
|
|
1129
|
+
this.removeFile(index);
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
},
|
|
1133
|
+
|
|
1134
|
+
removeFile(index) {
|
|
1135
|
+
state.mentionedFiles.splice(index, 1);
|
|
1136
|
+
this.renderMentionedFiles();
|
|
1137
|
+
},
|
|
1138
|
+
|
|
1139
|
+
clearFiles() {
|
|
1140
|
+
state.mentionedFiles = [];
|
|
1141
|
+
this.renderMentionedFiles();
|
|
1142
|
+
},
|
|
1143
|
+
|
|
1144
|
+
// Get file contents for mentioned files
|
|
1145
|
+
async getFileContents() {
|
|
1146
|
+
if (state.mentionedFiles.length === 0) {
|
|
1147
|
+
return [];
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const paths = state.mentionedFiles.map(f => f.path);
|
|
1151
|
+
try {
|
|
1152
|
+
const response = await API.readFiles(paths);
|
|
1153
|
+
if (response.success && response.data) {
|
|
1154
|
+
return response.data.files || [];
|
|
1155
|
+
}
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
console.error('Failed to read files:', error);
|
|
1158
|
+
}
|
|
1159
|
+
return [];
|
|
1160
|
+
},
|
|
1161
|
+
|
|
1162
|
+
// Format file context for LLM
|
|
1163
|
+
formatFileContext(files) {
|
|
1164
|
+
if (!files || files.length === 0) return '';
|
|
1165
|
+
|
|
1166
|
+
let context = '\n\n[Attached Files]\n\n';
|
|
1167
|
+
files.forEach(file => {
|
|
1168
|
+
if (file.error) {
|
|
1169
|
+
context += `--- ${file.path} ---\n[Error: ${file.error}]\n\n`;
|
|
1170
|
+
} else {
|
|
1171
|
+
context += `--- ${file.path} ---\n\`\`\`${file.language || 'text'}\n${file.content}\n\`\`\`\n\n`;
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
return context;
|
|
1175
|
+
},
|
|
1176
|
+
};
|
|
1177
|
+
|
|
722
1178
|
// ============================================
|
|
723
1179
|
// UI Rendering
|
|
724
1180
|
// ============================================
|
|
@@ -1182,13 +1638,18 @@ When reviewing code:
|
|
|
1182
1638
|
|
|
1183
1639
|
// Enter to send (Shift+Enter for new line)
|
|
1184
1640
|
elements.messageInput.addEventListener('keydown', (e) => {
|
|
1641
|
+
// First check if FileMention wants to handle this key
|
|
1642
|
+
if (FileMention.handleKeydown(e, elements.messageInput)) {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1185
1646
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1186
1647
|
e.preventDefault();
|
|
1187
1648
|
handleSend();
|
|
1188
1649
|
}
|
|
1189
1650
|
});
|
|
1190
1651
|
|
|
1191
|
-
// Auto-resize textarea
|
|
1652
|
+
// Auto-resize textarea and handle file mention
|
|
1192
1653
|
elements.messageInput.addEventListener('input', () => {
|
|
1193
1654
|
const el = elements.messageInput;
|
|
1194
1655
|
el.style.height = 'auto';
|
|
@@ -1203,6 +1664,9 @@ When reviewing code:
|
|
|
1203
1664
|
|
|
1204
1665
|
// Enable/disable send button
|
|
1205
1666
|
elements.sendBtn.disabled = !el.value.trim() || state.isStreaming;
|
|
1667
|
+
|
|
1668
|
+
// Handle file mention detection
|
|
1669
|
+
FileMention.handleInput(el);
|
|
1206
1670
|
});
|
|
1207
1671
|
|
|
1208
1672
|
// New chat button
|
|
@@ -1344,9 +1808,27 @@ When reviewing code:
|
|
|
1344
1808
|
elements.messageInput.style.height = 'auto';
|
|
1345
1809
|
elements.charCount.textContent = '';
|
|
1346
1810
|
|
|
1811
|
+
// Get file contents if any files are mentioned
|
|
1812
|
+
let fileContents = [];
|
|
1813
|
+
let fileContextStr = '';
|
|
1814
|
+
const hasMentionedFiles = state.mentionedFiles.length > 0;
|
|
1815
|
+
if (hasMentionedFiles) {
|
|
1816
|
+
fileContents = await FileMention.getFileContents();
|
|
1817
|
+
fileContextStr = FileMention.formatFileContext(fileContents);
|
|
1818
|
+
FileMention.clearFiles(); // Clear after getting contents
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// Build full message with file context
|
|
1822
|
+
const fullMessage = fileContextStr ? content + fileContextStr : content;
|
|
1823
|
+
|
|
1824
|
+
// Display user message (show original content, not with file context)
|
|
1825
|
+
const displayMessage = hasMentionedFiles
|
|
1826
|
+
? content + `\n\nš _${fileContents.length} file(s) attached_`
|
|
1827
|
+
: content;
|
|
1828
|
+
|
|
1347
1829
|
// Add user message
|
|
1348
|
-
Conversations.addMessage('user',
|
|
1349
|
-
UI.appendMessage('user',
|
|
1830
|
+
Conversations.addMessage('user', displayMessage);
|
|
1831
|
+
UI.appendMessage('user', displayMessage);
|
|
1350
1832
|
|
|
1351
1833
|
// Check token limit before sending
|
|
1352
1834
|
if (UI.checkTokenLimit()) {
|
|
@@ -1371,8 +1853,8 @@ When reviewing code:
|
|
|
1371
1853
|
const agent = UI.getAgentById(conversation.agent || state.selectedAgent);
|
|
1372
1854
|
const systemPrompt = agent.systemPrompt;
|
|
1373
1855
|
|
|
1374
|
-
// Stream response
|
|
1375
|
-
for await (const event of API.streamChat(
|
|
1856
|
+
// Stream response (send fullMessage with file context)
|
|
1857
|
+
for await (const event of API.streamChat(fullMessage, state.selectedModel, history, systemPrompt)) {
|
|
1376
1858
|
if (event.type === 'chunk') {
|
|
1377
1859
|
fullResponse += event.content;
|
|
1378
1860
|
UI.updateStreamingMessage(assistantMsgEl, fullResponse);
|
|
@@ -1471,6 +1953,9 @@ When reviewing code:
|
|
|
1471
1953
|
// Setup event listeners
|
|
1472
1954
|
setupEventListeners();
|
|
1473
1955
|
|
|
1956
|
+
// Initialize file mention system
|
|
1957
|
+
FileMention.init();
|
|
1958
|
+
|
|
1474
1959
|
// Load stats (only once at init)
|
|
1475
1960
|
loadStats();
|
|
1476
1961
|
|