From c8dd5b0563949df3358689a7aed9e1ae4921a813 Mon Sep 17 00:00:00 2001 From: qer Date: Fri, 26 Jun 2026 00:43:45 +0800 Subject: [PATCH 1/2] feat(web): add a copy button to user messages Mirror the assistant per-message copy button on user turns, copying the message text with the same checkmark feedback. --- .changeset/web-copy-user-message.md | 5 ++++ .../kimi-web/src/components/chat/ChatPane.vue | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 .changeset/web-copy-user-message.md diff --git a/.changeset/web-copy-user-message.md b/.changeset/web-copy-user-message.md new file mode 100644 index 0000000000..22b4803f51 --- /dev/null +++ b/.changeset/web-copy-user-message.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Add a copy button to user messages in the web chat. diff --git a/apps/kimi-web/src/components/chat/ChatPane.vue b/apps/kimi-web/src/components/chat/ChatPane.vue index 5217731cc7..82f6caf230 100644 --- a/apps/kimi-web/src/components/chat/ChatPane.vue +++ b/apps/kimi-web/src/components/chat/ChatPane.vue @@ -385,6 +385,20 @@ function copyAssistantRun(index: number): void { }).catch(() => {/* ignore */}); } +function copyUserMessage(turn: ChatTurn): void { + const text = turn.text; + if (!text.trim()) return; + void copyTextToClipboard(text).then((ok) => { + if (!ok) return; + copiedTurn.value = turn.id; + if (copiedTimer !== null) clearTimeout(copiedTimer); + copiedTimer = setTimeout(() => { + copiedTimer = null; + copiedTurn.value = null; + }, 1400); + }).catch(() => {/* ignore */}); +} + function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): boolean { if (turn.id !== streamingTurnId.value) return false; return block.sourceIndex === turnBlocks(turn).length - 1; @@ -652,6 +666,16 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): {{ t('filePreview.copy') }} + {{ formatDuration(turn.durationMs) }} From 0ad05ee5c00cf745b9e19c79b5188b8b4892a6b0 Mon Sep 17 00:00:00 2001 From: qer Date: Fri, 26 Jun 2026 00:50:44 +0800 Subject: [PATCH 2/2] fix(web): place user message copy button with undo and time Move the copy button out of the line-number role row into the modern bubble meta row, grouped with the undo and time buttons, with matching styling and tooltip. --- .../kimi-web/src/components/chat/ChatPane.vue | 63 +++++++++++++------ 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/apps/kimi-web/src/components/chat/ChatPane.vue b/apps/kimi-web/src/components/chat/ChatPane.vue index 82f6caf230..f0876a539b 100644 --- a/apps/kimi-web/src/components/chat/ChatPane.vue +++ b/apps/kimi-web/src/components/chat/ChatPane.vue @@ -511,6 +511,21 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): + - {{ formatDuration(turn.durationMs) }} @@ -1053,13 +1058,33 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): } .u-edit span { line-height: 1; } .u-edit:hover { opacity: 1; color: var(--blue); background: var(--hover); } +/* Copy button — icon-only, shares the undo button's muted→hover style. */ +.u-copy { + display: inline-flex; + align-items: center; + padding: 2px 5px; + background: none; + border: none; + border-radius: 5px; + color: var(--muted); + font: inherit; + font-size: calc(var(--ui-font-size) - 3px); + line-height: 1; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.12s, color 0.12s, background-color 0.12s; + min-height: 22px; + box-sizing: border-box; +} +.u-copy svg { display: block; flex: none; } +.u-copy:hover { opacity: 1; color: var(--blue); background: var(--hover); } /* Custom tooltip for the undo button: appears faster than the native title tooltip and avoids duplicating the browser's long default delay. */ -.u-edit[data-tooltip] { +.u-meta [data-tooltip] { position: relative; } -.u-edit[data-tooltip]::after, -.u-edit[data-tooltip]::before { +.u-meta [data-tooltip]::after, +.u-meta [data-tooltip]::before { position: absolute; left: 50%; transform: translateX(-50%); @@ -1070,7 +1095,7 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): transition-delay: 0s; z-index: 100; } -.u-edit[data-tooltip]::after { +.u-meta [data-tooltip]::after { content: attr(data-tooltip); bottom: calc(100% + 6px); padding: 4px 8px; @@ -1081,17 +1106,17 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): border-radius: 5px; white-space: nowrap; } -.u-edit[data-tooltip]::before { +.u-meta [data-tooltip]::before { content: ''; bottom: calc(100% + 2px); border-width: 4px; border-style: solid; border-color: var(--ink) transparent transparent transparent; } -.u-edit[data-tooltip]:hover::after, -.u-edit[data-tooltip]:hover::before, -.u-edit[data-tooltip]:focus-visible::after, -.u-edit[data-tooltip]:focus-visible::before { +.u-meta [data-tooltip]:hover::after, +.u-meta [data-tooltip]:hover::before, +.u-meta [data-tooltip]:focus-visible::after, +.u-meta [data-tooltip]:focus-visible::before { opacity: 1; visibility: visible; transition-delay: 0.25s;