From 2af2032b3879a149727582c325702186ff042def Mon Sep 17 00:00:00 2001 From: qer Date: Sat, 4 Jul 2026 01:22:46 +0800 Subject: [PATCH 1/8] 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. --- .changeset/refill-composer-attachments.md | 5 +++ apps/kimi-web/src/App.vue | 7 ++- .../kimi-web/src/components/chat/ChatPane.vue | 17 ++++---- .../kimi-web/src/components/chat/Composer.vue | 6 ++- .../src/components/chat/ConversationPane.vue | 43 +++++++++++++------ .../src/composables/useAttachmentUpload.ts | 41 ++++++++++++++++++ apps/kimi-web/test/attachment-upload.test.ts | 15 +++++++ 7 files changed, 110 insertions(+), 24 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 0000000000..7c8eb28b27 --- /dev/null +++ b/.changeset/refill-composer-attachments.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix queued media messages not loading back into the composer and keep attachments when undoing a message in the web chat. diff --git a/apps/kimi-web/src/App.vue b/apps/kimi-web/src/App.vue index 0cec2807e9..885362fc16 100644 --- a/apps/kimi-web/src/App.vue +++ b/apps/kimi-web/src/App.vue @@ -369,10 +369,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/ChatPane.vue b/apps/kimi-web/src/components/chat/ChatPane.vue index 24a5dece4c..bc1647a05f 100644 --- a/apps/kimi-web/src/components/chat/ChatPane.vue +++ b/apps/kimi-web/src/components/chat/ChatPane.vue @@ -199,7 +199,7 @@ const emit = defineEmits<{ /** Show an Edit/Write tool call's diff in the right-side panel. */ openToolDiff: [id: string]; /** Edit + resend the last user message (parent undoes, then refills composer). */ - editMessage: [text: string]; + editMessage: [payload: { text: string; images?: { url: string; alt?: string; kind: 'image' | 'video'; fileId?: string }[] }]; /** Fetch the next older page of messages (triggered by top sentinel visibility or click). */ loadOlderMessages: []; /** Remove a queued message by index. */ @@ -220,10 +220,10 @@ function hasImages(item: QueuedPromptView): boolean { return (item.attachments?.length ?? 0) > 0; } -function onQueueEdit(index: number, item: QueuedPromptView): void { - // Image-carrying prompts can't be round-tripped through the text composer, so - // they are remove-only (matches the previous dock queue behaviour). - if (hasImages(item)) return; +function onQueueEdit(index: number): void { + // Image/video attachments round-trip through the composer now (the composer + // can hold fileIds), so a queued prompt can be loaded back for edit whether or + // not it carries media. emit('editQueued', index); } @@ -353,7 +353,7 @@ async function onUndo(turn: ChatTurn): Promise { function confirmEditMessage(turn: ChatTurn): void { if (undoingTurnId.value !== null) return; undoingTurnId.value = turn.id; - emit('editMessage', turn.text); + emit('editMessage', { text: turn.text, images: turn.images }); // Fallback: if the server rewind never removes the turn (e.g. it failed), // release the guard so the user can retry. undoFallbackTimer = setTimeout(() => { @@ -721,9 +721,8 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }):