diff --git a/src/renderer/src/components/native-chat/NativeChatComposerActions.test.tsx b/src/renderer/src/components/native-chat/NativeChatComposerActions.test.tsx index ae315b4530a..ecffdbe9f94 100644 --- a/src/renderer/src/components/native-chat/NativeChatComposerActions.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatComposerActions.test.tsx @@ -121,7 +121,7 @@ describe('NativeChatComposerActions', () => { attachDisabled={false} dictationDisabled={false} sendDisabled - sendBlockedReason="Attach the image again or remove it" + sendBlockedReason="An image couldn't be brought back. Remove it to send." isWorking={false} isDictating={false} isDictationHoldMode={false} @@ -135,7 +135,9 @@ describe('NativeChatComposerActions', () => { /> ) - const send = screen.getByRole('button', { name: 'Attach the image again or remove it' }) + const send = screen.getByRole('button', { + name: "An image couldn't be brought back. Remove it to send." + }) expect(send.hasAttribute('disabled')).toBe(true) }) }) diff --git a/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx b/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx index e2ac63dda29..879c71bc991 100644 --- a/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx @@ -54,7 +54,7 @@ describe('NativeChatImageAttachmentPreview', () => { expect(mocks.useLocalImageSrc).toHaveBeenCalledWith(undefined, '', undefined) }) - it('shows an image the draft could not keep as one to attach again, and lets it be removed', () => { + it('shows a pasted image that could not come back by name, says it was not kept, and lets it be removed', () => { mocks.useLocalImageSrc.mockReturnValue(undefined) const onRemove = vi.fn() vi.stubGlobal('IntersectionObserver', undefined) @@ -67,10 +67,31 @@ describe('NativeChatImageAttachmentPreview', () => { expect( screen.getByRole('img', { - name: "Pasted image wasn't saved with this draft. Attach it again to send it." + name: "This pasted image couldn't be brought back with this draft. Remove it, and paste it again if you still need it." }) ).toBeTruthy() + expect(screen.getByText('Pasted image')).toBeTruthy() + expect(screen.getByText('Not kept')).toBeTruthy() screen.getByRole('button', { name: 'Remove attachment' }).click() expect(onRemove).toHaveBeenCalledWith('m1') }) + + it('shows a file that could not come back by name, with a visible attach-again hint', () => { + mocks.useLocalImageSrc.mockReturnValue(undefined) + vi.stubGlobal('IntersectionObserver', undefined) + render( + + ) + + expect( + screen.getByRole('img', { + name: "diagram.png couldn't be brought back with this draft. Attach it again or remove it." + }) + ).toBeTruthy() + expect(screen.getByText('diagram.png')).toBeTruthy() + expect(screen.getByText('Attach again')).toBeTruthy() + }) }) diff --git a/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx b/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx index f27687af0c3..f1ce3d2c730 100644 --- a/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx +++ b/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.tsx @@ -51,7 +51,8 @@ function RemoveAttachmentButton({ onRemove }: { onRemove: () => void }): React.J ) } -/** An image the draft names but couldn't keep: it waits to be attached again, or removed. */ +/** An image the draft names but couldn't bring back. A file can be attached again in its place; a + * pasted image can't be matched by a new paste, so its copy asks only for removal. */ function NativeChatUnavailableImageChip({ id, name, @@ -61,20 +62,34 @@ function NativeChatUnavailableImageChip({ name: string onRemove: (id: string) => void }): React.JSX.Element { - const explanation = translate( - 'components.native-chat.composer.imageNotSavedWithDraft', - "{{name}} wasn't saved with this draft. Attach it again to send it.", - { name: attachmentLabel(name) } - ) + const pasted = isNativeChatPastedImagePath(name) + const label = attachmentLabel(name) + const explanation = pasted + ? translate( + 'components.native-chat.composer.pastedImageNotBroughtBack', + "This pasted image couldn't be brought back with this draft. Remove it, and paste it again if you still need it." + ) + : translate( + 'components.native-chat.composer.imageNotBroughtBack', + "{{name}} couldn't be brought back with this draft. Attach it again or remove it.", + { name: label } + ) + const hint = pasted + ? translate('components.native-chat.composer.pastedImageNotKeptLabel', 'Not kept') + : translate('components.native-chat.composer.imageAttachAgainLabel', 'Attach again') return ( -
+
- + +
+ {label} + {hint} +
onRemove(id)} />
diff --git a/src/renderer/src/components/native-chat/native-chat-composer-draft-store.test.ts b/src/renderer/src/components/native-chat/native-chat-composer-draft-store.test.ts index 6bf65ee8ab2..f8a7ec1f830 100644 --- a/src/renderer/src/components/native-chat/native-chat-composer-draft-store.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-composer-draft-store.test.ts @@ -203,15 +203,37 @@ describe('native-chat composer draft store', () => { ) const reloaded = await reload() - reloaded.attachments.appendNativeChatAttachmentCache('tab-1:pane', [ - { id: 'again', path: '/Users/me/Desktop/shot.png' } - ]) + reloaded.attachments.appendNativeChatAttachmentCache( + 'tab-1:pane', + [{ id: 'again', path: '/Users/me/Desktop/shot.png' }], + { fromUser: true } + ) expect(reloaded.attachments.readNativeChatAttachmentCache('tab-1:pane')).toEqual([ { id: 'again', path: '/Users/me/Desktop/shot.png' }, IMAGES[0] ]) }) + it('adds an image Stop gives back next to a placeholder with its name, never in its place', async () => { + storage.setItem( + `${DRAFT_KEY_PREFIX}${encodeURIComponent('tab-1:pane')}`, + JSON.stringify({ + text: 'compare with this', + images: [{ id: 'm', path: '', unavailableName: 'image.png' }], + savedAt: 1 + }) + ) + const reloaded = await reload() + + reloaded.attachments.appendNativeChatAttachmentCache('tab-1:pane', [ + { id: 'withdrawn-cm-1-1', path: '/Users/me/Downloads/image.png' } + ]) + expect(reloaded.attachments.readNativeChatAttachmentCache('tab-1:pane')).toEqual([ + { id: 'm', path: '', unavailableName: 'image.png' }, + { id: 'withdrawn-cm-1-1', path: '/Users/me/Downloads/image.png' } + ]) + }) + it('appends a given-back message after a draft restored from a reload', async () => { modules.drafts.writeNativeChatDraftCache('tab-1:pane', 'mine') modules.store.flushNativeChatComposerDrafts() diff --git a/src/renderer/src/components/native-chat/native-chat-image-reattach.ts b/src/renderer/src/components/native-chat/native-chat-image-reattach.ts index 3591eb3487e..29d35cf90e9 100644 --- a/src/renderer/src/components/native-chat/native-chat-image-reattach.ts +++ b/src/renderer/src/components/native-chat/native-chat-image-reattach.ts @@ -1,7 +1,8 @@ import { translate } from '@/i18n/i18n' import type { NativeChatComposerImageAttachment } from './NativeChatComposerField' -/** Why the composer can't send while it holds images Orca couldn't keep; null when it holds none. */ +/** Why the composer can't send while it holds images that couldn't be brought back; null when it + * holds none. Removing always works; re-attaching only for a file, so the reason names removing. */ export function nativeChatAttachImagesAgainReason( attachments: readonly NativeChatComposerImageAttachment[] ): string | null { @@ -11,12 +12,12 @@ export function nativeChatAttachImagesAgainReason( } return count === 1 ? translate( - 'components.native-chat.composer.attachImageAgain', - 'Attach the image again or remove it' + 'components.native-chat.composer.removeImageToSend', + "An image couldn't be brought back. Remove it to send." ) : translate( - 'components.native-chat.composer.attachImagesAgain', - 'Attach the images again or remove them' + 'components.native-chat.composer.removeImagesToSend', + "Some images couldn't be brought back. Remove them to send." ) } diff --git a/src/renderer/src/components/native-chat/use-native-chat-composer-attachments.ts b/src/renderer/src/components/native-chat/use-native-chat-composer-attachments.ts index 21b37ddbb7c..f2049ba88a2 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-composer-attachments.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-composer-attachments.ts @@ -157,7 +157,8 @@ export function useNativeChatComposerAttachments({ id: nextAttachmentId(), path, ...(connectionId ? { connectionId } : {}) - })) + })), + { fromUser: true } ) }, [attachmentScopeKey, nextAttachmentId] @@ -233,9 +234,11 @@ export function useNativeChatComposerAttachments({ const resolvePendingImageAttachment = useCallback( (id: string, path: string, connectionId?: string | null) => { if (forgetLocalAttachment(id, true)) { - appendNativeChatAttachmentCache(attachmentScopeKey, [ - { id, path, ...(connectionId ? { connectionId } : {}) } - ]) + appendNativeChatAttachmentCache( + attachmentScopeKey, + [{ id, path, ...(connectionId ? { connectionId } : {}) }], + { fromUser: true } + ) } }, [attachmentScopeKey, forgetLocalAttachment] @@ -298,12 +301,13 @@ export function readNativeChatAttachmentCache( return readNativeChatComposerDraft(scopeKey).images.map((image) => ({ ...image })) } -/** Adds settled images after the ones the draft holds now; one with the name of an image to - * attach again takes its place. Saved at once: when Stop gives images back, the copy they came - * from goes right after this. */ +/** Adds settled images after the ones the draft holds now. Saved at once: when Stop gives images + * back, the copy they came from goes right after this. Only an image the user attaches + * (`fromUser`) takes the place of a placeholder with its file name, as a re-pick does. */ export function appendNativeChatAttachmentCache( scopeKey: string, - appended: readonly NativeChatComposerImageAttachment[] + appended: readonly NativeChatComposerImageAttachment[], + options?: { fromUser?: boolean } ): void { if (appended.length === 0) { return @@ -312,11 +316,13 @@ export function appendNativeChatAttachmentCache( for (const { id, path, connectionId } of appended) { // Preview URLs can retain the full clipboard Blob, so only the path is kept. const image = { id, path, ...(connectionId ? { connectionId } : {}) } - const marker = images.findIndex((held) => held.unavailableName === basename(path)) - if (marker === -1) { + const placeholder = options?.fromUser + ? images.findIndex((held) => held.unavailableName === basename(path)) + : -1 + if (placeholder === -1) { images.push(image) } else { - images[marker] = image + images[placeholder] = image } } updateNativeChatComposerDraft(scopeKey, { images }, 'immediate') diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index b803ccda70d..7fa1cbcf2db 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -17765,9 +17765,12 @@ "localAttachmentUnsupported": "Local attachments are not available for remote sessions.", "removeAttachment": "Remove attachment", "pastedImageLabel": "Pasted image", - "imageNotSavedWithDraft": "{{name}} wasn't saved with this draft. Attach it again to send it.", - "attachImageAgain": "Attach the image again or remove it", - "attachImagesAgain": "Attach the images again or remove them", + "imageNotBroughtBack": "{{name}} couldn't be brought back with this draft. Attach it again or remove it.", + "pastedImageNotBroughtBack": "This pasted image couldn't be brought back with this draft. Remove it, and paste it again if you still need it.", + "removeImageToSend": "An image couldn't be brought back. Remove it to send.", + "removeImagesToSend": "Some images couldn't be brought back. Remove them to send.", + "imageAttachAgainLabel": "Attach again", + "pastedImageNotKeptLabel": "Not kept", "imagePasteFailed": "Image paste failed.", "imageSaving": "Saving pasted image…", "worktreeNotReady": "Worktree not ready — try again in a moment.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index d89dff132b9..ab4630b73a4 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -15084,9 +15084,12 @@ "localAttachmentUnsupported": "Los archivos adjuntos locales no están disponibles para sesiones remotas.", "removeAttachment": "Quitar archivo adjunto", "pastedImageLabel": "Imagen pegada", - "imageNotSavedWithDraft": "{{name}} no se guardó con este borrador. Adjúntala de nuevo para enviarla.", - "attachImageAgain": "Adjunta la imagen de nuevo o quítala", - "attachImagesAgain": "Adjunta las imágenes de nuevo o quítalas", + "imageNotBroughtBack": "No se pudo recuperar la imagen {{name}} con este borrador. Vuelve a adjuntarla o quítala.", + "pastedImageNotBroughtBack": "No se pudo recuperar esta imagen pegada con este borrador. Quítala y vuelve a pegarla si aún la necesitas.", + "removeImageToSend": "No se pudo recuperar una imagen. Quítala para enviar.", + "removeImagesToSend": "No se pudieron recuperar algunas imágenes. Quítalas para enviar.", + "imageAttachAgainLabel": "Adjuntar de nuevo", + "pastedImageNotKeptLabel": "No se conservó", "imagePasteFailed": "Error al pegar imagen.", "worktreeNotReady": "Worktree no está listo — intenta de nuevo en un momento.", "pasteUnavailable": "No se puede pegar — este chat no acepta entradas en este momento.", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index f5b6686a760..f0ddc4f86d7 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -17568,9 +17568,12 @@ "localAttachmentUnsupported": "Les pièces jointes locales ne sont pas disponibles pour les sessions distantes.", "removeAttachment": "Retirer la pièce jointe", "pastedImageLabel": "Image collée", - "imageNotSavedWithDraft": "{{name}} n'a pas été enregistrée avec ce brouillon. Joignez-la à nouveau pour l'envoyer.", - "attachImageAgain": "Joignez à nouveau l'image ou retirez-la", - "attachImagesAgain": "Joignez à nouveau les images ou retirez-les", + "imageNotBroughtBack": "L'image {{name}} n'a pas pu être récupérée avec ce brouillon. Joignez-la à nouveau ou retirez-la.", + "pastedImageNotBroughtBack": "Cette image collée n'a pas pu être récupérée avec ce brouillon. Retirez-la, puis collez-la à nouveau si vous en avez encore besoin.", + "removeImageToSend": "Une image n'a pas pu être récupérée. Retirez-la pour envoyer.", + "removeImagesToSend": "Certaines images n'ont pas pu être récupérées. Retirez-les pour envoyer.", + "imageAttachAgainLabel": "Joindre à nouveau", + "pastedImageNotKeptLabel": "Non conservée", "imagePasteFailed": "Échec du collage de l'image.", "worktreeNotReady": "Worktree pas encore prêt — réessayez dans un instant.", "pasteUnavailable": "Impossible de coller — ce chat n'accepte pas de saisie pour le moment.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 2c204b78840..67443dcb9ac 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -17490,9 +17490,12 @@ "localAttachmentUnsupported": "ローカル添付ファイルはリモートセッションでは使用できません。", "removeAttachment": "添付ファイルを削除する", "pastedImageLabel": "貼り付けた画像", - "imageNotSavedWithDraft": "{{name}} はこの下書きに保存されませんでした。送信するにはもう一度添付してください。", - "attachImageAgain": "画像をもう一度添付するか、削除してください", - "attachImagesAgain": "画像をもう一度添付するか、削除してください", + "imageNotBroughtBack": "{{name}} をこの下書きと一緒に復元できませんでした。もう一度添付するか、削除してください。", + "pastedImageNotBroughtBack": "この貼り付けた画像をこの下書きと一緒に復元できませんでした。削除して、必要であればもう一度貼り付けてください。", + "removeImageToSend": "復元できなかった画像があります。送信するには削除してください。", + "removeImagesToSend": "復元できなかった画像があります。送信するには削除してください。", + "imageAttachAgainLabel": "もう一度添付", + "pastedImageNotKeptLabel": "保持されていません", "imagePasteFailed": "画像の貼り付けに失敗しました。", "worktreeNotReady": "ワークツリーが準備できていません — しばらくしてから再試行してください。", "pasteUnavailable": "貼り付けできません — このチャットは現在入力を受け付けていません。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 3a8e5a3b822..4828e4e3d4a 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -17490,9 +17490,12 @@ "localAttachmentUnsupported": "원격 세션에는 로컬 첨부 파일을 사용할 수 없습니다.", "removeAttachment": "첨부파일 삭제", "pastedImageLabel": "붙여넣은 이미지", - "imageNotSavedWithDraft": "{{name}}은(는) 이 초안에 저장되지 않았습니다. 보내려면 다시 첨부하세요.", - "attachImageAgain": "이미지를 다시 첨부하거나 제거하세요", - "attachImagesAgain": "이미지를 다시 첨부하거나 제거하세요", + "imageNotBroughtBack": "{{name}}을(를) 이 초안과 함께 복원하지 못했습니다. 다시 첨부하거나 제거하세요.", + "pastedImageNotBroughtBack": "이 붙여넣은 이미지를 이 초안과 함께 복원하지 못했습니다. 제거한 후 필요하면 다시 붙여넣으세요.", + "removeImageToSend": "복원하지 못한 이미지가 있습니다. 보내려면 제거하세요.", + "removeImagesToSend": "복원하지 못한 이미지가 있습니다. 보내려면 제거하세요.", + "imageAttachAgainLabel": "다시 첨부", + "pastedImageNotKeptLabel": "보존되지 않음", "imagePasteFailed": "이미지 붙여넣기 실패.", "worktreeNotReady": "워크트리가 준비되지 않았습니다 — 잠시 후 다시 시도하세요.", "pasteUnavailable": "붙여넣을 수 없습니다 — 이 채팅은 지금 입력을 받을 수 없습니다.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index b7af46e09fd..299df53d235 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -17507,9 +17507,12 @@ "localAttachmentUnsupported": "远程会话不支持本地附件。", "removeAttachment": "移除附件", "pastedImageLabel": "粘贴的图片", - "imageNotSavedWithDraft": "{{name}} 未随此草稿保存。请重新附加后再发送。", - "attachImageAgain": "请重新附加该图片或将其移除", - "attachImagesAgain": "请重新附加这些图片或将其移除", + "imageNotBroughtBack": "无法随此草稿恢复 {{name}}。请重新附加或将其移除。", + "pastedImageNotBroughtBack": "无法随此草稿恢复这张粘贴的图片。请将其移除,如仍需要请重新粘贴。", + "removeImageToSend": "有一张图片无法恢复。请将其移除后再发送。", + "removeImagesToSend": "有些图片无法恢复。请将其移除后再发送。", + "imageAttachAgainLabel": "重新附加", + "pastedImageNotKeptLabel": "未保留", "imagePasteFailed": "图片粘贴失败。", "worktreeNotReady": "工作树未就绪 — 请稍后重试。", "pasteUnavailable": "无法粘贴 — 此聊天当前不接受输入。",