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
17 changes: 16 additions & 1 deletion README_en.md → README-en.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
# Deep Code CLI
<div align="center">
<br/>
<br/>
<p align="center">
<a href='https://deepcode.vegamo.cn/'>
<img src='https://avatars.githubusercontent.com/u/118287711?s=200&v=4' width='100' alt="deepcode-cli"/>
</a>
</p>
<h1>Deep Code CLI</h1>

English · [中文](./README.md)

<br/>
</div>

[Deep Code](https://github.com/lessweb/deepcode-cli) is a terminal AI coding assistant optimized for the `deepseek-v4` model, with support for deep thinking, reasoning effort control, Agent Skills, and MCP (Model Context Protocol) integration.


## Installation

```bash
Expand Down Expand Up @@ -53,6 +67,7 @@ Deep Code CLI supports agent skills that allow you to extend the assistant's cap
| `/new` | Start a fresh conversation |
| `/resume` | Choose a previous conversation to continue |
| `/model` | Switch model, thinking mode, and reasoning effort |
| `/raw` | Toggle display mode (Normal / Lite / Raw scrollback) |
| `/init` | Initialize an AGENTS.md file (LLM project instructions) |
| `/skills` | List available skills |
| `/mcp` | View MCP server status and available tools |
Expand Down
16 changes: 15 additions & 1 deletion README_cn.md → README-zh_CN.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
# Deep Code CLI
<div align="center">
<br/>
<br/>
<p align="center">
<a href='https://deepcode.vegamo.cn/'>
<img src='https://avatars.githubusercontent.com/u/118287711?s=200&v=4' width='100' alt="deepcode-cli"/>
</a>
</p>
<h1>Deep Code CLI</h1>

[English](README-en.md) · 中文

<br/>
</div>

[Deep Code](https://github.com/lessweb/deepcode-cli) 是专为 `deepseek-v4` 模型优化的终端 AI 编码助手,支持深度思考、推理强度控制、Agent Skills 以及 MCP 集成。

Expand Down Expand Up @@ -53,6 +66,7 @@ Deep Code CLI 支持 agent skills,允许您扩展助手的能力:
| `/new` | 开始新对话 |
| `/resume` | 选择历史对话继续 |
| `/model` | 切换模型、思考模式和推理强度 |
| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯)|
| `/init` | 初始化 AGENTS.md 文件 |
| `/skills` | 列出可用 skills |
| `/mcp` | 查看 MCP 服务器状态和可用工具 |
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
# Deep Code CLI
<div align="center">
<br/>
<br/>
<p align="center">
<a href='https://deepcode.vegamo.cn/'>
<img src='https://avatars.githubusercontent.com/u/118287711?s=200&v=4' width='100' alt="deepcode-cli"/>
</a>
</p>
<h1>Deep Code CLI</h1>

[English](README-en.md) · 中文

<br/>
</div>

[Deep Code](https://github.com/lessweb/deepcode-cli) 是专为 `deepseek-v4` 模型优化的终端 AI 编码助手,支持深度思考、推理强度控制、Agent Skills 以及 MCP 集成。

Expand Down Expand Up @@ -53,6 +66,7 @@ Deep Code CLI 支持 agent skills,允许您扩展助手的能力:
| `/new` | 开始新对话 |
| `/resume` | 选择历史对话继续 |
| `/model` | 切换模型、思考模式和推理强度 |
| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯)|
| `/init` | 初始化 AGENTS.md 文件 |
| `/skills` | 列出可用 skills |
| `/mcp` | 查看 MCP 服务器状态和可用工具 |
Expand Down
4 changes: 2 additions & 2 deletions src/cli.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from "react";
import { render } from "ink";
import { App } from "./ui";
import { setShellIfWindows } from "./common/shell-utils";
import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./updateCheck";
import { AppContainer } from "./ui";

const args = process.argv.slice(2);
const packageInfo = readPackageInfo();
Expand Down Expand Up @@ -81,7 +81,7 @@ async function main(): Promise<void> {
const appInitialPrompt = initialPrompt;
initialPrompt = undefined;
const inkInstance = render(
<App
<AppContainer
projectRoot={projectRoot}
version={packageInfo.version}
initialPrompt={appInitialPrompt}
Expand Down
254 changes: 227 additions & 27 deletions src/tests/messageView.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { MessageView, parseDiffPreview } from "../ui";
import { parseDiffPreview } from "../ui";
import {
buildThinkingSummary,
renderMessageToStdout,
getUpdatePlanPreviewLines,
parseToolPayload,
} from "../ui/compoments/MessageView/utils";
import { RawMode } from "../ui/contexts";
import type { SessionMessage } from "../session";
import type { ToolSummary } from "../ui/compoments/MessageView/types";

test("parseDiffPreview removes headers and classifies lines", () => {
const lines = parseDiffPreview(
Expand All @@ -25,45 +33,237 @@ test("parseDiffPreview keeps nonstandard context lines", () => {

test("MessageView summarizes thinking content across lines", () => {
assert.equal(
getThinkingParams({
content: "Plan:\n\nInspect the code and update tests",
}),
buildThinkingSummary("Plan:\n\nInspect the code and update tests", null, RawMode.Lite),
"Plan: Inspect the code and update tests"
);
});

test("MessageView removes a trailing colon from thinking summaries", () => {
assert.equal(getThinkingParams({ content: "Planning:" }), "Planning");
test("MessageView removes a trailing colon from thinking summary", () => {
assert.equal(buildThinkingSummary("Planning:", null, RawMode.Lite), "Planning");
});

test("MessageView falls back to a reasoning placeholder for hidden reasoning content", () => {
test("MessageView falls back to a reasoning placeholder for hidden reasoning content in Lite mode", () => {
assert.equal(
getThinkingParams({
content: "",
messageParams: { reasoning_content: "hidden chain of thought" },
}),
buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.Lite),
"(reasoning...)"
);
});

function getThinkingParams(overrides: Partial<SessionMessage>): string {
const view = MessageView({ message: buildAssistantMessage(overrides) }) as any;
return view.props.children.props.params;
}
test("MessageView shows full reasoning content in Normal/Raw mode", () => {
assert.equal(
buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.None),
"hidden chain of thought"
);
assert.equal(
buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.Raw),
"hidden chain of thought"
);
});

function buildAssistantMessage(overrides: Partial<SessionMessage>): SessionMessage {
// --- renderMessageToStdout tests ---

function makeSessionMessage(overrides: Partial<SessionMessage> & Pick<SessionMessage, "role">): SessionMessage {
const now = new Date().toISOString();
return {
id: "message-1",
sessionId: "session-1",
id: overrides.id ?? `test-${Math.random().toString(36).slice(2)}`,
sessionId: overrides.sessionId ?? "test-session",
role: overrides.role,
content: overrides.content ?? null,
visible: overrides.visible ?? true,
compacted: overrides.compacted ?? false,
createTime: overrides.createTime ?? now,
updateTime: overrides.updateTime ?? now,
contentParams: overrides.contentParams ?? null,
messageParams: overrides.messageParams ?? null,
meta: overrides.meta,
html: overrides.html,
};
}

test("renderMessageToStdout returns empty for invisible messages", () => {
const msg = makeSessionMessage({ role: "user", content: "hello", visible: false });
assert.equal(renderMessageToStdout(msg, RawMode.Raw), "");
});

test("renderMessageToStdout renders user messages with > prefix", () => {
const msg = makeSessionMessage({ role: "user", content: "fix the bug" });
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("> fix the bug"));
});

test("renderMessageToStdout shows (no content) for empty user messages", () => {
const msg = makeSessionMessage({ role: "user", content: "" });
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("(no content)"));
});

test("renderMessageToStdout renders assistant non-thinking messages with ✦", () => {
const msg = makeSessionMessage({ role: "assistant", content: "Here is the fix" });
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("✦"));
assert.ok(output.includes("Here is the fix"));
});

test("renderMessageToStdout renders assistant thinking messages with ✧ Thinking", () => {
const msg = makeSessionMessage({
role: "assistant",
content: "",
contentParams: null,
messageParams: null,
compacted: false,
visible: true,
createTime: "2026-01-01T00:00:00.000Z",
updateTime: "2026-01-01T00:00:00.000Z",
content: "Plan:\nAnalyze the code",
meta: { asThinking: true },
...overrides,
});
const output = renderMessageToStdout(msg, RawMode.Lite);
assert.ok(output.includes("✧"));
assert.ok(output.includes("Thinking"));
assert.ok(output.includes("Plan: Analyze the code"));
});

test("renderMessageToStdout renders tool messages with ✧ and tool name", () => {
const payload = JSON.stringify({ name: "read", ok: true });
const msg = makeSessionMessage({ role: "tool", content: payload });
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("✧"));
assert.ok(output.includes("Read"));
});

test("renderMessageToStdout renders tool messages with resultMd output", () => {
const payload = JSON.stringify({ name: "read", ok: true });
const msg = makeSessionMessage({
role: "tool",
content: payload,
meta: { resultMd: "File content:\n line 1\n line 2" },
});
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("✧"));
assert.ok(output.includes("Read"));
assert.ok(output.includes("└ Result"));
assert.ok(output.includes("File content:"));
assert.ok(output.includes("line 1"));
});

test("renderMessageToStdout renders UpdatePlan tool messages with Plan preview and resultMd", () => {
const payload = JSON.stringify({
name: "UpdatePlan",
ok: true,
metadata: { plan: "Step 1: Analyze\nStep 2: Implement\nStep 3: Test" },
});
const msg = makeSessionMessage({
role: "tool",
content: payload,
meta: { resultMd: "Plan updated successfully" },
});
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("UpdatePlan"));
assert.ok(output.includes("└ Plan"));
assert.ok(output.includes("Step 1: Analyze"));
assert.ok(output.includes(" Result"));
assert.ok(output.includes("Plan updated successfully"));
});

test("renderMessageToStdout renders UpdatePlan tool messages with Plan preview", () => {
const payload = JSON.stringify({
name: "UpdatePlan",
ok: true,
metadata: { plan: "Step 1: Analyze\nStep 2: Implement\nStep 3: Test" },
});
const msg = makeSessionMessage({ role: "tool", content: payload });
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("UpdatePlan"));
assert.ok(output.includes("└ Plan"));
assert.ok(output.includes("Step 1: Analyze"));
assert.ok(output.includes("Step 2: Implement"));
// Verify resultMd is NOT included when meta.resultMd is absent
assert.ok(!output.includes("└ Result"));
});

test("renderMessageToStdout renders system model change messages", () => {
const msg = makeSessionMessage({
role: "system",
content: "Switched to deepseek-v4-pro",
meta: { isModelChange: true },
});
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("> Switched to deepseek-v4-pro"));
});

test("renderMessageToStdout renders system skill load messages", () => {
const msg = makeSessionMessage({
role: "system",
content: "",
meta: { skill: { name: "code-review", path: "", description: "" } },
});
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("⚡ Loaded skill: code-review"));
});

test("renderMessageToStdout renders system summary messages", () => {
const msg = makeSessionMessage({
role: "system",
content: "",
meta: { isSummary: true },
});
const output = renderMessageToStdout(msg, RawMode.Raw);
assert.ok(output.includes("(conversation summary inserted)"));
});

test("renderMessageToStdout returns empty for unknown system messages", () => {
const msg = makeSessionMessage({ role: "system", content: "" });
assert.equal(renderMessageToStdout(msg, RawMode.Raw), "");
});

// --- getUpdatePlanPreviewLines tests ---

test("getUpdatePlanPreviewLines returns empty for failed tool", () => {
const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: false, metadata: { plan: "Step 1" } };
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
});

test("getUpdatePlanPreviewLines returns empty for non-UpdatePlan tool", () => {
const summary: ToolSummary = { name: "edit", params: "", ok: true, metadata: { plan: "Step 1" } };
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
});

test("getUpdatePlanPreviewLines returns empty for missing plan metadata", () => {
const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: true, metadata: null };
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
});

test("getUpdatePlanPreviewLines returns empty for empty plan string", () => {
const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: true, metadata: { plan: "" } };
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
});

test("getUpdatePlanPreviewLines extracts plan lines and filters empty rows", () => {
const summary: ToolSummary = {
name: "UpdatePlan",
params: "",
ok: true,
metadata: { plan: "Step 1: Analyze\n\nStep 2: Implement\n \nStep 3: Test" },
};
}
assert.deepEqual(getUpdatePlanPreviewLines(summary), ["Step 1: Analyze", "Step 2: Implement", "Step 3: Test"]);
});

// --- parseToolPayload tests ---

test("parseToolPayload returns defaults for null content", () => {
const result = parseToolPayload(null);
assert.deepEqual(result, { name: null, ok: true, metadata: null });
});

test("parseToolPayload returns defaults for invalid JSON", () => {
const result = parseToolPayload("not valid json");
assert.deepEqual(result, { name: null, ok: true, metadata: null });
});

test("parseToolPayload parses valid JSON with name/ok/metadata", () => {
const result = parseToolPayload(JSON.stringify({ name: "read", ok: true, metadata: { file: "src/index.ts" } }));
assert.deepEqual(result, { name: "read", ok: true, metadata: { file: "src/index.ts" } });
});

test("parseToolPayload respects ok: false", () => {
const result = parseToolPayload(JSON.stringify({ name: "bash", ok: false, metadata: null }));
assert.deepEqual(result, { name: "bash", ok: false, metadata: null });
});

test("parseToolPayload trims whitespace from name", () => {
const result = parseToolPayload(JSON.stringify({ name: " read ", ok: true }));
assert.equal(result.name, "read");
});
Loading
Loading