Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README-en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions README-zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Skills 会按以下优先级扫描:
|---------------|--------------------|
| `Enter` | 发送消息 |
| `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) |
| `Ctrl+V` | 从剪贴板粘贴图片 |
| `Ctrl+V` | 从剪贴板粘贴图片(Windows系统是`Alt+V`) |
| `Ctrl+R` | 搜索当前及历史会话中的用户输入 |
| `Esc` | 中断当前模型回复 |
| 连续 `Ctrl+D` | 退出 |
Expand Down Expand Up @@ -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` 配置项手动覆盖。

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Skills 会按以下优先级扫描:
|---------------|--------------------|
| `Enter` | 发送消息 |
| `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) |
| `Ctrl+V` | 从剪贴板粘贴图片 |
| `Ctrl+V` | 从剪贴板粘贴图片(Windows系统是`Alt+V`) |
| `Ctrl+R` | 搜索当前及历史会话中的用户输入 |
| `Esc` | 中断当前模型回复 |
| 连续 `Ctrl+D` | 退出 |
Expand Down Expand Up @@ -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` 配置项手动覆盖。

Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ deepcode -p "总结这个项目"
| 发送消息 | `Enter` |
| 输入多行 | `Shift+Enter` 或 `Ctrl+J` |
| 中断当前回复 | `Esc` |
| 粘贴图片 | `Ctrl+V` |
| 粘贴图片 | `Ctrl+V` (macOS / Linux) 或 `Alt+V` (Windows) |
| 退出 | 连续按两次 `Ctrl+D`,或使用 `/exit` |

## 斜杠命令
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/cli-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

Expand Down Expand Up @@ -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",
Expand Down
155 changes: 155 additions & 0 deletions packages/cli/src/tests/image-paste-shortcut.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
5 changes: 3 additions & 2 deletions packages/cli/src/ui/views/PromptInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -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) => {
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/ui/views/WelcomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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" },
Expand Down Expand Up @@ -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)),
];
}

Expand Down
24 changes: 24 additions & 0 deletions packages/cli/src/utils/image-paste-shortcut.ts
Original file line number Diff line number Diff line change
@@ -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<InputKey, "ctrl" | "meta" | "paste">,
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));
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Skills 会按以下优先级扫描:
| ------------- | --------------------------- |
| `Enter` | 发送消息 |
| `Shift+Enter` | 插入换行(也可用 `Ctrl+J`) |
| `Ctrl+V` | 从剪贴板粘贴图片 |
| `Ctrl+V` | 从剪贴板粘贴图片(Windows系统是`Alt+V`) |
| `Esc` | 中断当前模型回复 |
| 连续 `Ctrl+D` | 退出 |

Expand All @@ -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` 配置项手动覆盖。

Expand Down
Loading