Skip to content

Commit 3ea87c9

Browse files
committed
feat: select files as contexts
1 parent 24f7bb5 commit 3ea87c9

File tree

10 files changed

+549
-68
lines changed

10 files changed

+549
-68
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## v4.6.3
4+
5+
* feat: chat with files (including text files and images)
6+
37
## v4.6.2
48

59
* feat: set selected prompt as system prompt

media/main.css

Lines changed: 154 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,13 @@ input:not([type='checkbox']),
176176
textarea {
177177
display: block;
178178
width: 100%;
179-
border: none;
180179
font-family: var(--vscode-font-family);
181180
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
182181
color: var(--vscode-input-foreground);
183182
outline-color: var(--vscode-input-border);
184183
background-color: var(--vscode-input-background);
184+
border: 1px solid var(--vscode-input-border);
185+
border-width: 1px;
185186
}
186187

187188
input::placeholder,
@@ -250,28 +251,83 @@ textarea::placeholder {
250251

251252
.textarea-wrapper {
252253
display: grid;
253-
max-height: 20rem;
254-
font-size: var(--vscode-font-size);
254+
position: relative;
255+
width: 100%;
256+
}
257+
258+
.textarea-wrapper>textarea {
259+
resize: none;
260+
overflow-x: auto;
261+
overflow-y: hidden;
262+
min-height: 40px;
263+
max-height: 240px;
264+
line-height: inherit;
265+
font-family: inherit;
266+
font-size: inherit;
267+
color: var(--vscode-input-foreground);
268+
background-color: var(--vscode-input-background);
269+
display: block;
270+
outline: none;
271+
box-sizing: border-box;
272+
border: 1px solid var(--vscode-focusBorder);
273+
border-radius: 2px;
274+
padding: 6px 8px;
275+
padding-right: 76px;
276+
white-space: nowrap;
277+
grid-area: 1 / 1 / 2 / 2;
255278
}
256279

257280
.textarea-wrapper::after {
258281
content: attr(data-replicated-value) " ";
259282
white-space: pre-wrap;
260283
visibility: hidden;
284+
grid-area: 1 / 1 / 2 / 2;
285+
border: 1px solid transparent;
286+
padding: 6px 8px;
287+
padding-right: 76px;
288+
box-sizing: border-box;
261289
}
262290

263-
.textarea-wrapper>textarea {
264-
resize: none;
265-
overflow: hidden auto;
266-
max-height: 20rem;
291+
.input-container {
292+
position: relative;
293+
width: 100%;
294+
display: grid;
267295
}
268296

269-
.textarea-wrapper>textarea,
270-
.textarea-wrapper::after {
271-
border: 1px solid black;
272-
padding: .5rem 5rem .5rem .5rem;
273-
font: inherit;
274-
grid-area: 1 / 1 / 2 / 2;
297+
/* 文件引用样式 */
298+
.file-reference {
299+
display: inline-block;
300+
background-color: rgba(24, 119, 232, 0.2);
301+
color: var(--vscode-input-foreground);
302+
padding: 2px 4px;
303+
margin: 0 2px;
304+
border-radius: 3px;
305+
font-size: inherit;
306+
white-space: nowrap;
307+
cursor: default;
308+
user-select: none;
309+
}
310+
311+
/* 输入框容器样式 */
312+
.scrollable-input-container {
313+
width: 100%;
314+
min-height: 40px;
315+
max-height: 240px;
316+
overflow-y: auto;
317+
background-color: var(--vscode-input-background);
318+
border: 1px solid var(--vscode-input-border);
319+
border-radius: 2px;
320+
}
321+
322+
/* 隐藏滚动条但保持功能 */
323+
.textarea-wrapper>textarea::-webkit-scrollbar {
324+
height: 0;
325+
width: 0;
326+
}
327+
328+
.textarea-wrapper>textarea {
329+
-ms-overflow-style: none;
330+
scrollbar-width: none;
275331
}
276332

277333
.pre-code-element:not(:last-child) {
@@ -480,4 +536,89 @@ pre>code {
480536
background-color: var(--vscode-textCodeBlock-background, var(--vscode-editor-background));
481537
border-radius: 6px;
482538
margin: 1em 0;
539+
}
540+
541+
.file-references-container {
542+
display: flex;
543+
flex-wrap: nowrap;
544+
overflow-x: auto;
545+
gap: 4px;
546+
padding: 4px 8px;
547+
min-height: 32px;
548+
border-bottom: 1px solid var(--vscode-input-border);
549+
scrollbar-width: none;
550+
-ms-overflow-style: none;
551+
}
552+
553+
.file-references-container::-webkit-scrollbar {
554+
display: none;
555+
}
556+
557+
.file-references-container:empty {
558+
display: none;
559+
}
560+
561+
.file-reference-tag {
562+
display: inline-flex;
563+
align-items: center;
564+
background-color: rgba(24, 119, 232, 0.2);
565+
color: var(--vscode-input-foreground);
566+
padding: 2px 8px;
567+
border-radius: 3px;
568+
font-size: inherit;
569+
white-space: nowrap;
570+
user-select: none;
571+
gap: 4px;
572+
}
573+
574+
.file-reference-tag .remove-file {
575+
cursor: pointer;
576+
opacity: 0.7;
577+
}
578+
579+
.file-reference-tag .remove-file:hover {
580+
opacity: 1;
581+
}
582+
583+
/* 修改按钮容器的定位和对齐 */
584+
#question-input-buttons {
585+
position: absolute;
586+
right: 8px;
587+
top: 50%;
588+
transform: translateY(-50%);
589+
display: flex;
590+
align-items: center;
591+
gap: 4px;
592+
pointer-events: auto;
593+
z-index: 1;
594+
}
595+
596+
/* 调整按钮样式 */
597+
#question-input-buttons button {
598+
display: flex;
599+
align-items: center;
600+
justify-content: center;
601+
width: 28px;
602+
height: 28px;
603+
min-width: 28px;
604+
min-height: 28px;
605+
padding: 0;
606+
border-radius: 4px;
607+
background: var(--vscode-button-secondaryBackground);
608+
}
609+
610+
#question-input-buttons svg {
611+
width: 16px;
612+
height: 16px;
613+
}
614+
615+
/* 添加悬停和焦点状态 */
616+
.textarea-wrapper>textarea:hover {
617+
border-color: var(--vscode-input-border);
618+
border-width: 1px;
619+
}
620+
621+
.textarea-wrapper>textarea:focus {
622+
border-color: var(--vscode-focusBorder);
623+
border-width: 1px;
483624
}

media/main.js

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -248,22 +248,70 @@
248248
case "setActivePrompt":
249249
showActivePrompt(message.name);
250250
break;
251+
case "insertFileReference":
252+
const input = document.getElementById("question-input");
253+
const fileRefsContainer = document.getElementById("file-references");
254+
255+
if (message.isAuto) {
256+
const autoTags = fileRefsContainer.querySelectorAll('.file-reference-tag[data-auto="true"]');
257+
autoTags.forEach(tag => tag.remove());
258+
}
259+
260+
// Add file type icon based on extension
261+
const isImage = message.fileName.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i);
262+
const fileIcon = isImage ?
263+
'<svg xmlns="http://www.w3.org/2000/svg" class="file-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M4 5h13v7h2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h8v-2H4V5zm16 14v-3h2v3c0 1.1-.9 2-2 2h-3v-2h3zM14 17h2v3h-2zM10 17h2v3h-2zM21 11v2h-2v-2z"/></svg>' :
264+
'<svg xmlns="http://www.w3.org/2000/svg" class="file-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/></svg>';
265+
266+
const fileTag = document.createElement('span');
267+
fileTag.className = 'file-reference-tag';
268+
if (message.isAuto) {
269+
fileTag.setAttribute('data-auto', 'true');
270+
}
271+
272+
fileTag.innerHTML = `
273+
${fileIcon}
274+
${message.displayName}
275+
<span class="remove-file" data-filepath="${message.fileName}">×</span>
276+
`;
277+
278+
fileTag.querySelector('.remove-file').addEventListener('click', function () {
279+
const filepath = this.getAttribute('data-filepath');
280+
this.parentElement.remove();
281+
vscode.postMessage({
282+
type: "removeFileReference",
283+
fileName: filepath
284+
});
285+
});
286+
287+
fileRefsContainer.appendChild(fileTag);
288+
289+
if (!message.isAuto) {
290+
const cursorPos = input.selectionStart;
291+
input.value = input.value.substring(0, cursorPos - 1) + input.value.substring(cursorPos);
292+
input.setSelectionRange(cursorPos - 1, cursorPos - 1);
293+
}
294+
break;
295+
case "clearFileReferences":
296+
document.getElementById('file-references').innerHTML = '';
297+
break;
251298
default:
252299
break;
253300
}
254301
});
255302

256303
const addFreeTextQuestion = () => {
257304
const input = document.getElementById("question-input");
258-
if (input.value?.length > 0) {
259-
// Ignore if input starts with / or #
260-
if (input.value.startsWith("/") || input.value.startsWith("#")) {
261-
return;
262-
}
305+
const value = input.value;
263306

307+
if (value.startsWith('/') || value.startsWith('#') || value.startsWith('@')) {
308+
return;
309+
}
310+
311+
if (value?.length > 0) {
264312
vscode.postMessage({
265313
type: "addFreeTextQuestion",
266-
value: input.value,
314+
value: value,
267315
});
268316

269317
input.value = "";
@@ -294,12 +342,29 @@
294342
};
295343

296344
document.getElementById('question-input').addEventListener("keydown", function (event) {
297-
if (event.key == "Enter" && !event.shiftKey && !event.isComposing) {
345+
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
298346
event.preventDefault();
347+
// 如果是命令,不阻止默认行为
348+
if (this.value.startsWith('/') || this.value.startsWith('#') || this.value.startsWith('@')) {
349+
return;
350+
}
299351
addFreeTextQuestion();
300352
}
301353
});
302354

355+
document.getElementById('question-input').addEventListener("keyup", function (event) {
356+
if (event.key === "@") {
357+
const cursorPos = this.selectionStart;
358+
const textBeforeCursor = this.value.substring(0, cursorPos);
359+
360+
if (cursorPos === 1 || textBeforeCursor.charAt(cursorPos - 2) === ' ') {
361+
vscode.postMessage({
362+
type: "searchFile"
363+
});
364+
}
365+
}
366+
});
367+
303368
document.addEventListener("click", (e) => {
304369
const targetButton = e.target.closest('button');
305370

@@ -496,6 +561,11 @@
496561

497562
$("#question-input").autocomplete({
498563
source: function (request, response) {
564+
if (request.term.startsWith('@')) {
565+
response([]);
566+
return;
567+
}
568+
499569
if (request.term.startsWith('#')) {
500570
vscode.postMessage({
501571
type: "searchPrompts",
@@ -518,7 +588,7 @@
518588

519589
response(message.titles.map(title => ({
520590
label: title.name,
521-
value: title.content, // Store the content in value
591+
value: title.content,
522592
promptId: title.id,
523593
name: title.name
524594
})));
@@ -546,7 +616,6 @@
546616
if (ui.item.isManagePrompt) {
547617
vscode.postMessage({ type: "togglePromptManager" });
548618
} else if (ui.item.promptId) {
549-
// Send the prompt selection message
550619
vscode.postMessage({
551620
type: "selectPrompt",
552621
prompt: {
@@ -567,12 +636,12 @@
567636
// Clear the input after selection
568637
$(this).val("");
569638
return false;
570-
}
639+
},
571640
});
572641

573-
// Add keydown handler to prevent sending when # is typed
642+
// Add keydown handler to prevent sending when # or @ is typed
574643
$("#question-input").on("keydown", function (event) {
575-
if (event.key === "Enter" && this.value.startsWith("#")) {
644+
if (event.key === "Enter" && (this.value.startsWith("#") || this.value.startsWith("@"))) {
576645
event.preventDefault();
577646
return false;
578647
}
@@ -775,6 +844,16 @@
775844
#qa-list, #introduction, #conversation-list {
776845
margin-top: 20px;
777846
}
778-
`;
779847
848+
.file-reference-tag {
849+
display: inline-flex;
850+
align-items: center;
851+
gap: 4px;
852+
}
853+
854+
.file-icon {
855+
display: inline-block;
856+
vertical-align: middle;
857+
}
858+
`;
780859
})();

media/prompt-manager.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@
142142
}
143143

144144
if (button.classList.contains('delete-prompt')) {
145-
console.log('Sending delete message for prompt:', promptId);
146145
vscode.postMessage({
147146
type: 'deletePrompt',
148147
id: promptId

0 commit comments

Comments
 (0)