mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-25 08:34:39 +00:00
feat(kimi-web): esc interrupt, abort toast, and filepath link fixes
This commit is contained in:
parent
fca2abe4b1
commit
35dbd29528
9 changed files with 114 additions and 10 deletions
5
.changeset/esc-interrupt-abort-toast.md
Normal file
5
.changeset/esc-interrupt-abort-toast.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-web": minor
|
||||
---
|
||||
|
||||
Add Escape key support to interrupt running prompts and show a transient "manually stopped" toast.
|
||||
5
.changeset/filepath-link-branch-filter.md
Normal file
5
.changeset/filepath-link-branch-filter.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-web": patch
|
||||
---
|
||||
|
||||
Ignore branch-like slash names without file extensions when detecting file path links.
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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<typeof setTimeout> | 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)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Manual-abort toast: shown when the user presses Escape to stop a prompt -->
|
||||
<Transition name="abort-toast">
|
||||
<div
|
||||
v-if="abortToastVisible"
|
||||
class="abort-toast"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span class="abort-toast-text">{{ t('conversation.manuallyAborted') }}</span>
|
||||
</div>
|
||||
</Transition>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ export default {
|
|||
compacting: 'Compacting context…',
|
||||
compacted: 'Context compacted ({before} → {after} tokens)',
|
||||
compactedPlain: 'Context compacted',
|
||||
manuallyAborted: 'Manually stopped',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ export default {
|
|||
compacting: '正在压缩上下文…',
|
||||
compacted: '上下文已压缩({before} → {after} tokens)',
|
||||
compactedPlain: '上下文已压缩',
|
||||
manuallyAborted: '您已手动终止',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -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<string, string> {
|
||||
const aliases = new Map<string, string>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue