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-code/test/tui/components/panels/footer-goal-badge.test.ts b/apps/kimi-code/test/tui/components/panels/footer-goal-badge.test.ts index 068761f7c6..365df3e4af 100644 --- a/apps/kimi-code/test/tui/components/panels/footer-goal-badge.test.ts +++ b/apps/kimi-code/test/tui/components/panels/footer-goal-badge.test.ts @@ -58,7 +58,7 @@ describe('FooterComponent — goal badge', () => { it('omits the badge when there is no goal', () => { const footer = new FooterComponent(baseState({ goal: null })); - expect(strip(footer.render(160)[0]!)).not.toMatch(/goal/); + expect(strip(footer.render(160)[0]!)).not.toContain('[goal'); }); it('shows status, elapsed, and a raw turn count for an unbounded active goal', () => { @@ -116,7 +116,7 @@ describe('FooterComponent — goal badge', () => { it('hides the badge for a completed goal', () => { const footer = new FooterComponent(baseState({ goal: goal({ status: 'complete' }) })); - expect(strip(footer.render(160)[0]!)).not.toMatch(/goal/); + expect(strip(footer.render(160)[0]!)).not.toContain('[goal'); }); it('singularizes a single turn', () => { 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 });