diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 6568b4aaee..a60c5fea41 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -17,7 +17,8 @@ import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage import { checkExistKey } from "../../shared/checkExistApiConfig" import { experimentDefault } from "../../shared/experiments" import { Terminal } from "../../integrations/terminal/Terminal" -import { openFile, openImage } from "../../integrations/misc/open-file" +import { openFile } from "../../integrations/misc/open-file" +import { openImage, saveImage } from "../../integrations/misc/image-handler" import { selectImages } from "../../integrations/misc/process-images" import { getTheme } from "../../integrations/theme/getTheme" import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery" @@ -423,7 +424,10 @@ export const webviewMessageHandler = async ( provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels }) break case "openImage": - openImage(message.text!) + openImage(message.text!, { values: message.values }) + break + case "saveImage": + saveImage(message.dataUri!) break case "openFile": openFile(message.text!, message.values as { create?: boolean; content?: string; line?: number }) diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 15c0692c58..31b9668a86 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -28,6 +28,11 @@ }, "errors": { "invalid_data_uri": "Format d'URI de dades no vàlid", + "error_copying_image": "Error copiant la imatge: {{errorMessage}}", + "error_saving_image": "Error desant la imatge: {{errorMessage}}", + "error_opening_image": "Error obrint la imatge: {{error}}", + "could_not_open_file": "No s'ha pogut obrir el fitxer: {{errorMessage}}", + "could_not_open_file_generic": "No s'ha pogut obrir el fitxer!", "checkpoint_timeout": "S'ha esgotat el temps en intentar restaurar el punt de control.", "checkpoint_failed": "Ha fallat la restauració del punt de control.", "no_workspace": "Si us plau, obre primer una carpeta de projecte", @@ -71,7 +76,9 @@ "custom_storage_path_set": "Ruta d'emmagatzematge personalitzada establerta: {{path}}", "default_storage_path": "S'ha reprès l'ús de la ruta d'emmagatzematge predeterminada", "settings_imported": "Configuració importada correctament.", - "share_link_copied": "Enllaç de compartició copiat al portapapers" + "share_link_copied": "Enllaç de compartició copiat al portapapers", + "image_copied_to_clipboard": "URI de dades de la imatge copiada al portapapers", + "image_saved": "Imatge desada a {{path}}" }, "answers": { "yes": "Sí", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index d9a4727dbe..ab0cd61ac9 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "Ungültiges Daten-URI-Format", + "error_copying_image": "Fehler beim Kopieren des Bildes: {{errorMessage}}", + "error_saving_image": "Fehler beim Speichern des Bildes: {{errorMessage}}", + "error_opening_image": "Fehler beim Öffnen des Bildes: {{error}}", + "could_not_open_file": "Datei konnte nicht geöffnet werden: {{errorMessage}}", + "could_not_open_file_generic": "Datei konnte nicht geöffnet werden!", "checkpoint_timeout": "Zeitüberschreitung beim Versuch, den Checkpoint wiederherzustellen.", "checkpoint_failed": "Fehler beim Wiederherstellen des Checkpoints.", "no_workspace": "Bitte öffne zuerst einen Projektordner", @@ -67,7 +72,9 @@ "custom_storage_path_set": "Benutzerdefinierter Speicherpfad festgelegt: {{path}}", "default_storage_path": "Auf Standardspeicherpfad zurückgesetzt", "settings_imported": "Einstellungen erfolgreich importiert.", - "share_link_copied": "Share-Link in die Zwischenablage kopiert" + "share_link_copied": "Share-Link in die Zwischenablage kopiert", + "image_copied_to_clipboard": "Bild-Daten-URI in die Zwischenablage kopiert", + "image_saved": "Bild gespeichert unter {{path}}" }, "answers": { "yes": "Ja", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index b3a47e60ad..7d359e6586 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "Invalid data URI format", + "error_copying_image": "Error copying image: {{errorMessage}}", + "error_opening_image": "Error opening image: {{error}}", + "error_saving_image": "Error saving image: {{errorMessage}}", + "could_not_open_file": "Could not open file: {{errorMessage}}", + "could_not_open_file_generic": "Could not open file!", "checkpoint_timeout": "Timed out when attempting to restore checkpoint.", "checkpoint_failed": "Failed to restore checkpoint.", "no_workspace": "Please open a project folder first", @@ -67,7 +72,9 @@ "custom_storage_path_set": "Custom storage path set: {{path}}", "default_storage_path": "Reverted to using default storage path", "settings_imported": "Settings imported successfully.", - "share_link_copied": "Share link copied to clipboard" + "share_link_copied": "Share link copied to clipboard", + "image_copied_to_clipboard": "Image data URI copied to clipboard", + "image_saved": "Image saved to {{path}}" }, "answers": { "yes": "Yes", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index b374b8559e..3fb8602131 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "Formato de URI de datos no válido", + "error_copying_image": "Error copiando la imagen: {{errorMessage}}", + "error_saving_image": "Error guardando la imagen: {{errorMessage}}", + "error_opening_image": "Error abriendo la imagen: {{error}}", + "could_not_open_file": "No se pudo abrir el archivo: {{errorMessage}}", + "could_not_open_file_generic": "¡No se pudo abrir el archivo!", "checkpoint_timeout": "Se agotó el tiempo al intentar restaurar el punto de control.", "checkpoint_failed": "Error al restaurar el punto de control.", "no_workspace": "Por favor, abre primero una carpeta de proyecto", @@ -67,7 +72,9 @@ "custom_storage_path_set": "Ruta de almacenamiento personalizada establecida: {{path}}", "default_storage_path": "Se ha vuelto a usar la ruta de almacenamiento predeterminada", "settings_imported": "Configuración importada correctamente.", - "share_link_copied": "Enlace de compartir copiado al portapapeles" + "share_link_copied": "Enlace de compartir copiado al portapapeles", + "image_copied_to_clipboard": "URI de datos de imagen copiada al portapapeles", + "image_saved": "Imagen guardada en {{path}}" }, "answers": { "yes": "Sí", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 77b1ea9786..70f22f4f9f 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "Format d'URI de données invalide", + "error_copying_image": "Erreur lors de la copie de l'image : {{errorMessage}}", + "error_saving_image": "Erreur lors de l'enregistrement de l'image : {{errorMessage}}", + "error_opening_image": "Erreur lors de l'ouverture de l'image : {{error}}", + "could_not_open_file": "Impossible d'ouvrir le fichier : {{errorMessage}}", + "could_not_open_file_generic": "Impossible d'ouvrir le fichier !", "checkpoint_timeout": "Expiration du délai lors de la tentative de rétablissement du checkpoint.", "checkpoint_failed": "Échec du rétablissement du checkpoint.", "no_workspace": "Veuillez d'abord ouvrir un espace de travail", @@ -67,7 +72,9 @@ "custom_storage_path_set": "Chemin de stockage personnalisé défini : {{path}}", "default_storage_path": "Retour au chemin de stockage par défaut", "settings_imported": "Paramètres importés avec succès.", - "share_link_copied": "Lien de partage copié dans le presse-papiers" + "share_link_copied": "Lien de partage copié dans le presse-papiers", + "image_copied_to_clipboard": "URI de données d'image copiée dans le presse-papiers", + "image_saved": "Image enregistrée dans {{path}}" }, "answers": { "yes": "Oui", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 635eeea900..7b97e41ffc 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "अमान्य डेटा URI फॉर्मेट", + "error_copying_image": "छवि कॉपी करने में त्रुटि: {{errorMessage}}", + "error_saving_image": "छवि सहेजने में त्रुटि: {{errorMessage}}", + "error_opening_image": "छवि खोलने में त्रुटि: {{error}}", + "could_not_open_file": "फ़ाइल नहीं खोली जा सकी: {{errorMessage}}", + "could_not_open_file_generic": "फ़ाइल नहीं खोली जा सकी!", "checkpoint_timeout": "चेकपॉइंट को पुनर्स्थापित करने का प्रयास करते समय टाइमआउट हो गया।", "checkpoint_failed": "चेकपॉइंट पुनर्स्थापित करने में विफल।", "no_workspace": "कृपया पहले प्रोजेक्ट फ़ोल्डर खोलें", @@ -66,8 +71,10 @@ "history_cleanup": "इतिहास से गायब फाइलों वाले {{count}} टास्क साफ किए गए।", "custom_storage_path_set": "कस्टम स्टोरेज पाथ सेट किया गया: {{path}}", "default_storage_path": "डिफ़ॉल्ट स्टोरेज पाथ का उपयोग पुनः शुरू किया गया", - "settings_imported": "सेटिंग्स सफलतापूर्वक इम्पोर्ट की गईं.", - "share_link_copied": "साझा लिंक क्लिपबोर्ड पर कॉपी किया गया" + "settings_imported": "सेटिंग्स सफलतापूर्वक इम्पोर्ट की गईं।", + "share_link_copied": "साझा लिंक क्लिपबोर्ड पर कॉपी किया गया", + "image_copied_to_clipboard": "छवि डेटा URI क्लिपबोर्ड में कॉपी की गई", + "image_saved": "छवि {{path}} में सहेजी गई" }, "answers": { "yes": "हां", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 44120ca0e9..21946d69e5 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "Formato URI dati non valido", + "error_copying_image": "Errore durante la copia dell'immagine: {{errorMessage}}", + "error_saving_image": "Errore durante il salvataggio dell'immagine: {{errorMessage}}", + "error_opening_image": "Errore durante l'apertura dell'immagine: {{error}}", + "could_not_open_file": "Impossibile aprire il file: {{errorMessage}}", + "could_not_open_file_generic": "Impossibile aprire il file!", "checkpoint_timeout": "Timeout durante il tentativo di ripristinare il checkpoint.", "checkpoint_failed": "Impossibile ripristinare il checkpoint.", "no_workspace": "Per favore, apri prima una cartella di progetto", @@ -67,7 +72,9 @@ "custom_storage_path_set": "Percorso di archiviazione personalizzato impostato: {{path}}", "default_storage_path": "Tornato al percorso di archiviazione predefinito", "settings_imported": "Impostazioni importate con successo.", - "share_link_copied": "Link di condivisione copiato negli appunti" + "share_link_copied": "Link di condivisione copiato negli appunti", + "image_copied_to_clipboard": "URI dati dell'immagine copiato negli appunti", + "image_saved": "Immagine salvata in {{path}}" }, "answers": { "yes": "Sì", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 3ab1bbf110..2cf2d50f29 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "データURIフォーマットが無効です", + "error_copying_image": "画像のコピー中にエラーが発生しました:{{errorMessage}}", + "error_saving_image": "画像の保存中にエラーが発生しました:{{errorMessage}}", + "error_opening_image": "画像を開く際にエラーが発生しました:{{error}}", + "could_not_open_file": "ファイルを開けませんでした:{{errorMessage}}", + "could_not_open_file_generic": "ファイルを開けませんでした!", "checkpoint_timeout": "チェックポイントの復元を試みる際にタイムアウトしました。", "checkpoint_failed": "チェックポイントの復元に失敗しました。", "no_workspace": "まずプロジェクトフォルダを開いてください", @@ -67,7 +72,9 @@ "custom_storage_path_set": "カスタムストレージパスが設定されました:{{path}}", "default_storage_path": "デフォルトのストレージパスに戻りました", "settings_imported": "設定が正常にインポートされました。", - "share_link_copied": "共有リンクがクリップボードにコピーされました" + "share_link_copied": "共有リンクがクリップボードにコピーされました", + "image_copied_to_clipboard": "画像データURIがクリップボードにコピーされました", + "image_saved": "画像を{{path}}に保存しました" }, "answers": { "yes": "はい", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index e0b0589743..52264fcdd5 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "잘못된 데이터 URI 형식", + "error_copying_image": "이미지 복사 중 오류 발생: {{errorMessage}}", + "error_saving_image": "이미지 저장 중 오류 발생: {{errorMessage}}", + "error_opening_image": "이미지 열기 중 오류 발생: {{error}}", + "could_not_open_file": "파일을 열 수 없습니다: {{errorMessage}}", + "could_not_open_file_generic": "파일을 열 수 없습니다!", "checkpoint_timeout": "체크포인트 복원을 시도하는 중 시간 초과되었습니다.", "checkpoint_failed": "체크포인트 복원에 실패했습니다.", "no_workspace": "먼저 프로젝트 폴더를 열어주세요", @@ -67,7 +72,9 @@ "custom_storage_path_set": "사용자 지정 저장 경로 설정됨: {{path}}", "default_storage_path": "기본 저장 경로로 되돌아갔습니다", "settings_imported": "설정이 성공적으로 가져와졌습니다.", - "share_link_copied": "공유 링크가 클립보드에 복사되었습니다" + "share_link_copied": "공유 링크가 클립보드에 복사되었습니다", + "image_copied_to_clipboard": "이미지 데이터 URI가 클립보드에 복사되었습니다", + "image_saved": "이미지가 {{path}}에 저장되었습니다" }, "answers": { "yes": "예", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index eb7d6f2bd4..26a856c15f 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "Ongeldig data-URI-formaat", + "error_copying_image": "Fout bij kopiëren van afbeelding: {{errorMessage}}", + "error_saving_image": "Fout bij opslaan van afbeelding: {{errorMessage}}", + "error_opening_image": "Fout bij openen van afbeelding: {{error}}", + "could_not_open_file": "Kon bestand niet openen: {{errorMessage}}", + "could_not_open_file_generic": "Kon bestand niet openen!", "checkpoint_timeout": "Time-out bij het herstellen van checkpoint.", "checkpoint_failed": "Herstellen van checkpoint mislukt.", "no_workspace": "Open eerst een projectmap", @@ -67,7 +72,9 @@ "custom_storage_path_set": "Aangepast opslagpad ingesteld: {{path}}", "default_storage_path": "Terug naar standaard opslagpad", "settings_imported": "Instellingen succesvol geïmporteerd.", - "share_link_copied": "Deellink gekopieerd naar klembord" + "share_link_copied": "Deellink gekopieerd naar klembord", + "image_copied_to_clipboard": "Afbeelding data-URI gekopieerd naar klembord", + "image_saved": "Afbeelding opgeslagen naar {{path}}" }, "answers": { "yes": "Ja", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 19aae381fe..1091b643e2 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "Nieprawidłowy format URI danych", + "error_copying_image": "Błąd kopiowania obrazu: {{errorMessage}}", + "error_saving_image": "Błąd zapisywania obrazu: {{errorMessage}}", + "error_opening_image": "Błąd otwierania obrazu: {{error}}", + "could_not_open_file": "Nie można otworzyć pliku: {{errorMessage}}", + "could_not_open_file_generic": "Nie można otworzyć pliku!", "checkpoint_timeout": "Upłynął limit czasu podczas próby przywrócenia punktu kontrolnego.", "checkpoint_failed": "Nie udało się przywrócić punktu kontrolnego.", "no_workspace": "Najpierw otwórz folder projektu", @@ -67,7 +72,9 @@ "custom_storage_path_set": "Ustawiono niestandardową ścieżkę przechowywania: {{path}}", "default_storage_path": "Wznowiono używanie domyślnej ścieżki przechowywania", "settings_imported": "Ustawienia zaimportowane pomyślnie.", - "share_link_copied": "Link udostępniania skopiowany do schowka" + "share_link_copied": "Link udostępniania skopiowany do schowka", + "image_copied_to_clipboard": "URI danych obrazu skopiowane do schowka", + "image_saved": "Obraz zapisany w {{path}}" }, "answers": { "yes": "Tak", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 30c0aa3cbe..6eb8fc7708 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -28,6 +28,11 @@ }, "errors": { "invalid_data_uri": "Formato de URI de dados inválido", + "error_copying_image": "Erro ao copiar imagem: {{errorMessage}}", + "error_saving_image": "Erro ao salvar imagem: {{errorMessage}}", + "error_opening_image": "Erro ao abrir imagem: {{error}}", + "could_not_open_file": "Não foi possível abrir o arquivo: {{errorMessage}}", + "could_not_open_file_generic": "Não foi possível abrir o arquivo!", "checkpoint_timeout": "Tempo esgotado ao tentar restaurar o ponto de verificação.", "checkpoint_failed": "Falha ao restaurar o ponto de verificação.", "no_workspace": "Por favor, abra primeiro uma pasta de projeto", @@ -71,7 +76,9 @@ "custom_storage_path_set": "Caminho de armazenamento personalizado definido: {{path}}", "default_storage_path": "Retornado ao caminho de armazenamento padrão", "settings_imported": "Configurações importadas com sucesso.", - "share_link_copied": "Link de compartilhamento copiado para a área de transferência" + "share_link_copied": "Link de compartilhamento copiado para a área de transferência", + "image_copied_to_clipboard": "URI de dados da imagem copiada para a área de transferência", + "image_saved": "Imagem salva em {{path}}" }, "answers": { "yes": "Sim", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index b773baa05e..a8e0479da1 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "Неверный формат URI данных", + "error_copying_image": "Ошибка копирования изображения: {{errorMessage}}", + "error_saving_image": "Ошибка сохранения изображения: {{errorMessage}}", + "error_opening_image": "Ошибка открытия изображения: {{error}}", + "could_not_open_file": "Не удалось открыть файл: {{errorMessage}}", + "could_not_open_file_generic": "Не удалось открыть файл!", "checkpoint_timeout": "Превышено время ожидания при попытке восстановления контрольной точки.", "checkpoint_failed": "Не удалось восстановить контрольную точку.", "no_workspace": "Пожалуйста, сначала откройте папку проекта", @@ -67,7 +72,9 @@ "custom_storage_path_set": "Установлен пользовательский путь хранения: {{path}}", "default_storage_path": "Возвращено использование пути хранения по умолчанию", "settings_imported": "Настройки успешно импортированы.", - "share_link_copied": "Ссылка для совместного использования скопирована в буфер обмена" + "share_link_copied": "Ссылка для совместного использования скопирована в буфер обмена", + "image_copied_to_clipboard": "URI данных изображения скопирован в буфер обмена", + "image_saved": "Изображение сохранено в {{path}}" }, "answers": { "yes": "Да", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 21ebea9614..182df50d1c 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "Geçersiz veri URI formatı", + "error_copying_image": "Resim kopyalanırken hata oluştu: {{errorMessage}}", + "error_saving_image": "Resim kaydedilirken hata oluştu: {{errorMessage}}", + "error_opening_image": "Resim açılırken hata oluştu: {{error}}", + "could_not_open_file": "Dosya açılamadı: {{errorMessage}}", + "could_not_open_file_generic": "Dosya açılamadı!", "checkpoint_timeout": "Kontrol noktasını geri yüklemeye çalışırken zaman aşımına uğradı.", "checkpoint_failed": "Kontrol noktası geri yüklenemedi.", "no_workspace": "Lütfen önce bir proje klasörü açın", @@ -67,7 +72,9 @@ "custom_storage_path_set": "Özel depolama yolu ayarlandı: {{path}}", "default_storage_path": "Varsayılan depolama yoluna geri dönüldü", "settings_imported": "Ayarlar başarıyla içe aktarıldı.", - "share_link_copied": "Paylaşım bağlantısı panoya kopyalandı" + "share_link_copied": "Paylaşım bağlantısı panoya kopyalandı", + "image_copied_to_clipboard": "Resim veri URI'si panoya kopyalandı", + "image_saved": "Resim {{path}} konumuna kaydedildi" }, "answers": { "yes": "Evet", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index e0044927fe..2f32da8f70 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "Định dạng URI dữ liệu không hợp lệ", + "error_copying_image": "Lỗi khi sao chép hình ảnh: {{errorMessage}}", + "error_saving_image": "Lỗi khi lưu hình ảnh: {{errorMessage}}", + "error_opening_image": "Lỗi khi mở hình ảnh: {{error}}", + "could_not_open_file": "Không thể mở tệp: {{errorMessage}}", + "could_not_open_file_generic": "Không thể mở tệp!", "checkpoint_timeout": "Đã hết thời gian khi cố gắng khôi phục điểm kiểm tra.", "checkpoint_failed": "Không thể khôi phục điểm kiểm tra.", "no_workspace": "Vui lòng mở thư mục dự án trước", @@ -67,7 +72,9 @@ "custom_storage_path_set": "Đã thiết lập đường dẫn lưu trữ tùy chỉnh: {{path}}", "default_storage_path": "Đã quay lại sử dụng đường dẫn lưu trữ mặc định", "settings_imported": "Cài đặt đã được nhập thành công.", - "share_link_copied": "Liên kết chia sẻ đã được sao chép vào clipboard" + "share_link_copied": "Liên kết chia sẻ đã được sao chép vào clipboard", + "image_copied_to_clipboard": "URI dữ liệu hình ảnh đã được sao chép vào clipboard", + "image_saved": "Hình ảnh đã được lưu vào {{path}}" }, "answers": { "yes": "Có", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 844da028a2..45fd6d9b58 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -23,7 +23,17 @@ "this_and_subsequent": "此消息及所有后续消息" }, "errors": { - "invalid_data_uri": "数据URI格式无效", + "invalid_mcp_config": "项目MCP配置格式无效", + "invalid_mcp_settings_format": "MCP设置JSON格式无效。请确保您的设置遵循正确的JSON格式。", + "invalid_mcp_settings_syntax": "MCP设置JSON格式无效。请检查您的设置文件是否有语法错误。", + "invalid_mcp_settings_validation": "MCP设置格式无效:{{errorMessages}}", + "failed_initialize_project_mcp": "初始化项目MCP服务器失败:{{error}}", + "invalid_data_uri": "数据 URI 格式无效", + "error_copying_image": "复制图片时出错:{{errorMessage}}", + "error_saving_image": "保存图片时出错:{{errorMessage}}", + "error_opening_image": "打开图片时出错:{{error}}", + "could_not_open_file": "无法打开文件:{{errorMessage}}", + "could_not_open_file_generic": "无法打开文件!", "checkpoint_timeout": "尝试恢复检查点时超时。", "checkpoint_failed": "恢复检查点失败。", "no_workspace": "请先打开项目文件夹", @@ -67,7 +77,9 @@ "custom_storage_path_set": "自定义存储路径已设置:{{path}}", "default_storage_path": "已恢复使用默认存储路径", "settings_imported": "设置已成功导入。", - "share_link_copied": "分享链接已复制到剪贴板" + "share_link_copied": "分享链接已复制到剪贴板", + "image_copied_to_clipboard": "图片数据 URI 已复制到剪贴板", + "image_saved": "图片已保存到 {{path}}" }, "answers": { "yes": "是", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 512acfac32..03e83b183a 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -24,6 +24,11 @@ }, "errors": { "invalid_data_uri": "資料 URI 格式無效", + "error_copying_image": "複製圖片時發生錯誤:{{errorMessage}}", + "error_saving_image": "儲存圖片時發生錯誤:{{errorMessage}}", + "error_opening_image": "開啟圖片時發生錯誤:{{error}}", + "could_not_open_file": "無法開啟檔案:{{errorMessage}}", + "could_not_open_file_generic": "無法開啟檔案!", "checkpoint_timeout": "嘗試恢復檢查點時超時。", "checkpoint_failed": "恢復檢查點失敗。", "no_workspace": "請先開啟專案資料夾", @@ -67,7 +72,9 @@ "custom_storage_path_set": "自訂儲存路徑已設定:{{path}}", "default_storage_path": "已恢復使用預設儲存路徑", "settings_imported": "設定已成功匯入。", - "share_link_copied": "分享連結已複製到剪貼簿" + "share_link_copied": "分享連結已複製到剪貼簿", + "image_copied_to_clipboard": "圖片資料 URI 已複製到剪貼簿", + "image_saved": "圖片已儲存至 {{path}}" }, "answers": { "yes": "是", diff --git a/src/integrations/misc/image-handler.ts b/src/integrations/misc/image-handler.ts new file mode 100644 index 0000000000..4cd7585df4 --- /dev/null +++ b/src/integrations/misc/image-handler.ts @@ -0,0 +1,92 @@ +import * as path from "path" +import * as os from "os" +import * as vscode from "vscode" +import { getWorkspacePath } from "../../utils/path" +import { t } from "../../i18n" + +export async function openImage(dataUri: string, options?: { values?: { action?: string } }) { + const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/) + if (!matches) { + vscode.window.showErrorMessage(t("common:errors.invalid_data_uri")) + return + } + const [, format, base64Data] = matches + const imageBuffer = Buffer.from(base64Data, "base64") + + // Default behavior: open the image + const tempFilePath = path.join(os.tmpdir(), `temp_image_${Date.now()}.${format}`) + try { + await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), imageBuffer) + // Check if this is a copy action + if (options?.values?.action === "copy") { + try { + // Read the image file + const imageData = await vscode.workspace.fs.readFile(vscode.Uri.file(tempFilePath)) + + // Convert to base64 for clipboard + const base64Image = Buffer.from(imageData).toString("base64") + const dataUri = `data:image/${format};base64,${base64Image}` + + // Use vscode.env.clipboard to copy the data URI + // Note: VSCode doesn't support copying binary image data directly to clipboard + // So we copy the data URI which can be pasted in many applications + await vscode.env.clipboard.writeText(dataUri) + + vscode.window.showInformationMessage(t("common:info.image_copied_to_clipboard")) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(t("common:errors.error_copying_image", { errorMessage })) + } finally { + // Clean up temp file + try { + await vscode.workspace.fs.delete(vscode.Uri.file(tempFilePath)) + } catch { + // Ignore cleanup errors + } + } + return + } + await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath)) + } catch (error) { + vscode.window.showErrorMessage(t("common:errors.error_opening_image", { error })) + } +} + +export async function saveImage(dataUri: string) { + const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/) + if (!matches) { + vscode.window.showErrorMessage(t("common:errors.invalid_data_uri")) + return + } + const [, format, base64Data] = matches + const imageBuffer = Buffer.from(base64Data, "base64") + + // Get workspace path or fallback to home directory + const workspacePath = getWorkspacePath() + const defaultPath = workspacePath || os.homedir() + const defaultFileName = `mermaid_diagram_${Date.now()}.${format}` + const defaultUri = vscode.Uri.file(path.join(defaultPath, defaultFileName)) + + // Show save dialog + const saveUri = await vscode.window.showSaveDialog({ + filters: { + Images: [format], + "All Files": ["*"], + }, + defaultUri: defaultUri, + }) + + if (!saveUri) { + // User cancelled the save dialog + return + } + + try { + // Write the image to the selected location + await vscode.workspace.fs.writeFile(saveUri, imageBuffer) + vscode.window.showInformationMessage(t("common:info.image_saved", { path: saveUri.fsPath })) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(t("common:errors.error_saving_image", { errorMessage })) + } +} diff --git a/src/integrations/misc/open-file.ts b/src/integrations/misc/open-file.ts index 9318e23766..f05c10dc96 100644 --- a/src/integrations/misc/open-file.ts +++ b/src/integrations/misc/open-file.ts @@ -2,23 +2,7 @@ import * as path from "path" import * as os from "os" import * as vscode from "vscode" import { arePathsEqual, getWorkspacePath } from "../../utils/path" - -export async function openImage(dataUri: string) { - const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/) - if (!matches) { - vscode.window.showErrorMessage("Invalid data URI format") - return - } - const [, format, base64Data] = matches - const imageBuffer = Buffer.from(base64Data, "base64") - const tempFilePath = path.join(os.tmpdir(), `temp_image_${Date.now()}.${format}`) - try { - await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), imageBuffer) - await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath)) - } catch (error) { - vscode.window.showErrorMessage(`Error opening image: ${error}`) - } -} +import { t } from "../../i18n" interface OpenFileOptions { create?: boolean @@ -151,9 +135,9 @@ export async function openFile(filePath: string, options: OpenFileOptions = {}) }) } catch (error) { if (error instanceof Error) { - vscode.window.showErrorMessage(`Could not open file: ${error.message}`) + vscode.window.showErrorMessage(t("common:errors.could_not_open_file", { errorMessage: error.message })) } else { - vscode.window.showErrorMessage(`Could not open file!`) + vscode.window.showErrorMessage(t("common:errors.could_not_open_file_generic")) } } } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d27b931f10..ae93e3ae76 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -52,6 +52,7 @@ export interface WebviewMessage { | "requestLmStudioModels" | "requestVsCodeLmModels" | "openImage" + | "saveImage" | "openFile" | "openMention" | "cancelTask" @@ -164,6 +165,7 @@ export interface WebviewMessage { text?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" disabled?: boolean + dataUri?: string askResponse?: ClineAskResponse apiConfiguration?: ProviderSettings images?: string[] diff --git a/webview-ui/src/components/common/IconButton.tsx b/webview-ui/src/components/common/IconButton.tsx new file mode 100644 index 0000000000..70a66ba9f1 --- /dev/null +++ b/webview-ui/src/components/common/IconButton.tsx @@ -0,0 +1,45 @@ +interface IconButtonProps { + icon: string + onClick?: (e: React.MouseEvent) => void + onMouseDown?: (e: React.MouseEvent) => void + onMouseUp?: (e: React.MouseEvent) => void + onMouseLeave?: (e: React.MouseEvent) => void + title?: string + size?: "small" | "medium" + variant?: "default" | "transparent" +} + +export function IconButton({ + icon, + onClick, + onMouseDown, + onMouseUp, + onMouseLeave, + title, + size = "medium", + variant = "default", +}: IconButtonProps) { + const sizeClasses = { + small: "w-6 h-6", + medium: "w-7 h-7", + } + + const variantClasses = { + default: "bg-transparent hover:bg-vscode-toolbar-hoverBackground", + transparent: "bg-transparent hover:bg-vscode-toolbar-hoverBackground", + } + + const handleClick = onClick || ((_event: React.MouseEvent) => {}) + + return ( + + ) +} diff --git a/webview-ui/src/components/common/MermaidActionButtons.tsx b/webview-ui/src/components/common/MermaidActionButtons.tsx new file mode 100644 index 0000000000..46ded57644 --- /dev/null +++ b/webview-ui/src/components/common/MermaidActionButtons.tsx @@ -0,0 +1,80 @@ +import React from "react" +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { IconButton } from "./IconButton" +import { ZoomControls } from "./ZoomControls" + +interface MermaidActionButtonsProps { + onZoom?: (e: React.MouseEvent) => void + onZoomIn?: () => void + onZoomOut?: () => void + onCopy: (e: React.MouseEvent) => void + onSave?: (e: React.MouseEvent) => void + onViewCode: () => void + onClose?: () => void + copyFeedback: boolean + showZoomControls?: boolean + zoomLevel?: number +} + +export const MermaidActionButtons: React.FC = ({ + onZoom, + onZoomIn, + onZoomOut, + onCopy, + onSave, + onViewCode, + onClose, + copyFeedback, + showZoomControls = false, + zoomLevel, +}) => { + const { t } = useAppTranslation() + + if (showZoomControls && onZoomOut && onZoomIn && zoomLevel !== undefined) { + return ( + <> + + { + e.stopPropagation() + onViewCode() + }} + title={t("common:mermaid.buttons.viewCode")} + /> + + + ) + } + + return ( + <> + {onZoom && } + { + e.stopPropagation() + onViewCode() + }} + title={t("common:mermaid.buttons.viewCode")} + /> + + {onSave && } + {onClose && } + + ) +} diff --git a/webview-ui/src/components/common/MermaidBlock.tsx b/webview-ui/src/components/common/MermaidBlock.tsx index c5fc8b30bb..229b957765 100644 --- a/webview-ui/src/components/common/MermaidBlock.tsx +++ b/webview-ui/src/components/common/MermaidBlock.tsx @@ -6,6 +6,7 @@ import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@src/i18n/TranslationContext" import { useCopyToClipboard } from "@src/utils/clipboard" import CodeBlock from "./CodeBlock" +import { MermaidButton } from "@/components/common/MermaidButton" // Removed previous attempts at static imports for individual diagram types // as the paths were incorrect for Mermaid v11.4.1 and caused errors. @@ -213,7 +214,9 @@ export default function MermaidBlock({ code }: MermaidBlockProps) { )} ) : ( - + + + )} ) @@ -243,10 +246,16 @@ async function svgToPng(svgEl: SVGElement): Promise { const serializer = new XMLSerializer() const svgString = serializer.serializeToString(svgClone) - const svgDataUrl = "data:image/svg+xml;base64," + btoa(decodeURIComponent(encodeURIComponent(svgString))) + + // Create a data URL directly + // First, ensure the SVG string is properly encoded + const encodedSvg = encodeURIComponent(svgString).replace(/'/g, "%27").replace(/"/g, "%22") + + const svgDataUrl = `data:image/svg+xml;charset=utf-8,${encodedSvg}` return new Promise((resolve, reject) => { const img = new Image() + img.onload = () => { const canvas = document.createElement("canvas") canvas.width = editorWidth diff --git a/webview-ui/src/components/common/MermaidButton.tsx b/webview-ui/src/components/common/MermaidButton.tsx new file mode 100644 index 0000000000..57d4c26b0a --- /dev/null +++ b/webview-ui/src/components/common/MermaidButton.tsx @@ -0,0 +1,246 @@ +import { useState, useCallback } from "react" +import { useCopyToClipboard } from "@src/utils/clipboard" +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { vscode } from "@src/utils/vscode" +import { MermaidActionButtons } from "./MermaidActionButtons" +import { Modal } from "./Modal" +import { TabButton } from "./TabButton" +import { IconButton } from "./IconButton" +import { ZoomControls } from "./ZoomControls" + +const MIN_ZOOM = 0.5 +const MAX_ZOOM = 20 + +export interface MermaidButtonProps { + containerRef: React.RefObject + code: string + isLoading: boolean + svgToPng: (svgEl: SVGElement) => Promise + children: React.ReactNode +} + +export function MermaidButton({ containerRef, code, isLoading, svgToPng, children }: MermaidButtonProps) { + const [showModal, setShowModal] = useState(false) + const [zoomLevel, setZoomLevel] = useState(1) + const [copyFeedback, setCopyFeedback] = useState(false) + const [isHovering, setIsHovering] = useState(false) + const [modalViewMode, setModalViewMode] = useState<"diagram" | "code">("diagram") + const [isDragging, setIsDragging] = useState(false) + const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 }) + const { copyWithFeedback } = useCopyToClipboard() + const { t } = useAppTranslation() + + /** + * Opens a modal with the diagram for zooming + */ + const handleZoom = async (e: React.MouseEvent) => { + e.stopPropagation() + setShowModal(true) + setZoomLevel(1) + setModalViewMode("diagram") + } + + /** + * Copies the diagram text to clipboard + */ + const handleCopy = async (e: React.MouseEvent) => { + e.stopPropagation() + + try { + await copyWithFeedback(code, e) + + // Show feedback + setCopyFeedback(true) + setTimeout(() => setCopyFeedback(false), 2000) + } catch (err) { + console.error("Error copying text:", err instanceof Error ? err.message : String(err)) + } + } + + /** + * Saves the diagram as an image file + */ + const handleSave = async (e: React.MouseEvent) => { + e.stopPropagation() + + // Get the SVG element from the container + const svgEl = containerRef.current?.querySelector("svg") + if (!svgEl) { + console.error("SVG element not found") + return + } + + try { + // Convert SVG to PNG + const pngDataUrl = await svgToPng(svgEl) + + // Send message to VSCode to save the image + vscode.postMessage({ + type: "saveImage", + dataUri: pngDataUrl, + }) + } catch (error) { + console.error("Error saving image:", error) + } + } + + /** + * Adjust zoom level in the modal + */ + const adjustZoom = (amount: number) => { + setZoomLevel((prev) => { + const newZoom = prev + amount + return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom)) + }) + } + + /** + * Handle wheel event for zooming with scroll wheel + */ + const handleWheel = useCallback((e: React.WheelEvent) => { + e.preventDefault() + e.stopPropagation() + + // Determine zoom direction and amount + // Negative deltaY means scrolling up (zoom in), positive means scrolling down (zoom out) + const delta = e.deltaY > 0 ? -0.2 : 0.2 + adjustZoom(delta) + }, []) + + /** + * Handle mouse enter event for diagram container + */ + const handleMouseEnter = () => { + setIsHovering(true) + } + + /** + * Handle mouse leave event for diagram container + */ + const handleMouseLeave = () => { + setIsHovering(false) + } + + return ( + <> +
+ {children} + {!isLoading && isHovering && ( +
+ { + setShowModal(true) + setModalViewMode("code") + setZoomLevel(1) + }} + copyFeedback={copyFeedback} + /> +
+ )} +
+ + setShowModal(false)}> +
+
+ setModalViewMode("diagram")} + /> + setModalViewMode("code")} + /> +
+ +
+ setShowModal(false)} + title={t("common:mermaid.buttons.close")} + /> +
+
+
+ {modalViewMode === "diagram" ? ( + <> +
{ + setIsDragging(true) + e.preventDefault() + }} + onMouseMove={(e) => { + if (isDragging) { + setDragPosition((prev) => ({ + x: prev.x + e.movementX / zoomLevel, + y: prev.y + e.movementY / zoomLevel, + })) + } + }} + onMouseUp={() => setIsDragging(false)} + onMouseLeave={() => setIsDragging(false)}> + {containerRef.current && containerRef.current.innerHTML && ( +
+ )} +
+
+ {Math.round(zoomLevel * 100)}% +
+ + ) : ( +