Skip to content

Commit 28b3722

Browse files
luoxuanzaoQoder-AI
andcommitted
feat(chat): copy generated images out of the conversation
Images an agent embeds in a reply render as plain <img> elements, so they never got Obsidian's own image menu and there was no way to get the picture out of the chat. Right-clicking one now offers "Copy image", which writes the bitmap to the system clipboard so it can be pasted into any application or another note. PNG/JPEG go through Electron's nativeImage + clipboard.writeImage, matching Obsidian's Live Preview behavior; other formats fall back to the async Clipboard API. The embed's HTML now carries its vault path so the menu can resolve the file, and the copy reports success or failure in a notice. Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
1 parent 484badf commit 28b3722

19 files changed

Lines changed: 384 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ version with its date and start a fresh empty `[Unreleased]` above it.
3535
`Model is queued (3 ahead · ~12s wait)...`, and returns to the usual
3636
wording as soon as the request leaves the queue.
3737

38+
- Images an agent embeds in a reply now offer **Copy image** in their
39+
right-click menu, which puts the bitmap on the system clipboard ready to
40+
paste into any application or another note.
41+
3842
## [1.0.14] - 2026-09-20
3943

4044
### Changed

‎src/features/chat/rendering/message-renderer.ts‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import {
2424
} from '../../../shared/markdown/markdown-math';
2525
import { replaceMentionTokensWithHtml } from '../../../shared/markdown/mention-chip';
2626
import type { ReferenceChipKind } from '../../../shared/mention/types';
27-
import { openReferenceChip } from '../../../shared/obsidian/compat';
27+
import { getVaultFileByPath, openReferenceChip } from '../../../shared/obsidian/compat';
28+
import { copyVaultImageToClipboard } from '../../../shared/obsidian/image-clipboard';
2829
import { TurnChangesModal } from '../changes/turn-changes-modal';
2930
import { collectTurnChanges } from '../changes/turn-file-changes';
3031
import { turnFileDisplayPath } from '../changes/turn-file-path';
@@ -844,6 +845,10 @@ export class MessageRenderer {
844845
this.enhanceMentionChips(el);
845846
}
846847

848+
if (processedMarkdown.includes('qoderian-embedded-image')) {
849+
this.enhanceEmbeddedImages(el);
850+
}
851+
847852
// Wrap pre elements and move buttons outside scroll area
848853
el.querySelectorAll('pre').forEach((pre) => {
849854
// Skip if already wrapped
@@ -921,6 +926,39 @@ export class MessageRenderer {
921926
});
922927
}
923928

929+
/**
930+
* Binds a copy action onto rendered vault images. Embeds render as plain
931+
* <img> elements, so Obsidian's own image context menu never applies here.
932+
*/
933+
private enhanceEmbeddedImages(el: HTMLElement): void {
934+
el.querySelectorAll<HTMLElement>('.qoderian-embedded-image').forEach((imageEl) => {
935+
const path = imageEl.dataset.qoderianImagePath;
936+
if (!path) return;
937+
938+
imageEl.addEventListener('contextmenu', (event) => {
939+
event.preventDefault();
940+
this.showImageCopyMenu(event, path);
941+
});
942+
});
943+
}
944+
945+
private showImageCopyMenu(event: MouseEvent, path: string): void {
946+
const menu = new Menu();
947+
menu.addItem((item) => {
948+
item
949+
.setTitle(t('chat.imageEmbed.copyImage'))
950+
.setIcon('copy')
951+
.onClick(() => {
952+
runRendererAction(async () => {
953+
const file = getVaultFileByPath(this.app, path);
954+
const copied = file ? await copyVaultImageToClipboard(this.app, file) : false;
955+
new Notice(copied ? t('chat.imageEmbed.copied') : t('chat.imageEmbed.copyFailed'));
956+
});
957+
});
958+
});
959+
menu.showAtMouseEvent(event);
960+
}
961+
924962
// ============================================
925963
// Copy Button
926964
// ============================================

‎src/i18n/locales/de.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
"closeSession": "Sitzung schließen"
6969
},
7070
"chat": {
71+
"imageEmbed": {
72+
"copyImage": "Bild kopieren",
73+
"copied": "Bild in die Zwischenablage kopiert",
74+
"copyFailed": "Bild konnte nicht kopiert werden"
75+
},
7176
"rewind": {
7277
"confirmMessage": "Zu diesem Punkt zurückspulen? Dateiänderungen nach dieser Nachricht werden rückgängig gemacht. Das Zurückspulen betrifft keine manuell oder über Bash bearbeiteten Dateien.",
7378
"confirmMessageConversationOnly": "Konversation zu diesem Punkt zurückspulen? Dateiänderungen bleiben erhalten.",

‎src/i18n/locales/en.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
"closeSession": "Close session"
6969
},
7070
"chat": {
71+
"imageEmbed": {
72+
"copyImage": "Copy image",
73+
"copied": "Image copied to clipboard",
74+
"copyFailed": "Failed to copy image"
75+
},
7176
"rewind": {
7277
"confirmMessage": "Rewind to this point? File changes after this message will be reverted. Rewinding does not affect files edited manually or via bash.",
7378
"confirmMessageConversationOnly": "Rewind conversation to this point? File changes will be kept.",

‎src/i18n/locales/es.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
"closeSession": "Cerrar sesión"
6969
},
7070
"chat": {
71+
"imageEmbed": {
72+
"copyImage": "Copiar imagen",
73+
"copied": "Imagen copiada al portapapeles",
74+
"copyFailed": "No se pudo copiar la imagen"
75+
},
7176
"rewind": {
7277
"confirmMessage": "¿Rebobinar a este punto? Los cambios de archivos después de este mensaje serán revertidos. El rebobinado no afecta archivos editados manualmente o mediante bash.",
7378
"confirmMessageConversationOnly": "¿Rebobinar la conversación hasta este punto? Se conservarán los cambios de archivos.",

‎src/i18n/locales/fr.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
"closeSession": "Fermer la session"
6969
},
7070
"chat": {
71+
"imageEmbed": {
72+
"copyImage": "Copier l'image",
73+
"copied": "Image copiée dans le presse-papiers",
74+
"copyFailed": "Échec de la copie de l'image"
75+
},
7176
"rewind": {
7277
"confirmMessage": "Rembobiner jusqu'à ce point ? Les modifications de fichiers après ce message seront annulées. Le rembobinage n'affecte pas les fichiers modifiés manuellement ou via bash.",
7378
"confirmMessageConversationOnly": "Rembobiner la conversation jusqu'à ce point ? Les modifications de fichiers seront conservées.",

‎src/i18n/locales/ja.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
"closeSession": "セッションを閉じる"
6969
},
7070
"chat": {
71+
"imageEmbed": {
72+
"copyImage": "画像をコピー",
73+
"copied": "画像をクリップボードにコピーしました",
74+
"copyFailed": "画像のコピーに失敗しました"
75+
},
7176
"rewind": {
7277
"confirmMessage": "この時点に巻き戻しますか?このメッセージ以降のファイル変更が元に戻されます。手動またはbashで編集されたファイルには影響しません。",
7378
"confirmMessageConversationOnly": "会話をこの時点に巻き戻しますか?ファイル変更は保持されます。",

‎src/i18n/locales/ko.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
"closeSession": "세션 닫기"
6969
},
7070
"chat": {
71+
"imageEmbed": {
72+
"copyImage": "이미지 복사",
73+
"copied": "이미지를 클립보드에 복사했습니다",
74+
"copyFailed": "이미지 복사 실패"
75+
},
7176
"rewind": {
7277
"confirmMessage": "이 시점으로 되감으시겠습니까? 이 메시지 이후의 파일 변경 사항이 되돌려집니다. 수동으로 또는 bash를 통해 편집된 파일에는 영향을 미치지 않습니다.",
7378
"confirmMessageConversationOnly": "대화를 이 시점으로 되감으시겠습니까? 파일 변경 사항은 유지됩니다.",

‎src/i18n/locales/pt.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
"closeSession": "Fechar sessão"
6969
},
7070
"chat": {
71+
"imageEmbed": {
72+
"copyImage": "Copiar imagem",
73+
"copied": "Imagem copiada para a área de transferência",
74+
"copyFailed": "Falha ao copiar a imagem"
75+
},
7176
"rewind": {
7277
"confirmMessage": "Retroceder até este ponto? As alterações de arquivos após esta mensagem serão revertidas. O retrocesso não afeta arquivos editados manualmente ou via bash.",
7378
"confirmMessageConversationOnly": "Retroceder a conversa até este ponto? As alterações de arquivos serão mantidas.",

‎src/i18n/locales/ru.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
"closeSession": "Закрыть сессию"
6969
},
7070
"chat": {
71+
"imageEmbed": {
72+
"copyImage": "Копировать изображение",
73+
"copied": "Изображение скопировано в буфер обмена",
74+
"copyFailed": "Не удалось скопировать изображение"
75+
},
7176
"rewind": {
7277
"confirmMessage": "Откатить до этой точки? Изменения файлов после этого сообщения будут отменены. Откат не затрагивает файлы, отредактированные вручную или через bash.",
7378
"confirmMessageConversationOnly": "Откатить разговор до этой точки? Изменения файлов будут сохранены.",

0 commit comments

Comments
 (0)