From 35dbd2952849cab33a0476a5916d8db172e8871d Mon Sep 17 00:00:00 2001 From: qer Date: Thu, 11 Jun 2026 16:31:39 +0800 Subject: [PATCH] feat(kimi-web): esc interrupt, abort toast, and filepath link fixes --- .changeset/esc-interrupt-abort-toast.md | 5 ++ .changeset/filepath-link-branch-filter.md | 5 ++ apps/kimi-web/src/components/ChatPane.vue | 15 ++-- .../src/components/ConversationPane.vue | 75 ++++++++++++++++++- apps/kimi-web/src/components/Markdown.vue | 10 ++- .../src/i18n/locales/en/conversation.ts | 1 + .../src/i18n/locales/zh/conversation.ts | 1 + apps/kimi-web/src/lib/filePathLinks.ts | 7 ++ apps/kimi-web/test/filePathLinks.test.ts | 5 ++ 9 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 .changeset/esc-interrupt-abort-toast.md create mode 100644 .changeset/filepath-link-branch-filter.md diff --git a/.changeset/esc-interrupt-abort-toast.md b/.changeset/esc-interrupt-abort-toast.md new file mode 100644 index 000000000..5a095775a --- /dev/null +++ b/.changeset/esc-interrupt-abort-toast.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-web": minor +--- + +Add Escape key support to interrupt running prompts and show a transient "manually stopped" toast. diff --git a/.changeset/filepath-link-branch-filter.md b/.changeset/filepath-link-branch-filter.md new file mode 100644 index 000000000..e111c8f19 --- /dev/null +++ b/.changeset/filepath-link-branch-filter.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-web": patch +--- + +Ignore branch-like slash names without file extensions when detecting file path links. diff --git a/apps/kimi-web/src/components/ChatPane.vue b/apps/kimi-web/src/components/ChatPane.vue index ef3048628..28472b81d 100644 --- a/apps/kimi-web/src/components/ChatPane.vue +++ b/apps/kimi-web/src/components/ChatPane.vue @@ -501,7 +501,6 @@ function turnBlocks(turn: ChatTurn): TurnBlock[] { height: auto; margin-top: 10px; overflow: visible; - cursor: pointer; } .a-cpbtn { @@ -519,20 +518,22 @@ function turnBlocks(turn: ChatTurn): TurnBlock[] { .a-cpbtn:hover { color: var(--ink); } +.a-cpbtn svg, +.a-cpbtn-text { + pointer-events: none; +} .a-cpbtn svg { flex: none; } .a-cpbtn-text { opacity: 0; - max-width: 0; - overflow: hidden; + max-width: none; + overflow: visible; white-space: nowrap; - transition: opacity 0.15s ease, max-width 0.15s ease; - cursor: pointer; + transition: opacity 0.15s ease; } .a-cpbtn:hover .a-cpbtn-text { opacity: 1; - max-width: 120px; } /* Touch devices: always show the copy buttons (no hover to reveal them) and give the bubble-layout button a comfortable tap size. */ @@ -604,7 +605,7 @@ function turnBlocks(turn: ChatTurn): TurnBlock[] { /* Mobile bubble layout sending placeholder */ .sending-placeholder { align-self: flex-start; - padding: 10px 14px; + padding: 10px 0; } /* Desktop line-turns sending placeholder */ diff --git a/apps/kimi-web/src/components/ConversationPane.vue b/apps/kimi-web/src/components/ConversationPane.vue index 08d10c9c2..9b40b3c34 100644 --- a/apps/kimi-web/src/components/ConversationPane.vue +++ b/apps/kimi-web/src/components/ConversationPane.vue @@ -474,9 +474,25 @@ function onVisibilityChange(): void { } } +// --------------------------------------------------------------------------- +// Manual-abort toast: shown when the user presses Escape to stop the prompt +// --------------------------------------------------------------------------- +const abortToastVisible = ref(false); +let abortToastTimer: ReturnType | null = null; +const ABORT_TOAST_DURATION = 3000; + +function showAbortToast(): void { + abortToastVisible.value = true; + if (abortToastTimer !== null) clearTimeout(abortToastTimer); + abortToastTimer = setTimeout(() => { + abortToastVisible.value = false; + }, ABORT_TOAST_DURATION); +} + function onKeyDown(event: KeyboardEvent): void { - if (event.key === 'Escape' && props.running) { + if (event.key === 'Escape' && (props.running || props.sending)) { event.preventDefault(); + showAbortToast(); emit('interrupt'); } } @@ -510,6 +526,7 @@ onUnmounted(() => { if (contentObserver) contentObserver.disconnect(); if (resizeObserver) resizeObserver.disconnect(); if (scrollRaf && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(scrollRaf); + if (abortToastTimer !== null) clearTimeout(abortToastTimer); if (typeof document !== 'undefined') { document.removeEventListener('visibilitychange', onVisibilityChange); document.removeEventListener('keydown', onKeyDown); @@ -763,6 +780,18 @@ onUnmounted(() => { @select-model="emit('selectModel', $event)" /> + + + +
+ {{ t('conversation.manuallyAborted') }} +
+
@@ -1100,4 +1129,48 @@ onUnmounted(() => { opacity: 0; transform: translateX(-50%) translateY(8px); } + +/* Manual-abort toast: centered near the top of the conversation pane */ +.abort-toast { + position: fixed; + top: 56px; + left: 50%; + transform: translateX(-50%); + z-index: 50; + padding: 8px 18px; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: 0 6px 22px rgba(0, 0, 0, 0.12); + font-size: 14px; + font-family: var(--sans); + color: var(--ink); + white-space: nowrap; + pointer-events: none; +} +.abort-toast-text { + display: flex; + align-items: center; + gap: 6px; +} +.abort-toast-text::before { + content: ''; + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--muted); + flex: none; +} + +/* Abort-toast enter/leave transition */ +.abort-toast-enter-active, +.abort-toast-leave-active { + transition: opacity 0.2s, transform 0.2s; +} +.abort-toast-enter-from, +.abort-toast-leave-to { + opacity: 0; + transform: translateX(-50%) translateY(-6px); +} diff --git a/apps/kimi-web/src/components/Markdown.vue b/apps/kimi-web/src/components/Markdown.vue index 4fbe91d89..f4da8c2c5 100644 --- a/apps/kimi-web/src/components/Markdown.vue +++ b/apps/kimi-web/src/components/Markdown.vue @@ -433,15 +433,21 @@ function copyDiff(code: string, idx: number) { font-size: 10px; } /* Copy button in the header */ -.md :deep(.copy-button) { +.md :deep(.code-block-header .copy-button), +.md :deep(.code-block-header .code-action-btn) { color: var(--muted); background: none; border: none; cursor: pointer; } -.md :deep(.copy-button:hover) { +.md :deep(.code-block-header .copy-button:hover), +.md :deep(.code-block-header .code-action-btn:hover) { color: var(--blue); } +.md :deep(.code-block-header .copy-button *), +.md :deep(.code-block-header .code-action-btn *) { + pointer-events: none; +} .md :deep(.code-block-content), .md :deep(.markstream-pre) { background: var(--panel); diff --git a/apps/kimi-web/src/i18n/locales/en/conversation.ts b/apps/kimi-web/src/i18n/locales/en/conversation.ts index cc5f4fac6..ea1e70a01 100644 --- a/apps/kimi-web/src/i18n/locales/en/conversation.ts +++ b/apps/kimi-web/src/i18n/locales/en/conversation.ts @@ -6,4 +6,5 @@ export default { compacting: 'Compacting context…', compacted: 'Context compacted ({before} → {after} tokens)', compactedPlain: 'Context compacted', + manuallyAborted: 'Manually stopped', } as const; diff --git a/apps/kimi-web/src/i18n/locales/zh/conversation.ts b/apps/kimi-web/src/i18n/locales/zh/conversation.ts index f5f748424..c69695516 100644 --- a/apps/kimi-web/src/i18n/locales/zh/conversation.ts +++ b/apps/kimi-web/src/i18n/locales/zh/conversation.ts @@ -6,4 +6,5 @@ export default { compacting: '正在压缩上下文…', compacted: '上下文已压缩({before} → {after} tokens)', compactedPlain: '上下文已压缩', + manuallyAborted: '您已手动终止', } as const; diff --git a/apps/kimi-web/src/lib/filePathLinks.ts b/apps/kimi-web/src/lib/filePathLinks.ts index 4aced079c..940c008f0 100644 --- a/apps/kimi-web/src/lib/filePathLinks.ts +++ b/apps/kimi-web/src/lib/filePathLinks.ts @@ -75,6 +75,11 @@ const PATH_RE = new RegExp( const TRAILING_PUNCTUATION_RE = /[),.;!?,。;!?)]+$/; +function hasCommonFileExtension(path: string): boolean { + const lower = path.toLowerCase(); + return COMMON_FILE_EXTENSIONS.some((ext) => lower.endsWith(`.${ext}`)); +} + export function collectFilePathAliases(text: string): Map { const aliases = new Map(); const attrPathRe = new RegExp( @@ -106,6 +111,8 @@ export function parseFilePathLinkCandidate( const basename = path.split('/').pop() ?? path; const hasSeparator = path.includes('/'); const hasKnownName = COMMON_FILENAMES.has(basename); + const hasKnownExtension = hasCommonFileExtension(basename); + if (hasSeparator && !hasKnownName && !hasKnownExtension) return null; if (!hasSeparator && !hasKnownName) { const alias = options.aliases?.get(basename); if (!alias) return null; diff --git a/apps/kimi-web/test/filePathLinks.test.ts b/apps/kimi-web/test/filePathLinks.test.ts index 857cb9ae3..abc678405 100644 --- a/apps/kimi-web/test/filePathLinks.test.ts +++ b/apps/kimi-web/test/filePathLinks.test.ts @@ -42,6 +42,11 @@ describe('file path links', () => { expect(parseFilePathLinkCandidate('hello')).toBeNull(); }); + it('ignores branch-like slash names without file extensions', () => { + expect(parseFilePathLinkCandidate('feat/web')).toBeNull(); + expect(findFilePathLinks('commit db8d21cd on feat/web.')).toEqual([]); + }); + it('finds multiple links in message text', () => { expect(findFilePathLinks('See apps/kimi-web/src/App.vue:11 and package.json.')).toEqual([ {