From be7c9916b019b19e057301c39bc7944fcac09414 Mon Sep 17 00:00:00 2001 From: qer Date: Sat, 4 Jul 2026 23:32:32 +0800 Subject: [PATCH] fix(web): refill attachments when editing a queued or undone message (#1357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(web): refill attachments when editing a queued or undone message Queued prompts that carry images/video are no longer remove-only: clicking one loads its text and attachments back into the composer. Undo ("edit & resend") now restores the message's attachments too, not just its text. useAttachmentUpload gains loadAttachments, which reuses the existing fileIds (no re-upload) and fetches authenticated blob URLs for protected getFileUrl previews so the refilled thumbnails don't 401. * fix(web): forward attachment refills through ChatDock When the normal chat dock is mounted (non-empty conversation), bindChatDock receives the ChatDock instance, but ChatDock only exposed loadForEdit/focus — so the new loadAttachmentsForEdit fell back to a no-op and editing a queued media prompt or undoing a media message still dropped the attachments. ChatDock now forwards loadAttachmentsForEdit to the underlying Composer. * fix(web): replace composer attachments when refilling edits loadForEdit(text) overwrites the composer text, but loadAttachments appended to any unsent draft attachments, so a later submit sent the stale draft files together with the edited message's files. Make loadAttachments replace the current session's attachments (revoking their object URLs) so an edit/undo replaces the whole composer, mirroring loadForEdit. * fix(web): clear stale attachments on text-only edits When the composer already has attachments loaded (for example after editing a queued media prompt) and the user then edits a text-only queued prompt or undoes a text-only message, loadComposerForEdit skipped loadAttachmentsForEdit (the only path that clears the strip) because the new attachment list was empty/undefined, so the next submit would send the stale media with the new text. Always call loadAttachmentsForEdit(attachments ?? []) so text-only edits replace the strip with an empty set. * fix(web): make fileId-less refilled media resendable When editing a user turn whose media was base64-inlined by the server (no fileId), loadAttachments used to add a non-uploading chip with no fileId, which handleSubmit silently drops on resend (and an image-only edit would not submit at all). Re-upload the data URL to obtain a fileId so the attachment is actually resendable; when re-upload is unavailable, skip the chip instead of showing a misleading ready attachment. Also factor a patchAttachment helper. * chore: prefix web changeset entry The gen-changesets rules require web-app changelog entries to start with 'web: ' so the synced release notes classify web UI changes correctly. * fix(web): don't dequeue a prompt when the composer is hidden When a queued media item is clicked while the dock is showing a pending question or approval, ChatDock has no nested Composer (only rendered in its v-else), so loadComposerForEdit no-ops — but handleEditQueued still dequeued the item, losing it. Make ChatDock.loadForEdit report whether the nested composer is present, have loadComposerForEdit return success, and only dequeue when the load actually succeeds. * fix(web): preserve URL-backed media when refilling composer When an undone turn contains media with source.kind === 'url' (a URL but no fileId), loadAttachments used to fall through and drop it. Re-upload the URL (data: or http(s):) to obtain a fileId so URL-backed media is preserved on edit/resend; if the URL can't be fetched (CORS / non-2xx) the chip is dropped instead of shown as a misleading ready attachment. --- .changeset/refill-composer-attachments.md | 5 + apps/kimi-web/src/App.vue | 7 +- .../kimi-web/src/components/chat/ChatDock.vue | 21 +++- .../kimi-web/src/components/chat/ChatPane.vue | 17 ++-- .../kimi-web/src/components/chat/Composer.vue | 6 +- .../src/components/chat/ConversationPane.vue | 55 ++++++++--- .../src/composables/useAttachmentUpload.ts | 95 +++++++++++++++++++ apps/kimi-web/test/attachment-upload.test.ts | 84 ++++++++++++++++ 8 files changed, 261 insertions(+), 29 deletions(-) create mode 100644 .changeset/refill-composer-attachments.md diff --git a/.changeset/refill-composer-attachments.md b/.changeset/refill-composer-attachments.md new file mode 100644 index 000000000..dfb5ae1b8 --- /dev/null +++ b/.changeset/refill-composer-attachments.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +web: Fix queued media messages not loading back into the composer and keep attachments when undoing a message. diff --git a/apps/kimi-web/src/App.vue b/apps/kimi-web/src/App.vue index 6da2ea200..f8e28149f 100644 --- a/apps/kimi-web/src/App.vue +++ b/apps/kimi-web/src/App.vue @@ -377,10 +377,13 @@ async function handleLoginSuccess(): Promise { // Edit + resend the last user message: undo the latest exchange on the daemon, // then drop that message's text back into the composer for editing. -async function handleEditMessage(text: string): Promise { +async function handleEditMessage(payload: { + text: string; + images?: { url: string; alt?: string; kind: 'image' | 'video'; fileId?: string }[]; +}): Promise { await client.undo(1); await nextTick(); - conversationPaneRef.value?.loadComposerForEdit(text); + conversationPaneRef.value?.loadComposerForEdit(payload.text, payload.images); } // Handler for slash commands emitted by Composer (via ConversationPane) diff --git a/apps/kimi-web/src/components/chat/ChatDock.vue b/apps/kimi-web/src/components/chat/ChatDock.vue index f76c97497..4078c1d14 100644 --- a/apps/kimi-web/src/components/chat/ChatDock.vue +++ b/apps/kimi-web/src/components/chat/ChatDock.vue @@ -80,12 +80,25 @@ const emit = defineEmits<{ }>(); const { t } = useI18n(); -const composerRef = ref<{ loadForEdit: (value: string) => void; focus: () => void } | null>(null); +const composerRef = ref<{ + loadForEdit: (value: string) => boolean; + loadAttachmentsForEdit: (atts: { fileId?: string; kind: 'image' | 'video'; url: string; name?: string }[]) => void; + focus: () => void; +} | null>(null); const workPanelRef = ref(null); const workbarRef = ref(null); -function loadForEdit(value: string): void { - composerRef.value?.loadForEdit(value); +function loadForEdit(value: string): boolean { + // The nested Composer is only rendered in ChatDock's v-else — when a pending + // question or approval is shown it is unmounted, so report unavailability so + // the caller doesn't dequeue a prompt it can't actually load. + if (!composerRef.value) return false; + composerRef.value.loadForEdit(value); + return true; +} + +function loadAttachmentsForEdit(atts: { fileId?: string; kind: 'image' | 'video'; url: string; name?: string }[]): void { + composerRef.value?.loadAttachmentsForEdit(atts); } function focus(): void { @@ -117,7 +130,7 @@ onUnmounted(() => { } }); -defineExpose({ loadForEdit, focus }); +defineExpose({ loadForEdit, loadAttachmentsForEdit, focus });