Skip to content

Commit e4d9b44

Browse files
authored
Merge pull request #89 from hqwlkj/feature/raw
feat: 新增 `/raw` 命令交互与终端消息直出
2 parents b173600 + 32da2ca commit e4d9b44

25 files changed

Lines changed: 1136 additions & 460 deletions

README_en.md renamed to README-en.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
# Deep Code CLI
1+
<div align="center">
2+
<br/>
3+
<br/>
4+
<p align="center">
5+
<a href='https://deepcode.vegamo.cn/'>
6+
<img src='https://avatars.githubusercontent.com/u/118287711?s=200&v=4' width='100' alt="deepcode-cli"/>
7+
</a>
8+
</p>
9+
<h1>Deep Code CLI</h1>
10+
11+
English · [中文](./README.md)
12+
13+
<br/>
14+
</div>
215

316
[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.
417

18+
519
## Installation
620

721
```bash
@@ -53,6 +67,7 @@ Deep Code CLI supports agent skills that allow you to extend the assistant's cap
5367
| `/new` | Start a fresh conversation |
5468
| `/resume` | Choose a previous conversation to continue |
5569
| `/model` | Switch model, thinking mode, and reasoning effort |
70+
| `/raw` | Toggle display mode (Normal / Lite / Raw scrollback) |
5671
| `/init` | Initialize an AGENTS.md file (LLM project instructions) |
5772
| `/skills` | List available skills |
5873
| `/mcp` | View MCP server status and available tools |

README_cn.md renamed to README-zh_CN.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
# Deep Code CLI
1+
<div align="center">
2+
<br/>
3+
<br/>
4+
<p align="center">
5+
<a href='https://deepcode.vegamo.cn/'>
6+
<img src='https://avatars.githubusercontent.com/u/118287711?s=200&v=4' width='100' alt="deepcode-cli"/>
7+
</a>
8+
</p>
9+
<h1>Deep Code CLI</h1>
10+
11+
[English](README-en.md) · 中文
12+
13+
<br/>
14+
</div>
215

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

@@ -53,6 +66,7 @@ Deep Code CLI 支持 agent skills,允许您扩展助手的能力:
5366
| `/new` | 开始新对话 |
5467
| `/resume` | 选择历史对话继续 |
5568
| `/model` | 切换模型、思考模式和推理强度 |
69+
| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯)|
5670
| `/init` | 初始化 AGENTS.md 文件 |
5771
| `/skills` | 列出可用 skills |
5872
| `/mcp` | 查看 MCP 服务器状态和可用工具 |

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
# Deep Code CLI
1+
<div align="center">
2+
<br/>
3+
<br/>
4+
<p align="center">
5+
<a href='https://deepcode.vegamo.cn/'>
6+
<img src='https://avatars.githubusercontent.com/u/118287711?s=200&v=4' width='100' alt="deepcode-cli"/>
7+
</a>
8+
</p>
9+
<h1>Deep Code CLI</h1>
10+
11+
[English](README-en.md) · 中文
12+
13+
<br/>
14+
</div>
215

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

@@ -53,6 +66,7 @@ Deep Code CLI 支持 agent skills,允许您扩展助手的能力:
5366
| `/new` | 开始新对话 |
5467
| `/resume` | 选择历史对话继续 |
5568
| `/model` | 切换模型、思考模式和推理强度 |
69+
| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯)|
5670
| `/init` | 初始化 AGENTS.md 文件 |
5771
| `/skills` | 列出可用 skills |
5872
| `/mcp` | 查看 MCP 服务器状态和可用工具 |

src/cli.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from "react";
22
import { render } from "ink";
3-
import { App } from "./ui";
43
import { setShellIfWindows } from "./common/shell-utils";
54
import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./updateCheck";
5+
import { AppContainer } from "./ui";
66

77
const args = process.argv.slice(2);
88
const packageInfo = readPackageInfo();
@@ -81,7 +81,7 @@ async function main(): Promise<void> {
8181
const appInitialPrompt = initialPrompt;
8282
initialPrompt = undefined;
8383
const inkInstance = render(
84-
<App
84+
<AppContainer
8585
projectRoot={projectRoot}
8686
version={packageInfo.version}
8787
initialPrompt={appInitialPrompt}

src/tests/messageView.test.ts

Lines changed: 227 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { test } from "node:test";
22
import assert from "node:assert/strict";
3-
import { MessageView, parseDiffPreview } from "../ui";
3+
import { parseDiffPreview } from "../ui";
4+
import {
5+
buildThinkingSummary,
6+
renderMessageToStdout,
7+
getUpdatePlanPreviewLines,
8+
parseToolPayload,
9+
} from "../ui/compoments/MessageView/utils";
10+
import { RawMode } from "../ui/contexts";
411
import type { SessionMessage } from "../session";
12+
import type { ToolSummary } from "../ui/compoments/MessageView/types";
513

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

2634
test("MessageView summarizes thinking content across lines", () => {
2735
assert.equal(
28-
getThinkingParams({
29-
content: "Plan:\n\nInspect the code and update tests",
30-
}),
36+
buildThinkingSummary("Plan:\n\nInspect the code and update tests", null, RawMode.Lite),
3137
"Plan: Inspect the code and update tests"
3238
);
3339
});
3440

35-
test("MessageView removes a trailing colon from thinking summaries", () => {
36-
assert.equal(getThinkingParams({ content: "Planning:" }), "Planning");
41+
test("MessageView removes a trailing colon from thinking summary", () => {
42+
assert.equal(buildThinkingSummary("Planning:", null, RawMode.Lite), "Planning");
3743
});
3844

39-
test("MessageView falls back to a reasoning placeholder for hidden reasoning content", () => {
45+
test("MessageView falls back to a reasoning placeholder for hidden reasoning content in Lite mode", () => {
4046
assert.equal(
41-
getThinkingParams({
42-
content: "",
43-
messageParams: { reasoning_content: "hidden chain of thought" },
44-
}),
47+
buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.Lite),
4548
"(reasoning...)"
4649
);
4750
});
4851

49-
function getThinkingParams(overrides: Partial<SessionMessage>): string {
50-
const view = MessageView({ message: buildAssistantMessage(overrides) }) as any;
51-
return view.props.children.props.params;
52-
}
52+
test("MessageView shows full reasoning content in Normal/Raw mode", () => {
53+
assert.equal(
54+
buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.None),
55+
"hidden chain of thought"
56+
);
57+
assert.equal(
58+
buildThinkingSummary("", { reasoning_content: "hidden chain of thought" }, RawMode.Raw),
59+
"hidden chain of thought"
60+
);
61+
});
5362

54-
function buildAssistantMessage(overrides: Partial<SessionMessage>): SessionMessage {
63+
// --- renderMessageToStdout tests ---
64+
65+
function makeSessionMessage(overrides: Partial<SessionMessage> & Pick<SessionMessage, "role">): SessionMessage {
66+
const now = new Date().toISOString();
5567
return {
56-
id: "message-1",
57-
sessionId: "session-1",
68+
id: overrides.id ?? `test-${Math.random().toString(36).slice(2)}`,
69+
sessionId: overrides.sessionId ?? "test-session",
70+
role: overrides.role,
71+
content: overrides.content ?? null,
72+
visible: overrides.visible ?? true,
73+
compacted: overrides.compacted ?? false,
74+
createTime: overrides.createTime ?? now,
75+
updateTime: overrides.updateTime ?? now,
76+
contentParams: overrides.contentParams ?? null,
77+
messageParams: overrides.messageParams ?? null,
78+
meta: overrides.meta,
79+
html: overrides.html,
80+
};
81+
}
82+
83+
test("renderMessageToStdout returns empty for invisible messages", () => {
84+
const msg = makeSessionMessage({ role: "user", content: "hello", visible: false });
85+
assert.equal(renderMessageToStdout(msg, RawMode.Raw), "");
86+
});
87+
88+
test("renderMessageToStdout renders user messages with > prefix", () => {
89+
const msg = makeSessionMessage({ role: "user", content: "fix the bug" });
90+
const output = renderMessageToStdout(msg, RawMode.Raw);
91+
assert.ok(output.includes("> fix the bug"));
92+
});
93+
94+
test("renderMessageToStdout shows (no content) for empty user messages", () => {
95+
const msg = makeSessionMessage({ role: "user", content: "" });
96+
const output = renderMessageToStdout(msg, RawMode.Raw);
97+
assert.ok(output.includes("(no content)"));
98+
});
99+
100+
test("renderMessageToStdout renders assistant non-thinking messages with ✦", () => {
101+
const msg = makeSessionMessage({ role: "assistant", content: "Here is the fix" });
102+
const output = renderMessageToStdout(msg, RawMode.Raw);
103+
assert.ok(output.includes("✦"));
104+
assert.ok(output.includes("Here is the fix"));
105+
});
106+
107+
test("renderMessageToStdout renders assistant thinking messages with ✧ Thinking", () => {
108+
const msg = makeSessionMessage({
58109
role: "assistant",
59-
content: "",
60-
contentParams: null,
61-
messageParams: null,
62-
compacted: false,
63-
visible: true,
64-
createTime: "2026-01-01T00:00:00.000Z",
65-
updateTime: "2026-01-01T00:00:00.000Z",
110+
content: "Plan:\nAnalyze the code",
66111
meta: { asThinking: true },
67-
...overrides,
112+
});
113+
const output = renderMessageToStdout(msg, RawMode.Lite);
114+
assert.ok(output.includes("✧"));
115+
assert.ok(output.includes("Thinking"));
116+
assert.ok(output.includes("Plan: Analyze the code"));
117+
});
118+
119+
test("renderMessageToStdout renders tool messages with ✧ and tool name", () => {
120+
const payload = JSON.stringify({ name: "read", ok: true });
121+
const msg = makeSessionMessage({ role: "tool", content: payload });
122+
const output = renderMessageToStdout(msg, RawMode.Raw);
123+
assert.ok(output.includes("✧"));
124+
assert.ok(output.includes("Read"));
125+
});
126+
127+
test("renderMessageToStdout renders tool messages with resultMd output", () => {
128+
const payload = JSON.stringify({ name: "read", ok: true });
129+
const msg = makeSessionMessage({
130+
role: "tool",
131+
content: payload,
132+
meta: { resultMd: "File content:\n line 1\n line 2" },
133+
});
134+
const output = renderMessageToStdout(msg, RawMode.Raw);
135+
assert.ok(output.includes("✧"));
136+
assert.ok(output.includes("Read"));
137+
assert.ok(output.includes("└ Result"));
138+
assert.ok(output.includes("File content:"));
139+
assert.ok(output.includes("line 1"));
140+
});
141+
142+
test("renderMessageToStdout renders UpdatePlan tool messages with Plan preview and resultMd", () => {
143+
const payload = JSON.stringify({
144+
name: "UpdatePlan",
145+
ok: true,
146+
metadata: { plan: "Step 1: Analyze\nStep 2: Implement\nStep 3: Test" },
147+
});
148+
const msg = makeSessionMessage({
149+
role: "tool",
150+
content: payload,
151+
meta: { resultMd: "Plan updated successfully" },
152+
});
153+
const output = renderMessageToStdout(msg, RawMode.Raw);
154+
assert.ok(output.includes("UpdatePlan"));
155+
assert.ok(output.includes("└ Plan"));
156+
assert.ok(output.includes("Step 1: Analyze"));
157+
assert.ok(output.includes(" Result"));
158+
assert.ok(output.includes("Plan updated successfully"));
159+
});
160+
161+
test("renderMessageToStdout renders UpdatePlan tool messages with Plan preview", () => {
162+
const payload = JSON.stringify({
163+
name: "UpdatePlan",
164+
ok: true,
165+
metadata: { plan: "Step 1: Analyze\nStep 2: Implement\nStep 3: Test" },
166+
});
167+
const msg = makeSessionMessage({ role: "tool", content: payload });
168+
const output = renderMessageToStdout(msg, RawMode.Raw);
169+
assert.ok(output.includes("UpdatePlan"));
170+
assert.ok(output.includes("└ Plan"));
171+
assert.ok(output.includes("Step 1: Analyze"));
172+
assert.ok(output.includes("Step 2: Implement"));
173+
// Verify resultMd is NOT included when meta.resultMd is absent
174+
assert.ok(!output.includes("└ Result"));
175+
});
176+
177+
test("renderMessageToStdout renders system model change messages", () => {
178+
const msg = makeSessionMessage({
179+
role: "system",
180+
content: "Switched to deepseek-v4-pro",
181+
meta: { isModelChange: true },
182+
});
183+
const output = renderMessageToStdout(msg, RawMode.Raw);
184+
assert.ok(output.includes("> Switched to deepseek-v4-pro"));
185+
});
186+
187+
test("renderMessageToStdout renders system skill load messages", () => {
188+
const msg = makeSessionMessage({
189+
role: "system",
190+
content: "",
191+
meta: { skill: { name: "code-review", path: "", description: "" } },
192+
});
193+
const output = renderMessageToStdout(msg, RawMode.Raw);
194+
assert.ok(output.includes("⚡ Loaded skill: code-review"));
195+
});
196+
197+
test("renderMessageToStdout renders system summary messages", () => {
198+
const msg = makeSessionMessage({
199+
role: "system",
200+
content: "",
201+
meta: { isSummary: true },
202+
});
203+
const output = renderMessageToStdout(msg, RawMode.Raw);
204+
assert.ok(output.includes("(conversation summary inserted)"));
205+
});
206+
207+
test("renderMessageToStdout returns empty for unknown system messages", () => {
208+
const msg = makeSessionMessage({ role: "system", content: "" });
209+
assert.equal(renderMessageToStdout(msg, RawMode.Raw), "");
210+
});
211+
212+
// --- getUpdatePlanPreviewLines tests ---
213+
214+
test("getUpdatePlanPreviewLines returns empty for failed tool", () => {
215+
const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: false, metadata: { plan: "Step 1" } };
216+
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
217+
});
218+
219+
test("getUpdatePlanPreviewLines returns empty for non-UpdatePlan tool", () => {
220+
const summary: ToolSummary = { name: "edit", params: "", ok: true, metadata: { plan: "Step 1" } };
221+
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
222+
});
223+
224+
test("getUpdatePlanPreviewLines returns empty for missing plan metadata", () => {
225+
const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: true, metadata: null };
226+
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
227+
});
228+
229+
test("getUpdatePlanPreviewLines returns empty for empty plan string", () => {
230+
const summary: ToolSummary = { name: "UpdatePlan", params: "", ok: true, metadata: { plan: "" } };
231+
assert.deepEqual(getUpdatePlanPreviewLines(summary), []);
232+
});
233+
234+
test("getUpdatePlanPreviewLines extracts plan lines and filters empty rows", () => {
235+
const summary: ToolSummary = {
236+
name: "UpdatePlan",
237+
params: "",
238+
ok: true,
239+
metadata: { plan: "Step 1: Analyze\n\nStep 2: Implement\n \nStep 3: Test" },
68240
};
69-
}
241+
assert.deepEqual(getUpdatePlanPreviewLines(summary), ["Step 1: Analyze", "Step 2: Implement", "Step 3: Test"]);
242+
});
243+
244+
// --- parseToolPayload tests ---
245+
246+
test("parseToolPayload returns defaults for null content", () => {
247+
const result = parseToolPayload(null);
248+
assert.deepEqual(result, { name: null, ok: true, metadata: null });
249+
});
250+
251+
test("parseToolPayload returns defaults for invalid JSON", () => {
252+
const result = parseToolPayload("not valid json");
253+
assert.deepEqual(result, { name: null, ok: true, metadata: null });
254+
});
255+
256+
test("parseToolPayload parses valid JSON with name/ok/metadata", () => {
257+
const result = parseToolPayload(JSON.stringify({ name: "read", ok: true, metadata: { file: "src/index.ts" } }));
258+
assert.deepEqual(result, { name: "read", ok: true, metadata: { file: "src/index.ts" } });
259+
});
260+
261+
test("parseToolPayload respects ok: false", () => {
262+
const result = parseToolPayload(JSON.stringify({ name: "bash", ok: false, metadata: null }));
263+
assert.deepEqual(result, { name: "bash", ok: false, metadata: null });
264+
});
265+
266+
test("parseToolPayload trims whitespace from name", () => {
267+
const result = parseToolPayload(JSON.stringify({ name: " read ", ok: true }));
268+
assert.equal(result.name, "read");
269+
});

0 commit comments

Comments
 (0)