Skip to content

Commit 3805820

Browse files
Ericonaldoclaude
andcommitted
fix: defer file upload to send time instead of paste time
Files are now stored as local File objects on paste/attach, and only uploaded to the server when the user clicks Send. Prevents uploading files that get removed before sending. Also shows file size in the attachment chip, and uses image/file icons. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 83545ed commit 3805820

1 file changed

Lines changed: 39 additions & 38 deletions

File tree

client/src/pages/AgentChat.tsx

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function AgentChat() {
7070
const [showHistoryPicker, setShowHistoryPicker] = useState(false);
7171
const [historyPickerIdx, setHistoryPickerIdx] = useState(0);
7272
const [historyRestoreTarget, setHistoryRestoreTarget] = useState<number | null>(null);
73-
const [attachedImages, setAttachedImages] = useState<Array<{ name: string; path: string }>>([]);
73+
const [attachedFiles, setAttachedFiles] = useState<File[]>([]);
7474
const [uploadingCount, setUploadingCount] = useState(0);
7575
const fileInputRef = useRef<HTMLInputElement>(null);
7676

@@ -553,57 +553,57 @@ export function AgentChat() {
553553
}
554554
};
555555

556-
const handleFileUpload = async (files: File[]) => {
557-
if (files.length === 0) return;
558-
setUploadingCount(prev => prev + files.length);
559-
for (const file of files) {
560-
try {
561-
const result = await api.uploadFile(file);
562-
setAttachedImages(prev => [...prev, { name: file.name, path: result.path }]);
563-
} catch (err) {
564-
addLocalMessage(`Failed to upload ${file.name}: ${err instanceof Error ? err.message : String(err)}`);
565-
} finally {
566-
setUploadingCount(prev => prev - 1);
567-
}
568-
}
569-
};
570-
571556
const handlePaste = (e: React.ClipboardEvent) => {
572557
const items = e.clipboardData?.items;
573558
if (!items) return;
574559

575560
const pasteFiles: File[] = [];
576561
for (let i = 0; i < items.length; i++) {
577562
const item = items[i];
578-
// Accept any file type from clipboard (images, PDFs, etc.)
579563
if (item.kind === 'file') {
580564
const file = item.getAsFile();
581565
if (file) pasteFiles.push(file);
582566
}
583567
}
584568
if (pasteFiles.length > 0) {
585569
e.preventDefault();
586-
handleFileUpload(pasteFiles);
570+
setAttachedFiles(prev => [...prev, ...pasteFiles]);
587571
}
588572
};
589573

590-
const removeAttachedImage = (index: number) => {
591-
setAttachedImages(prev => prev.filter((_, i) => i !== index));
574+
const removeAttachedFile = (index: number) => {
575+
setAttachedFiles(prev => prev.filter((_, i) => i !== index));
592576
};
593577

594-
const handleSend = () => {
595-
if ((!input.trim() && attachedImages.length === 0) || !id) return;
578+
const handleSend = async () => {
579+
if ((!input.trim() && attachedFiles.length === 0) || !id) return;
596580

597581
// Save to input history
598582
const trimmed = input.trim();
599583
const hist = inputHistoryRef.current;
600-
if (hist[0] !== trimmed) {
584+
if (trimmed && hist[0] !== trimmed) {
601585
hist.unshift(trimmed);
602586
if (hist.length > 50) hist.pop();
603587
}
604588
historyIdxRef.current = -1;
605589
savedInputRef.current = '';
606590

591+
// Upload attached files now (not earlier)
592+
const uploadedPaths: { name: string; path: string }[] = [];
593+
if (attachedFiles.length > 0) {
594+
setUploadingCount(attachedFiles.length);
595+
for (const file of attachedFiles) {
596+
try {
597+
const result = await api.uploadFile(file);
598+
uploadedPaths.push({ name: file.name, path: result.path });
599+
} catch (err) {
600+
addLocalMessage(`Failed to upload ${file.name}: ${err instanceof Error ? err.message : String(err)}`);
601+
} finally {
602+
setUploadingCount(prev => prev - 1);
603+
}
604+
}
605+
}
606+
607607
if (input.startsWith('/')) {
608608
// Handle commands with arguments (e.g., /compact [instructions])
609609
const parts = input.trim().split(/\s+/);
@@ -624,14 +624,14 @@ export function AgentChat() {
624624
}
625625
}
626626

627-
// Build message text with image paths prepended
628-
const imagePrefixes = attachedImages.map(img => {
629-
const isImage = /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(img.name);
630-
return isImage ? `[Image: ${img.path}]` : `[File: ${img.path}]`;
627+
// Build message text with file paths prepended
628+
const filePrefixes = uploadedPaths.map(f => {
629+
const isImage = /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(f.name);
630+
return isImage ? `[Image: ${f.path}]` : `[File: ${f.path}]`;
631631
}).join('\n');
632632
const userText = input.trim();
633-
const text = imagePrefixes
634-
? (userText ? `${imagePrefixes}\n\n${userText}` : imagePrefixes)
633+
const text = filePrefixes
634+
? (userText ? `${filePrefixes}\n\n${userText}` : filePrefixes)
635635
: userText;
636636

637637
if (!text) return;
@@ -646,7 +646,7 @@ export function AgentChat() {
646646
};
647647
});
648648
setInput('');
649-
setAttachedImages([]);
649+
setAttachedFiles([]);
650650
setInputRequired(null);
651651
api.sendMessage(id, text);
652652
};
@@ -944,8 +944,8 @@ export function AgentChat() {
944944
))}
945945
</div>
946946
)}
947-
{/* Image attachment indicator */}
948-
{(attachedImages.length > 0 || uploadingCount > 0) && (
947+
{/* File attachment indicator */}
948+
{(attachedFiles.length > 0 || uploadingCount > 0) && (
949949
<div style={{
950950
display: 'flex',
951951
flexWrap: 'wrap',
@@ -956,7 +956,7 @@ export function AgentChat() {
956956
borderRadius: 'var(--radius)',
957957
border: '1px solid var(--border)',
958958
}}>
959-
{attachedImages.map((img, i) => (
959+
{attachedFiles.map((file, i) => (
960960
<div key={i} style={{
961961
display: 'flex',
962962
alignItems: 'center',
@@ -967,10 +967,11 @@ export function AgentChat() {
967967
fontSize: 12,
968968
color: 'var(--text)',
969969
}}>
970-
<span style={{ fontSize: 14 }}>{'\uD83D\uDCCE'}</span>
971-
<span style={{ maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{img.name}</span>
970+
<span style={{ fontSize: 14 }}>{file.type.startsWith('image/') ? '\uD83D\uDDBC' : '\uD83D\uDCCE'}</span>
971+
<span style={{ maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{file.name}</span>
972+
<span style={{ color: 'var(--text-muted)', fontSize: 11 }}>({(file.size / 1024).toFixed(0)}KB)</span>
972973
<button
973-
onClick={() => removeAttachedImage(i)}
974+
onClick={() => removeAttachedFile(i)}
974975
style={{
975976
background: 'none',
976977
border: 'none',
@@ -995,7 +996,7 @@ export function AgentChat() {
995996
fontSize: 12,
996997
color: 'var(--text-muted)',
997998
}}>
998-
Uploading {uploadingCount} image{uploadingCount > 1 ? 's' : ''}...
999+
Uploading {uploadingCount} file{uploadingCount > 1 ? 's' : ''}...
9991000
</div>
10001001
)}
10011002
</div>
@@ -1009,7 +1010,7 @@ export function AgentChat() {
10091010
style={{ display: 'none' }}
10101011
onChange={(e) => {
10111012
if (e.target.files) {
1012-
handleFileUpload(Array.from(e.target.files));
1013+
setAttachedFiles(prev => [...prev, ...Array.from(e.target.files!)]);
10131014
e.target.value = '';
10141015
}
10151016
}}

0 commit comments

Comments
 (0)