fix(web): preserve selected text when copying over HTTP (#2120)

Co-authored-by: chenyicun <chenyicun@msh.team>
This commit is contained in:
Chen, Yicun 2026-07-24 16:38:36 +08:00 committed by GitHub
parent 3615b5da9f
commit 0d00a07c02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 46 additions and 14 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
web: Fix copying selected chat text over plain HTTP from replacing the clipboard with an event placeholder.

View file

@ -16,7 +16,7 @@ import { useIsDark } from '../../composables/useIsDark';
import type { FilePreviewRequest } from '../../types';
import { collectFilePathAliases, findFilePathLinks } from '../../lib/filePathLinks';
import { markdownRenderPlan } from '../../lib/markdownPerformance';
import { copyTextToClipboard } from '../../lib/clipboard';
import { copyCodeBlockFallback, copyTextToClipboard } from '../../lib/clipboard';
import * as katexWorkerModule from 'markstream-vue/workers/katexRenderer.worker?worker&type=module';
import * as mermaidWorkerModule from 'markstream-vue/workers/mermaidParser.worker?worker&type=module';
import Tooltip from '../ui/Tooltip.vue';
@ -338,15 +338,6 @@ const codeBlockProps = {
loading: false,
};
function copyCodeBlockFallback(code: string): void {
// markstream emits `copy` even when it skipped the write because the
// Clipboard API is unavailable. Reuse our plain-HTTP fallback in that case,
// while avoiding a duplicate write after markstream succeeds on HTTPS.
const clipboard = typeof navigator !== 'undefined' ? navigator.clipboard : undefined;
if (clipboard && typeof clipboard.writeText === 'function') return;
void copyTextToClipboard(code);
}
// Root cause for the "large session turns into code skeletons" failure:
// markstream mounts every code block in the loaded transcript, then shiki has
// to tokenize all of them. `loading: false` removes the visible skeleton gate,

View file

@ -30,6 +30,18 @@ export async function copyTextToClipboard(text: string): Promise<boolean> {
return legacyCopy(text);
}
/**
* Complete markstream's code-block copy on plain HTTP without intercepting
* native selection-copy events that bubble through MarkdownRender.
*/
export function copyCodeBlockFallback(payload: unknown): void {
if (typeof payload !== 'string') return;
const clipboard = typeof navigator !== 'undefined' ? navigator.clipboard : undefined;
if (clipboard && typeof clipboard.writeText === 'function') return;
void copyTextToClipboard(payload);
}
function legacyCopy(text: string): boolean {
if (typeof document === 'undefined' || typeof document.execCommand !== 'function') {
return false;

View file

@ -1,8 +1,9 @@
// Scenario: clipboard writes in secure and plain-HTTP web contexts.
// Responsibilities: preserve native selection copies and provide the legacy
// code-block fallback. The test stubs only navigator/document browser APIs.
// Run: pnpm --filter @moonshot-ai/kimi-web test -- clipboard.test.ts
import { afterEach, describe, expect, it, vi } from 'vitest';
import { copyTextToClipboard } from '../src/lib/clipboard';
// The web test suite runs in the default node environment (no jsdom), so we
// mock the tiny `navigator` / `document` surface that the helper touches.
import { copyCodeBlockFallback, copyTextToClipboard } from '../src/lib/clipboard';
interface FakeDocument {
execCommand: ReturnType<typeof vi.fn>;
@ -78,3 +79,26 @@ describe('copyTextToClipboard', () => {
await expect(copyTextToClipboard('nope')).resolves.toBe(false);
});
});
describe('code-block copy fallback', () => {
it('does not overwrite selected text when a native copy event bubbles on plain HTTP', () => {
installNavigator(undefined);
const doc = installDocument(true);
const copyEvent = { toString: () => '[object ClipboardEvent]' };
copyCodeBlockFallback(copyEvent);
expect(doc.execCommand).not.toHaveBeenCalled();
expect(doc.textarea.value).toBe('');
});
it('copies emitted code text when the Clipboard API is unavailable', () => {
installNavigator(undefined);
const doc = installDocument(true);
copyCodeBlockFallback('const host = "example.test";');
expect(doc.execCommand).toHaveBeenCalledWith('copy');
expect(doc.textarea.value).toBe('const host = "example.test";');
});
});