-
- {{ attachment.name }}
- {{ formatFileSize(attachment.size) }}
-
+
{{ attachment.contentType === 'inode/directory' ? '📁 ' : '' }}{{ attachment.name }}
+
{{ formatFileSize(attachment.size) }}
+
{{ attachment.path }}
+
@@ -443,6 +446,19 @@ defineExpose({
background: rgba(217, 119, 87, 0.24);
}
+.attachment-chip--dir {
+ border-style: dashed;
+}
+
+.attachment-chip__path {
+ font-size: 11px;
+ color: var(--mc-text-tertiary, #94a3b8);
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
/* 输入区域 */
.input-area {
display: flex;
diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index 32e3f607..ef7ea3c1 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -106,7 +106,7 @@ export default {
deleteConversationFailed: 'Failed to delete conversation',
switchModelFailed: 'Failed to switch model',
uploadFailed: 'File upload failed',
- dropToUpload: 'Drop files here to upload',
+ dropToUpload: 'Drop files or folders here',
copyFailed: 'Copy failed',
dateToday: 'Today',
dateYesterday: 'Yesterday',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index 95866b02..c5a1790f 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -106,7 +106,7 @@ export default {
deleteConversationFailed: '删除会话失败',
switchModelFailed: '切换模型失败',
uploadFailed: '文件上传失败',
- dropToUpload: '拖放文件到此处上传',
+ dropToUpload: '拖放文件或文件夹到此处',
copyFailed: '复制失败',
dateToday: '今天',
dateYesterday: '昨天',
diff --git a/mateclaw-ui/src/views/ChatConsole.vue b/mateclaw-ui/src/views/ChatConsole.vue
index 88e75c14..ddd578ea 100644
--- a/mateclaw-ui/src/views/ChatConsole.vue
+++ b/mateclaw-ui/src/views/ChatConsole.vue
@@ -240,13 +240,94 @@ function onDragLeave() {
}
}
-function onDrop(e: DragEvent) {
+async function onDrop(e: DragEvent) {
dragCounter = 0
isDragging.value = false
- const files = Array.from(e.dataTransfer?.files || [])
- if (files.length) {
- handleFileSelect(files)
+
+ const dtFiles = Array.from(e.dataTransfer?.files || [])
+ const items = Array.from(e.dataTransfer?.items || [])
+
+ const electronDirs: File[] = []
+ const webDirEntries: FileSystemDirectoryEntry[] = []
+ const regularFiles: File[] = []
+
+ for (let i = 0; i < items.length; i++) {
+ const entry = items[i].webkitGetAsEntry?.()
+ const file = dtFiles[i]
+ if (entry?.isDirectory) {
+ if ((file as any)?.path) {
+ // Electron: has absolute path
+ electronDirs.push(file)
+ } else {
+ // Web: need to recursively collect files
+ webDirEntries.push(entry as FileSystemDirectoryEntry)
+ }
+ } else if (file) {
+ regularFiles.push(file)
+ }
}
+
+ // Electron directories → record path reference
+ if (electronDirs.length) {
+ handleDirectoryAttach(electronDirs)
+ }
+ // Web directories → recursively collect files and upload
+ if (webDirEntries.length) {
+ const collected = await collectFilesFromEntries(webDirEntries)
+ if (collected.length) {
+ handleFileSelect(collected)
+ }
+ }
+ // Regular files → normal upload
+ if (regularFiles.length) {
+ handleFileSelect(regularFiles)
+ }
+}
+
+function handleDirectoryAttach(dirFiles: File[]) {
+ if (!currentConversationId.value) {
+ newConversation()
+ }
+ for (const dir of dirFiles) {
+ const dirPath = (dir as any).path as string
+ pendingAttachments.value.push({
+ name: dirPath.split('/').pop() || dir.name,
+ size: 0,
+ url: '',
+ storedName: '',
+ path: dirPath,
+ contentType: 'inode/directory',
+ })
+ }
+}
+
+async function collectFilesFromEntries(dirEntries: FileSystemDirectoryEntry[]): Promise
{
+ const files: File[] = []
+
+ async function readDir(dir: FileSystemDirectoryEntry) {
+ const reader = dir.createReader()
+ let batch: FileSystemEntry[]
+ do {
+ batch = await new Promise((resolve, reject) => {
+ reader.readEntries(resolve, reject)
+ })
+ for (const entry of batch) {
+ if (entry.isFile) {
+ const file = await new Promise((resolve, reject) => {
+ (entry as FileSystemFileEntry).file(resolve, reject)
+ })
+ files.push(file)
+ } else if (entry.isDirectory) {
+ await readDir(entry as FileSystemDirectoryEntry)
+ }
+ }
+ } while (batch.length > 0) // readEntries returns empty when done
+ }
+
+ for (const dir of dirEntries) {
+ await readDir(dir)
+ }
+ return files
}
const messageListRef = ref | null>(null)
@@ -815,8 +896,10 @@ async function handleFileSelect(files: File[]) {
}
}
-function removeAttachment(storedName: string) {
- pendingAttachments.value = pendingAttachments.value.filter(a => a.storedName !== storedName)
+function removeAttachment(key: string) {
+ pendingAttachments.value = pendingAttachments.value.filter(
+ a => a.storedName !== key && a.path !== key
+ )
}
function buildOutgoingParts(text: string, attachments: ChatAttachment[]): MessageContentPart[] {