-
Notifications
You must be signed in to change notification settings - Fork 929
feat(kimi-code): show clipboard image paste hint in footer #1028
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
809ad2c
feat(kimi-code): add lightweight clipboard image detection
liruifengv 65f78b6
fix(clipboard): correct Linux X11 image detection and extract shared …
liruifengv f39a6ec
fix(kimi-code): restore Wayland/WSL xclip fallback in clipboard image…
liruifengv 3ff75ca
feat(kimi-code): add clipboard image hint controller
liruifengv 307f6e1
fix(kimi-code): clipboard image hint focus race and cleanup
liruifengv 6f348f0
fix(kimi-code): prevent clipboard image hint from clearing unrelated …
liruifengv 1e947af
fix(clipboard-image-hint): lifecycle issues and platform-dependent tests
liruifengv 0b89ce0
fix(kimi-code): invalidate pending clipboard hint read on stop
liruifengv 205bef4
feat(kimi-code): wire clipboard image hint controller into TUI
liruifengv 4ce6274
style(kimi-code): wrap void expression in braces to fix lint warning
liruifengv 92815f8
style(kimi-code): prefer nullish coalescing in clipboard image detection
liruifengv f84f035
chore: add changeset for clipboard image footer hint
liruifengv b78be6f
fix(kimi-code): let clipboard image hint observe non-consuming focus …
liruifengv e3d9c1b
fix(kimi-code): extend clipboard image hint display duration to 4 sec…
liruifengv 0fd50dc
docs: mention clipboard image footer hint in interaction guide
liruifengv ea303a0
chore: downgrade clipboard image hint changeset to patch
liruifengv 3a38cab
Revert "docs: mention clipboard image footer hint in interaction guide"
liruifengv 5191221
fix(cli): avoid treating copied Finder files as images on macOS
liruifengv 3ece4f7
Merge branch 'main' into feat/clipboard-image-footer-hint
liruifengv f9544bf
fix(cli): align image detection with paste path on macOS and Windows
liruifengv b4c625b
Merge branch 'main' into feat/clipboard-image-footer-hint
liruifengv 5a6d21d
fix(tui): do not truncate inline image escape sequences
liruifengv 5f43623
fix(tui): clear stale rows when content shrinks
liruifengv 511c80d
chore: add changesets for inline image rendering fixes
liruifengv 313664b
Merge branch 'main' into feat/clipboard-image-footer-hint
liruifengv 9ee0c43
test(cli): stabilize pi-tui capability mocks in concurrent test runs
liruifengv 8dd8f80
test(cli): use setCapabilities instead of mocked getCapabilities
liruifengv abe011e
Merge branch 'main' into feat/clipboard-image-footer-hint
liruifengv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Show a transient footer hint when an image is detected in the clipboard, displaying the platform-appropriate paste shortcut. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Fix stale rows occasionally leaving duplicate input boxes after tall content shrinks. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Fix inline images being rendered as broken escape sequences in the transcript. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| // 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; |
124 changes: 124 additions & 0 deletions
124
apps/kimi-code/src/tui/controllers/clipboard-image-hint.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| 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 { TERMINAL_FOCUS_IN, TERMINAL_FOCUS_OUT } from '../utils/terminal-focus'; | ||
| import type { FooterComponent } from '../components/chrome/footer'; | ||
|
|
||
| export interface ClipboardImageHintHost { | ||
| readonly ui: TUI; | ||
| readonly footer: FooterComponent; | ||
| getModelSupportsImage(): boolean; | ||
| requestRender(): void; | ||
| } | ||
|
|
||
| function getPasteImageShortcut(): string { | ||
| return process.platform === 'win32' ? 'Alt+V' : 'Ctrl+V'; | ||
| } | ||
|
|
||
| export class ClipboardImageHintController { | ||
| private readonly host: ClipboardImageHintHost; | ||
| private disposeInputListener: (() => void) | undefined; | ||
| private debounceTimer: ReturnType<typeof setTimeout> | undefined; | ||
| private clearHintTimer: ReturnType<typeof setTimeout> | undefined; | ||
| private lastHintAtMs = 0; | ||
| private lastHintText: string | undefined; | ||
| private checkGeneration = 0; | ||
| private focused = true; | ||
|
|
||
| constructor(host: ClipboardImageHintHost) { | ||
| this.host = host; | ||
| } | ||
|
|
||
| start(): void { | ||
| this.disposeInputListener = this.host.ui.addInputListener((data) => { | ||
| this.handleInput(data); | ||
| }); | ||
| } | ||
|
|
||
| stop(): void { | ||
| this.clearDebounceTimer(); | ||
| this.clearClearHintTimer(); | ||
| this.disposeInputListener?.(); | ||
| this.disposeInputListener = undefined; | ||
|
|
||
| this.checkGeneration += 1; | ||
| this.clearOwnedHint(); | ||
| this.lastHintAtMs = 0; | ||
| } | ||
|
|
||
| private handleInput(data: string): void { | ||
| if (data === TERMINAL_FOCUS_IN) { | ||
| this.focused = true; | ||
| this.scheduleCheck(); | ||
| return; | ||
| } | ||
| if (data === TERMINAL_FOCUS_OUT) { | ||
| this.focused = false; | ||
| this.clearDebounceTimer(); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| private scheduleCheck(): void { | ||
| this.clearDebounceTimer(); | ||
| this.checkGeneration += 1; | ||
| const generation = this.checkGeneration; | ||
| this.debounceTimer = setTimeout(() => void this.runCheck(generation), FOCUS_DEBOUNCE_MS); | ||
| } | ||
|
|
||
| private clearDebounceTimer(): void { | ||
| if (this.debounceTimer !== undefined) { | ||
| clearTimeout(this.debounceTimer); | ||
| this.debounceTimer = undefined; | ||
| } | ||
| } | ||
|
|
||
| private clearClearHintTimer(): void { | ||
| if (this.clearHintTimer !== undefined) { | ||
| clearTimeout(this.clearHintTimer); | ||
| this.clearHintTimer = undefined; | ||
| } | ||
| } | ||
|
|
||
| private clearOwnedHint(): void { | ||
| if (this.host.footer.getTransientHint() === this.lastHintText) { | ||
| this.host.footer.setTransientHint(null); | ||
| this.host.requestRender(); | ||
| } | ||
| this.lastHintText = undefined; | ||
| } | ||
|
|
||
| private async runCheck(generation: number): Promise<void> { | ||
| if (!this.focused) return; | ||
| if (!this.host.getModelSupportsImage()) return; | ||
| if (Date.now() - this.lastHintAtMs < HINT_COOLDOWN_MS) return; | ||
|
|
||
| let hasImage = false; | ||
| try { | ||
| hasImage = await clipboardHasImage(); | ||
| } catch { | ||
| return; | ||
| } | ||
|
|
||
| if (generation !== this.checkGeneration) return; | ||
| if (!this.focused) return; | ||
| if (!hasImage) return; | ||
|
|
||
| const hintText = `Image in clipboard · ${getPasteImageShortcut()} to paste`; | ||
| this.clearClearHintTimer(); | ||
| this.lastHintText = hintText; | ||
| this.host.footer.setTransientHint(hintText); | ||
| this.host.requestRender(); | ||
| this.lastHintAtMs = Date.now(); | ||
|
|
||
| this.clearHintTimer = setTimeout(() => { | ||
| this.clearOwnedHint(); | ||
| }, HINT_DISPLAY_MS); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { readFileSync } from 'node:fs'; | ||
| import { spawnSync } from 'node:child_process'; | ||
|
|
||
| import type { ClipboardModule } from './clipboard-native'; | ||
|
|
||
| export type RunCommandOptions = { timeoutMs?: number; env?: NodeJS.ProcessEnv }; | ||
| export type RunCommand = ( | ||
| command: string, | ||
| args: string[], | ||
| options?: RunCommandOptions, | ||
| ) => { stdout: Buffer; ok: boolean }; | ||
|
|
||
| export const SUPPORTED_IMAGE_MIME_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'] as const; | ||
|
|
||
| export const DEFAULT_LIST_TIMEOUT_MS = 1000; | ||
| export const DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024; | ||
|
|
||
| export function baseMimeType(raw: string): string { | ||
| return raw.split(';')[0]?.trim().toLowerCase() ?? raw.toLowerCase(); | ||
| } | ||
|
|
||
| export function isSupportedImageMimeType(mime: string): boolean { | ||
| const base = baseMimeType(mime); | ||
| return (SUPPORTED_IMAGE_MIME_TYPES as readonly string[]).includes(base); | ||
| } | ||
|
|
||
| export function parseTargetList(output: Buffer): string[] { | ||
| return output | ||
| .toString('utf-8') | ||
| .split(/\r?\n/) | ||
| .map((t) => t.trim()) | ||
| .filter((t) => t.length > 0); | ||
| } | ||
|
|
||
| export function runCommand( | ||
| command: string, | ||
| args: string[], | ||
| options?: RunCommandOptions, | ||
| ): { stdout: Buffer; ok: boolean } { | ||
| const result = spawnSync(command, args, { | ||
| timeout: options?.timeoutMs ?? DEFAULT_LIST_TIMEOUT_MS, | ||
| maxBuffer: DEFAULT_MAX_BUFFER_BYTES, | ||
| env: options?.env, | ||
| }); | ||
| if (result.error !== undefined || result.status !== 0) { | ||
| return { ok: false, stdout: Buffer.alloc(0) }; | ||
| } | ||
| const stdout = Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? ''); | ||
| return { ok: true, stdout }; | ||
| } | ||
|
|
||
| export function isWaylandSession(env: NodeJS.ProcessEnv): boolean { | ||
| return Boolean(env['WAYLAND_DISPLAY']) || env['XDG_SESSION_TYPE'] === 'wayland'; | ||
| } | ||
|
|
||
| export function isWSL(env: NodeJS.ProcessEnv): boolean { | ||
| if (env['WSL_DISTRO_NAME'] !== undefined || env['WSLENV'] !== undefined) return true; | ||
| try { | ||
| return /microsoft|wsl/i.test(readFileSync('/proc/version', 'utf-8')); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| export function isFileLikeNativeFormat(format: string): boolean { | ||
| const f = format.toLowerCase(); | ||
| const base = baseMimeType(format); | ||
| return ( | ||
| f.includes('file-url') || | ||
| f.includes('file url') || | ||
| f.includes('nsfilenames') || | ||
| f.includes('com.apple.finder') || | ||
| base === 'text/uri-list' || | ||
| base === 'public.url' | ||
| ); | ||
| } | ||
|
|
||
| export function safeAvailableFormats(clip: ClipboardModule | null): string[] { | ||
| if (clip?.availableFormats === undefined) return []; | ||
| try { | ||
| return clip.availableFormats(); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.