mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
Merge pull request #651: fix(uploads): say how many files a mixed pick, drop or paste skipped
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> # Conflicts: # CLAUDE.md
This commit is contained in:
commit
5481fd599b
12 files changed
+230
-46
No files matched your search
@@ -1319,6 +1319,8 @@
|
||||
[/^Worktree (\S+) and branch (\S+) removed$/, (_m, wt, branch) => `已移除工作树 ${wt} 和分支 ${branch}`],
|
||||
[/^Worktree (\S+) removed$/, (_m, wt) => `已移除工作树 ${wt}`],
|
||||
[/^Worktree (\S+) was kept: ([\s\S]+)$/, (_m, wt, why) => `工作树 ${wt} 已保留:${why}`],
|
||||
// A pick, drop or paste that mixed admitted files with refused ones (image-input.js).
|
||||
[/^(\d+) files? skipped: unsupported type$/, (_m, n) => `已跳过 ${n} 个不支持类型的文件`],
|
||||
// Group names are user text: they pass through untranslated.
|
||||
[/^Move to "(.+)"$/, (_m, group) => `移到“${group}”`],
|
||||
// Session names are user text too.
|
||||
|
||||
@@ -101,12 +101,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (!this.activeSessionId) return;
|
||||
if (!e.dataTransfer || !e.dataTransfer.files.length) return;
|
||||
|
||||
const mediaFiles = Array.from(e.dataTransfer.files).filter((f) => this._promptAttachKind(f));
|
||||
if (mediaFiles.length === 0) {
|
||||
this.showToast('Unsupported file type', 'error');
|
||||
return;
|
||||
}
|
||||
this._uploadAndInsertImages(mediaFiles);
|
||||
const mediaFiles = this._admitPromptAttachments(Array.from(e.dataTransfer.files));
|
||||
if (mediaFiles.length > 0) this._uploadAndInsertImages(mediaFiles);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -122,11 +118,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
* Without the overlay (local echo off) there is nothing to buffer into, so the
|
||||
* text goes straight out. The trailing space keeps the next word off the path. */
|
||||
async _attachFromToolbar(files, sessionId) {
|
||||
const media = files.filter((f) => this._promptAttachKind(f));
|
||||
if (media.length === 0) {
|
||||
this.showToast('Unsupported file type', 'error');
|
||||
return;
|
||||
}
|
||||
const media = this._admitPromptAttachments(files);
|
||||
if (media.length === 0) return;
|
||||
if (!sessionId) return; // no captured session: refuse rather than guess at one
|
||||
const paths = await this._uploadAndInsertImages(media, { insert: false, sessionId });
|
||||
if (paths.length === 0) return;
|
||||
@@ -231,14 +224,16 @@ Object.assign(CodemanApp.prototype, {
|
||||
pasteConsumed = true;
|
||||
|
||||
// Check for images, videos or documents in clipboard items
|
||||
var imageFiles = [];
|
||||
var pastedFiles = [];
|
||||
var items = e.clipboardData && e.clipboardData.items;
|
||||
if (items) {
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var blob = items[i].getAsFile();
|
||||
if (blob && self._promptAttachKind(blob)) imageFiles.push(blob);
|
||||
if (blob) pastedFiles.push(blob);
|
||||
}
|
||||
}
|
||||
var text = e.clipboardData ? e.clipboardData.getData('text/plain') : '';
|
||||
var imageFiles = self._admitPromptAttachments(pastedFiles, { textPasted: !!text });
|
||||
|
||||
// Clean up the trap
|
||||
setTimeout(function() {
|
||||
@@ -256,7 +251,6 @@ Object.assign(CodemanApp.prototype, {
|
||||
// via raw sendInput() strips those markers and makes pasted input
|
||||
// indistinguishable from typed input, weakening the CLI's
|
||||
// prompt-injection defenses.
|
||||
var text = e.clipboardData ? e.clipboardData.getData('text/plain') : '';
|
||||
if (text && terminal) terminal.paste(text);
|
||||
}
|
||||
});
|
||||
@@ -290,6 +284,22 @@ Object.assign(CodemanApp.prototype, {
|
||||
return null;
|
||||
},
|
||||
|
||||
/** The files of a pick, drop or paste that _promptAttachKind admits, telling the
|
||||
* user about any it refuses: all of them is "Unsupported file type", some of
|
||||
* them a count of what was skipped while the rest still upload. A clipboard
|
||||
* paste that also carries text passes `textPasted`: when none of its files is
|
||||
* admitted the text is pasted instead, so that case stays quiet. */
|
||||
_admitPromptAttachments(files, { textPasted = false } = {}) {
|
||||
const admitted = files.filter((f) => this._promptAttachKind(f));
|
||||
const skipped = files.length - admitted.length;
|
||||
if (skipped > 0 && admitted.length > 0) {
|
||||
this.showToast(`${skipped} file${skipped > 1 ? 's' : ''} skipped: unsupported type`, 'info');
|
||||
} else if (skipped > 0 && !textPasted) {
|
||||
this.showToast('Unsupported file type', 'error');
|
||||
}
|
||||
return admitted;
|
||||
},
|
||||
|
||||
// Max images accepted in one batch (paste / drop / mobile picker). Each is
|
||||
// uploaded as its own request, so 20 stays under the server's 30 uploads/min
|
||||
// rate limit while covering "select a bunch of photos at once".
|
||||
|
||||
@@ -1339,8 +1339,9 @@ const KeyboardAccessoryBar = {
|
||||
this._syncComposerDraftIndicator();
|
||||
close({ preserveDraft: false });
|
||||
};
|
||||
const handleImages = async (files) => {
|
||||
const images = Array.from(files || []).filter((file) => app._promptAttachKind(file));
|
||||
// Takes files already admitted by app._admitPromptAttachments (the picker and
|
||||
// the paste below), which also toasts whatever it refused.
|
||||
const handleImages = async (images) => {
|
||||
if (images.length === 0 || typeof app._uploadAndInsertImages !== 'function') return;
|
||||
saveDraft();
|
||||
this._composerUploads.set(sessionId, (this._composerUploads.get(sessionId) || 0) + 1);
|
||||
@@ -1381,7 +1382,6 @@ const KeyboardAccessoryBar = {
|
||||
if (remaining > 0) this._composerUploads.set(sessionId, remaining);
|
||||
else this._composerUploads.delete(sessionId);
|
||||
syncUploadUi();
|
||||
if (overlay.isConnected) fileInput.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1389,9 +1389,11 @@ const KeyboardAccessoryBar = {
|
||||
textarea.addEventListener('paste', (event) => {
|
||||
const items = event.clipboardData?.items;
|
||||
if (!items) return;
|
||||
const images = Array.from(items)
|
||||
const files = Array.from(items)
|
||||
.map((item) => item.getAsFile())
|
||||
.filter((file) => file && app._promptAttachKind(file));
|
||||
.filter(Boolean);
|
||||
// Pasted text with no admitted file lands in the textarea natively: no toast.
|
||||
const images = app._admitPromptAttachments(files, { textPasted: !!event.clipboardData.getData('text/plain') });
|
||||
if (images.length > 0) {
|
||||
event.preventDefault();
|
||||
void handleImages(images);
|
||||
@@ -1404,7 +1406,12 @@ const KeyboardAccessoryBar = {
|
||||
}
|
||||
});
|
||||
imageButton.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', () => void handleImages(fileInput.files));
|
||||
fileInput.addEventListener('change', () => {
|
||||
const files = Array.from(fileInput.files || []);
|
||||
// Reset so picking the same refused file again still fires change (and its toast).
|
||||
fileInput.value = '';
|
||||
void handleImages(app._admitPromptAttachments(files));
|
||||
});
|
||||
overlay.querySelector('.prompt-composer-terminal').addEventListener('click', () => {
|
||||
// Hand the text BACK to the terminal prompt so both surfaces hold the same
|
||||
// prompt, the mirror of the adoption composePrompt() does on open. Only a
|
||||
@@ -1519,10 +1526,11 @@ const KeyboardAccessoryBar = {
|
||||
if (this._sendBracketedPaste(sessionId, text, 'Paste')) close();
|
||||
};
|
||||
|
||||
// Filter to what the server admits, close the dialog, and hand off to the
|
||||
// shared upload+insert pipeline. Returns true if any file was handled.
|
||||
const handleImages = (files) => {
|
||||
const images = Array.from(files || []).filter((f) => app._promptAttachKind(f));
|
||||
// Filter to what the server admits (app._admitPromptAttachments toasts what it
|
||||
// refuses), close the dialog, and hand off to the shared upload+insert
|
||||
// pipeline. Returns true if any file was handled.
|
||||
const handleImages = (files, options) => {
|
||||
const images = app._admitPromptAttachments(Array.from(files || []), options);
|
||||
if (images.length === 0) return false;
|
||||
close();
|
||||
// Upload to, and insert into, the shell this overlay was opened for.
|
||||
@@ -1532,21 +1540,24 @@ const KeyboardAccessoryBar = {
|
||||
|
||||
// File picker (camera / photo library / files) — the reliable mobile path.
|
||||
overlay.querySelector('.paste-image').addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', () => handleImages(fileInput.files));
|
||||
fileInput.addEventListener('change', () => {
|
||||
const files = Array.from(fileInput.files || []);
|
||||
// Reset so picking the same refused file again still fires change (and its toast).
|
||||
fileInput.value = '';
|
||||
handleImages(files);
|
||||
});
|
||||
|
||||
// Best-effort: capture images pasted straight into the textarea.
|
||||
textarea.addEventListener('paste', (e) => {
|
||||
const items = e.clipboardData && e.clipboardData.items;
|
||||
if (!items) return;
|
||||
const imageFiles = [];
|
||||
const files = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const blob = items[i].getAsFile();
|
||||
if (blob && app._promptAttachKind(blob)) imageFiles.push(blob);
|
||||
}
|
||||
if (imageFiles.length > 0) {
|
||||
e.preventDefault();
|
||||
handleImages(imageFiles);
|
||||
if (blob) files.push(blob);
|
||||
}
|
||||
// Pasted text with no admitted file lands in the textarea natively: no toast.
|
||||
if (handleImages(files, { textPasted: !!e.clipboardData.getData('text/plain') })) e.preventDefault();
|
||||
});
|
||||
|
||||
overlay.querySelector('.paste-cancel').addEventListener('click', close);
|
||||
|
||||
@@ -346,12 +346,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (!sessionId || files.length === 0) return;
|
||||
// The one classifier every pick and drop filter reads (image-input.js):
|
||||
// MIME first, then the name's extension, so documents and videos count too.
|
||||
const media = files.filter((f) => this._promptAttachKind?.(f));
|
||||
if (media.length === 0) {
|
||||
this.showToast?.('Unsupported file type', 'error');
|
||||
return;
|
||||
}
|
||||
this._uploadAndInsertImages?.(media, { sessionId });
|
||||
// It toasts what it refuses.
|
||||
const media = this._admitPromptAttachments(files);
|
||||
if (media.length > 0) this._uploadAndInsertImages?.(media, { sessionId });
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Reference in new issue
Block a user