From 9948ac08de494073a3c3de964b08e802b2d5f56e Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 24 Jun 2026 18:19:34 +0800 Subject: [PATCH 1/6] fix(kimi-code): show clipboard image paste hint only once per image The footer hint repeated on every terminal focus whenever an image remained in the clipboard, which became noisy. Replace the 30s time-based cooldown with a per-image gate: the hint shows once for a given image and stays quiet until the clipboard is observed empty and a new image appears. --- .changeset/clipboard-image-hint-once.md | 5 ++ .../src/tui/constant/clipboard-image-hint.ts | 1 - .../tui/controllers/clipboard-image-hint.ts | 28 +++++++---- .../controllers/clipboard-image-hint.test.ts | 47 +++++++++++++++++-- 4 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 .changeset/clipboard-image-hint-once.md diff --git a/.changeset/clipboard-image-hint-once.md b/.changeset/clipboard-image-hint-once.md new file mode 100644 index 0000000000..c6daa01152 --- /dev/null +++ b/.changeset/clipboard-image-hint-once.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Show the clipboard image paste hint only once per image, instead of repeating it on every terminal focus. diff --git a/apps/kimi-code/src/tui/constant/clipboard-image-hint.ts b/apps/kimi-code/src/tui/constant/clipboard-image-hint.ts index 922e30fdda..eccb3f3fb4 100644 --- a/apps/kimi-code/src/tui/constant/clipboard-image-hint.ts +++ b/apps/kimi-code/src/tui/constant/clipboard-image-hint.ts @@ -1,4 +1,3 @@ // Timing constants for the clipboard-image hint controller. export const FOCUS_DEBOUNCE_MS = 1_000; -export const HINT_COOLDOWN_MS = 30_000; export const HINT_DISPLAY_MS = 4_000; diff --git a/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts b/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts index 2999845d62..ac43859678 100644 --- a/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts +++ b/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts @@ -2,11 +2,7 @@ import type { TUI } from '@earendil-works/pi-tui'; import { clipboardHasImage } from '#/utils/clipboard/clipboard-has-image'; -import { - FOCUS_DEBOUNCE_MS, - HINT_COOLDOWN_MS, - HINT_DISPLAY_MS, -} from '../constant/clipboard-image-hint'; +import { FOCUS_DEBOUNCE_MS, HINT_DISPLAY_MS } from '../constant/clipboard-image-hint'; import { TERMINAL_FOCUS_IN, TERMINAL_FOCUS_OUT } from '../utils/terminal-focus'; import type { FooterComponent } from '../components/chrome/footer'; @@ -26,10 +22,14 @@ export class ClipboardImageHintController { private disposeInputListener: (() => void) | undefined; private debounceTimer: ReturnType | undefined; private clearHintTimer: ReturnType | undefined; - private lastHintAtMs = 0; private lastHintText: string | undefined; private checkGeneration = 0; private focused = true; + // Whether a detected clipboard image is allowed to trigger a hint. Starts + // armed; after showing a hint for an image it disarms so the same lingering + // image does not nag on every focus. A focus check that finds the clipboard + // empty re-arms it, so the next genuinely new image notifies again. + private armed = true; constructor(host: ClipboardImageHintHost) { this.host = host; @@ -49,7 +49,7 @@ export class ClipboardImageHintController { this.checkGeneration += 1; this.clearOwnedHint(); - this.lastHintAtMs = 0; + this.armed = true; } private handleInput(data: string): void { @@ -97,7 +97,6 @@ export class ClipboardImageHintController { private async runCheck(generation: number): Promise { if (!this.focused) return; if (!this.host.getModelSupportsImage()) return; - if (Date.now() - this.lastHintAtMs < HINT_COOLDOWN_MS) return; let hasImage = false; try { @@ -108,14 +107,23 @@ export class ClipboardImageHintController { if (generation !== this.checkGeneration) return; if (!this.focused) return; - if (!hasImage) return; + + if (!hasImage) { + // Clipboard holds no image, so the next image that appears is a new one + // worth notifying about. Re-arm and bail out. + this.armed = true; + return; + } + + // Same image we already notified about — stay quiet until it changes. + if (!this.armed) return; const hintText = `Image in clipboard · ${getPasteImageShortcut()} to paste`; this.clearClearHintTimer(); this.lastHintText = hintText; + this.armed = false; this.host.footer.setTransientHint(hintText); this.host.requestRender(); - this.lastHintAtMs = Date.now(); this.clearHintTimer = setTimeout(() => { this.clearOwnedHint(); diff --git a/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts b/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts index 435d1263b7..f81c8c6782 100644 --- a/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts +++ b/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts @@ -135,7 +135,7 @@ describe('ClipboardImageHintController', () => { controller.stop(); }); - it('respects cooldown between hints', async () => { + it('does not repeat the hint for the same lingering image', async () => { vi.mocked(clipboardHasImage).mockResolvedValue(true); const footer = createFakeFooter(); @@ -152,14 +152,55 @@ describe('ClipboardImageHintController', () => { ui.emitInput(TERMINAL_FOCUS_IN); await vi.advanceTimersByTimeAsync(1000); - expect(footer.getTransientHint()).not.toBeNull(); + expect(footer.getTransientHint()).toMatch(/Image in clipboard/); + expect(clipboardHasImage).toHaveBeenCalledTimes(1); + + // The same image is still in the clipboard: focusing again must not nag. + footer.setTransientHint(null); + ui.emitInput(TERMINAL_FOCUS_OUT); + ui.emitInput(TERMINAL_FOCUS_IN); + await vi.advanceTimersByTimeAsync(1000); + expect(footer.getTransientHint()).toBeNull(); + expect(clipboardHasImage).toHaveBeenCalledTimes(2); + + controller.stop(); + }); + + it('shows the hint again for a new image after the clipboard is cleared', async () => { + vi.mocked(clipboardHasImage).mockResolvedValue(true); + + const footer = createFakeFooter(); + const ui = createFakeTUI(); + const host: ClipboardImageHintHost = { + ui, + footer, + getModelSupportsImage: () => true, + requestRender: vi.fn(), + }; + const controller = new ClipboardImageHintController(host); + controller.start(); + + // First image: hint shows and the controller disarms. + ui.emitInput(TERMINAL_FOCUS_IN); + await vi.advanceTimersByTimeAsync(1000); + expect(footer.getTransientHint()).toMatch(/Image in clipboard/); + + // Clipboard cleared: the empty check re-arms the controller. + vi.mocked(clipboardHasImage).mockResolvedValue(false); footer.setTransientHint(null); ui.emitInput(TERMINAL_FOCUS_OUT); ui.emitInput(TERMINAL_FOCUS_IN); await vi.advanceTimersByTimeAsync(1000); expect(footer.getTransientHint()).toBeNull(); + // A genuinely new image: hint shows again. + vi.mocked(clipboardHasImage).mockResolvedValue(true); + ui.emitInput(TERMINAL_FOCUS_OUT); + ui.emitInput(TERMINAL_FOCUS_IN); + await vi.advanceTimersByTimeAsync(1000); + expect(footer.getTransientHint()).toMatch(/Image in clipboard/); + controller.stop(); }); @@ -459,7 +500,7 @@ describe('ClipboardImageHintController', () => { expect(footer.getTransientHint()).toBe(hintText); }); - it('does not inherit cooldown after stop and restart', async () => { + it('does not inherit disarmed state after stop and restart', async () => { vi.mocked(clipboardHasImage).mockResolvedValue(true); const footer = createFakeFooter(); From 077ca313b6b990bc9c68f4951389dca69c965df8 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 24 Jun 2026 18:50:46 +0800 Subject: [PATCH 2/6] fix(kimi-code): suppress clipboard image hint for images present at startup The footer hint fired during initialization whenever an image was already in the clipboard, treating it as new. The first clipboard observation after start now only establishes a baseline, so only images copied during the session trigger the hint. --- .changeset/clipboard-image-hint-once.md | 2 +- .../tui/controllers/clipboard-image-hint.ts | 23 ++- .../controllers/clipboard-image-hint.test.ts | 186 +++++++++++------- 3 files changed, 136 insertions(+), 75 deletions(-) diff --git a/.changeset/clipboard-image-hint-once.md b/.changeset/clipboard-image-hint-once.md index c6daa01152..81f1dca951 100644 --- a/.changeset/clipboard-image-hint-once.md +++ b/.changeset/clipboard-image-hint-once.md @@ -2,4 +2,4 @@ "@moonshot-ai/kimi-code": patch --- -Show the clipboard image paste hint only once per image, instead of repeating it on every terminal focus. +Show the clipboard image paste hint only when a new image is copied during the session, instead of repeating it on every terminal focus or showing it for images already in the clipboard at startup. diff --git a/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts b/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts index ac43859678..fed798bf90 100644 --- a/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts +++ b/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts @@ -25,10 +25,15 @@ export class ClipboardImageHintController { private lastHintText: string | undefined; private checkGeneration = 0; private focused = true; - // Whether a detected clipboard image is allowed to trigger a hint. Starts - // armed; after showing a hint for an image it disarms so the same lingering - // image does not nag on every focus. A focus check that finds the clipboard - // empty re-arms it, so the next genuinely new image notifies again. + // Whether the controller has completed its first clipboard observation since + // start. The first observation only establishes a baseline: an image already + // in the clipboard when the session starts is not "new", so it must not + // trigger a hint during initialization. + private initialized = false; + // Whether a detected clipboard image is allowed to trigger a hint. After + // showing a hint for an image it disarms so the same lingering image does + // not nag on every focus. A focus check that finds the clipboard empty + // re-arms it, so the next genuinely new image notifies again. private armed = true; constructor(host: ClipboardImageHintHost) { @@ -49,6 +54,7 @@ export class ClipboardImageHintController { this.checkGeneration += 1; this.clearOwnedHint(); + this.initialized = false; this.armed = true; } @@ -108,6 +114,15 @@ export class ClipboardImageHintController { if (generation !== this.checkGeneration) return; if (!this.focused) return; + // First observation after start only establishes the baseline. An image + // already in the clipboard when the session began is not "new", so we + // record the state and stay quiet instead of nagging during initialization. + if (!this.initialized) { + this.initialized = true; + this.armed = !hasImage; + return; + } + if (!hasImage) { // Clipboard holds no image, so the next image that appears is a new one // worth notifying about. Re-arm and bail out. diff --git a/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts b/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts index f81c8c6782..911f8e3c82 100644 --- a/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts +++ b/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts @@ -71,6 +71,22 @@ function createFakeTUIWithConsumingFocusTracker(): FakeTUI { } as unknown as FakeTUI; } +// Drive the controller through its first clipboard observation with an empty +// clipboard. The first observation only establishes a baseline and never shows +// a hint, leaving the controller armed and ready for the next new image. +async function primeEmptyBaseline(ui: FakeTUI): Promise { + vi.mocked(clipboardHasImage).mockResolvedValue(false); + ui.emitInput(TERMINAL_FOCUS_IN); + await vi.advanceTimersByTimeAsync(1000); +} + +// Simulate the user returning to the terminal and let the debounced check fire. +async function focusReturnAndFlush(ui: FakeTUI): Promise { + ui.emitInput(TERMINAL_FOCUS_OUT); + ui.emitInput(TERMINAL_FOCUS_IN); + await vi.advanceTimersByTimeAsync(1000); +} + describe('ClipboardImageHintController', () => { let platformSpy: ReturnType | undefined; @@ -86,7 +102,7 @@ describe('ClipboardImageHintController', () => { vi.useRealTimers(); }); - it('shows hint when focus returns and clipboard has image', async () => { + it('does not show a hint for an image already in the clipboard at startup', async () => { vi.mocked(clipboardHasImage).mockResolvedValue(true); const footer = createFakeFooter(); @@ -101,11 +117,35 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); - ui.emitInput(TERMINAL_FOCUS_OUT); + // First focus with an image already present: the check runs but only + // establishes the baseline, so no hint is shown during initialization. ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + expect(clipboardHasImage).toHaveBeenCalledTimes(1); + expect(footer.getTransientHint()).toBeNull(); + + controller.stop(); + }); + + it('shows hint when a new image is copied during the session', async () => { + const footer = createFakeFooter(); + const ui = createFakeTUI(); + const host: ClipboardImageHintHost = { + ui, + footer, + getModelSupportsImage: () => true, + requestRender: vi.fn(), + }; + + const controller = new ClipboardImageHintController(host); + controller.start(); + + await primeEmptyBaseline(ui); + + vi.mocked(clipboardHasImage).mockResolvedValue(true); + await focusReturnAndFlush(ui); + expect(footer.getTransientHint()).toMatch(/Image in clipboard/); expect(footer.getTransientHint()).toMatch(/Ctrl\+V/); @@ -136,8 +176,6 @@ describe('ClipboardImageHintController', () => { }); it('does not repeat the hint for the same lingering image', async () => { - vi.mocked(clipboardHasImage).mockResolvedValue(true); - const footer = createFakeFooter(); const ui = createFakeTUI(); const host: ClipboardImageHintHost = { @@ -150,25 +188,23 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + // Establish the baseline and show a hint for the first image. + await primeEmptyBaseline(ui); + vi.mocked(clipboardHasImage).mockResolvedValue(true); + await focusReturnAndFlush(ui); expect(footer.getTransientHint()).toMatch(/Image in clipboard/); - expect(clipboardHasImage).toHaveBeenCalledTimes(1); // The same image is still in the clipboard: focusing again must not nag. + vi.mocked(clipboardHasImage).mockClear(); footer.setTransientHint(null); - ui.emitInput(TERMINAL_FOCUS_OUT); - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + await focusReturnAndFlush(ui); expect(footer.getTransientHint()).toBeNull(); - expect(clipboardHasImage).toHaveBeenCalledTimes(2); + expect(clipboardHasImage).toHaveBeenCalledTimes(1); controller.stop(); }); it('shows the hint again for a new image after the clipboard is cleared', async () => { - vi.mocked(clipboardHasImage).mockResolvedValue(true); - const footer = createFakeFooter(); const ui = createFakeTUI(); const host: ClipboardImageHintHost = { @@ -181,32 +217,27 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); - // First image: hint shows and the controller disarms. - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + // Establish the baseline, then show a hint for the first image. + await primeEmptyBaseline(ui); + vi.mocked(clipboardHasImage).mockResolvedValue(true); + await focusReturnAndFlush(ui); expect(footer.getTransientHint()).toMatch(/Image in clipboard/); // Clipboard cleared: the empty check re-arms the controller. vi.mocked(clipboardHasImage).mockResolvedValue(false); footer.setTransientHint(null); - ui.emitInput(TERMINAL_FOCUS_OUT); - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + await focusReturnAndFlush(ui); expect(footer.getTransientHint()).toBeNull(); // A genuinely new image: hint shows again. vi.mocked(clipboardHasImage).mockResolvedValue(true); - ui.emitInput(TERMINAL_FOCUS_OUT); - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + await focusReturnAndFlush(ui); expect(footer.getTransientHint()).toMatch(/Image in clipboard/); controller.stop(); }); - it('clears hint after 2 seconds', async () => { - vi.mocked(clipboardHasImage).mockResolvedValue(true); - + it('clears the hint after the display duration', async () => { const footer = createFakeFooter(); const ui = createFakeTUI(); const host: ClipboardImageHintHost = { @@ -219,8 +250,9 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + await primeEmptyBaseline(ui); + vi.mocked(clipboardHasImage).mockResolvedValue(true); + await focusReturnAndFlush(ui); expect(footer.getTransientHint()).not.toBeNull(); await vi.advanceTimersByTimeAsync(4000); @@ -255,8 +287,6 @@ describe('ClipboardImageHintController', () => { }); it('handles rapid focus churn without duplicate checks or hints', async () => { - vi.mocked(clipboardHasImage).mockResolvedValue(true); - const footer = createFakeFooter(); const ui = createFakeTUI(); const host: ClipboardImageHintHost = { @@ -269,6 +299,10 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); + await primeEmptyBaseline(ui); + vi.mocked(clipboardHasImage).mockResolvedValue(true); + vi.mocked(clipboardHasImage).mockClear(); + for (let i = 0; i < 5; i++) { ui.emitInput(TERMINAL_FOCUS_OUT); ui.emitInput(TERMINAL_FOCUS_IN); @@ -341,8 +375,6 @@ describe('ClipboardImageHintController', () => { }); it('clears a displayed hint when stopped', async () => { - vi.mocked(clipboardHasImage).mockResolvedValue(true); - const footer = createFakeFooter(); const ui = createFakeTUI(); const host: ClipboardImageHintHost = { @@ -355,8 +387,9 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + await primeEmptyBaseline(ui); + vi.mocked(clipboardHasImage).mockResolvedValue(true); + await focusReturnAndFlush(ui); expect(footer.getTransientHint()).not.toBeNull(); controller.stop(); @@ -380,6 +413,7 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); + // First observation only establishes the baseline and sets no hint. ui.emitInput(TERMINAL_FOCUS_IN); await vi.advanceTimersByTimeAsync(1000); const otherHint = 'Other hint'; @@ -392,16 +426,6 @@ describe('ClipboardImageHintController', () => { }); it('uses only the latest clipboard read result after focus churn', async () => { - const deferreds: Array<{ resolve: (value: boolean) => void; promise: Promise }> = []; - vi.mocked(clipboardHasImage).mockImplementation(() => { - let resolve: (value: boolean) => void = () => {}; - const promise = new Promise((res) => { - resolve = res; - }); - deferreds.push({ resolve, promise }); - return promise; - }); - const footer = createFakeFooter(); const ui = createFakeTUI(); const host: ClipboardImageHintHost = { @@ -414,14 +438,28 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); + // Establish an empty baseline with a normal resolved read first. + await primeEmptyBaseline(ui); + + const deferreds: Array<{ resolve: (value: boolean) => void; promise: Promise }> = []; + vi.mocked(clipboardHasImage).mockImplementation(() => { + let resolve: (value: boolean) => void = () => {}; + const promise = new Promise((res) => { + resolve = res; + }); + deferreds.push({ resolve, promise }); + return promise; + }); + + ui.emitInput(TERMINAL_FOCUS_OUT); ui.emitInput(TERMINAL_FOCUS_IN); await vi.advanceTimersByTimeAsync(1000); - expect(clipboardHasImage).toHaveBeenCalledTimes(1); + expect(deferreds).toHaveLength(1); ui.emitInput(TERMINAL_FOCUS_OUT); ui.emitInput(TERMINAL_FOCUS_IN); await vi.advanceTimersByTimeAsync(1000); - expect(clipboardHasImage).toHaveBeenCalledTimes(2); + expect(deferreds).toHaveLength(2); deferreds[0]!.resolve(true); await vi.advanceTimersByTimeAsync(0); @@ -435,8 +473,6 @@ describe('ClipboardImageHintController', () => { }); it('keeps the existing auto-clear timer when a re-check exits early', async () => { - vi.mocked(clipboardHasImage).mockResolvedValue(true); - const footer = createFakeFooter(); const ui = createFakeTUI(); const host: ClipboardImageHintHost = { @@ -449,15 +485,14 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + await primeEmptyBaseline(ui); + vi.mocked(clipboardHasImage).mockResolvedValue(true); + await focusReturnAndFlush(ui); expect(footer.getTransientHint()).not.toBeNull(); // Trigger a re-check that exits early because the clipboard is now empty. vi.mocked(clipboardHasImage).mockResolvedValue(false); - ui.emitInput(TERMINAL_FOCUS_OUT); - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + await focusReturnAndFlush(ui); // The previous hint should still be visible because its auto-clear timer // was preserved through the re-check. @@ -471,8 +506,6 @@ describe('ClipboardImageHintController', () => { }); it('does not clear a matching hint owned by another caller after auto-clear', async () => { - vi.mocked(clipboardHasImage).mockResolvedValue(true); - const footer = createFakeFooter(); const ui = createFakeTUI(); const host: ClipboardImageHintHost = { @@ -485,8 +518,9 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + await primeEmptyBaseline(ui); + vi.mocked(clipboardHasImage).mockResolvedValue(true); + await focusReturnAndFlush(ui); const hintText = footer.getTransientHint(); expect(hintText).not.toBeNull(); @@ -500,7 +534,7 @@ describe('ClipboardImageHintController', () => { expect(footer.getTransientHint()).toBe(hintText); }); - it('does not inherit disarmed state after stop and restart', async () => { + it('re-establishes the baseline after stop and restart', async () => { vi.mocked(clipboardHasImage).mockResolvedValue(true); const footer = createFakeFooter(); @@ -515,26 +549,32 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); + // Image already present at start: baseline only, no hint. ui.emitInput(TERMINAL_FOCUS_IN); await vi.advanceTimersByTimeAsync(1000); - expect(footer.getTransientHint()).not.toBeNull(); + expect(footer.getTransientHint()).toBeNull(); controller.stop(); controller.start(); - footer.setTransientHint(null); - ui.emitInput(TERMINAL_FOCUS_OUT); - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + // After restart the image is still present: baseline again, no hint. + await focusReturnAndFlush(ui); + expect(footer.getTransientHint()).toBeNull(); - expect(footer.getTransientHint()).not.toBeNull(); + // Clipboard cleared: re-arms the controller. + vi.mocked(clipboardHasImage).mockResolvedValue(false); + await focusReturnAndFlush(ui); + expect(footer.getTransientHint()).toBeNull(); + + // A genuinely new image: hint shows. + vi.mocked(clipboardHasImage).mockResolvedValue(true); + await focusReturnAndFlush(ui); + expect(footer.getTransientHint()).toMatch(/Image in clipboard/); controller.stop(); }); it('observes focus events even when another listener consumes them', async () => { - vi.mocked(clipboardHasImage).mockResolvedValue(true); - const footer = createFakeFooter(); const ui = createFakeTUIWithConsumingFocusTracker(); const host: ClipboardImageHintHost = { @@ -557,11 +597,17 @@ describe('ClipboardImageHintController', () => { return undefined; }); + // Baseline observation (consumed), then a new image on the next focus. + vi.mocked(clipboardHasImage).mockResolvedValue(false); + ui.emitInput(TERMINAL_FOCUS_IN); + await vi.advanceTimersByTimeAsync(1000); + + vi.mocked(clipboardHasImage).mockResolvedValue(true); ui.emitInput(TERMINAL_FOCUS_OUT); ui.emitInput(TERMINAL_FOCUS_IN); await vi.advanceTimersByTimeAsync(1000); - expect(consumedEvents).toEqual([TERMINAL_FOCUS_OUT, TERMINAL_FOCUS_IN]); + expect(consumedEvents).toEqual([TERMINAL_FOCUS_IN, TERMINAL_FOCUS_OUT, TERMINAL_FOCUS_IN]); expect(footer.getTransientHint()).toMatch(/Image in clipboard/); controller.stop(); @@ -570,7 +616,6 @@ describe('ClipboardImageHintController', () => { it('shows Alt+V shortcut on Windows', async () => { platformSpy?.mockRestore(); vi.spyOn(process, 'platform', 'get').mockReturnValue('win32'); - vi.mocked(clipboardHasImage).mockResolvedValue(true); const footer = createFakeFooter(); const ui = createFakeTUI(); @@ -584,8 +629,9 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); - ui.emitInput(TERMINAL_FOCUS_IN); - await vi.advanceTimersByTimeAsync(1000); + await primeEmptyBaseline(ui); + vi.mocked(clipboardHasImage).mockResolvedValue(true); + await focusReturnAndFlush(ui); expect(footer.getTransientHint()).toMatch(/Alt\+V/); From ccf405532c200c8372f8ece77bb383c9f0315c32 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 24 Jun 2026 19:52:10 +0800 Subject: [PATCH 3/6] fix(kimi-code): show hint for first image copied after startup --- .../tui/controllers/clipboard-image-hint.ts | 20 +++++++++ .../controllers/clipboard-image-hint.test.ts | 42 ++++++++++++++++--- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts b/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts index fed798bf90..ac1fd8c119 100644 --- a/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts +++ b/apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts @@ -44,6 +44,7 @@ export class ClipboardImageHintController { this.disposeInputListener = this.host.ui.addInputListener((data) => { this.handleInput(data); }); + void this.establishInitialBaseline(); } stop(): void { @@ -100,6 +101,25 @@ export class ClipboardImageHintController { this.lastHintText = undefined; } + private async establishInitialBaseline(): Promise { + if (!this.host.getModelSupportsImage()) return; + + this.checkGeneration += 1; + const generation = this.checkGeneration; + + let hasImage = false; + try { + hasImage = await clipboardHasImage(); + } catch { + return; + } + + if (generation !== this.checkGeneration) return; + + this.initialized = true; + this.armed = !hasImage; + } + private async runCheck(generation: number): Promise { if (!this.focused) return; if (!this.host.getModelSupportsImage()) return; diff --git a/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts b/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts index 911f8e3c82..0c69ebac94 100644 --- a/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts +++ b/apps/kimi-code/test/tui/controllers/clipboard-image-hint.test.ts @@ -117,12 +117,13 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); - // First focus with an image already present: the check runs but only - // establishes the baseline, so no hint is shown during initialization. + // Startup baseline observes the image already present; the focus check + // runs too but must stay quiet for that same image. ui.emitInput(TERMINAL_FOCUS_IN); await vi.advanceTimersByTimeAsync(1000); - expect(clipboardHasImage).toHaveBeenCalledTimes(1); + // One call is the startup baseline; the second is the focus check. + expect(clipboardHasImage).toHaveBeenCalledTimes(2); expect(footer.getTransientHint()).toBeNull(); controller.stop(); @@ -152,6 +153,32 @@ describe('ClipboardImageHintController', () => { controller.stop(); }); + it('shows hint for the first image copied after startup when startup baseline was empty', async () => { + const footer = createFakeFooter(); + const ui = createFakeTUI(); + const host: ClipboardImageHintHost = { + ui, + footer, + getModelSupportsImage: () => true, + requestRender: vi.fn(), + }; + + const controller = new ClipboardImageHintController(host); + controller.start(); + + await vi.advanceTimersByTimeAsync(0); + expect(clipboardHasImage).toHaveBeenCalledTimes(1); + expect(footer.getTransientHint()).toBeNull(); + + vi.mocked(clipboardHasImage).mockResolvedValue(true); + await focusReturnAndFlush(ui); + + expect(footer.getTransientHint()).toMatch(/Image in clipboard/); + expect(footer.getTransientHint()).toMatch(/Ctrl\+V/); + + controller.stop(); + }); + it('does not show hint when model does not support images', async () => { vi.mocked(clipboardHasImage).mockResolvedValue(true); @@ -276,6 +303,9 @@ describe('ClipboardImageHintController', () => { const controller = new ClipboardImageHintController(host); controller.start(); + await vi.advanceTimersByTimeAsync(0); + vi.mocked(clipboardHasImage).mockClear(); + ui.emitInput(TERMINAL_FOCUS_IN); ui.emitInput(TERMINAL_FOCUS_OUT); await vi.advanceTimersByTimeAsync(1000); @@ -335,7 +365,8 @@ describe('ClipboardImageHintController', () => { ui.emitInput(TERMINAL_FOCUS_IN); await vi.advanceTimersByTimeAsync(1000); - expect(clipboardHasImage).toHaveBeenCalledTimes(1); + // One call is the startup baseline; the second is the debounced focus check. + expect(clipboardHasImage).toHaveBeenCalledTimes(2); ui.emitInput(TERMINAL_FOCUS_OUT); await vi.advanceTimersByTimeAsync(1500); @@ -366,7 +397,8 @@ describe('ClipboardImageHintController', () => { ui.emitInput(TERMINAL_FOCUS_IN); await vi.advanceTimersByTimeAsync(1000); - expect(clipboardHasImage).toHaveBeenCalledTimes(1); + // One call is the startup baseline; the second is the debounced focus check. + expect(clipboardHasImage).toHaveBeenCalledTimes(2); controller.stop(); resolveDeferred(true); From 38b864726a6651cfa85e3aec08af717e419bebc4 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 24 Jun 2026 20:47:26 +0800 Subject: [PATCH 4/6] fix(kimi-code): make clipboard image probe non-blocking The startup baseline probe in ClipboardImageHintController calls clipboardHasImage(), which on Linux/WSL ran wl-paste/xclip/powershell via spawnSync. The probe only reaches its first await after those synchronous calls, so a slow or wedged helper could freeze the TUI launch for up to the 1s-2s tool timeouts even when the user never focuses with an image. Add an async runCommandAsync built on spawn with timeout-based kill, and route the Linux/WSL image detection through it so the event loop is never blocked. Keep the synchronous runCommand for the explicit paste-read path. --- .../src/utils/clipboard/clipboard-common.ts | 75 ++++++++++++++++++- .../utils/clipboard/clipboard-has-image.ts | 32 ++++---- .../utils/clipboard/clipboard-common.test.ts | 30 ++++++++ .../clipboard/clipboard-has-image.test.ts | 28 +++---- 4 files changed, 134 insertions(+), 31 deletions(-) create mode 100644 apps/kimi-code/test/utils/clipboard/clipboard-common.test.ts diff --git a/apps/kimi-code/src/utils/clipboard/clipboard-common.ts b/apps/kimi-code/src/utils/clipboard/clipboard-common.ts index 9844dc4fda..dc0f60886e 100644 --- a/apps/kimi-code/src/utils/clipboard/clipboard-common.ts +++ b/apps/kimi-code/src/utils/clipboard/clipboard-common.ts @@ -1,5 +1,5 @@ import { readFileSync } from 'node:fs'; -import { spawnSync } from 'node:child_process'; +import { spawn, spawnSync } from 'node:child_process'; import type { ClipboardModule } from './clipboard-native'; @@ -9,6 +9,11 @@ export type RunCommand = ( args: string[], options?: RunCommandOptions, ) => { stdout: Buffer; ok: boolean }; +export type RunCommandAsync = ( + command: string, + args: string[], + options?: RunCommandOptions, +) => Promise<{ stdout: Buffer; ok: boolean }>; export const SUPPORTED_IMAGE_MIME_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'] as const; @@ -49,6 +54,74 @@ export function runCommand( return { ok: true, stdout }; } +/** + * Non-blocking counterpart of `runCommand`. Used by the clipboard image probe + * on the startup path so a slow or wedged helper (notably `powershell.exe` on + * WSL, or a stuck `wl-paste`/`xclip`) cannot freeze the event loop. The child + * is killed and the promise resolves with `ok: false` once `timeoutMs` elapses + * or the captured stdout exceeds `DEFAULT_MAX_BUFFER_BYTES`. + */ +export function runCommandAsync( + command: string, + args: string[], + options?: RunCommandOptions, +): Promise<{ stdout: Buffer; ok: boolean }> { + const timeoutMs = options?.timeoutMs ?? DEFAULT_LIST_TIMEOUT_MS; + return new Promise((resolve) => { + let child; + try { + child = spawn(command, args, { + env: options?.env, + stdio: ['ignore', 'pipe', 'ignore'], + }); + } catch { + resolve({ ok: false, stdout: Buffer.alloc(0) }); + return; + } + + const chunks: Buffer[] = []; + let totalBytes = 0; + let settled = false; + let timer: ReturnType; + + // Marks the promise as settled and clears the timeout. Returns true only for + // the first caller, so each event handler below resolves at most once. + const claim = (): boolean => { + if (settled) return false; + settled = true; + clearTimeout(timer); + return true; + }; + + timer = setTimeout(() => { + child.kill(); + if (claim()) resolve({ ok: false, stdout: Buffer.alloc(0) }); + }, timeoutMs); + + child.stdout?.on('data', (chunk: Buffer) => { + totalBytes += chunk.length; + if (totalBytes > DEFAULT_MAX_BUFFER_BYTES) { + child.kill(); + if (claim()) resolve({ ok: false, stdout: Buffer.alloc(0) }); + return; + } + chunks.push(chunk); + }); + + child.on('error', () => { + if (claim()) resolve({ ok: false, stdout: Buffer.alloc(0) }); + }); + + child.on('close', (code) => { + if (code !== 0) { + if (claim()) resolve({ ok: false, stdout: Buffer.alloc(0) }); + return; + } + if (claim()) resolve({ ok: true, stdout: Buffer.concat(chunks) }); + }); + }); +} + export function isWaylandSession(env: NodeJS.ProcessEnv): boolean { return Boolean(env['WAYLAND_DISPLAY']) || env['XDG_SESSION_TYPE'] === 'wayland'; } diff --git a/apps/kimi-code/src/utils/clipboard/clipboard-has-image.ts b/apps/kimi-code/src/utils/clipboard/clipboard-has-image.ts index 54f5f3be1c..dc696d31ce 100644 --- a/apps/kimi-code/src/utils/clipboard/clipboard-has-image.ts +++ b/apps/kimi-code/src/utils/clipboard/clipboard-has-image.ts @@ -5,32 +5,32 @@ import { isWaylandSession, isWSL, parseTargetList, - runCommand, + runCommandAsync, safeAvailableFormats, - type RunCommand, + type RunCommandAsync, } from './clipboard-common'; import { clipboard, type ClipboardModule } from './clipboard-native'; const DEFAULT_POWERSHELL_TIMEOUT_MS = 2000; -function hasImageViaWlPaste(run: RunCommand): boolean { - const list = run('wl-paste', ['--list-types'], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS }); +async function hasImageViaWlPaste(run: RunCommandAsync): Promise { + const list = await run('wl-paste', ['--list-types'], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS }); if (!list.ok) return false; return parseTargetList(list.stdout).some((t) => isSupportedImageMimeType(t)); } -function hasImageViaXclip(run: RunCommand): boolean { - const targets = run('xclip', ['-selection', 'clipboard', '-t', 'TARGETS', '-o'], { +async function hasImageViaXclip(run: RunCommandAsync): Promise { + const targets = await run('xclip', ['-selection', 'clipboard', '-t', 'TARGETS', '-o'], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS, }); if (!targets.ok) return false; return parseTargetList(targets.stdout).some((t) => isSupportedImageMimeType(t)); } -function hasImageViaPowerShell(run: RunCommand): boolean { +async function hasImageViaPowerShell(run: RunCommandAsync): Promise { const script = "Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); ($img -ne $null)"; - const result = run('powershell.exe', ['-NoProfile', '-Command', script], { + const result = await run('powershell.exe', ['-NoProfile', '-Command', script], { timeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS, }); if (!result.ok) return false; @@ -58,12 +58,12 @@ export async function clipboardHasImage(options?: { env?: NodeJS.ProcessEnv; platform?: NodeJS.Platform; clipboard?: ClipboardModule | null; - runCommand?: RunCommand; + runCommand?: RunCommandAsync; }): Promise { const env = options?.env ?? process.env; const platform = options?.platform ?? process.platform; const clip = options?.clipboard ?? clipboard; - const run = options?.runCommand ?? runCommand; + const run = options?.runCommand ?? runCommandAsync; if (env['TERMUX_VERSION'] !== undefined) return false; @@ -71,19 +71,19 @@ export async function clipboardHasImage(options?: { const wayland = isWaylandSession(env); const wsl = isWSL(env); - let xclipResult: boolean | undefined; - const xclipHasImage = (): boolean => { + let xclipResult: Promise | undefined; + const xclipHasImage = (): Promise => { xclipResult ??= hasImageViaXclip(run); return xclipResult; }; if (wayland || wsl) { - if (hasImageViaWlPaste(run)) return true; - if (xclipHasImage()) return true; + if (await hasImageViaWlPaste(run)) return true; + if (await xclipHasImage()) return true; } - if (wsl && hasImageViaPowerShell(run)) return true; + if (wsl && (await hasImageViaPowerShell(run))) return true; if (!wayland) { - if (xclipHasImage()) return true; + if (await xclipHasImage()) return true; if (await hasImageViaNative(clip)) return true; } return false; diff --git a/apps/kimi-code/test/utils/clipboard/clipboard-common.test.ts b/apps/kimi-code/test/utils/clipboard/clipboard-common.test.ts new file mode 100644 index 0000000000..c6aba57a4b --- /dev/null +++ b/apps/kimi-code/test/utils/clipboard/clipboard-common.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; + +import { runCommandAsync } from '#/utils/clipboard/clipboard-common'; + +describe('runCommandAsync', () => { + it('resolves with stdout for a successful command', async () => { + const result = await runCommandAsync(process.execPath, ['-e', 'process.stdout.write("hello")']); + expect(result.ok).toBe(true); + expect(result.stdout.toString('utf-8')).toBe('hello'); + }); + + it('resolves ok:false for a non-zero exit', async () => { + const result = await runCommandAsync(process.execPath, ['-e', 'process.exit(3)']); + expect(result.ok).toBe(false); + }); + + it('does not block when the command exceeds the timeout', async () => { + const timeoutMs = 100; + const start = Date.now(); + // The child would idle for 30s if left running; runCommandAsync must kill + // it and resolve well before that so a wedged helper cannot freeze launch. + const result = await runCommandAsync(process.execPath, ['-e', 'setTimeout(() => {}, 30000)'], { + timeoutMs, + }); + const elapsed = Date.now() - start; + + expect(result.ok).toBe(false); + expect(elapsed).toBeLessThan(5000); + }); +}); diff --git a/apps/kimi-code/test/utils/clipboard/clipboard-has-image.test.ts b/apps/kimi-code/test/utils/clipboard/clipboard-has-image.test.ts index 5aeb0f1379..16edebb2fd 100644 --- a/apps/kimi-code/test/utils/clipboard/clipboard-has-image.test.ts +++ b/apps/kimi-code/test/utils/clipboard/clipboard-has-image.test.ts @@ -25,7 +25,7 @@ describe('clipboardHasImage', () => { it('returns false on macOS when native clipboard reports no image', async () => { const clip = fakeClipboard({ hasImage: vi.fn(() => false) }); - const runCommand = vi.fn(() => ({ stdout: Buffer.alloc(0), ok: false })); + const runCommand = vi.fn(async () => ({ stdout: Buffer.alloc(0), ok: false })); const result = await clipboardHasImage({ platform: 'darwin', clipboard: clip, runCommand }); expect(result).toBe(false); expect(runCommand).not.toHaveBeenCalledWith('osascript', expect.anything(), expect.anything()); @@ -52,7 +52,7 @@ describe('clipboardHasImage', () => { }); it('detects image on Wayland via wl-paste list-types', async () => { - const runCommand = vi.fn((command: string, args: string[]) => { + const runCommand = vi.fn(async (command: string, args: string[]) => { if (command === 'wl-paste' && args[0] === '--list-types') { return { stdout: Buffer.from('text/plain\nimage/png\n'), ok: true }; } @@ -63,7 +63,7 @@ describe('clipboardHasImage', () => { }); it('returns false on Wayland when target list contains unsupported MIME types', async () => { - const runCommand = vi.fn((command: string, args: string[]) => { + const runCommand = vi.fn(async (command: string, args: string[]) => { if (command === 'wl-paste' && args[0] === '--list-types') { return { stdout: Buffer.from('text/plain\nimage/bmp\n'), ok: true }; } @@ -80,7 +80,7 @@ describe('clipboardHasImage', () => { }); it('falls back to xclip on Wayland when wl-paste reports no image', async () => { - const runCommand = vi.fn((command: string, args: string[]) => { + const runCommand = vi.fn(async (command: string, args: string[]) => { if (command === 'wl-paste' && args[0] === '--list-types') { return { stdout: Buffer.from('text/plain\n'), ok: true }; } @@ -99,7 +99,7 @@ describe('clipboardHasImage', () => { }); it('detects image on X11 via xclip TARGETS', async () => { - const runCommand = vi.fn((command: string, args: string[]) => { + const runCommand = vi.fn(async (command: string, args: string[]) => { if (command === 'xclip' && args.includes('TARGETS')) { return { stdout: Buffer.from('TARGETS\nimage/jpeg\n'), ok: true }; } @@ -110,7 +110,7 @@ describe('clipboardHasImage', () => { }); it('returns false on X11 when target list contains unsupported MIME types', async () => { - const runCommand = vi.fn((command: string, args: string[]) => { + const runCommand = vi.fn(async (command: string, args: string[]) => { if (command === 'xclip' && args.includes('TARGETS')) { return { stdout: Buffer.from('TARGETS\nimage/tiff\n'), ok: true }; } @@ -122,7 +122,7 @@ describe('clipboardHasImage', () => { }); it('returns false on X11 when target list is empty', async () => { - const runCommand = vi.fn((command: string, args: string[]) => { + const runCommand = vi.fn(async (command: string, args: string[]) => { if (command === 'xclip' && args.includes('TARGETS')) { return { stdout: Buffer.from('TARGETS\n'), ok: true }; } @@ -135,20 +135,20 @@ describe('clipboardHasImage', () => { it('falls back to native hasImage on Linux X11 when xclip fails', async () => { const clip = fakeClipboard({ hasImage: vi.fn(() => true) }); - const runCommand = vi.fn(() => ({ stdout: Buffer.alloc(0), ok: false })); + const runCommand = vi.fn(async () => ({ stdout: Buffer.alloc(0), ok: false })); const result = await clipboardHasImage({ platform: 'linux', env: {}, clipboard: clip, runCommand }); expect(result).toBe(true); }); it('returns false on Linux X11 when xclip and native both fail', async () => { const clip = fakeClipboard({ hasImage: vi.fn(() => false) }); - const runCommand = vi.fn(() => ({ stdout: Buffer.alloc(0), ok: false })); + const runCommand = vi.fn(async () => ({ stdout: Buffer.alloc(0), ok: false })); const result = await clipboardHasImage({ platform: 'linux', env: {}, clipboard: clip, runCommand }); expect(result).toBe(false); }); it('detects WSL via WSL_DISTRO_NAME and checks PowerShell', async () => { - const runCommand = vi.fn((command: string) => { + const runCommand = vi.fn(async (command: string) => { if (command === 'powershell.exe') { return { stdout: Buffer.from('True\n'), ok: true }; } @@ -163,7 +163,7 @@ describe('clipboardHasImage', () => { }); it('detects WSL via WSLENV and checks PowerShell', async () => { - const runCommand = vi.fn((command: string) => { + const runCommand = vi.fn(async (command: string) => { if (command === 'powershell.exe') { return { stdout: Buffer.from('True\n'), ok: true }; } @@ -178,7 +178,7 @@ describe('clipboardHasImage', () => { }); it('returns false on Linux when runCommand fails for all fallbacks', async () => { - const runCommand = vi.fn(() => ({ stdout: Buffer.alloc(0), ok: false })); + const runCommand = vi.fn(async () => ({ stdout: Buffer.alloc(0), ok: false })); const clip = fakeClipboard({ hasImage: vi.fn(() => false) }); const result = await clipboardHasImage({ platform: 'linux', env: {}, runCommand, clipboard: clip }); expect(result).toBe(false); @@ -186,7 +186,7 @@ describe('clipboardHasImage', () => { it('detects image on Windows via native clipboard', async () => { const clip = fakeClipboard({ hasImage: vi.fn(() => true) }); - const runCommand = vi.fn(() => ({ stdout: Buffer.alloc(0), ok: false })); + const runCommand = vi.fn(async () => ({ stdout: Buffer.alloc(0), ok: false })); const result = await clipboardHasImage({ platform: 'win32', clipboard: clip, runCommand }); expect(result).toBe(true); expect(runCommand).not.toHaveBeenCalledWith('powershell.exe', expect.anything(), expect.anything()); @@ -194,7 +194,7 @@ describe('clipboardHasImage', () => { it('returns false on Windows when native clipboard reports no image', async () => { const clip = fakeClipboard({ hasImage: vi.fn(() => false) }); - const runCommand = vi.fn((command: string) => { + const runCommand = vi.fn(async (command: string) => { if (command === 'powershell.exe') { return { stdout: Buffer.from('True\n'), ok: true }; } From 253c2f7f4b79e2991b2aee7b9aece0e99e9dfc67 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 24 Jun 2026 20:50:18 +0800 Subject: [PATCH 5/6] chore: add changeset for non-blocking clipboard probe --- .changeset/clipboard-startup-nonblocking.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clipboard-startup-nonblocking.md diff --git a/.changeset/clipboard-startup-nonblocking.md b/.changeset/clipboard-startup-nonblocking.md new file mode 100644 index 0000000000..daad13c93e --- /dev/null +++ b/.changeset/clipboard-startup-nonblocking.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix a launch freeze on Linux and WSL caused by slow clipboard image detection. From ac2be3659b35a8624e7ca9403704d9b50a356755 Mon Sep 17 00:00:00 2001 From: liruifengv Date: Wed, 24 Jun 2026 20:57:19 +0800 Subject: [PATCH 6/6] chore: simplify clipboard image hint changeset --- .changeset/clipboard-image-hint-once.md | 2 +- .changeset/clipboard-startup-nonblocking.md | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 .changeset/clipboard-startup-nonblocking.md diff --git a/.changeset/clipboard-image-hint-once.md b/.changeset/clipboard-image-hint-once.md index 81f1dca951..2e14713e4c 100644 --- a/.changeset/clipboard-image-hint-once.md +++ b/.changeset/clipboard-image-hint-once.md @@ -2,4 +2,4 @@ "@moonshot-ai/kimi-code": patch --- -Show the clipboard image paste hint only when a new image is copied during the session, instead of repeating it on every terminal focus or showing it for images already in the clipboard at startup. +Improve the image paste hint. diff --git a/.changeset/clipboard-startup-nonblocking.md b/.changeset/clipboard-startup-nonblocking.md deleted file mode 100644 index daad13c93e..0000000000 --- a/.changeset/clipboard-startup-nonblocking.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@moonshot-ai/kimi-code": patch ---- - -Fix a launch freeze on Linux and WSL caused by slow clipboard image detection.