Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-web-clipboard-copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix clipboard copy actions in the web UI when served over plain HTTP.
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
11 changes: 7 additions & 4 deletions apps/kimi-web/src/components/FilePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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 */});
});
}

// ---------------------------------------------------------------------------
Expand Down
28 changes: 21 additions & 7 deletions apps/kimi-web/src/components/SessionRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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<void> {
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
Expand Down Expand Up @@ -212,8 +219,14 @@ defineExpose({ closeMenu, cancelArchive });

<!-- Kebab dropdown -->
<div ref="menuRef" v-if="menuOpen" class="menu" @click.stop>
<button class="menu-item copy-id" @click.stop="copySessionId">
{{ copiedId ? '已复制 ✓' : '复制 Session ID ⧉' }}
<button class="menu-item copy-id" :class="{ failed: copyFailed }" @click.stop="copySessionId">
{{
copyFailed
? t('sidebar.copyFailed')
: copiedId
? t('sidebar.copied')
: t('sidebar.copySessionId')
}}
</button>
<div class="menu-divider" />
<button class="menu-item" @click.stop="startRename">{{ t('sidebar.rename') }}</button>
Expand Down Expand Up @@ -375,6 +388,7 @@ defineExpose({ closeMenu, cancelArchive });
}
.menu-item:hover { background: var(--panel2); }
.menu-item.archive { color: var(--err); }
.menu-item.failed { color: var(--err); }

.menu-divider {
height: 1px;
Expand Down
5 changes: 3 additions & 2 deletions apps/kimi-web/src/components/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { serverEndpointLabel } from '../api/config';
import { copyTextToClipboard } from '../lib/clipboard';
import type { Session, WorkspaceGroup as WorkspaceGroupType, WorkspaceView } from '../types';
import SessionRow from './SessionRow.vue';
import WorkspaceGroup from './WorkspaceGroup.vue';
Expand Down Expand Up @@ -249,7 +250,7 @@ function closeGhMenu(): void {

function copyPathFromMenu(): void {
if (ghMenuTarget.value) {
void navigator.clipboard.writeText(ghMenuTarget.value.root);
void copyTextToClipboard(ghMenuTarget.value.root);
}
closeGhMenu();
}
Expand Down Expand Up @@ -352,7 +353,7 @@ function closeWsMenu(): void {
}

function copyWsPath(ws: WorkspaceView): void {
void navigator.clipboard.writeText(ws.root);
void copyTextToClipboard(ws.root);
closeWsMenu();
}

Expand Down
5 changes: 3 additions & 2 deletions apps/kimi-web/src/components/WarningToasts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import type { AppNotice, AppWarning } from '../api/types';
import { copyTextToClipboard } from '../lib/clipboard';

const props = defineProps<{ warnings: AppWarning[] }>();
const emit = defineEmits<{ dismiss: [index: number] }>();
Expand Down Expand Up @@ -120,8 +121,8 @@ function toggleDetails(toast: ToastItem): void {
}

async function copyDetails(toast: ToastItem): Promise<void> {
if (!navigator.clipboard?.writeText) return;
await navigator.clipboard.writeText(formatWarningForCopy(toast.warning));
const ok = await copyTextToClipboard(formatWarningForCopy(toast.warning));
if (!ok) return;
toast.copied = true;
const prev = copiedTimers.get(toast.id);
if (prev) clearTimeout(prev);
Expand Down
6 changes: 4 additions & 2 deletions apps/kimi-web/src/components/chat/ChatHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<script setup lang="ts">
import { computed, nextTick, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { copyTextToClipboard } from '../../lib/clipboard';

const { t } = useI18n();

Expand Down Expand Up @@ -124,12 +125,13 @@ function onCopyFinalSummary(): void {
const copiedId = ref(false);
function copySessionId(): void {
if (!props.sessionId) return;
navigator.clipboard.writeText(props.sessionId).then(() => {
void copyTextToClipboard(props.sessionId).then((ok) => {
if (!ok) return;
copiedId.value = true;
setTimeout(() => {
copiedId.value = false;
}, 1200);
}).catch(() => { /* ignore */ });
});
}

// ---------------------------------------------------------------------------
Expand Down
10 changes: 7 additions & 3 deletions apps/kimi-web/src/components/chat/ChatPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import AgentCard from './AgentCard.vue';
import AgentGroup from './AgentGroup.vue';
import MoonSpinner from '../MoonSpinner.vue';
import { formatMessageTime } from '../../lib/formatMessageTime';
import { copyTextToClipboard } from '../../lib/clipboard';
import {
assistantRenderBlocks,
formatDuration,
Expand Down Expand Up @@ -296,7 +297,8 @@ function copyConversation(): void {
}
}
const markdown = lines.join('\n\n---\n\n');
navigator.clipboard.writeText(markdown).then(() => {
void copyTextToClipboard(markdown).then((ok) => {
if (!ok) return;
copiedConversation.value = true;
emit('copyConversationCopied');
if (copiedConversationTimer !== null) clearTimeout(copiedConversationTimer);
Expand Down Expand Up @@ -334,7 +336,8 @@ function finalSummaryText(): string {
function copyFinalSummary(): void {
const text = finalSummaryText();
if (!text.trim()) return;
navigator.clipboard.writeText(text).then(() => {
void copyTextToClipboard(text).then((ok) => {
if (!ok) return;
copiedConversation.value = true;
emit('copyConversationCopied');
if (copiedConversationTimer !== null) clearTimeout(copiedConversationTimer);
Expand Down Expand Up @@ -362,7 +365,8 @@ function copyAssistantRun(index: number): void {
if (!turn) return;
const text = assistantRunFinalText(index);
if (!text.trim()) return;
navigator.clipboard.writeText(text).then(() => {
void copyTextToClipboard(text).then((ok) => {
if (!ok) return;
copiedTurn.value = turn.id;
if (copiedTimer !== null) clearTimeout(copiedTimer);
copiedTimer = setTimeout(() => {
Expand Down
19 changes: 8 additions & 11 deletions apps/kimi-web/src/components/chat/Markdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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';
// px-based CSS build (our app is px, not rem). Imported here so the styles
// load wherever Markdown is used; scoped overrides below re-skin it to
// Terminal Pro. Importing the same file from multiple components is a no-op
Expand Down Expand Up @@ -335,17 +336,13 @@ function diffLines(code: string): { cls: string; text: string }[] {
// Copy state for local diff blocks (keyed by segment index).
const copiedDiff = ref<number | null>(null);
function copyDiff(code: string, idx: number) {
navigator.clipboard
.writeText(code)
.then(() => {
copiedDiff.value = idx;
setTimeout(() => {
copiedDiff.value = null;
}, 1400);
})
.catch(() => {
/* ignore */
});
void copyTextToClipboard(code).then((ok) => {
if (!ok) return;
copiedDiff.value = idx;
setTimeout(() => {
copiedDiff.value = null;
}, 1400);
});
}
</script>

Expand Down
10 changes: 5 additions & 5 deletions apps/kimi-web/src/components/chat/OpenInMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { computed, nextTick, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { safeGetString, safeSetString, STORAGE_KEYS } from '../../lib/storage';
import { copyTextToClipboard } from '../../lib/clipboard';

const { t } = useI18n();

Expand Down Expand Up @@ -164,11 +165,10 @@ function handleQuickOpen(): void {
const copiedPath = ref(false);
async function copyPath(): Promise<void> {
if (!props.workDir) return;
try {
await navigator.clipboard.writeText(props.workDir);
copiedPath.value = true;
setTimeout(() => { copiedPath.value = false; }, 1200);
} catch { /* ignore */ }
const ok = await copyTextToClipboard(props.workDir);
if (!ok) return;
copiedPath.value = true;
setTimeout(() => { copiedPath.value = false; }, 1200);
}
</script>

Expand Down
12 changes: 5 additions & 7 deletions apps/kimi-web/src/components/chat/TasksPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import type { TaskItem } from '../../types';
import { copyTextToClipboard } from '../../lib/clipboard';

defineProps<{ tasks: TaskItem[] }>();

Expand Down Expand Up @@ -47,13 +48,10 @@ function statusClass(state: string): string {
}

async function copyToClipboard(text: string, taskId: string, set: Set<string>): Promise<void> {
try {
await navigator.clipboard.writeText(text);
set.add(taskId);
setTimeout(() => set.delete(taskId), 1500);
} catch {
// Ignore clipboard failures (e.g. denied permission).
}
const ok = await copyTextToClipboard(text);
if (!ok) return;
set.add(taskId);
setTimeout(() => set.delete(taskId), 1500);
}

async function copyTaskCommand(task: TaskItem): Promise<void> {
Expand Down
12 changes: 5 additions & 7 deletions apps/kimi-web/src/components/dialogs/LoginDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useDialogFocus } from '../../composables/useDialogFocus';
import { copyTextToClipboard } from '../../lib/clipboard';

const { t } = useI18n();

Expand Down Expand Up @@ -160,13 +161,10 @@ async function retryFlow(): Promise<void> {

async function copyCode(): Promise<void> {
if (!flow.value) return;
try {
await navigator.clipboard.writeText(flow.value.userCode);
copied.value = true;
setTimeout(() => { copied.value = false; }, 2000);
} catch {
// clipboard unavailable — ignore
}
const ok = await copyTextToClipboard(flow.value.userCode);
if (!ok) return;
copied.value = true;
setTimeout(() => { copied.value = false; }, 2000);
}

async function close(): Promise<void> {
Expand Down
3 changes: 2 additions & 1 deletion apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import type { Session, WorkspaceGroup, WorkspaceView } from '../../types';
import { copyTextToClipboard } from '../../lib/clipboard';
import BottomSheet from '../dialogs/BottomSheet.vue';

const { t } = useI18n();
Expand Down Expand Up @@ -188,7 +189,7 @@ function toggleWsMenu(id: string): void {
confirmingWsDeleteId.value = null;
}
function onCopyWsPath(ws: WorkspaceView): void {
void navigator.clipboard.writeText(ws.root);
void copyTextToClipboard(ws.root);
wsMenuFor.value = null;
}
function onDeleteWorkspace(id: string): void {
Expand Down
12 changes: 5 additions & 7 deletions apps/kimi-web/src/debug/KapDebugView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Dev tooling: labels are intentionally not localized. -->
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { copyTextToClipboard } from '../lib/clipboard';
import {
clearTrace,
downloadTraceLog,
Expand Down Expand Up @@ -121,13 +122,10 @@ function entryJson(e: TraceEntry): string {
}

async function copyEntry(e: TraceEntry): Promise<void> {
try {
await navigator.clipboard.writeText(entryJson(e));
copiedId.value = e.id;
setTimeout(() => { if (copiedId.value === e.id) copiedId.value = null; }, 1500);
} catch {
// clipboard unavailable
}
const ok = await copyTextToClipboard(entryJson(e));
if (!ok) return;
copiedId.value = e.id;
setTimeout(() => { if (copiedId.value === e.id) copiedId.value = null; }, 1500);
}

function exportJsonl(): void {
Expand Down
3 changes: 3 additions & 0 deletions apps/kimi-web/src/i18n/locales/en/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export default {
options: 'Options',
rename: 'Rename',
copyPath: 'Copy path',
copySessionId: 'Copy session ID ⧉',
copied: 'Copied ✓',
copyFailed: 'Copy failed',
archive: 'Archive',
fork: 'Fork session',
delete: 'Delete',
Expand Down
3 changes: 3 additions & 0 deletions apps/kimi-web/src/i18n/locales/zh/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export default {
options: '选项',
rename: '重命名',
copyPath: '复制路径',
copySessionId: '复制 Session ID ⧉',
copied: '已复制 ✓',
copyFailed: '复制失败',
archive: '归档',
fork: '分叉会话',
delete: '删除',
Expand Down
Loading
Loading