Browse Source

add mermaid buttons (#4547)

* add mermaid buttons

* feat: Add Modal, TabButton, and ZoomControls components

* feat: Add error handling messages for image operations and file opening

* mermaid: Add drag functionality and support contious zooming

* add active color for tabbutton

* refactor zoom controls

* refactor: Remove unused svgToPng prop and simplify handleCopy function

* Move zoom to constants and increase max zoom

* feat: add save image functionality and refactor image handling

* feat: add translations

---------

Co-authored-by: Daniel Riccio <[email protected]>
Co-authored-by: Matt Rubens <[email protected]>
axb 8 months ago
parent
commit
dfcf8fe760
45 changed files with 1360 additions and 59 deletions
  1. 6 2
      src/core/webview/webviewMessageHandler.ts
  2. 8 1
      src/i18n/locales/ca/common.json
  3. 8 1
      src/i18n/locales/de/common.json
  4. 8 1
      src/i18n/locales/en/common.json
  5. 8 1
      src/i18n/locales/es/common.json
  6. 8 1
      src/i18n/locales/fr/common.json
  7. 9 2
      src/i18n/locales/hi/common.json
  8. 8 1
      src/i18n/locales/it/common.json
  9. 8 1
      src/i18n/locales/ja/common.json
  10. 8 1
      src/i18n/locales/ko/common.json
  11. 8 1
      src/i18n/locales/nl/common.json
  12. 8 1
      src/i18n/locales/pl/common.json
  13. 8 1
      src/i18n/locales/pt-BR/common.json
  14. 8 1
      src/i18n/locales/ru/common.json
  15. 8 1
      src/i18n/locales/tr/common.json
  16. 8 1
      src/i18n/locales/vi/common.json
  17. 14 2
      src/i18n/locales/zh-CN/common.json
  18. 8 1
      src/i18n/locales/zh-TW/common.json
  19. 92 0
      src/integrations/misc/image-handler.ts
  20. 3 19
      src/integrations/misc/open-file.ts
  21. 2 0
      src/shared/WebviewMessage.ts
  22. 45 0
      webview-ui/src/components/common/IconButton.tsx
  23. 80 0
      webview-ui/src/components/common/MermaidActionButtons.tsx
  24. 11 2
      webview-ui/src/components/common/MermaidBlock.tsx
  25. 246 0
      webview-ui/src/components/common/MermaidButton.tsx
  26. 20 0
      webview-ui/src/components/common/Modal.tsx
  27. 26 0
      webview-ui/src/components/common/TabButton.tsx
  28. 91 0
      webview-ui/src/components/common/ZoomControls.tsx
  29. 35 1
      webview-ui/src/i18n/locales/ca/common.json
  30. 35 1
      webview-ui/src/i18n/locales/de/common.json
  31. 35 1
      webview-ui/src/i18n/locales/en/common.json
  32. 35 1
      webview-ui/src/i18n/locales/es/common.json
  33. 35 1
      webview-ui/src/i18n/locales/fr/common.json
  34. 35 1
      webview-ui/src/i18n/locales/hi/common.json
  35. 35 1
      webview-ui/src/i18n/locales/it/common.json
  36. 35 1
      webview-ui/src/i18n/locales/ja/common.json
  37. 35 1
      webview-ui/src/i18n/locales/ko/common.json
  38. 35 1
      webview-ui/src/i18n/locales/nl/common.json
  39. 35 1
      webview-ui/src/i18n/locales/pl/common.json
  40. 35 1
      webview-ui/src/i18n/locales/pt-BR/common.json
  41. 35 1
      webview-ui/src/i18n/locales/ru/common.json
  42. 35 1
      webview-ui/src/i18n/locales/tr/common.json
  43. 35 1
      webview-ui/src/i18n/locales/vi/common.json
  44. 35 1
      webview-ui/src/i18n/locales/zh-CN/common.json
  45. 35 1
      webview-ui/src/i18n/locales/zh-TW/common.json

+ 6 - 2
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 })

+ 8 - 1
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í",

+ 8 - 1
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",

+ 8 - 1
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",

+ 8 - 1
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í",

+ 8 - 1
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",

+ 9 - 2
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": "हां",

+ 8 - 1
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ì",

+ 8 - 1
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": "はい",

+ 8 - 1
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": "예",

+ 8 - 1
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",

+ 8 - 1
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",

+ 8 - 1
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",

+ 8 - 1
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": "Да",

+ 8 - 1
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",

+ 8 - 1
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ó",

+ 14 - 2
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": "是",

+ 8 - 1
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": "是",

+ 92 - 0
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 }))
+	}
+}

+ 3 - 19
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"))
 		}
 	}
 }

+ 2 - 0
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[]

+ 45 - 0
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 (
+		<button
+			className={`${sizeClasses[size]} flex items-center justify-center border-none text-vscode-editor-foreground cursor-pointer rounded-[3px] ${variantClasses[variant]}`}
+			onClick={handleClick}
+			onMouseDown={onMouseDown}
+			onMouseUp={onMouseUp}
+			onMouseLeave={onMouseLeave}
+			title={title}>
+			<span className={`codicon codicon-${icon}`}></span>
+		</button>
+	)
+}

+ 80 - 0
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<MermaidActionButtonsProps> = ({
+	onZoom,
+	onZoomIn,
+	onZoomOut,
+	onCopy,
+	onSave,
+	onViewCode,
+	onClose,
+	copyFeedback,
+	showZoomControls = false,
+	zoomLevel,
+}) => {
+	const { t } = useAppTranslation()
+
+	if (showZoomControls && onZoomOut && onZoomIn && zoomLevel !== undefined) {
+		return (
+			<>
+				<ZoomControls
+					zoomLevel={zoomLevel}
+					onZoomIn={onZoomIn}
+					onZoomOut={onZoomOut}
+					zoomInTitle={t("common:mermaid.buttons.zoomIn")}
+					zoomOutTitle={t("common:mermaid.buttons.zoomOut")}
+				/>
+				<IconButton
+					icon="code"
+					onClick={(e: React.MouseEvent) => {
+						e.stopPropagation()
+						onViewCode()
+					}}
+					title={t("common:mermaid.buttons.viewCode")}
+				/>
+				<IconButton
+					icon={copyFeedback ? "check" : "copy"}
+					onClick={onCopy}
+					title={t("common:mermaid.buttons.copy")}
+				/>
+			</>
+		)
+	}
+
+	return (
+		<>
+			{onZoom && <IconButton icon="zoom-in" onClick={onZoom} title={t("common:mermaid.buttons.zoom")} />}
+			<IconButton
+				icon="code"
+				onClick={(e: React.MouseEvent) => {
+					e.stopPropagation()
+					onViewCode()
+				}}
+				title={t("common:mermaid.buttons.viewCode")}
+			/>
+			<IconButton
+				icon={copyFeedback ? "check" : "copy"}
+				onClick={onCopy}
+				title={t("common:mermaid.buttons.copy")}
+			/>
+			{onSave && <IconButton icon="save" onClick={onSave} title={t("common:mermaid.buttons.save")} />}
+			{onClose && <IconButton icon="close" onClick={onClose} title={t("common:mermaid.buttons.close")} />}
+		</>
+	)
+}

+ 11 - 2
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) {
 					)}
 				</div>
 			) : (
-				<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
+				<MermaidButton containerRef={containerRef} code={code} isLoading={isLoading} svgToPng={svgToPng}>
+					<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading}></SvgContainer>
+				</MermaidButton>
 			)}
 		</MermaidBlockContainer>
 	)
@@ -243,10 +246,16 @@ async function svgToPng(svgEl: SVGElement): Promise<string> {
 
 	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

+ 246 - 0
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<HTMLDivElement>
+	code: string
+	isLoading: boolean
+	svgToPng: (svgEl: SVGElement) => Promise<string>
+	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 (
+		<>
+			<div className="relative w-full" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
+				{children}
+				{!isLoading && isHovering && (
+					<div className="absolute bottom-2 right-2 flex gap-1 bg-black/70 rounded p-0.5 z-10 opacity-100 transition-opacity duration-200 ease-in-out">
+						<MermaidActionButtons
+							onZoom={handleZoom}
+							onCopy={handleCopy}
+							onSave={handleSave}
+							onViewCode={() => {
+								setShowModal(true)
+								setModalViewMode("code")
+								setZoomLevel(1)
+							}}
+							copyFeedback={copyFeedback}
+						/>
+					</div>
+				)}
+			</div>
+
+			<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
+				<div className="flex justify-between items-center border-b border-vscode-editorGroup-border">
+					<div className="flex gap-0">
+						<TabButton
+							icon="graph"
+							label={t("common:mermaid.tabs.diagram")}
+							isActive={modalViewMode === "diagram"}
+							onClick={() => setModalViewMode("diagram")}
+						/>
+						<TabButton
+							icon="code"
+							label={t("common:mermaid.tabs.code")}
+							isActive={modalViewMode === "code"}
+							onClick={() => setModalViewMode("code")}
+						/>
+					</div>
+
+					<div className="pr-3">
+						<IconButton
+							icon="close"
+							onClick={() => setShowModal(false)}
+							title={t("common:mermaid.buttons.close")}
+						/>
+					</div>
+				</div>
+				<div
+					className="flex-1 p-4 pb-[60px] overflow-auto flex items-center justify-center"
+					onWheel={modalViewMode === "diagram" ? handleWheel : undefined}>
+					{modalViewMode === "diagram" ? (
+						<>
+							<div
+								style={{
+									transform: `scale(${zoomLevel}) translate(${dragPosition.x}px, ${dragPosition.y}px)`,
+									transformOrigin: "center center",
+									transition: isDragging ? "none" : "transform 0.1s ease",
+									cursor: isDragging ? "grabbing" : "grab",
+								}}
+								onMouseDown={(e) => {
+									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 && (
+									<div dangerouslySetInnerHTML={{ __html: containerRef.current.innerHTML }} />
+								)}
+							</div>
+							<div className="absolute bottom-4 left-4 bg-vscode-editor-background border border-vscode-editorGroup-border rounded px-2 py-1 text-xs text-vscode-descriptionForeground pointer-events-none opacity-80">
+								{Math.round(zoomLevel * 100)}%
+							</div>
+						</>
+					) : (
+						<textarea
+							className="w-full min-h-[200px] bg-vscode-editor-background text-vscode-editor-foreground border border-vscode-editorGroup-border rounded-[3px] p-2 font-mono resize-y outline-none"
+							readOnly
+							value={code}
+							style={{ height: "100%", minHeight: "unset", fontSize: "var(--vscode-editor-font-size)" }}
+						/>
+					)}
+				</div>
+				<div className="absolute bottom-0 right-0 left-0 p-3 flex items-center justify-end gap-2 bg-vscode-editor-background border-t border-vscode-editorGroup-border rounded-b">
+					{modalViewMode === "diagram" ? (
+						<>
+							<ZoomControls
+								zoomLevel={zoomLevel}
+								zoomInTitle={t("common:mermaid.buttons.zoomIn")}
+								zoomOutTitle={t("common:mermaid.buttons.zoomOut")}
+								useContinuousZoom={true}
+								adjustZoom={adjustZoom}
+								zoomInStep={0.2}
+								zoomOutStep={-0.2}
+							/>
+							<IconButton
+								icon={copyFeedback ? "check" : "copy"}
+								onClick={handleCopy}
+								title={t("common:mermaid.buttons.copy")}
+							/>
+							<IconButton icon="save" onClick={handleSave} title={t("common:mermaid.buttons.save")} />
+						</>
+					) : (
+						<IconButton
+							icon={copyFeedback ? "check" : "copy"}
+							onClick={(e) => {
+								e.stopPropagation()
+								copyWithFeedback(code, e)
+							}}
+							title={t("common:mermaid.buttons.copy")}
+						/>
+					)}
+				</div>
+			</Modal>
+		</>
+	)
+}

+ 20 - 0
webview-ui/src/components/common/Modal.tsx

@@ -0,0 +1,20 @@
+interface ModalProps {
+	isOpen: boolean
+	onClose: () => void
+	children: React.ReactNode
+	className?: string
+}
+
+export function Modal({ isOpen, onClose, children, className = "" }: ModalProps) {
+	if (!isOpen) return null
+
+	return (
+		<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-[1000]" onClick={onClose}>
+			<div
+				className={`bg-vscode-editor-background rounded w-[90%] h-[90%] max-w-[1200px] flex flex-col shadow-[0_5px_15px_rgba(0,0,0,0.5)] border border-vscode-editorGroup-border relative ${className}`}
+				onClick={(e) => e.stopPropagation()}>
+				{children}
+			</div>
+		</div>
+	)
+}

+ 26 - 0
webview-ui/src/components/common/TabButton.tsx

@@ -0,0 +1,26 @@
+interface TabButtonProps {
+	icon: string
+	label: string
+	isActive: boolean
+	onClick: () => void
+}
+
+export function TabButton({ icon, label, isActive, onClick }: TabButtonProps) {
+	const activeClasses =
+		"bg-vscode-editor-background border-b-2 border-vscode-focusBorder text-vscode-editor-foreground"
+	const inactiveClasses =
+		"bg-transparent border-b-2 border-transparent text-vscode-descriptionForeground hover:text-vscode-editor-foreground hover:bg-vscode-toolbar-hoverBackground"
+
+	return (
+		<button
+			className={`px-4 py-2 border-none cursor-pointer flex items-center gap-1.5 text-[13px] transition-all duration-200 ease-in-out ${
+				isActive ? activeClasses : inactiveClasses
+			}`}
+			onClick={onClick}>
+			<span
+				className={`codicon codicon-${icon} text-sm`}
+				style={isActive ? { color: "var(--vscode-focusBorder)" } : undefined}></span>
+			{label}
+		</button>
+	)
+}

+ 91 - 0
webview-ui/src/components/common/ZoomControls.tsx

@@ -0,0 +1,91 @@
+import { IconButton } from "./IconButton"
+import { useRef, useEffect } from "react"
+
+interface ZoomControlsProps {
+	zoomLevel: number
+	zoomInTitle?: string
+	zoomOutTitle?: string
+	useContinuousZoom?: boolean
+	adjustZoom?: (amount: number) => void
+	zoomInStep?: number
+	zoomOutStep?: number
+	onZoomIn?: () => void
+	onZoomOut?: () => void
+}
+
+export function ZoomControls({
+	zoomLevel,
+	zoomInTitle,
+	zoomOutTitle,
+	useContinuousZoom = false,
+	adjustZoom,
+	zoomInStep = 0.1,
+	zoomOutStep = -0.1,
+	onZoomIn,
+	onZoomOut,
+}: ZoomControlsProps) {
+	const zoomIntervalRef = useRef<NodeJS.Timeout | null>(null)
+
+	/**
+	 * Start continuous zoom on mouse down
+	 */
+	const startContinuousZoom = (amount: number) => {
+		if (!useContinuousZoom || !adjustZoom) return
+
+		// Clear any existing interval first
+		if (zoomIntervalRef.current) {
+			clearInterval(zoomIntervalRef.current)
+		}
+
+		// Immediately apply first zoom adjustment
+		adjustZoom(amount)
+
+		// Set up interval for continuous zooming
+		zoomIntervalRef.current = setInterval(() => {
+			adjustZoom(amount)
+		}, 150) // Adjust every 150ms while button is held down
+	}
+
+	/**
+	 * Stop continuous zoom on mouse up or mouse leave
+	 */
+	const stopContinuousZoom = () => {
+		if (zoomIntervalRef.current) {
+			clearInterval(zoomIntervalRef.current)
+			zoomIntervalRef.current = null
+		}
+	}
+
+	// Clean up interval on unmount
+	useEffect(() => {
+		return () => {
+			if (zoomIntervalRef.current) {
+				clearInterval(zoomIntervalRef.current)
+			}
+		}
+	}, [])
+
+	return (
+		<div className="flex items-center gap-2">
+			<IconButton
+				icon="zoom-out"
+				title={zoomOutTitle}
+				onClick={!useContinuousZoom ? onZoomOut || (() => adjustZoom?.(zoomOutStep)) : undefined}
+				onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomOutStep) : undefined}
+				onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
+				onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
+			/>
+			<div className="text-sm text-vscode-editor-foreground min-w-[50px] text-center">
+				{Math.round(zoomLevel * 100)}%
+			</div>
+			<IconButton
+				icon="zoom-in"
+				title={zoomInTitle}
+				onClick={!useContinuousZoom ? onZoomIn || (() => adjustZoom?.(zoomInStep)) : undefined}
+				onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomInStep) : undefined}
+				onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
+				onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
+			/>
+		</div>
+	)
+}

+ 35 - 1
webview-ui/src/i18n/locales/ca/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Generant diagrama mermaid...",
-		"render_error": "No es pot renderitzar el diagrama"
+		"render_error": "No es pot renderitzar el diagrama",
+		"buttons": {
+			"zoom": "Zoom",
+			"zoomIn": "Ampliar",
+			"zoomOut": "Reduir",
+			"copy": "Copiar",
+			"save": "Desar imatge",
+			"viewCode": "Veure codi",
+			"viewDiagram": "Veure diagrama",
+			"close": "Tancar"
+		},
+		"modal": {
+			"codeTitle": "Codi Mermaid"
+		},
+		"tabs": {
+			"diagram": "Diagrama",
+			"code": "Codi"
+		},
+		"feedback": {
+			"imageCopied": "Imatge copiada al porta-retalls",
+			"copyError": "Error copiant la imatge"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Format d'URI de dades no vàlid",
+			"copyingImage": "Error copiant la imatge: {{error}}",
+			"openingImage": "Error obrint la imatge: {{error}}",
+			"pathNotExists": "El camí no existeix: {{path}}",
+			"couldNotOpen": "No s'ha pogut obrir el fitxer: {{error}}",
+			"couldNotOpenGeneric": "No s'ha pogut obrir el fitxer!"
+		},
+		"success": {
+			"imageDataUriCopied": "URI de dades de la imatge copiada al porta-retalls"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/de/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Mermaid-Diagramm wird generiert...",
-		"render_error": "Diagramm kann nicht gerendert werden"
+		"render_error": "Diagramm kann nicht gerendert werden",
+		"buttons": {
+			"zoom": "Zoom",
+			"zoomIn": "Vergrößern",
+			"zoomOut": "Verkleinern",
+			"copy": "Kopieren",
+			"save": "Bild speichern",
+			"viewCode": "Code anzeigen",
+			"viewDiagram": "Diagramm anzeigen",
+			"close": "Schließen"
+		},
+		"modal": {
+			"codeTitle": "Mermaid-Code"
+		},
+		"tabs": {
+			"diagram": "Diagramm",
+			"code": "Code"
+		},
+		"feedback": {
+			"imageCopied": "Bild in die Zwischenablage kopiert",
+			"copyError": "Fehler beim Kopieren des Bildes"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Ungültiges Daten-URI-Format",
+			"copyingImage": "Fehler beim Kopieren des Bildes: {{error}}",
+			"openingImage": "Fehler beim Öffnen des Bildes: {{error}}",
+			"pathNotExists": "Pfad existiert nicht: {{path}}",
+			"couldNotOpen": "Datei konnte nicht geöffnet werden: {{error}}",
+			"couldNotOpenGeneric": "Datei konnte nicht geöffnet werden!"
+		},
+		"success": {
+			"imageDataUriCopied": "Bild-Daten-URI in die Zwischenablage kopiert"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/en/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Generating mermaid diagram...",
-		"render_error": "Unable to Render Diagram"
+		"render_error": "Unable to Render Diagram",
+		"buttons": {
+			"zoom": "Zoom",
+			"zoomIn": "Zoom In",
+			"zoomOut": "Zoom Out",
+			"copy": "Copy",
+			"save": "Save Image",
+			"viewCode": "View Code",
+			"viewDiagram": "View Diagram",
+			"close": "Close"
+		},
+		"modal": {
+			"codeTitle": "Mermaid Code"
+		},
+		"tabs": {
+			"diagram": "Diagram",
+			"code": "Code"
+		},
+		"feedback": {
+			"imageCopied": "Image copied to clipboard",
+			"copyError": "Error copying image"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Invalid data URI format",
+			"copyingImage": "Error copying image: {{error}}",
+			"openingImage": "Error opening image: {{error}}",
+			"pathNotExists": "Path does not exist: {{path}}",
+			"couldNotOpen": "Could not open file: {{error}}",
+			"couldNotOpenGeneric": "Could not open file!"
+		},
+		"success": {
+			"imageDataUriCopied": "Image data URI copied to clipboard"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/es/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Generando diagrama mermaid...",
-		"render_error": "No se puede renderizar el diagrama"
+		"render_error": "No se puede renderizar el diagrama",
+		"buttons": {
+			"zoom": "Zoom",
+			"zoomIn": "Ampliar",
+			"zoomOut": "Reducir",
+			"copy": "Copiar",
+			"save": "Guardar imagen",
+			"viewCode": "Ver código",
+			"viewDiagram": "Ver diagrama",
+			"close": "Cerrar"
+		},
+		"modal": {
+			"codeTitle": "Código Mermaid"
+		},
+		"tabs": {
+			"diagram": "Diagrama",
+			"code": "Código"
+		},
+		"feedback": {
+			"imageCopied": "Imagen copiada al portapapeles",
+			"copyError": "Error copiando la imagen"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Formato de URI de datos inválido",
+			"copyingImage": "Error copiando la imagen: {{error}}",
+			"openingImage": "Error abriendo la imagen: {{error}}",
+			"pathNotExists": "La ruta no existe: {{path}}",
+			"couldNotOpen": "No se pudo abrir el archivo: {{error}}",
+			"couldNotOpenGeneric": "¡No se pudo abrir el archivo!"
+		},
+		"success": {
+			"imageDataUriCopied": "URI de datos de imagen copiada al portapapeles"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/fr/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Génération du diagramme mermaid...",
-		"render_error": "Impossible de rendre le diagramme"
+		"render_error": "Impossible de rendre le diagramme",
+		"buttons": {
+			"zoom": "Zoom",
+			"zoomIn": "Agrandir",
+			"zoomOut": "Réduire",
+			"copy": "Copier",
+			"save": "Enregistrer l'image",
+			"viewCode": "Voir le code",
+			"viewDiagram": "Voir le diagramme",
+			"close": "Fermer"
+		},
+		"modal": {
+			"codeTitle": "Code Mermaid"
+		},
+		"tabs": {
+			"diagram": "Diagramme",
+			"code": "Code"
+		},
+		"feedback": {
+			"imageCopied": "Image copiée dans le presse-papiers",
+			"copyError": "Erreur lors de la copie de l'image"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Format d'URI de données invalide",
+			"copyingImage": "Erreur lors de la copie de l'image : {{error}}",
+			"openingImage": "Erreur lors de l'ouverture de l'image : {{error}}",
+			"pathNotExists": "Le chemin n'existe pas : {{path}}",
+			"couldNotOpen": "Impossible d'ouvrir le fichier : {{error}}",
+			"couldNotOpenGeneric": "Impossible d'ouvrir le fichier !"
+		},
+		"success": {
+			"imageDataUriCopied": "URI de données d'image copiée dans le presse-papiers"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/hi/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "मरमेड डायग्राम जनरेट हो रहा है...",
-		"render_error": "डायग्राम रेंडर नहीं किया जा सकता"
+		"render_error": "डायग्राम रेंडर नहीं किया जा सकता",
+		"buttons": {
+			"zoom": "ज़ूम",
+			"zoomIn": "बड़ा करें",
+			"zoomOut": "छोटा करें",
+			"copy": "कॉपी करें",
+			"save": "छवि सहेजें",
+			"viewCode": "कोड देखें",
+			"viewDiagram": "डायग्राम देखें",
+			"close": "बंद करें"
+		},
+		"modal": {
+			"codeTitle": "मरमेड कोड"
+		},
+		"tabs": {
+			"diagram": "डायग्राम",
+			"code": "कोड"
+		},
+		"feedback": {
+			"imageCopied": "इमेज क्लिपबोर्ड में कॉपी हो गई",
+			"copyError": "इमेज कॉपी करने में त्रुटि"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "अमान्य डेटा URI फॉर्मेट",
+			"copyingImage": "इमेज कॉपी करने में त्रुटि: {{error}}",
+			"openingImage": "इमेज खोलने में त्रुटि: {{error}}",
+			"pathNotExists": "पथ मौजूद नहीं है: {{path}}",
+			"couldNotOpen": "फ़ाइल नहीं खोली जा सकी: {{error}}",
+			"couldNotOpenGeneric": "फ़ाइल नहीं खोली जा सकी!"
+		},
+		"success": {
+			"imageDataUriCopied": "इमेज डेटा URI क्लिपबोर्ड में कॉपी हो गया"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/it/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Generazione del diagramma mermaid...",
-		"render_error": "Impossibile renderizzare il diagramma"
+		"render_error": "Impossibile renderizzare il diagramma",
+		"buttons": {
+			"zoom": "Zoom",
+			"zoomIn": "Ingrandisci",
+			"zoomOut": "Riduci",
+			"copy": "Copia",
+			"save": "Salva immagine",
+			"viewCode": "Visualizza codice",
+			"viewDiagram": "Visualizza diagramma",
+			"close": "Chiudi"
+		},
+		"modal": {
+			"codeTitle": "Codice Mermaid"
+		},
+		"tabs": {
+			"diagram": "Diagramma",
+			"code": "Codice"
+		},
+		"feedback": {
+			"imageCopied": "Immagine copiata negli appunti",
+			"copyError": "Errore nella copia dell'immagine"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Formato URI dati non valido",
+			"copyingImage": "Errore nella copia dell'immagine: {{error}}",
+			"openingImage": "Errore nell'apertura dell'immagine: {{error}}",
+			"pathNotExists": "Il percorso non esiste: {{path}}",
+			"couldNotOpen": "Impossibile aprire il file: {{error}}",
+			"couldNotOpenGeneric": "Impossibile aprire il file!"
+		},
+		"success": {
+			"imageDataUriCopied": "URI dati immagine copiato negli appunti"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/ja/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Mermaidダイアグラムを生成中...",
-		"render_error": "ダイアグラムをレンダリングできません"
+		"render_error": "ダイアグラムをレンダリングできません",
+		"buttons": {
+			"zoom": "ズーム",
+			"zoomIn": "拡大",
+			"zoomOut": "縮小",
+			"copy": "コピー",
+			"save": "画像を保存",
+			"viewCode": "コードを表示",
+			"viewDiagram": "ダイアグラムを表示",
+			"close": "閉じる"
+		},
+		"modal": {
+			"codeTitle": "Mermaidコード"
+		},
+		"tabs": {
+			"diagram": "ダイアグラム",
+			"code": "コード"
+		},
+		"feedback": {
+			"imageCopied": "画像をクリップボードにコピーしました",
+			"copyError": "画像のコピーエラー"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "無効なデータURI形式",
+			"copyingImage": "画像のコピーエラー: {{error}}",
+			"openingImage": "画像を開く際のエラー: {{error}}",
+			"pathNotExists": "パスが存在しません: {{path}}",
+			"couldNotOpen": "ファイルを開けませんでした: {{error}}",
+			"couldNotOpenGeneric": "ファイルを開けませんでした!"
+		},
+		"success": {
+			"imageDataUriCopied": "画像データURIをクリップボードにコピーしました"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/ko/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "머메이드 다이어그램 생성 중...",
-		"render_error": "다이어그램을 렌더링할 수 없음"
+		"render_error": "다이어그램을 렌더링할 수 없음",
+		"buttons": {
+			"zoom": "줌",
+			"zoomIn": "확대",
+			"zoomOut": "축소",
+			"copy": "복사",
+			"save": "이미지 저장",
+			"viewCode": "코드 보기",
+			"viewDiagram": "다이어그램 보기",
+			"close": "닫기"
+		},
+		"modal": {
+			"codeTitle": "머메이드 코드"
+		},
+		"tabs": {
+			"diagram": "다이어그램",
+			"code": "코드"
+		},
+		"feedback": {
+			"imageCopied": "이미지가 클립보드에 복사됨",
+			"copyError": "이미지 복사 오류"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "잘못된 데이터 URI 형식",
+			"copyingImage": "이미지 복사 오류: {{error}}",
+			"openingImage": "이미지 열기 오류: {{error}}",
+			"pathNotExists": "경로가 존재하지 않음: {{path}}",
+			"couldNotOpen": "파일을 열 수 없음: {{error}}",
+			"couldNotOpenGeneric": "파일을 열 수 없습니다!"
+		},
+		"success": {
+			"imageDataUriCopied": "이미지 데이터 URI가 클립보드에 복사됨"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/nl/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Mermaid-diagram genereren...",
-		"render_error": "Kan diagram niet weergeven"
+		"render_error": "Kan diagram niet weergeven",
+		"buttons": {
+			"zoom": "Zoom",
+			"zoomIn": "Inzoomen",
+			"zoomOut": "Uitzoomen",
+			"copy": "Kopiëren",
+			"save": "Afbeelding opslaan",
+			"viewCode": "Code bekijken",
+			"viewDiagram": "Diagram bekijken",
+			"close": "Sluiten"
+		},
+		"modal": {
+			"codeTitle": "Mermaid-code"
+		},
+		"tabs": {
+			"diagram": "Diagram",
+			"code": "Code"
+		},
+		"feedback": {
+			"imageCopied": "Afbeelding gekopieerd naar klembord",
+			"copyError": "Fout bij kopiëren van afbeelding"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Ongeldig data-URI-formaat",
+			"copyingImage": "Fout bij kopiëren van afbeelding: {{error}}",
+			"openingImage": "Fout bij openen van afbeelding: {{error}}",
+			"pathNotExists": "Pad bestaat niet: {{path}}",
+			"couldNotOpen": "Kon bestand niet openen: {{error}}",
+			"couldNotOpenGeneric": "Kon bestand niet openen!"
+		},
+		"success": {
+			"imageDataUriCopied": "Afbeelding data-URI gekopieerd naar klembord"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/pl/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Generowanie diagramu mermaid...",
-		"render_error": "Nie można renderować diagramu"
+		"render_error": "Nie można renderować diagramu",
+		"buttons": {
+			"zoom": "Powiększenie",
+			"zoomIn": "Powiększ",
+			"zoomOut": "Pomniejsz",
+			"copy": "Kopiuj",
+			"save": "Zapisz obraz",
+			"viewCode": "Zobacz kod",
+			"viewDiagram": "Zobacz diagram",
+			"close": "Zamknij"
+		},
+		"modal": {
+			"codeTitle": "Kod Mermaid"
+		},
+		"tabs": {
+			"diagram": "Diagram",
+			"code": "Kod"
+		},
+		"feedback": {
+			"imageCopied": "Obraz skopiowany do schowka",
+			"copyError": "Błąd kopiowania obrazu"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Nieprawidłowy format URI danych",
+			"copyingImage": "Błąd kopiowania obrazu: {{error}}",
+			"openingImage": "Błąd otwierania obrazu: {{error}}",
+			"pathNotExists": "Ścieżka nie istnieje: {{path}}",
+			"couldNotOpen": "Nie można otworzyć pliku: {{error}}",
+			"couldNotOpenGeneric": "Nie można otworzyć pliku!"
+		},
+		"success": {
+			"imageDataUriCopied": "URI danych obrazu skopiowane do schowka"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/pt-BR/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Gerando diagrama mermaid...",
-		"render_error": "Não foi possível renderizar o diagrama"
+		"render_error": "Não foi possível renderizar o diagrama",
+		"buttons": {
+			"zoom": "Zoom",
+			"zoomIn": "Ampliar",
+			"zoomOut": "Reduzir",
+			"copy": "Copiar",
+			"save": "Salvar imagem",
+			"viewCode": "Ver código",
+			"viewDiagram": "Ver diagrama",
+			"close": "Fechar"
+		},
+		"modal": {
+			"codeTitle": "Código Mermaid"
+		},
+		"tabs": {
+			"diagram": "Diagrama",
+			"code": "Código"
+		},
+		"feedback": {
+			"imageCopied": "Imagem copiada para a área de transferência",
+			"copyError": "Erro ao copiar imagem"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Formato de URI de dados inválido",
+			"copyingImage": "Erro ao copiar imagem: {{error}}",
+			"openingImage": "Erro ao abrir imagem: {{error}}",
+			"pathNotExists": "Caminho não existe: {{path}}",
+			"couldNotOpen": "Não foi possível abrir o arquivo: {{error}}",
+			"couldNotOpenGeneric": "Não foi possível abrir o arquivo!"
+		},
+		"success": {
+			"imageDataUriCopied": "URI de dados da imagem copiada para a área de transferência"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/ru/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Создание диаграммы mermaid...",
-		"render_error": "Не удалось отобразить диаграмму"
+		"render_error": "Не удалось отобразить диаграмму",
+		"buttons": {
+			"zoom": "Масштаб",
+			"zoomIn": "Увеличить",
+			"zoomOut": "Уменьшить",
+			"copy": "Копировать",
+			"save": "Сохранить изображение",
+			"viewCode": "Посмотреть код",
+			"viewDiagram": "Посмотреть диаграмму",
+			"close": "Закрыть"
+		},
+		"modal": {
+			"codeTitle": "Код Mermaid"
+		},
+		"tabs": {
+			"diagram": "Диаграмма",
+			"code": "Код"
+		},
+		"feedback": {
+			"imageCopied": "Изображение скопировано в буфер обмена",
+			"copyError": "Ошибка копирования изображения"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Неверный формат URI данных",
+			"copyingImage": "Ошибка копирования изображения: {{error}}",
+			"openingImage": "Ошибка открытия изображения: {{error}}",
+			"pathNotExists": "Путь не существует: {{path}}",
+			"couldNotOpen": "Не удалось открыть файл: {{error}}",
+			"couldNotOpenGeneric": "Не удалось открыть файл!"
+		},
+		"success": {
+			"imageDataUriCopied": "URI данных изображения скопирован в буфер обмена"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/tr/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Mermaid diyagramı oluşturuluyor...",
-		"render_error": "Diyagram render edilemiyor"
+		"render_error": "Diyagram render edilemiyor",
+		"buttons": {
+			"zoom": "Yakınlaştır",
+			"zoomIn": "Büyüt",
+			"zoomOut": "Küçült",
+			"copy": "Kopyala",
+			"save": "Resmi kaydet",
+			"viewCode": "Kodu görüntüle",
+			"viewDiagram": "Diyagramı görüntüle",
+			"close": "Kapat"
+		},
+		"modal": {
+			"codeTitle": "Mermaid Kodu"
+		},
+		"tabs": {
+			"diagram": "Diyagram",
+			"code": "Kod"
+		},
+		"feedback": {
+			"imageCopied": "Görsel panoya kopyalandı",
+			"copyError": "Görsel kopyalama hatası"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Geçersiz veri URI formatı",
+			"copyingImage": "Görsel kopyalama hatası: {{error}}",
+			"openingImage": "Görsel açma hatası: {{error}}",
+			"pathNotExists": "Yol mevcut değil: {{path}}",
+			"couldNotOpen": "Dosya açılamadı: {{error}}",
+			"couldNotOpenGeneric": "Dosya açılamadı!"
+		},
+		"success": {
+			"imageDataUriCopied": "Görsel veri URI'si panoya kopyalandı"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/vi/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "Đang tạo biểu đồ mermaid...",
-		"render_error": "Không thể hiển thị biểu đồ"
+		"render_error": "Không thể hiển thị biểu đồ",
+		"buttons": {
+			"zoom": "Thu phóng",
+			"zoomIn": "Phóng to",
+			"zoomOut": "Thu nhỏ",
+			"copy": "Sao chép",
+			"save": "Lưu hình ảnh",
+			"viewCode": "Xem mã",
+			"viewDiagram": "Xem biểu đồ",
+			"close": "Đóng"
+		},
+		"modal": {
+			"codeTitle": "Mã Mermaid"
+		},
+		"tabs": {
+			"diagram": "Biểu đồ",
+			"code": "Mã"
+		},
+		"feedback": {
+			"imageCopied": "Hình ảnh đã được sao chép vào clipboard",
+			"copyError": "Lỗi sao chép hình ảnh"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "Định dạng URI dữ liệu không hợp lệ",
+			"copyingImage": "Lỗi sao chép hình ảnh: {{error}}",
+			"openingImage": "Lỗi mở hình ảnh: {{error}}",
+			"pathNotExists": "Đường dẫn không tồn tại: {{path}}",
+			"couldNotOpen": "Không thể mở tệp: {{error}}",
+			"couldNotOpenGeneric": "Không thể mở tệp!"
+		},
+		"success": {
+			"imageDataUriCopied": "URI dữ liệu hình ảnh đã được sao chép vào clipboard"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/zh-CN/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "生成 Mermaid 图表中...",
-		"render_error": "无法渲染图表"
+		"render_error": "无法渲染图表",
+		"buttons": {
+			"zoom": "缩放",
+			"zoomIn": "放大",
+			"zoomOut": "缩小",
+			"copy": "复制",
+			"save": "保存图片",
+			"viewCode": "查看代码",
+			"viewDiagram": "查看图表",
+			"close": "关闭"
+		},
+		"modal": {
+			"codeTitle": "Mermaid 代码"
+		},
+		"tabs": {
+			"diagram": "图表",
+			"code": "代码"
+		},
+		"feedback": {
+			"imageCopied": "图片已复制到剪贴板",
+			"copyError": "复制图片时出错"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "无效的数据 URI 格式",
+			"copyingImage": "复制图片时出错: {{error}}",
+			"openingImage": "打开图片时出错: {{error}}",
+			"pathNotExists": "路径不存在: {{path}}",
+			"couldNotOpen": "无法打开文件: {{error}}",
+			"couldNotOpenGeneric": "无法打开文件!"
+		},
+		"success": {
+			"imageDataUriCopied": "图片数据 URI 已复制到剪贴板"
+		}
 	}
 }

+ 35 - 1
webview-ui/src/i18n/locales/zh-TW/common.json

@@ -16,6 +16,40 @@
 	},
 	"mermaid": {
 		"loading": "產生 Mermaid 圖表中...",
-		"render_error": "無法渲染圖表"
+		"render_error": "無法渲染圖表",
+		"buttons": {
+			"zoom": "縮放",
+			"zoomIn": "放大",
+			"zoomOut": "縮小",
+			"copy": "複製",
+			"save": "儲存圖片",
+			"viewCode": "檢視程式碼",
+			"viewDiagram": "檢視圖表",
+			"close": "關閉"
+		},
+		"modal": {
+			"codeTitle": "Mermaid 程式碼"
+		},
+		"tabs": {
+			"diagram": "圖表",
+			"code": "程式碼"
+		},
+		"feedback": {
+			"imageCopied": "圖片已複製到剪貼簿",
+			"copyError": "複製圖片時發生錯誤"
+		}
+	},
+	"file": {
+		"errors": {
+			"invalidDataUri": "無效的資料 URI 格式",
+			"copyingImage": "複製圖片時發生錯誤: {{error}}",
+			"openingImage": "開啟圖片時發生錯誤: {{error}}",
+			"pathNotExists": "路徑不存在: {{path}}",
+			"couldNotOpen": "無法開啟檔案: {{error}}",
+			"couldNotOpenGeneric": "無法開啟檔案!"
+		},
+		"success": {
+			"imageDataUriCopied": "圖片資料 URI 已複製到剪貼簿"
+		}
 	}
 }