diff --git a/README_en.md b/README-en.md
similarity index 94%
rename from README_en.md
rename to README-en.md
index ee5a103..4c78cbd 100644
--- a/README_en.md
+++ b/README-en.md
@@ -1,7 +1,21 @@
-# Deep Code CLI
+
+
+
+
+
+
+
+
+
Deep Code CLI
+
+English · [中文](./README.md)
+
+
+
[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
@@ -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 |
diff --git a/README_cn.md b/README-zh_CN.md
similarity index 94%
rename from README_cn.md
rename to README-zh_CN.md
index 69d28c8..98346b6 100644
--- a/README_cn.md
+++ b/README-zh_CN.md
@@ -1,4 +1,17 @@
-# Deep Code CLI
+
+
+
+
+
+
+
+
+
Deep Code CLI
+
+[English](README-en.md) · 中文
+
+
+
[Deep Code](https://github.com/lessweb/deepcode-cli) 是专为 `deepseek-v4` 模型优化的终端 AI 编码助手,支持深度思考、推理强度控制、Agent Skills 以及 MCP 集成。
@@ -53,6 +66,7 @@ Deep Code CLI 支持 agent skills,允许您扩展助手的能力:
| `/new` | 开始新对话 |
| `/resume` | 选择历史对话继续 |
| `/model` | 切换模型、思考模式和推理强度 |
+| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯)|
| `/init` | 初始化 AGENTS.md 文件 |
| `/skills` | 列出可用 skills |
| `/mcp` | 查看 MCP 服务器状态和可用工具 |
diff --git a/README.md b/README.md
index 69d28c8..98346b6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,17 @@
-# Deep Code CLI
+
+
+
+
+
+
+
+
+
Deep Code CLI
+
+[English](README-en.md) · 中文
+
+
+
[Deep Code](https://github.com/lessweb/deepcode-cli) 是专为 `deepseek-v4` 模型优化的终端 AI 编码助手,支持深度思考、推理强度控制、Agent Skills 以及 MCP 集成。
@@ -53,6 +66,7 @@ Deep Code CLI 支持 agent skills,允许您扩展助手的能力:
| `/new` | 开始新对话 |
| `/resume` | 选择历史对话继续 |
| `/model` | 切换模型、思考模式和推理强度 |
+| `/raw` | 切换显示模式(Normal / Lite / Raw 滚动回溯)|
| `/init` | 初始化 AGENTS.md 文件 |
| `/skills` | 列出可用 skills |
| `/mcp` | 查看 MCP 服务器状态和可用工具 |
diff --git a/src/cli.tsx b/src/cli.tsx
index 435499a..66ceb7d 100644
--- a/src/cli.tsx
+++ b/src/cli.tsx
@@ -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();
@@ -81,7 +81,7 @@ async function main(): Promise {
const appInitialPrompt = initialPrompt;
initialPrompt = undefined;
const inkInstance = render(
- {
const lines = parseDiffPreview(
@@ -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): 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 {
+// --- renderMessageToStdout tests ---
+
+function makeSessionMessage(overrides: Partial & Pick): 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");
+});
diff --git a/src/tests/slashCommands.test.ts b/src/tests/slashCommands.test.ts
index bba5244..34b48d0 100644
--- a/src/tests/slashCommands.test.ts
+++ b/src/tests/slashCommands.test.ts
@@ -19,7 +19,7 @@ test("buildSlashCommands prefixes skills before built-ins", () => {
assert.equal(items[0].kind, "skill");
assert.equal(items[0].name, "skill-writer");
const builtinNames = items.filter((i) => i.kind !== "skill").map((i) => i.name);
- assert.deepEqual(builtinNames, ["skills", "model", "new", "init", "resume", "continue", "mcp", "exit"]);
+ assert.deepEqual(builtinNames, ["skills", "model", "new", "init", "resume", "continue", "mcp", "raw", "exit"]);
});
test("filterSlashCommands matches partial prefixes", () => {
@@ -80,6 +80,13 @@ test("findExactSlashCommand returns built-in /model", () => {
assert.equal(item?.kind, "model");
});
+test("findExactSlashCommand returns built-in /raw", () => {
+ const items = buildSlashCommands(skills);
+ const item = findExactSlashCommand(items, "/raw");
+ assert.ok(item);
+ assert.equal(item?.kind, "raw");
+});
+
test("findExactSlashCommand returns the matching skill", () => {
const items = buildSlashCommands(skills);
const item = findExactSlashCommand(items, "/code-review");
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index 8d8dca1..582abaf 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -6,10 +6,10 @@ import * as os from "os";
import * as path from "path";
import OpenAI from "openai";
import {
- SessionManager,
type LlmStreamProgress,
type MessageMeta,
type SessionEntry,
+ SessionManager,
type SessionMessage,
type SessionStatus,
type SkillInfo,
@@ -17,13 +17,13 @@ import {
} from "../session";
import {
applyModelConfigSelection,
- resolveSettingsSources,
type DeepcodingSettings,
type ModelConfigSelection,
type ResolvedDeepcodingSettings,
+ resolveSettingsSources,
} from "../settings";
import { PromptInput, type PromptSubmission } from "./PromptInput";
-import { MessageView } from "./MessageView";
+import { MessageView, RawModeExitPrompt } from "./compoments";
import { SessionList } from "./SessionList";
import { buildLoadingText } from "./loadingText";
import { findExpandedThinkingId } from "./thinkingState";
@@ -32,11 +32,13 @@ import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt";
import { McpStatusList } from "./McpStatusList";
import { ProcessStdoutView } from "./ProcessStdoutView";
import {
+ type AskUserQuestionAnswers,
findPendingAskUserQuestion,
formatAskUserQuestionAnswers,
- type AskUserQuestionAnswers,
} from "./askUserQuestion";
import { buildExitSummaryText } from "./exitSummary";
+import { RawMode, useRawModeContext } from "./contexts";
+import { renderMessageToStdout } from "./compoments/MessageView/utils";
const DEFAULT_MODEL = "deepseek-v4-pro";
const DEFAULT_BASE_URL = "https://api.deepseek.com";
@@ -45,16 +47,21 @@ type View = "chat" | "session-list" | "mcp-status";
type AppProps = {
projectRoot: string;
- version?: string;
initialPrompt?: string;
onRestart?: () => void;
};
-export function App({ projectRoot, version = "", initialPrompt, onRestart }: AppProps): React.ReactElement {
+export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactElement {
const { exit } = useApp();
const { stdout, write } = useStdout();
const { columns } = useWindowSize();
+ const { mode, setMode } = useRawModeContext();
const initialPromptSubmittedRef = useRef(false);
+ const processStdoutRef = useRef