From 47d3c21abe3c3582d24e7c1109bdf19e0818c90d Mon Sep 17 00:00:00 2001
From: hcyang
Date: Mon, 18 May 2026 18:13:34 +0800
Subject: [PATCH 01/11] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=20/raw=20?=
=?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=94=AF=E6=8C=81=E5=8F=8A=E7=9B=B8=E5=85=B3?=
=?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=92=8C=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 RawMode 功能,包括 Normal、Lite 和 Raw scrollback 模式
- App 组件中集成 RawMode 上下文及切换逻辑,支持在 Raw 模式下直接向 stdout 渲染消息
- 增加 RawModeExitPrompt 组件,支持按 ESC 退出原始模式
- 新增 RawModelDropdown 组件,提供原始模式选择下拉菜单
- 在 PromptInput 中集成原始模式选择交互及状态管理
- 调整消息视图实现,拆分 MessageView 到 compoments 目录,支持根据 RawMode 呈现不同内容
- 新建 AppContainer 组件,包装 App 并提供版本上下文和 RawModeProvider
- 修改 SlashCommand 体系,支持内置 /raw 命令及对应测试覆盖
- 更新 cli 入口,使用 AppContainer 替换直接渲染 App,传递版本信息
- 移除旧 MessageView 文件,重构消息渲染逻辑
- 优化 SlashCommandMenu 显示,支持命令参数提示显示
- 更新相关测试,支持原始模式功能验证
---
src/cli.tsx | 4 +-
src/tests/messageView.test.ts | 51 +--
src/tests/slashCommands.test.ts | 9 +-
src/ui/App.tsx | 69 +++-
src/ui/AppContainer.tsx | 21 ++
src/ui/MessageView.tsx | 355 ------------------
src/ui/PromptInput.tsx | 27 +-
src/ui/SlashCommandMenu.tsx | 5 +-
src/ui/WelcomeScreen.tsx | 11 +-
src/ui/compoments/MessageView/index.tsx | 183 +++++++++
.../{ => compoments/MessageView}/markdown.ts | 0
src/ui/compoments/MessageView/types.ts | 19 +
src/ui/compoments/MessageView/utils.ts | 255 +++++++++++++
src/ui/compoments/RawModeExitPrompt/index.tsx | 15 +
src/ui/compoments/RawModelDropdown/index.tsx | 55 +++
src/ui/compoments/index.ts | 3 +
src/ui/contexts/AppContext.tsx | 15 +
src/ui/contexts/RawModeContext.tsx | 40 ++
src/ui/contexts/index.ts | 3 +
src/ui/index.ts | 5 +-
src/ui/slashCommands.ts | 22 +-
21 files changed, 750 insertions(+), 417 deletions(-)
create mode 100644 src/ui/AppContainer.tsx
delete mode 100644 src/ui/MessageView.tsx
create mode 100644 src/ui/compoments/MessageView/index.tsx
rename src/ui/{ => compoments/MessageView}/markdown.ts (100%)
create mode 100644 src/ui/compoments/MessageView/types.ts
create mode 100644 src/ui/compoments/MessageView/utils.ts
create mode 100644 src/ui/compoments/RawModeExitPrompt/index.tsx
create mode 100644 src/ui/compoments/RawModelDropdown/index.tsx
create mode 100644 src/ui/compoments/index.ts
create mode 100644 src/ui/contexts/AppContext.tsx
create mode 100644 src/ui/contexts/RawModeContext.tsx
create mode 100644 src/ui/contexts/index.ts
diff --git a/src/cli.tsx b/src/cli.tsx
index 435499a..e8e8659 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/AppContainer";
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 +26,29 @@ 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;
-}
-
-function buildAssistantMessage(overrides: Partial): SessionMessage {
- return {
- id: "message-1",
- sessionId: "session-1",
- 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",
- meta: { asThinking: true },
- ...overrides,
- };
-}
+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"
+ );
+});
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 e56111f..1c9bac4 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,12 +47,11 @@ 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();
@@ -75,6 +76,10 @@ export function App({ projectRoot, version = "", initialPrompt, onRestart }: App
const [showProcessStdout, setShowProcessStdout] = useState(false);
const processStdoutRef = useRef
Deep Code CLI
-[English](./README_en.md) · 中文
+[English](README-en.md) · 中文
From 7e5eeda26829b14eb3ed503b550db06c1145acf6 Mon Sep 17 00:00:00 2001
From: hcyang
Date: Tue, 19 May 2026 15:05:00 +0800
Subject: [PATCH 09/11] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=20raw=20?=
=?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=8B=E6=B6=88=E6=81=AF=E7=9B=B4=E6=8E=A5?=
=?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在 Raw 模式下,使用 process.stdout.write 直接输出所有可见消息
- 清屏并重置光标位置,避免 Ink 组件干扰
- 显示提示信息,指导用户按 ESC 退出 raw 模式
- 优化终端尺寸变化时的重绘逻辑
- 更新依赖,确保 raw 模式变动触发重新渲染
---
src/ui/App.tsx | 26 +++++++++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index 9189df6..e39fd03 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -434,8 +434,31 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
}
lastRenderedColumnsRef.current = stableColumns;
+ if (mode === RawMode.Raw) {
+ // In raw mode, re-render all messages directly to stdout at the new width.
+ // Use process.stdout.write instead of writeRef to avoid Ink interference.
+ process.stdout.write("\u001B[2J\u001B[3J\u001B[H");
+ const activeSessionId = sessionManager.getActiveSessionId();
+ const allMessages = activeSessionId ? loadVisibleMessages(sessionManager, activeSessionId) : [];
+ for (const msg of allMessages) {
+ process.stdout.write("\n");
+ process.stdout.write(renderMessageToStdout(msg, mode) + "\n\n");
+ }
+ if (allMessages.length > 0) {
+ process.stdout.write("\n\n");
+ process.stdout.write(chalk.dim("Press ESC to exit raw mode"));
+ } else {
+ process.stdout.write("\n");
+ process.stdout.write(chalk.dim("(No messages in this session yet. Start chatting to see them here.)"));
+ process.stdout.write("\n\n");
+ process.stdout.write(chalk.dim("Press ESC to exit raw mode"));
+ }
+ return;
+ }
+
// Force full redraw on terminal resize to avoid stale wrapped rows.
writeRef.current("\u001B[2J\u001B[H");
+
setMessages([]);
setShowWelcome(false);
setWelcomeNonce((n) => n + 1);
@@ -447,7 +470,8 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
setMessages(nextMessages);
setShowWelcome(true);
}, 0);
- }, [busy, sessionManager, stableColumns, stdout]);
+ }, [busy, mode, sessionManager, stableColumns, stdout]);
+
const screenWidth = useMemo(() => stableColumns ?? stdout?.columns ?? 80, [stableColumns, stdout]);
const promptHistory = useMemo(() => {
return messages
From faf10c3e087d214bf863e9df14040176e30de821 Mon Sep 17 00:00:00 2001
From: hcyang
Date: Tue, 19 May 2026 15:12:08 +0800
Subject: [PATCH 10/11] =?UTF-8?q?refactor(ui):=20=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=AE=BD=E5=BA=A6=E7=9B=B8=E5=85=B3=E7=9A=84?=
=?UTF-8?q?=E7=8A=B6=E6=80=81=E5=92=8C=E5=BC=95=E7=94=A8=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 合并并调整了关于窗口宽度columns的使用,去除了stableColumns状态
- 引用lastRenderedColumnsRef改为直接使用columns,避免延迟更新
- 将多个相关的useRef(writeRef、rawModeRef、messagesRef、processStdoutRef)移至同一位置声明
- 调整useEffect依赖项,改为监听columns代替stableColumns
- 优化RawMode下消息重绘逻辑,确保宽度变化时重新渲染
- 统一了screenWidth的计算逻辑,简化代码结构
---
src/ui/App.tsx | 30 ++++++++++++------------------
1 file changed, 12 insertions(+), 18 deletions(-)
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index e39fd03..582abaf 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -55,7 +55,13 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
const { exit } = useApp();
const { stdout, write } = useStdout();
const { columns } = useWindowSize();
+ const { mode, setMode } = useRawModeContext();
const initialPromptSubmittedRef = useRef(false);
+ const processStdoutRef = useRef