diff --git a/README-en.md b/README-en.md index 35bfad69..7f728325 100644 --- a/README-en.md +++ b/README-en.md @@ -91,7 +91,7 @@ Skills are discovered from these locations, in priority order: |------------------|----------------------------------------------------------| | `Enter` | Send the prompt | | `Shift+Enter` | Insert a newline (also `Ctrl+J`) | -| `Ctrl+V` | Paste an image from the clipboard | +| `Ctrl+V` | Paste an image from the clipboard (`Alt+V` on Windows) | | `Ctrl+R` | Search user input in the current and previous sessions | | `Esc` | Interrupt the current model turn | | `Ctrl+D` twice | Quit Deep Code | @@ -120,7 +120,7 @@ Yes. Deep Code offers a full-featured VSCode extension, available on the [VSCode ### Does Deep Code support understanding images? -Yes. The `deepseek-flash` model can read local images directly, or you can paste images from the clipboard with `Ctrl+V`, so the model can see the image content directly. +Yes. The `deepseek-flash` model can read local images directly, or you can paste images from the clipboard with `Ctrl+V` (`Alt+V` on Windows), so the model can see the image content directly. Non-multimodal models such as `deepseek-v4-pro` and `deepseek-v4-flash` continue to use the `UnderstandImage` image-understanding tool. Deep Code detects model capabilities automatically; you can also override the detection with the `multimodal` setting. diff --git a/README-zh_CN.md b/README-zh_CN.md index 7e29b275..e9057470 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -90,7 +90,7 @@ Skills 会按以下优先级扫描: |---------------|--------------------| | `Enter` | 发送消息 | | `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) | -| `Ctrl+V` | 从剪贴板粘贴图片 | +| `Ctrl+V` | 从剪贴板粘贴图片(Windows系统是`Alt+V`) | | `Ctrl+R` | 搜索当前及历史会话中的用户输入 | | `Esc` | 中断当前模型回复 | | 连续 `Ctrl+D` | 退出 | @@ -119,7 +119,7 @@ Deep Code 的收益来自于工具约束、上下文管理、Agent Skills 和权 ### Deep Code 是否支持理解图片? -支持。`deepseek-flash` 模型支持直接读取本地图片或使用`ctrl+v`从剪贴板粘贴图片,让模型直接看到图片内容。 +支持。`deepseek-flash` 模型支持直接读取本地图片或使用`ctrl+v`(Windows系统是`alt+v`)从剪贴板粘贴图片,让模型直接看到图片内容。 `deepseek-v4-pro`、`deepseek-v4-flash` 等非多模态模型仍会使用 `UnderstandImage` 识图工具。Deep Code 会自动判断模型能力,也可通过 `multimodal` 配置项手动覆盖。 diff --git a/README.md b/README.md index 7e29b275..e9057470 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Skills 会按以下优先级扫描: |---------------|--------------------| | `Enter` | 发送消息 | | `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) | -| `Ctrl+V` | 从剪贴板粘贴图片 | +| `Ctrl+V` | 从剪贴板粘贴图片(Windows系统是`Alt+V`) | | `Ctrl+R` | 搜索当前及历史会话中的用户输入 | | `Esc` | 中断当前模型回复 | | 连续 `Ctrl+D` | 退出 | @@ -119,7 +119,7 @@ Deep Code 的收益来自于工具约束、上下文管理、Agent Skills 和权 ### Deep Code 是否支持理解图片? -支持。`deepseek-flash` 模型支持直接读取本地图片或使用`ctrl+v`从剪贴板粘贴图片,让模型直接看到图片内容。 +支持。`deepseek-flash` 模型支持直接读取本地图片或使用`ctrl+v`(Windows系统是`alt+v`)从剪贴板粘贴图片,让模型直接看到图片内容。 `deepseek-v4-pro`、`deepseek-v4-flash` 等非多模态模型仍会使用 `UnderstandImage` 识图工具。Deep Code 会自动判断模型能力,也可通过 `multimodal` 配置项手动覆盖。 diff --git a/docs/quickstart.md b/docs/quickstart.md index 4d8fb564..d2b944a7 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -109,7 +109,7 @@ deepcode -p "总结这个项目" | 发送消息 | `Enter` | | 输入多行 | `Shift+Enter` 或 `Ctrl+J` | | 中断当前回复 | `Esc` | -| 粘贴图片 | `Ctrl+V` | +| 粘贴图片 | `Ctrl+V` (macOS / Linux) 或 `Alt+V` (Windows) | | 退出 | 连续按两次 `Ctrl+D`,或使用 `/exit` | ## 斜杠命令 diff --git a/docs/quickstart_en.md b/docs/quickstart_en.md index b07ff7bb..83dd0b6e 100644 --- a/docs/quickstart_en.md +++ b/docs/quickstart_en.md @@ -109,7 +109,7 @@ Before editing files, propose a plan for adding pagination to the user list. | Send message | `Enter` | | Insert a newline | `Shift+Enter` or `Ctrl+J` | | Interrupt the current response | `Esc` | -| Paste an image | `Ctrl+V` | +| Paste an image | `Ctrl+V` (macOS / Linux) or `Alt+V` (Windows) | | Quit | Press `Ctrl+D` twice, or use `/exit` | ## Slash Commands diff --git a/packages/cli/src/cli-args.ts b/packages/cli/src/cli-args.ts index 97acee2c..903fafb6 100644 --- a/packages/cli/src/cli-args.ts +++ b/packages/cli/src/cli-args.ts @@ -9,6 +9,7 @@ import { getCliVersion } from "./utils/version"; import { writeStderrLine } from "./utils/stdio-helpers"; import { hideBin } from "yargs/helpers"; import { isValidSessionId } from "@vegamo/deepcode-core/common/session-id"; +import { getImagePasteShortcutLabel } from "./utils/image-paste-shortcut"; export { isValidSessionId }; @@ -60,7 +61,7 @@ const EPILOG = [ " home/end Move within the current line", " alt+left/right Move by word", " ctrl+w Delete the previous word", - " ctrl+v Paste an image from the clipboard", + ` ${getImagePasteShortcutLabel().toLowerCase().padEnd(17)}Paste an image from the clipboard`, " ctrl+x Clear pasted images", " esc Interrupt the current model turn", " / Open the skills/commands menu", diff --git a/packages/cli/src/tests/image-paste-shortcut.test.ts b/packages/cli/src/tests/image-paste-shortcut.test.ts new file mode 100644 index 00000000..cbbfb158 --- /dev/null +++ b/packages/cli/src/tests/image-paste-shortcut.test.ts @@ -0,0 +1,155 @@ +import assert from "node:assert/strict"; +import os from "node:os"; +import { test, type TestContext } from "node:test"; +import React from "react"; +import { renderToString } from "ink"; +import { getImagePasteShortcutLabel, isImagePasteShortcut } from "../utils/image-paste-shortcut"; +import { dispatchTerminalInput, parseTerminalInput } from "../ui/hooks/useTerminalInput"; +import { PromptInput } from "../ui/views/PromptInput"; +import { buildWelcomeTips } from "../ui/views/WelcomeScreen"; +import { stripTerminalControls } from "./terminal-output"; + +const environments: { + name: string; + platform: NodeJS.Platform; + release?: string; + env?: NodeJS.ProcessEnv; + label: string; + ctrl: boolean; + alt: boolean; +}[] = [ + { name: "win32", platform: "win32", label: "Alt+V", ctrl: false, alt: true }, + { name: "darwin", platform: "darwin", label: "Ctrl+V", ctrl: true, alt: false }, + { name: "linux", platform: "linux", label: "Ctrl+V", ctrl: true, alt: false }, + { + name: "wsl-distro", + platform: "linux", + env: { WSL_DISTRO_NAME: "Ubuntu" }, + label: "Ctrl+V / Alt+V", + ctrl: true, + alt: true, + }, + { + name: "wsl-interop", + platform: "linux", + env: { WSL_INTEROP: "/run/WSL/123_interop" }, + label: "Ctrl+V / Alt+V", + ctrl: true, + alt: true, + }, + { + name: "wsl1-kernel", + platform: "linux", + release: "4.4.0-19041-Microsoft", + label: "Ctrl+V / Alt+V", + ctrl: true, + alt: true, + }, + { + name: "wsl2-kernel", + platform: "linux", + release: "6.6.87.2-microsoft-standard-WSL2", + label: "Ctrl+V / Alt+V", + ctrl: true, + alt: true, + }, + { + name: "win32-with-wsl-env", + platform: "win32", + env: { WSL_DISTRO_NAME: "Ubuntu" }, + label: "Alt+V", + ctrl: false, + alt: true, + }, +]; + +function mockEnvironment(t: TestContext, environment: (typeof environments)[number]): void { + const descriptor = Object.getOwnPropertyDescriptor(process, "platform")!; + Object.defineProperty(process, "platform", { value: environment.platform }); + t.after(() => Object.defineProperty(process, "platform", descriptor)); + t.mock.method(os, "release", () => environment.release ?? "6.8.0-generic"); + for (const key of ["WSL_DISTRO_NAME", "WSL_INTEROP"]) { + const original = process.env[key]; + const value = environment.env?.[key]; + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + t.after(() => { + if (original === undefined) delete process.env[key]; + else process.env[key] = original; + }); + } +} + +for (const environment of environments) { + const { name, platform, label, ctrl, alt } = environment; + + test(`${name}: only the image paste shortcuts trigger image reading`, (t) => { + mockEnvironment(t, environment); + for (const [raw, expected] of [ + ["\u001bv", alt], + ["\u001bV", alt], + ["\u0016", ctrl], + ["v", false], + ["V", false], + ["\u001bx", false], + ["\u0018", false], + ["pasted text", false], + ] as const) { + const events: boolean[] = []; + dispatchTerminalInput(raw, (input, key) => events.push(isImagePasteShortcut(input, key, platform))); + assert.deepEqual(events, [expected], JSON.stringify(raw)); + } + + const shortcut = parseTerminalInput(platform === "win32" ? "\u001bv" : "\u0016"); + assert.equal(isImagePasteShortcut(shortcut.input, { ...shortcut.key, paste: true }, platform), false); + assert.equal(isImagePasteShortcut("v", { ctrl: true, meta: true, paste: false }, platform), false); + assert.equal(getImagePasteShortcutLabel(platform), label); + }); + + test(`${name}: help, welcome tips and prompt footer show ${label}`, async (t) => { + mockEnvironment(t, environment); + + assert.equal(getImagePasteShortcutLabel(), label); + const shortcut = parseTerminalInput(platform === "win32" ? "\u001bv" : "\u0016"); + assert.equal(isImagePasteShortcut(shortcut.input, shortcut.key), true); + + const tips = buildWelcomeTips([]); + assert.equal(tips.find((tip) => tip.description === "Paste an image from the clipboard")?.label, label); + + const footer = stripTerminalControls( + renderToString( + React.createElement(PromptInput, { + projectRoot: process.cwd(), + screenWidth: 160, + skills: [], + modelConfig: { model: "test", thinkingEnabled: false, reasoningEffort: "high" }, + promptHistory: [], + busy: false, + disabled: true, + planMode: false, + onSubmit() {}, + onPromptDraftApplied() {}, + onModelConfigChange: () => "", + onPermissionSettingsChange: () => "", + onPlanModeChange() {}, + onInterrupt() {}, + }), + { columns: 160 } + ) + ); + assert.ok(footer.includes(`${label.toLowerCase()} image`), footer); + if (!ctrl) assert.ok(!footer.includes("ctrl+v image"), footer); + if (!alt) assert.ok(!footer.includes("alt+v image"), footer); + + // Reload the module so its static help text uses the simulated platform. + const moduleUrl = new URL(`../cli-args.ts?image-paste-platform=${name}`, import.meta.url).href; + const { parseArguments } = await import(moduleUrl); + const output: string[] = []; + t.mock.method(console, "log", (...args: unknown[]) => output.push(args.join(" "))); + await parseArguments(["--help"]); + const help = output.join("\n"); + assert.ok(help.includes(`${label.toLowerCase().padEnd(17)}Paste an image from the clipboard`), help); + if (!ctrl) assert.ok(!help.includes(`${"ctrl+v".padEnd(17)}Paste an image from the clipboard`), help); + if (!alt) assert.ok(!help.includes(`${"alt+v".padEnd(17)}Paste an image from the clipboard`), help); + }); +} diff --git a/packages/cli/src/ui/views/PromptInput.tsx b/packages/cli/src/ui/views/PromptInput.tsx index 7d90ac6a..1f8be8f4 100644 --- a/packages/cli/src/ui/views/PromptInput.tsx +++ b/packages/cli/src/ui/views/PromptInput.tsx @@ -51,6 +51,7 @@ import { } from "../core/file-mentions"; import type { FileMentionItem } from "../core/file-mentions"; import { readClipboardImageAsync } from "../core/clipboard"; +import { getImagePasteShortcutLabel, isImagePasteShortcut } from "../../utils/image-paste-shortcut"; import { useTerminalInput, usePasteHandling, @@ -242,7 +243,7 @@ export const PromptInput = React.memo(function PromptInput({ ? statusMessage : busy ? busyStatusText - : `enter send · shift+enter newline · @ files · ctrl+v image · / commands · ctrl+d exit${pasteHint}`; + : `enter send · shift+enter newline · @ files · ${getImagePasteShortcutLabel().toLowerCase()} image · / commands · ctrl+d exit${pasteHint}`; const showFooterText = useMemo( () => showMenu || @@ -465,7 +466,7 @@ export const PromptInput = React.memo(function PromptInput({ return; } - if (key.ctrl && (input === "v" || input === "V")) { + if (isImagePasteShortcut(input, key)) { setStatusMessage("Reading clipboard..."); readClipboardImageAsync() .then((image) => { diff --git a/packages/cli/src/ui/views/WelcomeScreen.tsx b/packages/cli/src/ui/views/WelcomeScreen.tsx index b2efbaa0..bef35f3b 100644 --- a/packages/cli/src/ui/views/WelcomeScreen.tsx +++ b/packages/cli/src/ui/views/WelcomeScreen.tsx @@ -7,6 +7,7 @@ import { ThemedGradient } from "./ThemedGradient"; import { AsciiLogo } from "../ascii-art"; import { useAppContext } from "../contexts"; import { formatHomeRelativePath } from "../core/display-path"; +import { getImagePasteShortcutLabel } from "../../utils/image-paste-shortcut"; export { formatHomeRelativePath } from "../core/display-path"; @@ -20,10 +21,10 @@ type WelcomeScreenProps = { const TITLE_PANEL_WIDTH = 70; const PANEL_CONTENT_HEIGHT = 8; -const SHORTCUT_TIPS = [ +const shortcutTips = () => [ { label: "Enter", description: "Send the prompt" }, { label: "Shift+Enter", description: "Insert a newline" }, - { label: "Ctrl+V", description: "Paste an image from the clipboard" }, + { label: getImagePasteShortcutLabel(), description: "Paste an image from the clipboard" }, { label: "Ctrl+R", description: "Search prompt history" }, { label: "Esc", description: "Interrupt the current model turn" }, { label: "/", description: "Open the skills and commands menu" }, @@ -110,7 +111,7 @@ export function buildWelcomeTips(skills: SkillInfo[]): Array<{ label: string; de return [ ...slashTips, - ...SHORTCUT_TIPS.filter((tip) => !BUILTIN_SLASH_COMMANDS.some((command) => command.label === tip.label)), + ...shortcutTips().filter((tip) => !BUILTIN_SLASH_COMMANDS.some((command) => command.label === tip.label)), ]; } diff --git a/packages/cli/src/utils/image-paste-shortcut.ts b/packages/cli/src/utils/image-paste-shortcut.ts new file mode 100644 index 00000000..f3fa4616 --- /dev/null +++ b/packages/cli/src/utils/image-paste-shortcut.ts @@ -0,0 +1,24 @@ +import os from "node:os"; +import type { InputKey } from "../ui/hooks/useTerminalInput"; + +function isWsl(platform: NodeJS.Platform): boolean { + return ( + platform === "linux" && + (Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) || /microsoft/i.test(os.release())) + ); +} + +export function getImagePasteShortcutLabel(platform: NodeJS.Platform = process.platform): string { + if (platform === "win32") return "Alt+V"; + return isWsl(platform) ? "Ctrl+V / Alt+V" : "Ctrl+V"; +} + +export function isImagePasteShortcut( + input: string, + key: Pick, + platform: NodeJS.Platform = process.platform +): boolean { + if (key.paste || (input !== "v" && input !== "V")) return false; + if (platform === "win32") return key.meta && !key.ctrl; + return (key.ctrl && !key.meta) || (key.meta && !key.ctrl && isWsl(platform)); +} diff --git a/packages/core/templates/skills/bundled/deepcode-self-refer/references/README.md b/packages/core/templates/skills/bundled/deepcode-self-refer/references/README.md index e464270a..b633edbe 100644 --- a/packages/core/templates/skills/bundled/deepcode-self-refer/references/README.md +++ b/packages/core/templates/skills/bundled/deepcode-self-refer/references/README.md @@ -91,7 +91,7 @@ Skills 会按以下优先级扫描: | ------------- | --------------------------- | | `Enter` | 发送消息 | | `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) | -| `Ctrl+V` | 从剪贴板粘贴图片 | +| `Ctrl+V` | 从剪贴板粘贴图片(Windows系统是`Alt+V`) | | `Esc` | 中断当前模型回复 | | 连续 `Ctrl+D` | 退出 | @@ -111,7 +111,7 @@ Skills 会按以下优先级扫描: ### Deep Code 是否支持理解图片? -支持。`deepseek-flash` 和 `deepseek-v4-flash-vision-exp` 模型支持直接读取本地图片或使用`ctrl+v`从剪贴板粘贴图片,让模型直接看到图片内容。 +支持。`deepseek-flash` 和 `deepseek-v4-flash-vision-exp` 模型支持直接读取本地图片或使用`ctrl+v`(Windows系统是`alt+v`)从剪贴板粘贴图片,让模型直接看到图片内容。 `deepseek-v4-pro`、`deepseek-v4-flash` 等非多模态模型仍会使用 `UnderstandImage` 识图工具。Deep Code 会自动判断模型能力,也可通过 `multimodal` 配置项手动覆盖。