feat(ui): enable drag-and-drop attachments and image previews in chat (#390)
* feat(BottomTerminalDock): add close button next to the fullscreen toggle in the dock * style: replace hardcoded gradient with theme value in shine text variant * fix(header): adapt instance button for desktop only * refactor(chat): polish sticky turn UX and message action rows for better readability Switch to stable sticky-only turn behavior and redesign user/assistant action controls (placement, hover rules, ordering, spacing, selection-safe clamp) to reduce visual noise and improve interaction flow. * feat(chat/message): refactor buttons in messages footer * feat: enhance image preview functionality in chat messages - Added a new ImagePreviewDialog component to handle image previews with navigation support. - Updated ToolOutputDialog to utilize the new ImagePreviewDialog for displaying images. - Modified the ToolPopupContent type to include a gallery of images and an index for the current image. - Removed the old inline image display logic from ToolOutputDialog. - Improved file handling in the file store, including better MIME type guessing and handling of server paths. - Introduced a new API endpoint for handling large session message payloads, allowing for better management of multi-file attachments. - Updated the VSCode bridge to support session message requests with appropriate headers and body handling. * feat(proxy): implement SSE forwarding and enhance generic API request handling * feat(chat): support submitting only queued messages * feat: add image preview transition state * fix: default VSCode view to draft and fixed sessions list regression
This commit is contained in:
committed by
GitHub
parent
39e625d8ec
commit
844562749d
@@ -478,56 +478,6 @@ class OpencodeService {
|
||||
return lowerMime === 'image/heic' || lowerMime === 'image/heif';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress and resize image using Canvas.
|
||||
*/
|
||||
private async compressImage(dataUrl: string, mimeType: string, quality = 0.8, maxWidth = 2048): Promise<string> {
|
||||
if (typeof document === 'undefined') return dataUrl;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
if (width > maxWidth || height > maxWidth) {
|
||||
if (width > height) {
|
||||
height = Math.round(height * (maxWidth / width));
|
||||
width = maxWidth;
|
||||
} else {
|
||||
width = Math.round(width * (maxWidth / height));
|
||||
height = maxWidth;
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
resolve(dataUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
let targetMime = mimeType;
|
||||
// Convert large PNGs to JPEG to save space
|
||||
if (mimeType === 'image/png' && dataUrl.length > 2.5 * 1024 * 1024) {
|
||||
targetMime = 'image/jpeg';
|
||||
}
|
||||
|
||||
try {
|
||||
resolve(canvas.toDataURL(targetMime, quality));
|
||||
} catch {
|
||||
resolve(dataUrl);
|
||||
}
|
||||
};
|
||||
img.onerror = () => resolve(dataUrl);
|
||||
img.src = dataUrl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HEIC image to JPEG.
|
||||
* Returns the original file if conversion fails.
|
||||
@@ -592,23 +542,6 @@ class OpencodeService {
|
||||
return this.convertHeicToJpeg(file);
|
||||
}
|
||||
|
||||
// Handle large image compression (Resize > 2048px or > 1MB)
|
||||
if (file.mime.startsWith('image/') && (file.mime === 'image/jpeg' || file.mime === 'image/png' || file.mime === 'image/webp')) {
|
||||
// > ~1MB base64
|
||||
if (file.url.length > 1.33 * 1024 * 1024) {
|
||||
const compressedUrl = await this.compressImage(file.url, file.mime);
|
||||
const newMime = compressedUrl.startsWith('data:image/jpeg') ? 'image/jpeg' : file.mime;
|
||||
// Update the file object with compressed data
|
||||
// We return a new object to avoid mutating the original file ref if used elsewhere,
|
||||
// but here we just return the part.
|
||||
return {
|
||||
...file,
|
||||
mime: newMime,
|
||||
url: compressedUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle text MIME normalization
|
||||
if (!this.shouldNormalizeToTextPlain(file.mime)) {
|
||||
return file;
|
||||
@@ -647,6 +580,7 @@ class OpencodeService {
|
||||
agent?: string;
|
||||
variant?: string;
|
||||
files?: Array<{
|
||||
id?: string;
|
||||
type: 'file';
|
||||
mime: string;
|
||||
filename?: string;
|
||||
@@ -657,6 +591,7 @@ class OpencodeService {
|
||||
text: string;
|
||||
synthetic?: boolean;
|
||||
files?: Array<{
|
||||
id?: string;
|
||||
type: 'file';
|
||||
mime: string;
|
||||
filename?: string;
|
||||
@@ -696,6 +631,7 @@ class OpencodeService {
|
||||
for (const file of params.files) {
|
||||
const normalized = await this.normalizeFilePart(file);
|
||||
const filePart: FilePartInput = {
|
||||
...(file.id ? { id: file.id } : {}),
|
||||
type: 'file',
|
||||
mime: normalized.mime,
|
||||
filename: normalized.filename,
|
||||
@@ -719,6 +655,7 @@ class OpencodeService {
|
||||
for (const file of additional.files) {
|
||||
const normalized = await this.normalizeFilePart(file);
|
||||
const filePart: FilePartInput = {
|
||||
...(file.id ? { id: file.id } : {}),
|
||||
type: 'file',
|
||||
mime: normalized.mime,
|
||||
filename: normalized.filename,
|
||||
|
||||
Reference in New Issue
Block a user