From 711bb8ac8d4eb68883dfd6eb73e9af515d5629a4 Mon Sep 17 00:00:00 2001 From: qer Date: Tue, 23 Jun 2026 21:56:15 +0800 Subject: [PATCH 1/2] fix(web): make clipboard copy work over plain HTTP The Clipboard API (navigator.clipboard) is only exposed in secure contexts. When the web UI is served over plain HTTP, every copy action threw synchronously and silently failed. Route all copy call sites through a helper that falls back to execCommand('copy') in insecure contexts, and surface success or failure feedback to the user. --- .changeset/fix-web-clipboard-copy.md | 5 ++ apps/kimi-web/src/components/FilePreview.vue | 11 +-- apps/kimi-web/src/components/SessionRow.vue | 28 ++++++-- apps/kimi-web/src/components/Sidebar.vue | 5 +- .../kimi-web/src/components/WarningToasts.vue | 5 +- .../src/components/chat/ChatHeader.vue | 6 +- .../kimi-web/src/components/chat/ChatPane.vue | 10 ++- .../kimi-web/src/components/chat/Markdown.vue | 19 +++--- .../src/components/chat/OpenInMenu.vue | 10 +-- .../src/components/chat/TasksPane.vue | 12 ++-- .../src/components/dialogs/LoginDialog.vue | 12 ++-- .../components/mobile/MobileSwitcherSheet.vue | 3 +- apps/kimi-web/src/debug/KapDebugView.vue | 12 ++-- apps/kimi-web/src/i18n/locales/en/sidebar.ts | 3 + apps/kimi-web/src/i18n/locales/zh/sidebar.ts | 3 + apps/kimi-web/src/lib/clipboard.ts | 59 ++++++++++++++++ apps/kimi-web/test/clipboard.test.ts | 68 +++++++++++++++++++ 17 files changed, 213 insertions(+), 58 deletions(-) create mode 100644 .changeset/fix-web-clipboard-copy.md create mode 100644 apps/kimi-web/src/lib/clipboard.ts create mode 100644 apps/kimi-web/test/clipboard.test.ts diff --git a/.changeset/fix-web-clipboard-copy.md b/.changeset/fix-web-clipboard-copy.md new file mode 100644 index 0000000000..3ec4efc565 --- /dev/null +++ b/.changeset/fix-web-clipboard-copy.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix clipboard copy actions in the web UI when served over plain HTTP. diff --git a/apps/kimi-web/src/components/FilePreview.vue b/apps/kimi-web/src/components/FilePreview.vue index 948e502166..5f0b08ec25 100644 --- a/apps/kimi-web/src/components/FilePreview.vue +++ b/apps/kimi-web/src/components/FilePreview.vue @@ -5,6 +5,7 @@ import { computed, inject, nextTick, provide, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import Markdown from './chat/Markdown.vue'; import type { FileData, FilePreviewRequest } from '../types'; +import { copyTextToClipboard } from '../lib/clipboard'; const { t } = useI18n(); @@ -251,18 +252,20 @@ const copiedPath = ref(false); function copyContent(): void { if (!props.file) return; - navigator.clipboard.writeText(sourceText.value).then(() => { + void copyTextToClipboard(sourceText.value).then((ok) => { + if (!ok) return; copied.value = true; setTimeout(() => { copied.value = false; }, 1400); - }).catch(() => {/* ignore */}); + }); } function copyPath(): void { if (!props.file) return; - navigator.clipboard.writeText(props.file.path).then(() => { + void copyTextToClipboard(props.file.path).then((ok) => { + if (!ok) return; copiedPath.value = true; setTimeout(() => { copiedPath.value = false; }, 1400); - }).catch(() => {/* ignore */}); + }); } // --------------------------------------------------------------------------- diff --git a/apps/kimi-web/src/components/SessionRow.vue b/apps/kimi-web/src/components/SessionRow.vue index 51e3e2786f..94c0b65ed5 100644 --- a/apps/kimi-web/src/components/SessionRow.vue +++ b/apps/kimi-web/src/components/SessionRow.vue @@ -5,6 +5,7 @@ import { nextTick, onUnmounted, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import type { Session } from '../types'; +import { copyTextToClipboard } from '../lib/clipboard'; const { t } = useI18n(); @@ -84,11 +85,17 @@ function cancelRename(): void { // Copy session ID const copiedId = ref(false); -function copySessionId(): void { - navigator.clipboard.writeText(props.session.id).then(() => { - copiedId.value = true; - setTimeout(() => { copiedId.value = false; }, 1200); - }).catch(() => {/* ignore */}); +const copyFailed = ref(false); +async function copySessionId(): Promise { + const ok = await copyTextToClipboard(props.session.id); + copiedId.value = ok; + copyFailed.value = !ok; + // Keep the menu open briefly so the result text is visible, then close. + setTimeout(() => { + copiedId.value = false; + copyFailed.value = false; + closeMenu(); + }, 1500); } // Fork this session into a new child session @@ -212,8 +219,14 @@ defineExpose({ closeMenu, cancelArchive });