From 0d8b4838b35b4582e3d203de07d5e0b1d7c14465 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 16:27:20 +0800 Subject: [PATCH 01/15] =?UTF-8?q?refactor(ui):=20=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E5=B9=B6=E9=87=8D=E6=9E=84=E8=AE=BE=E7=BD=AE=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0=E8=87=B3utils=E7=9B=AE?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将App.tsx中的设置读写及相关辅助函数移除 - 在ui/utils/index.ts新增对应工具函数实现,包括读取写入用户及项目设置 - 将DEFAULT_MODEL和DEFAULT_BASE_URL常量移至constants.ts统一管理 - 优化导入路径,修正组件和工具的引用路径 - 修改RawModeContext和openai-client模块中依赖import路径 - 统一和调整DropdownMenu相关组件的导入路径 - 使代码结构更清晰,职责划分更明确,提高维护性 --- src/common/openai-client.ts | 2 +- src/tests/dropdownMenu.test.ts | 2 +- src/ui/App.tsx | 190 ++---------------- src/ui/AppContainer.tsx | 2 +- .../DropdownMenu/index.tsx} | 0 src/ui/components/FileMentionMenu/index.tsx | 2 +- src/ui/components/ModelsDropdown/index.tsx | 2 +- src/ui/components/RawModelDropdown/index.tsx | 2 +- src/ui/components/SkillsDropdown/index.tsx | 4 +- src/ui/components/index.ts | 1 + src/ui/constants.ts | 5 +- src/ui/contexts/RawModeContext.tsx | 2 +- src/ui/index.ts | 2 +- src/ui/utils/index.ts | 170 +++++++++++++++- 14 files changed, 198 insertions(+), 188 deletions(-) rename src/ui/{DropdownMenu.tsx => components/DropdownMenu/index.tsx} (100%) diff --git a/src/common/openai-client.ts b/src/common/openai-client.ts index c1c3e4dd..8c40cdfe 100644 --- a/src/common/openai-client.ts +++ b/src/common/openai-client.ts @@ -3,7 +3,7 @@ import * as os from "os"; import * as path from "path"; import OpenAI from "openai"; import { Agent, fetch as undiciFetch } from "undici"; -import { resolveCurrentSettings } from "../ui/App"; +import { resolveCurrentSettings } from "../ui"; // Custom undici Agent with a 180-second keepAlive timeout. The default // global fetch (undici) only keeps connections alive for 4 seconds, which diff --git a/src/tests/dropdownMenu.test.ts b/src/tests/dropdownMenu.test.ts index 3e4e3ef5..e6a0a1a4 100644 --- a/src/tests/dropdownMenu.test.ts +++ b/src/tests/dropdownMenu.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { calculateVisibleStart } from "../ui/DropdownMenu"; +import { calculateVisibleStart } from "../ui/components/DropdownMenu"; test("calculateVisibleStart centers active item when possible", () => { // 10 items, max 5 visible, active index 4 (middle) diff --git a/src/ui/App.tsx b/src/ui/App.tsx index ae94fa06..24640b03 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,9 +1,6 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { Box, Static, Text, useApp, useStdout, useWindowSize } from "ink"; import chalk from "chalk"; -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; import { createOpenAIClient } from "../common/openai-client"; import { type LlmStreamProgress, @@ -17,17 +14,11 @@ import { type UndoTarget, type UserPromptContent, } from "../session"; -import { - applyModelConfigSelection, - type DeepcodingSettings, - type ModelConfigSelection, - type ResolvedDeepcodingSettings, - resolveSettingsSources, -} from "../settings"; -import { PromptInput, type PromptDraft, type PromptSubmission } from "./PromptInput"; +import { type ModelConfigSelection } from "../settings"; +import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptInput"; import { MessageView, RawModeExitPrompt } from "./components"; import { SessionList } from "./SessionList"; -import { UndoSelector, type UndoRestoreMode } from "./UndoSelector"; +import { type UndoRestoreMode, UndoSelector } from "./UndoSelector"; import { buildLoadingText } from "./loadingText"; import { findExpandedThinkingId } from "./thinkingState"; import { WelcomeScreen } from "./WelcomeScreen"; @@ -43,12 +34,19 @@ import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPromp import { buildExitSummaryText } from "./exitSummary"; import { RawMode, useRawModeContext } from "./contexts"; import { renderMessageToStdout } from "./components/MessageView/utils"; -import { renderRawModeMessages } from "./utils"; +import { + buildPromptDraftFromSessionMessage, + buildStatusLine, + buildSyntheticUserMessage, + formatModelConfig, + isCollapsedThinking, + isCurrentSessionEmpty, + renderRawModeMessages, + resolveCurrentSettings, + writeModelConfigSelection, +} from "./utils"; import { ANSI_CLEAR_SCREEN } from "./constants"; -const DEFAULT_MODEL = "deepseek-v4-pro"; -const DEFAULT_BASE_URL = "https://api.deepseek.com"; - type View = "chat" | "session-list" | "undo" | "mcp-status"; type AppProps = { @@ -807,163 +805,3 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl } export default App; - -function isCollapsedThinking(message: SessionMessage, expandedId: string | null): boolean { - if (message.role !== "assistant") { - return false; - } - if (!message.meta?.asThinking) { - return false; - } - return message.id !== expandedId; -} - -function buildSyntheticUserMessage(content: string, imageCount: number): SessionMessage { - const now = new Date().toISOString(); - return { - id: `local-${Math.random().toString(36).slice(2)}`, - sessionId: "local", - role: "user", - content, - contentParams: - imageCount > 0 - ? Array.from({ length: imageCount }, () => ({ - type: "image_url", - image_url: { url: "" }, - })) - : null, - messageParams: null, - compacted: false, - visible: true, - createTime: now, - updateTime: now, - }; -} - -export function buildPromptDraftFromSessionMessage(message: SessionMessage, nonce: number): PromptDraft { - return { - nonce, - text: typeof message.content === "string" ? message.content : "", - imageUrls: extractImageUrlsFromContentParams(message.contentParams), - }; -} - -function extractImageUrlsFromContentParams(contentParams: unknown): string[] { - const params = Array.isArray(contentParams) ? contentParams : contentParams ? [contentParams] : []; - const imageUrls: string[] = []; - for (const param of params) { - if (!param || typeof param !== "object") { - continue; - } - const record = param as { type?: unknown; image_url?: { url?: unknown } }; - const url = record.image_url?.url; - if (record.type === "image_url" && typeof url === "string" && url) { - imageUrls.push(url); - } - } - return imageUrls; -} - -function isCurrentSessionEmpty(sessionManager: SessionManager): boolean { - const activeSessionId = sessionManager.getActiveSessionId(); - return !activeSessionId || !sessionManager.getSession(activeSessionId); -} - -function buildStatusLine(entry: SessionEntry): string { - const parts: string[] = []; - parts.push(`status: ${entry.status}`); - if (typeof entry.activeTokens === "number" && entry.activeTokens > 0) { - parts.push(`tokens: ${entry.activeTokens}`); - } - if (entry.failReason) { - parts.push(`fail: ${entry.failReason}`); - } - return parts.join(" · "); -} - -export function readSettings(): DeepcodingSettings | null { - return readSettingsFile(getUserSettingsPath()); -} - -export function readProjectSettings(projectRoot: string = process.cwd()): DeepcodingSettings | null { - return readSettingsFile(getProjectSettingsPath(projectRoot)); -} - -function readSettingsFile(settingsPath: string): DeepcodingSettings | null { - try { - if (!fs.existsSync(settingsPath)) { - return null; - } - const raw = fs.readFileSync(settingsPath, "utf8"); - return JSON.parse(raw) as DeepcodingSettings; - } catch { - return null; - } -} - -export function writeSettings(settings: DeepcodingSettings): void { - const settingsPath = getUserSettingsPath(); - writeSettingsFile(settingsPath, settings); -} - -export function writeProjectSettings(settings: DeepcodingSettings, projectRoot: string = process.cwd()): void { - const settingsPath = getProjectSettingsPath(projectRoot); - writeSettingsFile(settingsPath, settings); -} - -function writeSettingsFile(settingsPath: string, settings: DeepcodingSettings): void { - fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); - fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); -} - -export function writeModelConfigSelection( - selection: ModelConfigSelection, - current: ModelConfigSelection = resolveCurrentSettings(), - projectRoot: string = process.cwd() -): { changed: boolean; settings: DeepcodingSettings } { - const projectSettingsPath = getProjectSettingsPath(projectRoot); - const shouldWriteProjectSettings = fs.existsSync(projectSettingsPath); - const rawSettings = shouldWriteProjectSettings ? readProjectSettings(projectRoot) : readSettings(); - const result = applyModelConfigSelection(rawSettings, current, selection); - if (result.changed) { - if (shouldWriteProjectSettings) { - writeProjectSettings(result.settings, projectRoot); - } else { - writeSettings(result.settings); - } - } - return result; -} - -export function resolveCurrentSettings(projectRoot: string = process.cwd()): ResolvedDeepcodingSettings { - return resolveSettingsSources( - readSettings(), - readProjectSettings(projectRoot), - { - model: DEFAULT_MODEL, - baseURL: DEFAULT_BASE_URL, - }, - process.env - ); -} - -export { createOpenAIClient } from "../common/openai-client"; - -function getUserSettingsPath(): string { - return path.join(os.homedir(), ".deepcode", "settings.json"); -} - -function getProjectSettingsPath(projectRoot: string): string { - return path.join(projectRoot, ".deepcode", "settings.json"); -} - -function formatThinkingMode(settings: Pick): string { - if (!settings.thinkingEnabled) { - return "no thinking"; - } - return `thinking ${settings.reasoningEffort}`; -} - -function formatModelConfig(settings: ModelConfigSelection): string { - return `${settings.model}, ${formatThinkingMode(settings)}`; -} diff --git a/src/ui/AppContainer.tsx b/src/ui/AppContainer.tsx index c8b31773..f36eb4aa 100644 --- a/src/ui/AppContainer.tsx +++ b/src/ui/AppContainer.tsx @@ -1,7 +1,7 @@ import React from "react"; import { AppContext } from "./contexts"; import App from "./App"; -import { RawModeProvider } from "./contexts/RawModeContext"; +import { RawModeProvider } from "./contexts"; const AppContainer: React.FC<{ projectRoot: string; diff --git a/src/ui/DropdownMenu.tsx b/src/ui/components/DropdownMenu/index.tsx similarity index 100% rename from src/ui/DropdownMenu.tsx rename to src/ui/components/DropdownMenu/index.tsx diff --git a/src/ui/components/FileMentionMenu/index.tsx b/src/ui/components/FileMentionMenu/index.tsx index ce9a8ee8..b1c77b4a 100644 --- a/src/ui/components/FileMentionMenu/index.tsx +++ b/src/ui/components/FileMentionMenu/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { Box, Text } from "ink"; import { useInput } from "ink"; -import DropdownMenu from "../../DropdownMenu"; +import DropdownMenu from "../DropdownMenu"; import type { FileMentionItem, FileMentionToken } from "../../fileMentions"; type Props = { diff --git a/src/ui/components/ModelsDropdown/index.tsx b/src/ui/components/ModelsDropdown/index.tsx index bdd68ab4..6e807569 100644 --- a/src/ui/components/ModelsDropdown/index.tsx +++ b/src/ui/components/ModelsDropdown/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { useInput } from "ink"; -import DropdownMenu from "../../DropdownMenu"; +import DropdownMenu from "../DropdownMenu"; import type { ModelConfigSelection, ReasoningEffort } from "../../../settings"; type ModelStep = "model" | "thinking"; diff --git a/src/ui/components/RawModelDropdown/index.tsx b/src/ui/components/RawModelDropdown/index.tsx index 33970136..67f053c9 100644 --- a/src/ui/components/RawModelDropdown/index.tsx +++ b/src/ui/components/RawModelDropdown/index.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useInput } from "ink"; -import DropdownMenu from "../../DropdownMenu"; +import DropdownMenu from "../DropdownMenu"; import type { RawMode } from "../../contexts"; import { RAW_COMMAND_MODELS, useRawModeContext } from "../../contexts"; diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx index b320d249..9704f32b 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -1,4 +1,4 @@ -import DropdownMenu from "../../DropdownMenu"; +import Index from "../DropdownMenu"; import React, { useEffect, useState } from "react"; import type { SkillInfo } from "../../../session"; import { useInput } from "ink"; @@ -52,7 +52,7 @@ const SkillsDropdown: React.FC<{ } return ( - 0 + ? Array.from({ length: imageCount }, () => ({ + type: "image_url", + image_url: { url: "" }, + })) + : null, + messageParams: null, + compacted: false, + visible: true, + createTime: now, + updateTime: now, + }; +} + +export function buildPromptDraftFromSessionMessage(message: SessionMessage, nonce: number): PromptDraft { + return { + nonce, + text: typeof message.content === "string" ? message.content : "", + imageUrls: extractImageUrlsFromContentParams(message.contentParams), + }; +} + +export function extractImageUrlsFromContentParams(contentParams: unknown): string[] { + const params = Array.isArray(contentParams) ? contentParams : contentParams ? [contentParams] : []; + const imageUrls: string[] = []; + for (const param of params) { + if (!param || typeof param !== "object") { + continue; + } + const record = param as { type?: unknown; image_url?: { url?: unknown } }; + const url = record.image_url?.url; + if (record.type === "image_url" && typeof url === "string" && url) { + imageUrls.push(url); + } + } + return imageUrls; +} + +export function isCurrentSessionEmpty(sessionManager: SessionManager): boolean { + const activeSessionId = sessionManager.getActiveSessionId(); + return !activeSessionId || !sessionManager.getSession(activeSessionId); +} + +export function buildStatusLine(entry: SessionEntry): string { + const parts: string[] = []; + parts.push(`status: ${entry.status}`); + if (typeof entry.activeTokens === "number" && entry.activeTokens > 0) { + parts.push(`tokens: ${entry.activeTokens}`); + } + if (entry.failReason) { + parts.push(`fail: ${entry.failReason}`); + } + return parts.join(" · "); +} + +export function readSettings(): DeepcodingSettings | null { + return readSettingsFile(getUserSettingsPath()); +} + +export function readProjectSettings(projectRoot: string = process.cwd()): DeepcodingSettings | null { + return readSettingsFile(getProjectSettingsPath(projectRoot)); +} + +export function readSettingsFile(settingsPath: string): DeepcodingSettings | null { + try { + if (!fs.existsSync(settingsPath)) { + return null; + } + const raw = fs.readFileSync(settingsPath, "utf8"); + return JSON.parse(raw) as DeepcodingSettings; + } catch { + return null; + } +} + +export function writeSettings(settings: DeepcodingSettings): void { + const settingsPath = getUserSettingsPath(); + writeSettingsFile(settingsPath, settings); +} + +export function writeProjectSettings(settings: DeepcodingSettings, projectRoot: string = process.cwd()): void { + const settingsPath = getProjectSettingsPath(projectRoot); + writeSettingsFile(settingsPath, settings); +} + +function writeSettingsFile(settingsPath: string, settings: DeepcodingSettings): void { + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); + fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); +} + +export function writeModelConfigSelection( + selection: ModelConfigSelection, + current: ModelConfigSelection = resolveCurrentSettings(), + projectRoot: string = process.cwd() +): { changed: boolean; settings: DeepcodingSettings } { + const projectSettingsPath = getProjectSettingsPath(projectRoot); + const shouldWriteProjectSettings = fs.existsSync(projectSettingsPath); + const rawSettings = shouldWriteProjectSettings ? readProjectSettings(projectRoot) : readSettings(); + const result = applyModelConfigSelection(rawSettings, current, selection); + if (result.changed) { + if (shouldWriteProjectSettings) { + writeProjectSettings(result.settings, projectRoot); + } else { + writeSettings(result.settings); + } + } + return result; +} + +export function resolveCurrentSettings(projectRoot: string = process.cwd()): ResolvedDeepcodingSettings { + return resolveSettingsSources( + readSettings(), + readProjectSettings(projectRoot), + { + model: DEFAULT_MODEL, + baseURL: DEFAULT_BASE_URL, + }, + process.env + ); +} + +export function getUserSettingsPath(): string { + return path.join(os.homedir(), ".deepcode", "settings.json"); +} + +export function getProjectSettingsPath(projectRoot: string): string { + return path.join(projectRoot, ".deepcode", "settings.json"); +} + +export function formatThinkingMode( + settings: Pick +): string { + if (!settings.thinkingEnabled) { + return "no thinking"; + } + return `thinking ${settings.reasoningEffort}`; +} + +export function formatModelConfig(settings: ModelConfigSelection): string { + return `${settings.model}, ${formatThinkingMode(settings)}`; +} From 74a85e9c10f1405c550fa95b259247dac0704a91 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 16:57:32 +0800 Subject: [PATCH 02/15] =?UTF-8?q?refactor(settings):=20=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E8=AF=BB=E5=86=99=E5=8F=8A?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=B8=B8=E9=87=8F=E8=87=B3settings=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将默认模型和基础URL常量移至settings模块,统一管理 - 实现用户配置和项目配置的读取、写入接口 - 提供获取用户和项目配置路径的工具函数 - 清理ui/utils中的相关冗余代码,简化代码结构 - 统一类型引用,调整session、session-types、settings和common之间的导入路径 - 移除AsciiArt模块内容,清理无用代码 --- Screenshot_2026-05-23_195028.png | 0 docs/issue_0522.md | 241 --------------------- src/cli.tsx | 2 +- src/common/openai-client.ts | 2 +- src/{ => common}/updateCheck.ts | 4 +- src/prompt.ts | 5 +- src/session-types.ts | 162 ++++++++++++++ src/session.ts | 190 ++-------------- src/settings.ts | 88 ++++++++ src/tests/askUserQuestion.test.ts | 2 +- src/tests/exitSummary.test.ts | 2 +- src/tests/messageView.test.ts | 2 +- src/tests/promptInputKeys.test.ts | 2 +- src/tests/session.test.ts | 3 +- src/tests/sessionList.test.ts | 2 +- src/tests/slashCommands.test.ts | 2 +- src/tests/thinkingState.test.ts | 2 +- src/tests/updateCheck.test.ts | 2 +- src/ui/App.tsx | 29 ++- src/{ => ui}/AsciiArt.ts | 0 src/ui/AskUserQuestionPrompt.tsx | 2 +- src/ui/PermissionPrompt.tsx | 5 +- src/ui/ProcessStdoutView.tsx | 2 +- src/ui/PromptInput.tsx | 5 +- src/ui/SessionList.tsx | 2 +- src/ui/SlashCommandMenu.tsx | 2 +- src/ui/UndoSelector.tsx | 2 +- src/ui/WelcomeScreen.tsx | 4 +- src/ui/askUserQuestion.ts | 2 +- src/ui/components/MessageView/types.ts | 2 +- src/ui/components/MessageView/utils.ts | 2 +- src/ui/components/SkillsDropdown/index.tsx | 2 +- src/ui/constants.ts | 5 - src/ui/exitSummary.ts | 2 +- src/ui/index.ts | 8 +- src/ui/loadingText.ts | 2 +- src/ui/slashCommands.ts | 2 +- src/ui/thinkingState.ts | 17 +- src/ui/utils/index.ts | 95 +------- 39 files changed, 346 insertions(+), 559 deletions(-) delete mode 100644 Screenshot_2026-05-23_195028.png delete mode 100644 docs/issue_0522.md rename src/{ => common}/updateCheck.ts (98%) create mode 100644 src/session-types.ts rename src/{ => ui}/AsciiArt.ts (100%) diff --git a/Screenshot_2026-05-23_195028.png b/Screenshot_2026-05-23_195028.png deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/issue_0522.md b/docs/issue_0522.md deleted file mode 100644 index 2e9fd1a1..00000000 --- a/docs/issue_0522.md +++ /dev/null @@ -1,241 +0,0 @@ -# Deep Code Permission System (设计文档) - -scopes是枚举值,列表如下: - -``` -# PermissionScope -read-in-cwd -read-out-cwd -write-in-cwd -write-out-cwd -delete-in-cwd -delete-out-cwd -query-git-log -mutate-git-log -network -mcp -``` - -settings.json的配置项(例子): - -``` -{ - "permissions": { - "allow": [ - "write-in-cwd" - ], - "deny": [ - "write-out-cwd" - ], - "ask": [ - "read-out-cwd" - ], - "defaultMode": "allowAll|askAll" // 默认是allowAll - } -} -``` - -工具和PermissionScope可能的对应关系: - -- read: read-in-cwd, read-out-cwd -- write: write-in-cwd, write-out-cwd -- edit: write-in-cwd, write-out-cwd -- WebSearch: network -- mcp__*: mcp -- bash: 每一次bash命令需要的scope在sideEffects字段中。如果sideEffects字段为undefined|null,或者sideEffects包含了"unknown"则总是ask -- 其他: 无权限要求,总是允许 - -## bash tool的参数schema新增sideEffects字段 - -目标:让LLM在每一次调用`bash`时显式声明该命令可能需要的权限范围,后端只信任这个结构化字段,不从自然语言`description`中推断权限。 - -需要同步修改两处schema: - -1. `src/prompt.ts`里的`getTools()`内置`bash`工具定义。 -2. `templates/tools/bash.md`里的`bash`工具说明和JSON schema示例。 - -新增字段: - -``` -sideEffects: PermissionScope[] | ["unknown"] -``` - -`bash`可声明的scope只包含文件系统、Git历史和网络权限,不包含`mcp`: - -``` -read-in-cwd -read-out-cwd -write-in-cwd -write-out-cwd -delete-in-cwd -delete-out-cwd -query-git-log -mutate-git-log -network -unknown -``` - -建议schema如下: - -```json -{ - "type": "object", - "properties": { - "command": { - "description": "The command to execute", - "type": "string" - }, - "description": { - "description": "Clear, concise description of what this command does in active voice.", - "type": "string" - }, - "sideEffects": { - "description": "Permission scopes required by this bash command. Use [] only for commands that do not read, write, delete, or access the network. Use [\"unknown\"] when the effects cannot be classified safely.", - "type": "array", - "items": { - "type": "string", - "enum": [ - "read-in-cwd", - "read-out-cwd", - "write-in-cwd", - "write-out-cwd", - "delete-in-cwd", - "delete-out-cwd", - "query-git-log", - "mutate-git-log", - "network", - "unknown" - ] - }, - "uniqueItems": true - } - }, - "required": [ - "command", - "sideEffects" - ], - "additionalProperties": false -} -``` - -字段语义: - -- `sideEffects: []`表示命令不需要权限,例如`date`、`node --version`这类只读取进程环境或输出版本信息的命令。 -- `sideEffects`必须按最小必要权限填写;例如`rg foo src`是`["read-in-cwd"]`,`npm install`通常是`["write-in-cwd", "network"]`。 -- 如果命令访问项目目录之外的路径,需要使用`*-out-cwd`;例如`cat /etc/hosts`是`["read-out-cwd"]`。 -- 删除类操作使用`delete-*`;如果同一条命令还会写入其他文件,再同时声明对应的`write-*`。 -- 查询Git历史使用`query-git-log`;例如`git log`、`git show HEAD`、`git blame`、`git diff HEAD~1..HEAD`这类读取提交历史、提交对象或历史diff的命令。 -- 修改Git历史或引用使用`mutate-git-log`;例如`git commit`、`git reset`、`git rebase`、`git merge`、`git cherry-pick`、`git tag`这类会创建提交、移动引用或改写提交图的命令。 -- Git命令如果同时读写工作区文件,也需要同时声明文件系统scope;例如`git checkout -- src/foo.ts`需要`["write-in-cwd"]`,`git reset --hard HEAD~1`需要`["write-in-cwd", "mutate-git-log"]`。 -- `unknown`只能单独出现为`["unknown"]`,不能和其他scope混用。 - -示例: - -```json -{ "command": "date", "description": "Show current date", "sideEffects": [] } -{ "command": "rg \"TODO\" src", "description": "Search TODO markers in source files", "sideEffects": ["read-in-cwd"] } -{ "command": "npm install", "description": "Install package dependencies", "sideEffects": ["write-in-cwd", "network"] } -{ "command": "rm -rf dist", "description": "Delete build output directory", "sideEffects": ["delete-in-cwd"] } -{ "command": "curl -s https://example.com", "description": "Fetch example.com response", "sideEffects": ["network"] } -{ "command": "git show --stat HEAD", "description": "Show file statistics for HEAD", "sideEffects": ["query-git-log"] } -{ "command": "git blame src/prompt.ts", "description": "Show line authorship for prompt source", "sideEffects": ["read-in-cwd", "query-git-log"] } -{ "command": "git reset --hard HEAD~1", "description": "Reset branch and worktree to previous commit", "sideEffects": ["write-in-cwd", "mutate-git-log"] } -``` - -## 核心数据结构设计 - -``` -export type UserPromptContent = { - text?: string; - imageUrls?: string[]; - skills?: SkillInfo[]; -+ permissions?: [{toolCallId: "...", permission: "allow|deny"}]; -+ alwaysAllows?: [""]; -}; - -export type SessionEntry = { - id: string; - ... - toolCalls: unknown[] | null; // 例如:[{"id":"...","function":{"name":"bash","arguments":"{\"command\": \"...\", \"description\": \"...\"}"}}] - status: SessionStatus; -+ askPermissions?: [{toolCallId: "...", scopes: [""], name: "...", command: "...", description?: "..."}]; -}; - -export type SessionStatus = "... | "completed" | "interrupted" | "ask_permission"; // 新增 ask_permission 状态 - -export type SessionMessage = { - ... - meta?: MessageMeta; - ... -}; - -export type MessageMeta = { - ... -+ permissions?: [{toolCallId: "...", permission: "allow|deny|ask"}]; -+ userPrompt?: UserPromptContent; //对于role为user的消息,持久化userPrompt可方便后续排查问题 -}; -``` - -## 前端流程 - -如果当前会话状态不是ask_permission,则保持现状。会话状态是ask_permission时: - -对SessionEntry.askPermissions中每一个toolCallId的每一个scope,显示权限弹窗(示例): - -``` - - - - - - Do you want to proceed? - ❯ 1. Yes - 2. Yes, and always allow - 3. No -``` - -注意对于read/write/edit的``,格式可以是"工具名称+相对或绝对文件路径",例如:`read ~/dev/main.c` - -如果在权限弹窗过程中,用户按Esc,则走现有的interrupt流程(会话状态也应该变成"interrupted")。 - -提醒注意一种情况:例如askPermissions里面有好几个item的scopes是`["write-in-cwd"]`,如果用户已经在第一个权限弹窗选了"always allow write in CWD `~/dev/qrcode_test/`",则后面的几个scopes是`["write-in-cwd"]`的item就不用显示权限弹窗了。 - -如果用户完成了所有权限弹窗的选择,则判断: - -1. 如果用户提交的结果中包含deny,则需要用户输入user prompt,按回车手动提交replySession()。 - - 如果用户没有输入user prompt就退出了,或者切换到了其他会话。则重新开始这个会话时,由于会话状态还是ask_permission,则会重新显示权限弹窗,要求用户选择。 -2. 如果用户提交的结果中不包含deny,则以`/continue`作为UserPromptContent.text内容,前端自动提交replySession()。 - - -## 后端流程 - -后端主要是对replySession()和activateSession()进行升级: - -1. 支持传入UserPromptContent.permissions和alwaysAllows -2. 如果UserPromptContent.alwaysAllows非空,将其中的scopes追加写入项目级别的settings.json配置文件(`permissions.allow`字段),避免重复写入已存在的项。 -3. 检查当前会话消息列表末尾是否存在连续的role为assistant的有tool_calls的消息,也就是"待执行消息"。如果没有,则走现有流程。 -4. 对于每一条待执行消息,先检查UserPromptContent.permissions中对应的toolCallId的用户授权是allow还是deny - - 如果是allow,则正常执行这个toolCall - - 如果是deny,则直接返回失败结果,报错信息提示LLM用户禁用相关权限。例如: - ``` - { - "ok": false, - "name": "edit", - "error": "用户已禁用了在项目目录之外修改文件的权限,请不要尝试用任何方式修改目录之外的文件" - } - ``` -5. 如果对于某条待执行消息,在UserPromptContent.permissions没有出现对应的toolCallId的用户授权,则检查它的 SessionMessage.meta.permissions[].permission 是allow还是deny还是ask - - 如果是allow,则正常执行这个toolCall - - 如果是deny,则直接返回失败结果,报错信息提示LLM用户禁用相关权限 - - 如果是ask,则直接返回失败结果,报错信息提示LLM用户未授权执行。例如: - ``` - { - "ok": false, - "name": "edit", - "error": "用户暂未授权执行,如果有必要,可重新尝试执行" - } - ``` - - 如果不存在,则正常执行这个toolCall(兼容老版本会话数据) -6. 当LLM返回了新的待执行消息时,不要立即执行,而是: - 1. 根据配置的permissions和defaultMode,计算出SessionMessage.meta.permissions字段 - 2. 如果存在一个待执行消息的SessionMessage.meta.permissions[].permission是ask,则把SessionEntry.status设置为"ask_permission",并设置好SessionEntry.askPermissions,然后退出activateSession,这样就回到了上面的前端流程。 diff --git a/src/cli.tsx b/src/cli.tsx index c3876ae5..d179203e 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render } from "ink"; import { setShellIfWindows } from "./common/shell-utils"; -import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./updateCheck"; +import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/updateCheck"; import { AppContainer } from "./ui"; const args = process.argv.slice(2); diff --git a/src/common/openai-client.ts b/src/common/openai-client.ts index 8c40cdfe..ee3dd667 100644 --- a/src/common/openai-client.ts +++ b/src/common/openai-client.ts @@ -3,7 +3,7 @@ import * as os from "os"; import * as path from "path"; import OpenAI from "openai"; import { Agent, fetch as undiciFetch } from "undici"; -import { resolveCurrentSettings } from "../ui"; +import { resolveCurrentSettings } from "../settings"; // Custom undici Agent with a 180-second keepAlive timeout. The default // global fetch (undici) only keeps connections alive for 4 seconds, which diff --git a/src/updateCheck.ts b/src/common/updateCheck.ts similarity index 98% rename from src/updateCheck.ts rename to src/common/updateCheck.ts index fcd9bfba..09c0273c 100644 --- a/src/updateCheck.ts +++ b/src/common/updateCheck.ts @@ -5,8 +5,8 @@ import * as os from "os"; import * as path from "path"; import { render, type Instance } from "ink"; import chalk from "chalk"; -import { UpdatePrompt, type UpdatePromptChoice } from "./ui"; -import { killProcessTree } from "./common/process-tree"; +import { UpdatePrompt, type UpdatePromptChoice } from "../ui"; +import { killProcessTree } from "./process-tree"; export type PackageInfo = { name: string; diff --git a/src/prompt.ts b/src/prompt.ts index ba9bf231..4fcd06d6 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -4,7 +4,7 @@ import * as os from "os"; import * as path from "path"; import { fileURLToPath } from "url"; import ejs from "ejs"; -import type { SessionMessage } from "./session"; +import type { SessionMessage } from "./session-types"; import { findGitBashPath, resolveShellPath } from "./common/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; @@ -166,8 +166,7 @@ function getCurrentDateAndModelPrompt(model?: string): string { export function getSystemPrompt(_projectRoot: string, options: PromptToolOptions = {}): string { const toolDocs = readToolDocs(getExtensionRoot(), options); - const basePrompt = toolDocs ? `${SYSTEM_PROMPT_BASE}\n\n# Available Tools\n\n${toolDocs}` : SYSTEM_PROMPT_BASE; - return basePrompt; + return toolDocs ? `${SYSTEM_PROMPT_BASE}\n\n# Available Tools\n\n${toolDocs}` : SYSTEM_PROMPT_BASE; } export function getCompactPrompt(sessionMessages: SessionMessage[]): string { diff --git a/src/session-types.ts b/src/session-types.ts new file mode 100644 index 00000000..33119cc3 --- /dev/null +++ b/src/session-types.ts @@ -0,0 +1,162 @@ +import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; +import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "./common/permissions"; +import type { CreateOpenAIClient } from "./tools/executor"; +import { DEEPSEEK_V4_MODELS } from "./common/model-capabilities"; + +export type SessionStatus = + | "failed" + | "pending" + | "processing" + | "waiting_for_user" + | "completed" + | "interrupted" + | "ask_permission" + | "permission_denied"; + +export type ModelUsage = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + completion_tokens_details?: Record; + prompt_tokens_details?: Record; + prompt_cache_hit_tokens?: number; + prompt_cache_miss_tokens?: number; + total_reqs?: number; +}; + +export type SessionProcessEntry = { + startTime: string; + command: string; + timeoutMs?: number; + deadlineAt?: string; + timedOut?: boolean; +}; + +export type BashTimeoutAdjustment = { + processId: string; + timeoutMs: number; + deadlineAt: string; + timedOut: boolean; +}; + +export type SessionEntry = { + id: string; + summary: string | null; + assistantReply: string | null; + assistantThinking: string | null; + assistantRefusal: string | null; + toolCalls: unknown[] | null; + status: SessionStatus; + failReason: string | null; + usage: ModelUsage | null; + usagePerModel: Record | null; + activeTokens: number; + createTime: string; + updateTime: string; + processes: Map | null; + askPermissions?: AskPermissionRequest[]; +}; + +export type SessionsIndex = { + version: 1; + entries: SessionEntry[]; + originalPath: string; +}; + +export type SessionMessageRole = "system" | "user" | "assistant" | "tool"; + +export type MessageMeta = { + function?: unknown; + paramsMd?: string; + resultMd?: string; + asThinking?: boolean; + isSummary?: boolean; + isModelChange?: boolean; + skill?: SkillInfo; + permissions?: MessageToolPermission[]; + userPrompt?: UserPromptContent; +}; + +export type SessionMessage = { + id: string; + sessionId: string; + role: SessionMessageRole; + content: string | null; + contentParams: unknown | null; + messageParams: unknown | null; + compacted: boolean; + visible: boolean; + createTime: string; + updateTime: string; + meta?: MessageMeta; + html?: string; + checkpointHash?: string; +}; + +export type UndoTarget = { + message: SessionMessage; + index: number; + canRestoreCode: boolean; +}; + +export type UserPromptContent = { + text?: string; + imageUrls?: string[]; + skills?: SkillInfo[]; + permissions?: UserToolPermission[]; + alwaysAllows?: PermissionScope[]; +}; + +export type SkillInfo = { + name: string; + path: string; + description: string; + isLoaded?: boolean; +}; + +export type SessionManagerOptions = { + projectRoot: string; + createOpenAIClient: CreateOpenAIClient; + getResolvedSettings: () => { + model: string; + webSearchTool?: string; + mcpServers?: Record; + permissions?: Required; + }; + renderMarkdown: (text: string) => string; + onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; + onSessionEntryUpdated?: (entry: SessionEntry) => void; + onLlmStreamProgress?: (progress: LlmStreamProgress) => void; + onMcpStatusChanged?: () => void; + onProcessStdout?: (pid: number, chunk: string) => void; +}; + +export type LlmStreamProgress = { + requestId: string; + sessionId?: string; + startedAt: string; + estimatedTokens: number; + formattedTokens: string; + phase: "start" | "update" | "end"; +}; + +const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; +const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; + +export function getCompactPromptTokenThreshold(model: string): number { + return DEEPSEEK_V4_MODELS.has(model) + ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD + : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; +} + +function isUsageRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +export function getTotalTokens(usage: ModelUsage | null | undefined): number { + if (!isUsageRecord(usage)) { + return 0; + } + const totalTokens = (usage as Record).total_tokens; + return typeof totalTokens === "number" ? totalTokens : 0; +} diff --git a/src/session.ts b/src/session.ts index a9fc39e8..bf501671 100644 --- a/src/session.ts +++ b/src/session.ts @@ -5,10 +5,10 @@ import * as crypto from "crypto"; import { fileURLToPath } from "url"; import matter from "gray-matter"; import ejs from "ejs"; -import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "openai/resources/chat/completions"; +import type { ChatCompletionContentPart, ChatCompletionMessageParam } from "openai/resources/chat/completions"; import { launchNotifyScript } from "./common/notify"; import { buildThinkingRequestOptions } from "./common/openai-thinking"; -import { DEEPSEEK_V4_MODELS, supportsMultimodal } from "./common/model-capabilities"; +import { supportsMultimodal } from "./common/model-capabilities"; import { getCompactPrompt, getDefaultSkillPrompt, @@ -18,15 +18,15 @@ import { type ToolDefinition, } from "./prompt"; import { - ToolExecutor, type CreateOpenAIClient, type ProcessTimeoutControl, type ProcessTimeoutInfo, type ToolCallExecution, type ToolExecutionHooks, + ToolExecutor, } from "./tools/executor"; import { McpManager } from "./mcp/mcp-manager"; -import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; +import type { McpServerConfig, PermissionSettings } from "./settings"; import { logApiError } from "./common/error-logger"; import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; import { killProcessTree } from "./common/process-tree"; @@ -37,29 +37,34 @@ import { buildPermissionToolExecution, computeToolCallPermissions, hasUserPermissionReplies, + type MessageToolPermission, normalizeAskPermissions, parseToolCallForPermissions, - type AskPermissionRequest, - type MessageToolPermission, type PermissionToolCall, type UserToolPermission, } from "./common/permissions"; -export type { PermissionScope } from "./settings"; -export type { - AskPermissionRequest, - AskPermissionScope, - BashPermissionScope, - MessageToolPermission, - PermissionDecision, - UserToolPermission, -} from "./common/permissions"; +import { + type BashTimeoutAdjustment, + getCompactPromptTokenThreshold, + getTotalTokens, + type LlmStreamProgress, + type MessageMeta, + type ModelUsage, + type SessionEntry, + type SessionManagerOptions, + type SessionMessage, + type SessionProcessEntry, + type SessionsIndex, + type SessionStatus, + type SkillInfo, + type UndoTarget, + type UserPromptContent, +} from "./session-types"; const MAX_SESSION_ENTRIES = 50; const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; const NEW_PROMPT_REPORT_TIMEOUT_MS = 3000; -const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; -const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; type ChatCompletionDebugOptions = { enabled?: boolean; @@ -68,12 +73,6 @@ type ChatCompletionDebugOptions = { params?: Record; }; -export function getCompactPromptTokenThreshold(model: string): number { - return DEEPSEEK_V4_MODELS.has(model) - ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD - : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; -} - function isUsageRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } @@ -144,151 +143,6 @@ function getExtensionRoot(): string { return path.resolve(path.dirname(currentFilePath), ".."); } -function getTotalTokens(usage: ModelUsage | null | undefined): number { - if (!isUsageRecord(usage)) { - return 0; - } - const totalTokens = usage.total_tokens; - return typeof totalTokens === "number" ? totalTokens : 0; -} - -export type SessionStatus = - | "failed" - | "pending" - | "processing" - | "waiting_for_user" - | "completed" - | "interrupted" - | "ask_permission" - | "permission_denied"; - -export type ModelUsage = { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - completion_tokens_details?: Record; - prompt_tokens_details?: Record; - prompt_cache_hit_tokens?: number; - prompt_cache_miss_tokens?: number; - total_reqs?: number; -}; - -export type SessionProcessEntry = { - startTime: string; - command: string; - timeoutMs?: number; - deadlineAt?: string; - timedOut?: boolean; -}; - -export type BashTimeoutAdjustment = { - processId: string; - timeoutMs: number; - deadlineAt: string; - timedOut: boolean; -}; - -export type SessionEntry = { - id: string; - summary: string | null; - assistantReply: string | null; - assistantThinking: string | null; - assistantRefusal: string | null; - toolCalls: unknown[] | null; - status: SessionStatus; - failReason: string | null; - usage: ModelUsage | null; - usagePerModel: Record | null; - activeTokens: number; - createTime: string; - updateTime: string; - processes: Map | null; // {pid: process info} - askPermissions?: AskPermissionRequest[]; -}; - -export type SessionsIndex = { - version: 1; - entries: SessionEntry[]; - originalPath: string; -}; - -export type SessionMessageRole = "system" | "user" | "assistant" | "tool"; - -export type MessageMeta = { - function?: unknown; - paramsMd?: string; - resultMd?: string; - asThinking?: boolean; - isSummary?: boolean; - isModelChange?: boolean; - skill?: SkillInfo; - permissions?: MessageToolPermission[]; - userPrompt?: UserPromptContent; -}; - -export type SessionMessage = { - id: string; - sessionId: string; - role: SessionMessageRole; - content: string | null; - contentParams: unknown | null; - messageParams: unknown | null; - compacted: boolean; - visible: boolean; - createTime: string; - updateTime: string; - meta?: MessageMeta; - html?: string; - checkpointHash?: string; -}; - -export type UndoTarget = { - message: SessionMessage; - index: number; - canRestoreCode: boolean; -}; - -export type UserPromptContent = { - text?: string; - imageUrls?: string[]; - skills?: SkillInfo[]; - permissions?: UserToolPermission[]; - alwaysAllows?: PermissionScope[]; -}; - -export type SkillInfo = { - name: string; - path: string; - description: string; - isLoaded?: boolean; -}; - -type SessionManagerOptions = { - projectRoot: string; - createOpenAIClient: CreateOpenAIClient; - getResolvedSettings: () => { - model: string; - webSearchTool?: string; - mcpServers?: Record; - permissions?: Required; - }; - renderMarkdown: (text: string) => string; - onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; - onSessionEntryUpdated?: (entry: SessionEntry) => void; - onLlmStreamProgress?: (progress: LlmStreamProgress) => void; - onMcpStatusChanged?: () => void; - onProcessStdout?: (pid: number, chunk: string) => void; -}; - -export type LlmStreamProgress = { - requestId: string; - sessionId?: string; - startedAt: string; - estimatedTokens: number; - formattedTokens: string; - phase: "start" | "update" | "end"; -}; - export class SessionManager { private readonly projectRoot: string; private readonly createOpenAIClient: CreateOpenAIClient; diff --git a/src/settings.ts b/src/settings.ts index e0b17768..b7a7a777 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,4 +1,7 @@ import { defaultsToThinkingMode } from "./common/model-capabilities"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; export type DeepcodingEnv = Record & { MODEL?: string; @@ -370,3 +373,88 @@ export function applyModelConfigSelection( return { settings: next, changed: true }; } + +// --------------------------------------------------------------------------- +// Default constants +// --------------------------------------------------------------------------- + +export const DEFAULT_MODEL = "deepseek-v4-pro"; +export const DEFAULT_BASE_URL = "https://api.deepseek.com"; + +// --------------------------------------------------------------------------- +// Settings file I/O +// --------------------------------------------------------------------------- + +export function getUserSettingsPath(): string { + return path.join(os.homedir(), ".deepcode", "settings.json"); +} + +export function getProjectSettingsPath(projectRoot: string): string { + return path.join(projectRoot, ".deepcode", "settings.json"); +} + +export function readSettingsFile(settingsPath: string): DeepcodingSettings | null { + try { + if (!fs.existsSync(settingsPath)) { + return null; + } + const raw = fs.readFileSync(settingsPath, "utf8"); + return JSON.parse(raw) as DeepcodingSettings; + } catch { + return null; + } +} + +export function readSettings(): DeepcodingSettings | null { + return readSettingsFile(getUserSettingsPath()); +} + +export function readProjectSettings(projectRoot: string = process.cwd()): DeepcodingSettings | null { + return readSettingsFile(getProjectSettingsPath(projectRoot)); +} + +function writeSettingsFile(settingsPath: string, settings: DeepcodingSettings): void { + fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); + fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); +} + +export function writeSettings(settings: DeepcodingSettings): void { + const settingsPath = getUserSettingsPath(); + writeSettingsFile(settingsPath, settings); +} + +export function writeProjectSettings(settings: DeepcodingSettings, projectRoot: string = process.cwd()): void { + const settingsPath = getProjectSettingsPath(projectRoot); + writeSettingsFile(settingsPath, settings); +} + +export function writeModelConfigSelection( + selection: ModelConfigSelection, + current: ModelConfigSelection = resolveCurrentSettings(), + projectRoot: string = process.cwd() +): { changed: boolean; settings: DeepcodingSettings } { + const projectSettingsPath = getProjectSettingsPath(projectRoot); + const shouldWriteProjectSettings = fs.existsSync(projectSettingsPath); + const rawSettings = shouldWriteProjectSettings ? readProjectSettings(projectRoot) : readSettings(); + const result = applyModelConfigSelection(rawSettings, current, selection); + if (result.changed) { + if (shouldWriteProjectSettings) { + writeProjectSettings(result.settings, projectRoot); + } else { + writeSettings(result.settings); + } + } + return result; +} + +export function resolveCurrentSettings(projectRoot: string = process.cwd()): ResolvedDeepcodingSettings { + return resolveSettingsSources( + readSettings(), + readProjectSettings(projectRoot), + { + model: DEFAULT_MODEL, + baseURL: DEFAULT_BASE_URL, + }, + process.env + ); +} diff --git a/src/tests/askUserQuestion.test.ts b/src/tests/askUserQuestion.test.ts index f7543512..89907b07 100644 --- a/src/tests/askUserQuestion.test.ts +++ b/src/tests/askUserQuestion.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findPendingAskUserQuestion, formatAskUserQuestionAnswers, formatAskUserQuestionDecline } from "../ui"; -import type { SessionMessage } from "../session"; +import type { SessionMessage } from "../session-types"; function message(content: unknown): SessionMessage { const now = "2026-04-29T00:00:00.000Z"; diff --git a/src/tests/exitSummary.test.ts b/src/tests/exitSummary.test.ts index 5ea4b579..651f8b96 100644 --- a/src/tests/exitSummary.test.ts +++ b/src/tests/exitSummary.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { buildExitSummaryText } from "../ui"; -import type { ModelUsage, SessionEntry } from "../session"; +import type { ModelUsage, SessionEntry } from "../session-types"; const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*m/g, ""); diff --git a/src/tests/messageView.test.ts b/src/tests/messageView.test.ts index b806dbd1..c9dfa8b3 100644 --- a/src/tests/messageView.test.ts +++ b/src/tests/messageView.test.ts @@ -8,7 +8,7 @@ import { parseToolPayload, } from "../ui/components/MessageView/utils"; import { RawMode } from "../ui/contexts"; -import type { SessionMessage } from "../session"; +import type { SessionMessage } from "../session-types"; import type { ToolSummary } from "../ui/components/MessageView/types"; test("parseDiffPreview removes headers and classifies lines", () => { diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts index 4f8b4d95..3b24b213 100644 --- a/src/tests/promptInputKeys.test.ts +++ b/src/tests/promptInputKeys.test.ts @@ -27,7 +27,7 @@ import { insertText, backspace, } from "../ui"; -import type { SessionMessage, SkillInfo } from "../session"; +import type { SessionMessage, SkillInfo } from "../session-types"; function collectDispatchedInput(data: string) { const events: ReturnType[] = []; diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 95de8e35..5615ff55 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -5,7 +5,8 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { GitFileHistory } from "../common/file-history"; -import { SessionManager, type SessionMessage } from "../session"; +import { type SessionMessage } from "../session-types"; +import { SessionManager } from "../session"; const originalFetch = globalThis.fetch; const originalConsoleWarn = console.warn; diff --git a/src/tests/sessionList.test.ts b/src/tests/sessionList.test.ts index 6fe41c70..edae36b2 100644 --- a/src/tests/sessionList.test.ts +++ b/src/tests/sessionList.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { formatSessionTitle, filterSessions, formatSessionStatus } from "../ui"; -import type { SessionEntry } from "../session"; +import type { SessionEntry } from "../session-types"; test("formatSessionTitle replaces newlines with spaces", () => { assert.equal(formatSessionTitle("first line\nsecond line\r\nthird"), "first line second line third"); diff --git a/src/tests/slashCommands.test.ts b/src/tests/slashCommands.test.ts index 30d77eeb..d352f3a5 100644 --- a/src/tests/slashCommands.test.ts +++ b/src/tests/slashCommands.test.ts @@ -7,7 +7,7 @@ import { formatSlashCommandDescription, formatSlashCommandLabel, } from "../ui"; -import type { SkillInfo } from "../session"; +import type { SkillInfo } from "../session-types"; const skills: SkillInfo[] = [ { name: "skill-writer", path: "~/.agents/skills/skill-writer/SKILL.md", description: "Write a SKILL.md" }, diff --git a/src/tests/thinkingState.test.ts b/src/tests/thinkingState.test.ts index 8f2a0e30..f50ab935 100644 --- a/src/tests/thinkingState.test.ts +++ b/src/tests/thinkingState.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findExpandedThinkingId } from "../ui"; -import type { SessionMessage } from "../session"; +import type { SessionMessage } from "../session-types"; function buildMessage( id: string, diff --git a/src/tests/updateCheck.test.ts b/src/tests/updateCheck.test.ts index ce77fe5e..23682de2 100644 --- a/src/tests/updateCheck.test.ts +++ b/src/tests/updateCheck.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { compareVersions, parseNpmViewVersion } from "../updateCheck"; +import { compareVersions, parseNpmViewVersion } from "../common/updateCheck"; test("compareVersions orders semantic versions", () => { assert.equal(compareVersions("0.1.4", "0.1.3"), 1); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 24640b03..b140574d 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -2,18 +2,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta import { Box, Static, Text, useApp, useStdout, useWindowSize } from "ink"; import chalk from "chalk"; import { createOpenAIClient } from "../common/openai-client"; -import { - type LlmStreamProgress, - type MessageMeta, - type PermissionScope, - type SessionEntry, - SessionManager, - type SessionMessage, - type SessionStatus, - type SkillInfo, - type UndoTarget, - type UserPromptContent, -} from "../session"; +import type { PermissionScope } from "../settings"; import { type ModelConfigSelection } from "../settings"; import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptInput"; import { MessageView, RawModeExitPrompt } from "./components"; @@ -39,13 +28,23 @@ import { buildStatusLine, buildSyntheticUserMessage, formatModelConfig, - isCollapsedThinking, isCurrentSessionEmpty, renderRawModeMessages, - resolveCurrentSettings, - writeModelConfigSelection, } from "./utils"; +import { resolveCurrentSettings, writeModelConfigSelection } from "../settings"; +import { isCollapsedThinking } from "./thinkingState"; import { ANSI_CLEAR_SCREEN } from "./constants"; +import type { + LlmStreamProgress, + MessageMeta, + SessionEntry, + SessionMessage, + SessionStatus, + SkillInfo, + UndoTarget, + UserPromptContent, +} from "../session-types"; +import { SessionManager } from "../session"; type View = "chat" | "session-list" | "undo" | "mcp-status"; diff --git a/src/AsciiArt.ts b/src/ui/AsciiArt.ts similarity index 100% rename from src/AsciiArt.ts rename to src/ui/AsciiArt.ts diff --git a/src/ui/AskUserQuestionPrompt.tsx b/src/ui/AskUserQuestionPrompt.tsx index 7c76ae38..c84b6200 100644 --- a/src/ui/AskUserQuestionPrompt.tsx +++ b/src/ui/AskUserQuestionPrompt.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; import type { AskUserQuestionAnswers, AskUserQuestionItem } from "./askUserQuestion"; -import { useTerminalInput } from "./PromptInput"; +import { useTerminalInput } from "./prompt"; type Props = { questions: AskUserQuestionItem[]; diff --git a/src/ui/PermissionPrompt.tsx b/src/ui/PermissionPrompt.tsx index 03881a58..dd2d8ebf 100644 --- a/src/ui/PermissionPrompt.tsx +++ b/src/ui/PermissionPrompt.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import type { AskPermissionRequest, AskPermissionScope, PermissionScope, UserToolPermission } from "../session"; -import { useTerminalInput } from "./PromptInput"; +import { useTerminalInput } from "./prompt"; +import type { AskPermissionRequest, AskPermissionScope, UserToolPermission } from "../common/permissions"; +import type { PermissionScope } from "../settings"; export type PermissionPromptResult = { permissions: UserToolPermission[]; diff --git a/src/ui/ProcessStdoutView.tsx b/src/ui/ProcessStdoutView.tsx index bc76a2f1..b47e0cdd 100644 --- a/src/ui/ProcessStdoutView.tsx +++ b/src/ui/ProcessStdoutView.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text } from "ink"; import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../common/bash-timeout"; -import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../session"; +import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../session-types"; import { useTerminalInput } from "./prompt"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index 8c808e9e..dd124689 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -46,7 +46,6 @@ import { } from "./fileMentions"; import type { FileMentionItem } from "./fileMentions"; import { readClipboardImageAsync } from "./clipboard"; -import type { PermissionScope, SessionEntry, SkillInfo, UserToolPermission } from "../session"; // Re-exported from prompt modules for backward compatibility export { useTerminalInput, parseTerminalInput, dispatchTerminalInput } from "./prompt"; @@ -61,8 +60,10 @@ import { useTerminalFocusReporting, } from "./prompt"; import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; -import type { ModelConfigSelection } from "../settings"; +import type { ModelConfigSelection, PermissionScope } from "../settings"; import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "./components"; +import type { SessionEntry, SkillInfo } from "../session-types"; +import type { UserToolPermission } from "../common/permissions"; export type PromptSubmission = { text: string; diff --git a/src/ui/SessionList.tsx b/src/ui/SessionList.tsx index 2d83b847..4ea620e7 100644 --- a/src/ui/SessionList.tsx +++ b/src/ui/SessionList.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { SessionEntry, SessionStatus } from "../session"; +import type { SessionEntry, SessionStatus } from "../session-types"; import { truncate } from "./components/MessageView/utils"; type Props = { diff --git a/src/ui/SlashCommandMenu.tsx b/src/ui/SlashCommandMenu.tsx index df599b54..ddd79251 100644 --- a/src/ui/SlashCommandMenu.tsx +++ b/src/ui/SlashCommandMenu.tsx @@ -3,7 +3,7 @@ import type { SlashCommandItem } from "./slashCommands"; import { ARGS_SEPARATOR } from "./constants"; import React from "react"; import { Box, Text } from "ink"; -import type { SkillInfo } from "../session"; +import type { SkillInfo } from "../session-types"; type SlashCommandMenuProps = { items: SlashCommandItem[]; diff --git a/src/ui/UndoSelector.tsx b/src/ui/UndoSelector.tsx index fad3e178..e41993e2 100644 --- a/src/ui/UndoSelector.tsx +++ b/src/ui/UndoSelector.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { UndoTarget } from "../session"; +import type { UndoTarget } from "../session-types"; export type UndoRestoreMode = "code-and-conversation" | "conversation"; diff --git a/src/ui/WelcomeScreen.tsx b/src/ui/WelcomeScreen.tsx index 7e740d1f..2e6b7cfa 100644 --- a/src/ui/WelcomeScreen.tsx +++ b/src/ui/WelcomeScreen.tsx @@ -2,11 +2,11 @@ import React, { useMemo, useState } from "react"; import { Box, Text } from "ink"; import * as os from "node:os"; import path from "node:path"; -import type { SkillInfo } from "../session"; +import type { SkillInfo } from "../session-types"; import type { ResolvedDeepcodingSettings } from "../settings"; import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "./slashCommands"; import { ThemedGradient } from "./ThemedGradient"; -import { AsciiLogo } from "../AsciiArt"; +import { AsciiLogo } from "./AsciiArt"; import { useAppContext } from "./contexts"; type WelcomeScreenProps = { diff --git a/src/ui/askUserQuestion.ts b/src/ui/askUserQuestion.ts index 8d168d86..813f7cf8 100644 --- a/src/ui/askUserQuestion.ts +++ b/src/ui/askUserQuestion.ts @@ -1,4 +1,4 @@ -import type { SessionMessage, SessionStatus } from "../session"; +import type { SessionMessage, SessionStatus } from "../session-types"; export type AskUserQuestionOption = { label: string; diff --git a/src/ui/components/MessageView/types.ts b/src/ui/components/MessageView/types.ts index 743eb2dc..3d734f89 100644 --- a/src/ui/components/MessageView/types.ts +++ b/src/ui/components/MessageView/types.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../../session"; +import type { SessionMessage } from "../../../session-types"; export type MessageViewProps = { message: SessionMessage; diff --git a/src/ui/components/MessageView/utils.ts b/src/ui/components/MessageView/utils.ts index af5391d8..164b5fb4 100644 --- a/src/ui/components/MessageView/utils.ts +++ b/src/ui/components/MessageView/utils.ts @@ -1,5 +1,5 @@ import type { DiffPreviewLine, ToolSummary } from "./types"; -import type { SessionMessage } from "../../../session"; +import type { SessionMessage } from "../../../session-types"; import { RawMode } from "../../contexts"; import chalk from "chalk"; diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx index 9704f32b..db446040 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -1,6 +1,6 @@ import Index from "../DropdownMenu"; import React, { useEffect, useState } from "react"; -import type { SkillInfo } from "../../../session"; +import type { SkillInfo } from "../../../session-types"; import { useInput } from "ink"; import { isSkillSelected } from "../../SlashCommandMenu"; diff --git a/src/ui/constants.ts b/src/ui/constants.ts index ad5ed0e1..7b336f10 100644 --- a/src/ui/constants.ts +++ b/src/ui/constants.ts @@ -1,8 +1,3 @@ -/** Default model to use for completions. */ -export const DEFAULT_MODEL = "deepseek-v4-pro"; -/** Default base URL for API requests. */ -export const DEFAULT_BASE_URL = "https://api.deepseek.com"; - /** Separator used when rendering command arguments inline (e.g., `arg1 | arg2 | arg3`). */ export const ARGS_SEPARATOR = " | "; diff --git a/src/ui/exitSummary.ts b/src/ui/exitSummary.ts index c55d9ce8..95e11e7b 100644 --- a/src/ui/exitSummary.ts +++ b/src/ui/exitSummary.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; import gradientString from "gradient-string"; -import type { ModelUsage, SessionEntry } from "../session"; +import type { ModelUsage, SessionEntry } from "../session-types"; type ExitSummaryInput = { session: SessionEntry | null; diff --git a/src/ui/index.ts b/src/ui/index.ts index 2ac3ee7c..f3cd41a7 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -11,8 +11,10 @@ export { writeProjectSettings, writeModelConfigSelection, resolveCurrentSettings, - buildPromptDraftFromSessionMessage, -} from "./utils"; + DEFAULT_MODEL, + DEFAULT_BASE_URL, +} from "../settings"; +export { buildPromptDraftFromSessionMessage } from "./utils"; export { createOpenAIClient } from "../common/openai-client"; export { default as AppContainer } from "./AppContainer"; export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; @@ -95,5 +97,5 @@ export { type FileMentionItem, type FileMentionToken, } from "./fileMentions"; -export { findExpandedThinkingId } from "./thinkingState"; +export { findExpandedThinkingId, isCollapsedThinking } from "./thinkingState"; export { buildExitSummaryText } from "./exitSummary"; diff --git a/src/ui/loadingText.ts b/src/ui/loadingText.ts index bfb97d4c..71304055 100644 --- a/src/ui/loadingText.ts +++ b/src/ui/loadingText.ts @@ -1,4 +1,4 @@ -import type { LlmStreamProgress, SessionEntry } from "../session"; +import type { LlmStreamProgress, SessionEntry } from "../session-types"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/slashCommands.ts b/src/ui/slashCommands.ts index 6d9b7cc1..2677a231 100644 --- a/src/ui/slashCommands.ts +++ b/src/ui/slashCommands.ts @@ -1,4 +1,4 @@ -import type { SkillInfo } from "../session"; +import type { SkillInfo } from "../session-types"; export type SlashCommandKind = | "skill" diff --git a/src/ui/thinkingState.ts b/src/ui/thinkingState.ts index 6f419e24..aad6d212 100644 --- a/src/ui/thinkingState.ts +++ b/src/ui/thinkingState.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../session"; +import type { SessionMessage } from "../session-types"; /** * Returns the message id of the assistant "thinking" message that should stay @@ -21,3 +21,18 @@ export function findExpandedThinkingId(messages: SessionMessage[]): string | nul } return expanded; } + +/** + * Returns whether a message's thinking block should be rendered collapsed. + * A thinking message is collapsed when its id does not match the currently + * expanded thinking id. + */ +export function isCollapsedThinking(message: SessionMessage, expandedId: string | null): boolean { + if (message.role !== "assistant") { + return false; + } + if (!message.meta?.asThinking) { + return false; + } + return message.id !== expandedId; +} diff --git a/src/ui/utils/index.ts b/src/ui/utils/index.ts index bded46bb..4a201466 100644 --- a/src/ui/utils/index.ts +++ b/src/ui/utils/index.ts @@ -1,15 +1,10 @@ import chalk from "chalk"; -import type { SessionManager, SessionMessage } from "../../session"; -import { type SessionEntry } from "../../session"; import { renderMessageToStdout } from "../components/MessageView/utils"; import type { RawMode } from "../contexts"; import type { PromptDraft } from "../PromptInput"; -import type { DeepcodingSettings, ModelConfigSelection } from "../../settings"; -import { applyModelConfigSelection, type ResolvedDeepcodingSettings, resolveSettingsSources } from "../../settings"; -import fs from "fs"; -import path from "path"; -import os from "os"; -import { DEFAULT_BASE_URL, DEFAULT_MODEL } from "../constants"; +import type { ModelConfigSelection } from "../../settings"; +import type { SessionEntry, SessionMessage } from "../../session-types"; +import type { SessionManager } from "../../session"; /** * Render all messages directly to stdout for Raw mode display. @@ -31,16 +26,6 @@ export function renderRawModeMessages(allMessages: SessionMessage[], mode: strin } } -export function isCollapsedThinking(message: SessionMessage, expandedId: string | null): boolean { - if (message.role !== "assistant") { - return false; - } - if (!message.meta?.asThinking) { - return false; - } - return message.id !== expandedId; -} - export function buildSyntheticUserMessage(content: string, imageCount: number): SessionMessage { const now = new Date().toISOString(); return { @@ -104,80 +89,6 @@ export function buildStatusLine(entry: SessionEntry): string { return parts.join(" · "); } -export function readSettings(): DeepcodingSettings | null { - return readSettingsFile(getUserSettingsPath()); -} - -export function readProjectSettings(projectRoot: string = process.cwd()): DeepcodingSettings | null { - return readSettingsFile(getProjectSettingsPath(projectRoot)); -} - -export function readSettingsFile(settingsPath: string): DeepcodingSettings | null { - try { - if (!fs.existsSync(settingsPath)) { - return null; - } - const raw = fs.readFileSync(settingsPath, "utf8"); - return JSON.parse(raw) as DeepcodingSettings; - } catch { - return null; - } -} - -export function writeSettings(settings: DeepcodingSettings): void { - const settingsPath = getUserSettingsPath(); - writeSettingsFile(settingsPath, settings); -} - -export function writeProjectSettings(settings: DeepcodingSettings, projectRoot: string = process.cwd()): void { - const settingsPath = getProjectSettingsPath(projectRoot); - writeSettingsFile(settingsPath, settings); -} - -function writeSettingsFile(settingsPath: string, settings: DeepcodingSettings): void { - fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); - fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8"); -} - -export function writeModelConfigSelection( - selection: ModelConfigSelection, - current: ModelConfigSelection = resolveCurrentSettings(), - projectRoot: string = process.cwd() -): { changed: boolean; settings: DeepcodingSettings } { - const projectSettingsPath = getProjectSettingsPath(projectRoot); - const shouldWriteProjectSettings = fs.existsSync(projectSettingsPath); - const rawSettings = shouldWriteProjectSettings ? readProjectSettings(projectRoot) : readSettings(); - const result = applyModelConfigSelection(rawSettings, current, selection); - if (result.changed) { - if (shouldWriteProjectSettings) { - writeProjectSettings(result.settings, projectRoot); - } else { - writeSettings(result.settings); - } - } - return result; -} - -export function resolveCurrentSettings(projectRoot: string = process.cwd()): ResolvedDeepcodingSettings { - return resolveSettingsSources( - readSettings(), - readProjectSettings(projectRoot), - { - model: DEFAULT_MODEL, - baseURL: DEFAULT_BASE_URL, - }, - process.env - ); -} - -export function getUserSettingsPath(): string { - return path.join(os.homedir(), ".deepcode", "settings.json"); -} - -export function getProjectSettingsPath(projectRoot: string): string { - return path.join(projectRoot, ".deepcode", "settings.json"); -} - export function formatThinkingMode( settings: Pick ): string { From 33fdcd63e41d2296dbe8d389d9d91d51453f6339 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 17:54:15 +0800 Subject: [PATCH 03/15] =?UTF-8?q?refactor(prompt):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=B2=98=E8=B4=B4=E5=92=8C=E5=8E=86=E5=8F=B2=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将粘贴处理逻辑提取到单独的 usePasteHandling hook 中 - 将历史导航逻辑提取到单独的 useHistoryNavigation hook 中 - 用 hook 替代 PromptInput 内部状态管理,简化组件代码 - 支持大文本粘贴显示可折叠的占位符标记 - 实现粘贴内容的展开与折叠控制 - 重置粘贴状态时清理所有历史数据 - 修正状态同步,提升粘贴和历史浏览体验 - 对外提供相关类型定义和状态、操作接口 --- src/ui/PromptInput.tsx | 152 ++++------------------------ src/ui/prompt/history-navigation.ts | 65 ++++++++++++ src/ui/prompt/index.ts | 6 ++ src/ui/prompt/paste-handling.ts | 150 +++++++++++++++++++++++++++ 4 files changed, 240 insertions(+), 133 deletions(-) create mode 100644 src/ui/prompt/history-navigation.ts create mode 100644 src/ui/prompt/paste-handling.ts diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index dd124689..ab8955ef 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -6,16 +6,13 @@ import { EMPTY_BUFFER, PASTE_MARKER_REGEX, backspace, - cleanPasteContent, deleteForward, deletePasteMarkerBackward, deletePasteMarkerForward, deleteWordBefore, deleteWordAfter, expandPasteMarkers, - findPasteMarkerContaining, getCurrentSlashToken, - hasActivePasteMarkers, insertText, isEmpty, killLine, @@ -53,6 +50,8 @@ export type { InputKey } from "./prompt"; import { useTerminalInput } from "./prompt"; import type { InputKey } from "./prompt"; +import { usePasteHandling } from "./prompt/paste-handling"; +import { useHistoryNavigation } from "./prompt/history-navigation"; import { useHiddenTerminalCursor, useTerminalExtendedKeys, @@ -150,21 +149,22 @@ export const PromptInput = React.memo(function PromptInput({ const [showModelDropdown, setShowModelDropdown] = useState(false); const [fileMentionItems, setFileMentionItems] = useState(() => scanFileMentionItems(projectRoot)); const [dismissedFileMentionKey, setDismissedFileMentionKey] = useState(null); - const [historyCursor, setHistoryCursor] = useState(-1); - const [draftBeforeHistory, setDraftBeforeHistory] = useState(null); const [hasTerminalFocus, setHasTerminalFocus] = useState(true); const lastCtrlDAt = React.useRef(0); const undoRedoRef = React.useRef(createPromptUndoRedoState()); const wasBusyRef = React.useRef(busy); const hadFileMentionTokenRef = React.useRef(false); const appliedDraftNonceRef = React.useRef(null); - const pastesRef = React.useRef>(new Map()); - const pasteCounterRef = React.useRef(0); - // Track expanded paste regions for toggle (Ctrl+O expand / collapse). - const expandedRegionsRef = React.useRef>( - new Map() + + const { historyCursor, navigateHistory, exitHistoryBrowsing } = useHistoryNavigation( + buffer, + setBuffer, + promptHistory ); + const { pastesRef, handlePaste, expandPasteMarkerAtCursor, resetPastes, hasCollapsedMarkers, hasExpandedRegions } = + usePasteHandling(buffer, updateBuffer, setStatusMessage); + const fileMentionToken = getCurrentFileMentionToken(buffer); const hasFileMentionToken = fileMentionToken !== null; const fileMentionKey = fileMentionToken ? `${fileMentionToken.start}:${fileMentionToken.query}` : null; @@ -191,8 +191,6 @@ export const PromptInput = React.memo(function PromptInput({ const showMenu = slashMenu.length > 0; const promptHistoryKey = React.useMemo(() => promptHistory.join("\0"), [promptHistory]); const hasRunningProcess = runningProcesses && runningProcesses.size > 0; - const hasCollapsedMarkers = hasActivePasteMarkers(buffer.text, pastesRef.current); - const hasExpandedRegions = expandedRegionsRef.current.size > 0; const processOrPasteHint = hasRunningProcess ? " · ctrl+o view output" : hasCollapsedMarkers @@ -268,17 +266,14 @@ export const PromptInput = React.memo(function PromptInput({ setSelectedSkills([]); setShowSkillsDropdown(false); setOpenRawModelDropdown(false); - setHistoryCursor(-1); - setDraftBeforeHistory(null); + exitHistoryBrowsing(); clearPromptUndoRedoState(undoRedoRef.current); - pastesRef.current.clear(); - expandedRegionsRef.current.clear(); - }, [promptDraft]); + resetPastes(); + }, [promptDraft, exitHistoryBrowsing, resetPastes]); useEffect(() => { - setHistoryCursor(-1); - setDraftBeforeHistory(null); - }, [promptHistoryKey]); + exitHistoryBrowsing(); + }, [promptHistoryKey, exitHistoryBrowsing]); useTerminalInput( (input, key) => { @@ -338,8 +333,7 @@ export const PromptInput = React.memo(function PromptInput({ } else if (!isEmpty(buffer)) { setBuffer(EMPTY_BUFFER); clearUndoRedoStacks(); - pastesRef.current.clear(); - expandedRegionsRef.current.clear(); + resetPastes(); } else { setStatusMessage("press ctrl+d to exit"); } @@ -529,8 +523,7 @@ export const PromptInput = React.memo(function PromptInput({ } if (key.ctrl && (input === "u" || input === "U")) { updateBuffer(() => EMPTY_BUFFER); - pastesRef.current.clear(); - expandedRegionsRef.current.clear(); + resetPastes(); return; } if (key.ctrl && (input === "w" || input === "W")) { @@ -594,11 +587,6 @@ export const PromptInput = React.memo(function PromptInput({ clearPromptUndoRedoState(undoRedoRef.current); } - function exitHistoryBrowsing(): void { - setHistoryCursor(-1); - setDraftBeforeHistory(null); - } - function updateBuffer(updater: (state: PromptBufferState) => PromptBufferState): void { exitHistoryBrowsing(); setBuffer((current) => { @@ -608,107 +596,6 @@ export const PromptInput = React.memo(function PromptInput({ }); } - function handlePaste(pastedText: string): void { - const totalChars = pastedText.length; - - if (totalChars <= 1000) { - const newlineCount = (pastedText.match(/\n/g) ?? []).length; - if (newlineCount <= 9) { - const clean = pastedText - .replace(/\r\n|\r/g, "\n") - .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") - .replace(/\t/g, " "); - updateBuffer((s) => insertText(s, clean)); - return; - } - } - - // Large paste: store raw text, insert marker with line/char count. - const lineCount = (pastedText.match(/\n/g) ?? []).length + 1; - pasteCounterRef.current += 1; - const pasteId = pasteCounterRef.current; - pastesRef.current.set(pasteId, pastedText); - - const marker = - lineCount > 10 ? `[paste #${pasteId} +${lineCount} lines]` : `[paste #${pasteId} ${totalChars} chars]`; - - updateBuffer((s) => insertText(s, marker)); - } - - function expandPasteMarkerAtCursor(): void { - // First, try to collapse an already-expanded region at the cursor. - for (const [id, region] of expandedRegionsRef.current) { - if (buffer.cursor >= region.start && buffer.cursor <= region.end) { - // Collapse back to marker. - expandedRegionsRef.current.delete(id); - pastesRef.current.set(id, region.content); - setTimeout(() => { - updateBuffer((s) => { - const text = s.text.slice(0, region.start) + region.marker + s.text.slice(region.end); - return { text, cursor: region.start + region.marker.length }; - }); - }, 0); - return; - } - } - - // No expanded region at cursor — try to expand a paste marker. - const marker = findPasteMarkerContaining(buffer); - if (!marker) { - setStatusMessage("No paste marker at cursor"); - return; - } - const content = pastesRef.current.get(marker.id); - if (!content) { - setStatusMessage("Paste content not found"); - return; - } - - const pasteId = marker.id; - const originalMarker = buffer.text.slice(marker.start, marker.end); - pastesRef.current.delete(pasteId); - - setTimeout(() => { - updateBuffer((s) => { - const text = s.text.slice(0, marker.start) + cleanPasteContent(content) + s.text.slice(marker.end); - const newEnd = marker.start + content.length; - expandedRegionsRef.current.set(pasteId, { - start: marker.start, - end: newEnd, - content, - marker: originalMarker, - }); - return { text, cursor: marker.start }; - }); - }, 0); - } - - function navigateHistory(direction: -1 | 1): void { - if (promptHistory.length === 0) { - return; - } - - const previousCursor = historyCursor === -1 ? promptHistory.length : historyCursor; - const nextCursor = Math.max(0, Math.min(promptHistory.length, previousCursor + direction)); - const draft = historyCursor === -1 ? buffer.text : draftBeforeHistory; - - if (historyCursor === -1) { - setDraftBeforeHistory(buffer.text); - } - - if (nextCursor === promptHistory.length) { - const text = draft ?? ""; - setBuffer({ text, cursor: text.length }); - setHistoryCursor(-1); - setDraftBeforeHistory(null); - return; - } - - const text = promptHistory[nextCursor] ?? ""; - setBuffer({ text, cursor: text.length }); - setHistoryCursor(nextCursor); - } - function insertFileMentionSelection(item: FileMentionItem): void { if (!fileMentionToken) { return; @@ -723,9 +610,8 @@ export const PromptInput = React.memo(function PromptInput({ setImageUrls([]); setSelectedSkills([]); setShowSkillsDropdown(false); - pastesRef.current.clear(); - expandedRegionsRef.current.clear(); - pasteCounterRef.current = 0; + exitHistoryBrowsing(); + resetPastes(); } function handleSlashSelection(item: SlashCommandItem): void { diff --git a/src/ui/prompt/history-navigation.ts b/src/ui/prompt/history-navigation.ts new file mode 100644 index 00000000..22ad0f63 --- /dev/null +++ b/src/ui/prompt/history-navigation.ts @@ -0,0 +1,65 @@ +import type React from "react"; +import { useCallback, useState } from "react"; +import type { PromptBufferState } from "../promptBuffer"; + +export type HistoryNavigationState = { + historyCursor: number; + draftBeforeHistory: string | null; +}; + +export type HistoryNavigationActions = { + /** + * Navigate through prompt history. Pass -1 for previous, 1 for next. + * Stores current draft before entering history mode and restores it when + * scrolling past the last entry. + */ + navigateHistory: (direction: -1 | 1) => void; + /** Exit history browsing mode, restoring the pre-history draft if any. */ + exitHistoryBrowsing: () => void; +}; + +export function useHistoryNavigation( + buffer: PromptBufferState, + setBuffer: React.Dispatch>, + promptHistory: string[] +): HistoryNavigationState & HistoryNavigationActions { + const [historyCursor, setHistoryCursor] = useState(-1); + const [draftBeforeHistory, setDraftBeforeHistory] = useState(null); + + const exitHistoryBrowsing = useCallback((): void => { + setHistoryCursor(-1); + setDraftBeforeHistory(null); + }, []); + + function navigateHistory(direction: -1 | 1): void { + if (promptHistory.length === 0) { + return; + } + + const previousCursor = historyCursor === -1 ? promptHistory.length : historyCursor; + const nextCursor = Math.max(0, Math.min(promptHistory.length, previousCursor + direction)); + + if (historyCursor === -1) { + setDraftBeforeHistory(buffer.text); + } + + if (nextCursor === promptHistory.length) { + const text = draftBeforeHistory ?? ""; + setBuffer({ text, cursor: text.length }); + setHistoryCursor(-1); + setDraftBeforeHistory(null); + return; + } + + const text = promptHistory[nextCursor] ?? ""; + setBuffer({ text, cursor: text.length }); + setHistoryCursor(nextCursor); + } + + return { + historyCursor, + draftBeforeHistory, + navigateHistory, + exitHistoryBrowsing, + }; +} diff --git a/src/ui/prompt/index.ts b/src/ui/prompt/index.ts index 6435f620..56e07251 100644 --- a/src/ui/prompt/index.ts +++ b/src/ui/prompt/index.ts @@ -9,3 +9,9 @@ export { useTerminalFocusReporting, getPromptCursorPlacement, } from "./cursor"; + +export { usePasteHandling } from "./paste-handling"; +export type { PasteRegion, PasteHandlingState, PasteHandlingActions } from "./paste-handling"; + +export { useHistoryNavigation } from "./history-navigation"; +export type { HistoryNavigationState, HistoryNavigationActions } from "./history-navigation"; diff --git a/src/ui/prompt/paste-handling.ts b/src/ui/prompt/paste-handling.ts new file mode 100644 index 00000000..63c2c547 --- /dev/null +++ b/src/ui/prompt/paste-handling.ts @@ -0,0 +1,150 @@ +import type React from "react"; +import { useRef, useState } from "react"; +import type { PromptBufferState } from "../promptBuffer"; +import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../promptBuffer"; + +export type PasteRegion = { + start: number; + end: number; + content: string; + marker: string; +}; + +export type PasteHandlingState = { + /** Ref holding all paste content keyed by paste ID. */ + pastesRef: React.RefObject>; + /** Ref holding expanded paste regions for Ctrl+O toggle. */ + expandedRegionsRef: React.RefObject>; + /** Counter for generating unique paste IDs. */ + pasteCounterRef: React.RefObject; + /** Whether any paste marker is currently collapsed. */ + hasCollapsedMarkers: boolean; + /** Whether any paste region has been expanded. */ + hasExpandedRegions: boolean; +}; + +export type PasteHandlingActions = { + /** + * Process pasted text. Short pastes (<1000 chars, ≤9 newlines) are inserted + * inline. Larger pastes receive a collapsible marker. + */ + handlePaste: (pastedText: string) => void; + /** Expand a collapsed paste marker at the cursor, or collapse an expanded region. */ + expandPasteMarkerAtCursor: () => void; + /** Reset all paste-related state. */ + resetPastes: () => void; +}; + +export function usePasteHandling( + buffer: PromptBufferState, + updateBuffer: (updater: (state: PromptBufferState) => PromptBufferState) => void, + setStatusMessage: (msg: string | null) => void +): PasteHandlingState & PasteHandlingActions { + const pastesRef = useRef>(new Map()); + const pasteCounterRef = useRef(0); + const expandedRegionsRef = useRef>(new Map()); + const [hasCollapsedMarkers, setHasCollapsedMarkers] = useState(false); + const [hasExpandedRegions, setHasExpandedRegions] = useState(false); + + function refreshDerivedFlags(): void { + setHasCollapsedMarkers(hasActivePasteMarkers(buffer.text, pastesRef.current)); + setHasExpandedRegions(expandedRegionsRef.current.size > 0); + } + + function handlePaste(pastedText: string): void { + const totalChars = pastedText.length; + + if (totalChars <= 1000) { + const newlineCount = (pastedText.match(/\n/g) ?? []).length; + if (newlineCount <= 9) { + const clean = pastedText + .replace(/\r\n|\r/g, "\n") + .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") + .replace(/\t/g, " "); + updateBuffer((s) => insertText(s, clean)); + return; + } + } + + // Large paste: store raw text, insert marker. + const lineCount = (pastedText.match(/\n/g) ?? []).length + 1; + pasteCounterRef.current += 1; + const pasteId = pasteCounterRef.current; + pastesRef.current.set(pasteId, pastedText); + + const marker = + lineCount > 10 ? `[paste #${pasteId} +${lineCount} lines]` : `[paste #${pasteId} ${totalChars} chars]`; + + updateBuffer((s) => insertText(s, marker)); + refreshDerivedFlags(); + } + + function expandPasteMarkerAtCursor(): void { + // Collapse an already-expanded region at the cursor. + for (const [id, region] of expandedRegionsRef.current) { + if (buffer.cursor >= region.start && buffer.cursor <= region.end) { + expandedRegionsRef.current.delete(id); + pastesRef.current.set(id, region.content); + setTimeout(() => { + updateBuffer((s) => { + const text = s.text.slice(0, region.start) + region.marker + s.text.slice(region.end); + return { text, cursor: region.start + region.marker.length }; + }); + refreshDerivedFlags(); + }, 0); + refreshDerivedFlags(); + return; + } + } + + // Expand a paste marker. + const marker = findPasteMarkerContaining(buffer); + if (!marker) { + setStatusMessage("No paste marker at cursor"); + return; + } + const content = pastesRef.current.get(marker.id); + if (!content) { + setStatusMessage("Paste content not found"); + return; + } + + const pasteId = marker.id; + const originalMarker = buffer.text.slice(marker.start, marker.end); + pastesRef.current.delete(pasteId); + + setTimeout(() => { + updateBuffer((s) => { + const text = s.text.slice(0, marker.start) + cleanPasteContent(content) + s.text.slice(marker.end); + const newEnd = marker.start + content.length; + expandedRegionsRef.current.set(pasteId, { + start: marker.start, + end: newEnd, + content, + marker: originalMarker, + }); + return { text, cursor: marker.start }; + }); + refreshDerivedFlags(); + }, 0); + refreshDerivedFlags(); + } + + function resetPastes(): void { + pastesRef.current.clear(); + expandedRegionsRef.current.clear(); + pasteCounterRef.current = 0; + refreshDerivedFlags(); + } + + return { + pastesRef, + expandedRegionsRef, + pasteCounterRef, + hasCollapsedMarkers, + hasExpandedRegions, + handlePaste, + expandPasteMarkerAtCursor, + resetPastes, + }; +} From febcd93593d2a435cd83eafd5857cd8394719ba8 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 18:19:48 +0800 Subject: [PATCH 04/15] =?UTF-8?q?refactor(core):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=EF=BC=8C=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E5=A4=9A=E4=B8=AA=E6=A8=A1=E5=9D=97=E8=87=B3=20core=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 ui 目录下的多个模块移动至 ui/core 目录,调整相关导入路径以适配新结构 - 将 common 目录下部分模块重命名移动至 common/runtime 和 common/system,更新导入引用 - 更新测试文件中相关导入路径,确保测试代码正常执行 - 新增 session/utils.ts,抽取并集中管理 getCompactPromptTokenThreshold 与 getTotalTokens 方法 - 调整部分导入钩子名称由 prompt 变更为 hooks,提升代码组织一致性 - 修正部分日志及进程管理模块的导入路径到对应的 logging 和 system 子目录 - 更新 UI 组件和工具相关代码以适应新的模块路径和结构变化 --- src/cli.tsx | 2 +- src/common/file-utils.ts | 2 +- src/common/{ => logging}/debug-logger.ts | 0 src/common/{ => logging}/error-logger.ts | 0 src/common/permissions.ts | 2 +- src/common/{ => runtime}/file-history.ts | 0 src/common/{ => runtime}/runtime.ts | 2 +- src/common/{ => runtime}/state.ts | 2 +- src/common/{ => system}/bash-timeout.ts | 0 src/common/{ => system}/process-tree.ts | 0 src/common/{ => system}/shell-utils.ts | 0 src/common/updateCheck.ts | 2 +- src/mcp/mcp-client.ts | 2 +- src/prompt.ts | 2 +- src/session-types.ts | 22 -------------- src/session.ts | 13 ++++---- src/session/utils.ts | 23 ++++++++++++++ src/tests/clipboard.test.ts | 6 ++-- src/tests/debug-logger.test.ts | 2 +- src/tests/fileMentions.test.ts | 2 +- src/tests/process-tree.test.ts | 2 +- src/tests/promptUndoRedo.test.ts | 2 +- src/tests/session.test.ts | 2 +- src/tests/shell-utils.test.ts | 4 +-- src/tools/bash-handler.ts | 6 ++-- src/tools/edit-handler.ts | 4 +-- src/tools/read-handler.ts | 2 +- src/tools/update-plan-handler.ts | 2 +- src/tools/write-handler.ts | 10 +++++-- src/ui/App.tsx | 8 ++--- src/ui/AskUserQuestionPrompt.tsx | 4 +-- src/ui/PermissionPrompt.tsx | 2 +- src/ui/ProcessStdoutView.tsx | 6 ++-- src/ui/PromptInput.tsx | 30 +++++++++---------- src/ui/SlashCommandMenu.tsx | 4 +-- src/ui/WelcomeScreen.tsx | 2 +- src/ui/components/FileMentionMenu/index.tsx | 2 +- src/ui/{ => core}/askUserQuestion.ts | 2 +- src/ui/{ => core}/clipboard.ts | 0 src/ui/{ => core}/fileMentions.ts | 0 src/ui/{ => core}/loadingText.ts | 2 +- src/ui/{ => core}/promptBuffer.ts | 0 src/ui/{ => core}/promptUndoRedo.ts | 0 src/ui/{ => core}/slashCommands.ts | 2 +- src/ui/{ => core}/thinkingState.ts | 2 +- src/ui/{prompt => hooks}/cursor.ts | 2 +- .../{prompt => hooks}/history-navigation.ts | 2 +- src/ui/{prompt => hooks}/index.ts | 0 src/ui/{prompt => hooks}/paste-handling.ts | 4 +-- src/ui/{prompt => hooks}/useTerminalInput.ts | 0 src/ui/index.ts | 29 ++++++------------ 51 files changed, 108 insertions(+), 113 deletions(-) rename src/common/{ => logging}/debug-logger.ts (100%) rename src/common/{ => logging}/error-logger.ts (100%) rename src/common/{ => runtime}/file-history.ts (100%) rename src/common/{ => runtime}/runtime.ts (98%) rename src/common/{ => runtime}/state.ts (98%) rename src/common/{ => system}/bash-timeout.ts (100%) rename src/common/{ => system}/process-tree.ts (100%) rename src/common/{ => system}/shell-utils.ts (100%) create mode 100644 src/session/utils.ts rename src/ui/{ => core}/askUserQuestion.ts (98%) rename src/ui/{ => core}/clipboard.ts (100%) rename src/ui/{ => core}/fileMentions.ts (100%) rename src/ui/{ => core}/loadingText.ts (96%) rename src/ui/{ => core}/promptBuffer.ts (100%) rename src/ui/{ => core}/promptUndoRedo.ts (100%) rename src/ui/{ => core}/slashCommands.ts (98%) rename src/ui/{ => core}/thinkingState.ts (95%) rename src/ui/{prompt => hooks}/cursor.ts (99%) rename src/ui/{prompt => hooks}/history-navigation.ts (96%) rename src/ui/{prompt => hooks}/index.ts (100%) rename src/ui/{prompt => hooks}/paste-handling.ts (97%) rename src/ui/{prompt => hooks}/useTerminalInput.ts (100%) diff --git a/src/cli.tsx b/src/cli.tsx index d179203e..de26bf2c 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,6 +1,6 @@ import React from "react"; import { render } from "ink"; -import { setShellIfWindows } from "./common/shell-utils"; +import { setShellIfWindows } from "./common/system/shell-utils"; import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/updateCheck"; import { AppContainer } from "./ui"; diff --git a/src/common/file-utils.ts b/src/common/file-utils.ts index 6656172e..72c83c0a 100644 --- a/src/common/file-utils.ts +++ b/src/common/file-utils.ts @@ -1,6 +1,6 @@ import * as fs from "fs"; import * as path from "path"; -import type { FileState, FileLineEnding } from "./state"; +import type { FileState, FileLineEnding } from "./runtime/state"; export type FileReadMetadata = { content: string; diff --git a/src/common/debug-logger.ts b/src/common/logging/debug-logger.ts similarity index 100% rename from src/common/debug-logger.ts rename to src/common/logging/debug-logger.ts diff --git a/src/common/error-logger.ts b/src/common/logging/error-logger.ts similarity index 100% rename from src/common/error-logger.ts rename to src/common/logging/error-logger.ts diff --git a/src/common/permissions.ts b/src/common/permissions.ts index 564bfeb8..1ebca8c2 100644 --- a/src/common/permissions.ts +++ b/src/common/permissions.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import type { DeepcodingSettings, PermissionScope, PermissionSettings } from "../settings"; -import { isAbsoluteFilePath, normalizeFilePath } from "./state"; +import { isAbsoluteFilePath, normalizeFilePath } from "./runtime/state"; export type BashPermissionScope = Exclude | "unknown"; diff --git a/src/common/file-history.ts b/src/common/runtime/file-history.ts similarity index 100% rename from src/common/file-history.ts rename to src/common/runtime/file-history.ts diff --git a/src/common/runtime.ts b/src/common/runtime/runtime.ts similarity index 98% rename from src/common/runtime.ts rename to src/common/runtime/runtime.ts index b1195d8d..756dc819 100644 --- a/src/common/runtime.ts +++ b/src/common/runtime/runtime.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { ToolExecutionContext, ToolExecutionResult } from "../tools/executor"; +import type { ToolExecutionContext, ToolExecutionResult } from "../../tools/executor"; export type ValidationResult = { ok: true; input: Record } | { ok: false; error: string }; diff --git a/src/common/state.ts b/src/common/runtime/state.ts similarity index 98% rename from src/common/state.ts rename to src/common/runtime/state.ts index add27f35..122a1aca 100644 --- a/src/common/state.ts +++ b/src/common/runtime/state.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { posixPathToWindowsPath } from "./shell-utils"; +import { posixPathToWindowsPath } from "../system/shell-utils"; export type FileLineEnding = "LF" | "CRLF"; diff --git a/src/common/bash-timeout.ts b/src/common/system/bash-timeout.ts similarity index 100% rename from src/common/bash-timeout.ts rename to src/common/system/bash-timeout.ts diff --git a/src/common/process-tree.ts b/src/common/system/process-tree.ts similarity index 100% rename from src/common/process-tree.ts rename to src/common/system/process-tree.ts diff --git a/src/common/shell-utils.ts b/src/common/system/shell-utils.ts similarity index 100% rename from src/common/shell-utils.ts rename to src/common/system/shell-utils.ts diff --git a/src/common/updateCheck.ts b/src/common/updateCheck.ts index 09c0273c..6baa58f7 100644 --- a/src/common/updateCheck.ts +++ b/src/common/updateCheck.ts @@ -6,7 +6,7 @@ import * as path from "path"; import { render, type Instance } from "ink"; import chalk from "chalk"; import { UpdatePrompt, type UpdatePromptChoice } from "../ui"; -import { killProcessTree } from "./process-tree"; +import { killProcessTree } from "./system/process-tree"; export type PackageInfo = { name: string; diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts index 26a7a321..4ea0eca4 100644 --- a/src/mcp/mcp-client.ts +++ b/src/mcp/mcp-client.ts @@ -1,7 +1,7 @@ import { spawn, type ChildProcess } from "child_process"; import { createInterface, type Interface } from "readline"; import * as path from "path"; -import { killProcessTree } from "../common/process-tree"; +import { killProcessTree } from "../common/system/process-tree"; type JsonRpcRequest = { jsonrpc: "2.0"; diff --git a/src/prompt.ts b/src/prompt.ts index 4fcd06d6..ce8b6a64 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -5,7 +5,7 @@ import * as path from "path"; import { fileURLToPath } from "url"; import ejs from "ejs"; import type { SessionMessage } from "./session-types"; -import { findGitBashPath, resolveShellPath } from "./common/shell-utils"; +import { findGitBashPath, resolveShellPath } from "./common/system/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; const COMPACT_PROMPT_BASE = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions. diff --git a/src/session-types.ts b/src/session-types.ts index 33119cc3..ffc836f7 100644 --- a/src/session-types.ts +++ b/src/session-types.ts @@ -1,7 +1,6 @@ import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "./common/permissions"; import type { CreateOpenAIClient } from "./tools/executor"; -import { DEEPSEEK_V4_MODELS } from "./common/model-capabilities"; export type SessionStatus = | "failed" @@ -139,24 +138,3 @@ export type LlmStreamProgress = { formattedTokens: string; phase: "start" | "update" | "end"; }; - -const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; -const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; - -export function getCompactPromptTokenThreshold(model: string): number { - return DEEPSEEK_V4_MODELS.has(model) - ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD - : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; -} - -function isUsageRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -export function getTotalTokens(usage: ModelUsage | null | undefined): number { - if (!isUsageRecord(usage)) { - return 0; - } - const totalTokens = (usage as Record).total_tokens; - return typeof totalTokens === "number" ? totalTokens : 0; -} diff --git a/src/session.ts b/src/session.ts index bf501671..fa881707 100644 --- a/src/session.ts +++ b/src/session.ts @@ -27,11 +27,11 @@ import { } from "./tools/executor"; import { McpManager } from "./mcp/mcp-manager"; import type { McpServerConfig, PermissionSettings } from "./settings"; -import { logApiError } from "./common/error-logger"; -import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; -import { killProcessTree } from "./common/process-tree"; -import { GitFileHistory } from "./common/file-history"; -import { getSnippet } from "./common/state"; +import { logApiError } from "./common/logging/error-logger"; +import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/logging/debug-logger"; +import { killProcessTree } from "./common/system/process-tree"; +import { GitFileHistory } from "./common/runtime/file-history"; +import { getSnippet } from "./common/runtime/state"; import { appendProjectPermissionAllows, buildPermissionToolExecution, @@ -44,10 +44,9 @@ import { type UserToolPermission, } from "./common/permissions"; +import { getCompactPromptTokenThreshold, getTotalTokens } from "./session/utils"; import { type BashTimeoutAdjustment, - getCompactPromptTokenThreshold, - getTotalTokens, type LlmStreamProgress, type MessageMeta, type ModelUsage, diff --git a/src/session/utils.ts b/src/session/utils.ts new file mode 100644 index 00000000..3860a841 --- /dev/null +++ b/src/session/utils.ts @@ -0,0 +1,23 @@ +import { DEEPSEEK_V4_MODELS } from "../common/model-capabilities"; +import type { ModelUsage } from "../session-types"; + +const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; +const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; + +export function getCompactPromptTokenThreshold(model: string): number { + return DEEPSEEK_V4_MODELS.has(model) + ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD + : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; +} + +function isUsageRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +export function getTotalTokens(usage: ModelUsage | null | undefined): number { + if (!isUsageRecord(usage)) { + return 0; + } + const totalTokens = (usage as Record).total_tokens; + return typeof totalTokens === "number" ? totalTokens : 0; +} diff --git a/src/tests/clipboard.test.ts b/src/tests/clipboard.test.ts index dbe9ff95..3ca892eb 100644 --- a/src/tests/clipboard.test.ts +++ b/src/tests/clipboard.test.ts @@ -5,7 +5,7 @@ import * as os from "os"; import * as path from "path"; // eslint-disable-next-line @typescript-eslint/consistent-type-imports -type ClipboardModule = typeof import("../ui/clipboard"); +type ClipboardModule = typeof import("../ui/core/clipboard"); const ORIGINAL_PATH = process.env.PATH; const ORIGINAL_PLATFORM = process.platform; @@ -30,7 +30,7 @@ function withPlatform(platform: NodeJS.Platform, fn: () => T): T { test("readClipboardImage returns null when no clipboard helpers are installed", async () => { // Reload module so it picks up the patched PATH at spawn time. - const moduleUrl = new URL(`../ui/clipboard.ts?t=${Date.now()}`, import.meta.url).href; + const moduleUrl = new URL(`../ui/core/clipboard.ts?t=${Date.now()}`, import.meta.url).href; const { readClipboardImage } = (await import(moduleUrl)) as ClipboardModule; const result = withCleanPath(() => readClipboardImage()); assert.equal(result, null); @@ -63,7 +63,7 @@ test( { mode: 0o755 } ); - const moduleUrl = new URL(`../ui/clipboard.ts?t=${Date.now()}`, import.meta.url).href; + const moduleUrl = new URL(`../ui/core/clipboard.ts?t=${Date.now()}`, import.meta.url).href; const { readClipboardImage } = (await import(moduleUrl)) as ClipboardModule; process.env.PATH = binDir; diff --git a/src/tests/debug-logger.test.ts b/src/tests/debug-logger.test.ts index 7b1aad40..374da743 100644 --- a/src/tests/debug-logger.test.ts +++ b/src/tests/debug-logger.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { getDebugLogPath, logOpenAIChatCompletionDebug } from "../common/debug-logger"; +import { getDebugLogPath, logOpenAIChatCompletionDebug } from "../common/logging/debug-logger"; test("debug logger appends full entries without rotation", () => { const originalHome = process.env.HOME; diff --git a/src/tests/fileMentions.test.ts b/src/tests/fileMentions.test.ts index b382eeed..50a6dc41 100644 --- a/src/tests/fileMentions.test.ts +++ b/src/tests/fileMentions.test.ts @@ -10,7 +10,7 @@ import { replaceCurrentFileMentionToken, scanFileMentionItems, type FileMentionItem, -} from "../ui/fileMentions"; +} from "../ui/core/fileMentions"; test("getCurrentFileMentionToken detects bare @file tokens under the cursor", () => { assert.deepEqual(getCurrentFileMentionToken({ text: "review @src/app.ts please", cursor: 10 }), { diff --git a/src/tests/process-tree.test.ts b/src/tests/process-tree.test.ts index 1dd08a1e..97c68248 100644 --- a/src/tests/process-tree.test.ts +++ b/src/tests/process-tree.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { killProcessTree, runWindowsTaskkill } from "../common/process-tree"; +import { killProcessTree, runWindowsTaskkill } from "../common/system/process-tree"; test("runWindowsTaskkill invokes taskkill for the full process tree", () => { const calls: Array<{ command: string; args: string[]; options: { stdio: "ignore"; windowsHide: true } }> = []; diff --git a/src/tests/promptUndoRedo.test.ts b/src/tests/promptUndoRedo.test.ts index c1999f15..26360c04 100644 --- a/src/tests/promptUndoRedo.test.ts +++ b/src/tests/promptUndoRedo.test.ts @@ -8,7 +8,7 @@ import { recordPromptEdit, redoPromptEdit, undoPromptEdit, -} from "../ui/promptUndoRedo"; +} from "../ui/core/promptUndoRedo"; test("prompt undo and redo restore edited buffer states", () => { const history = createPromptUndoRedoState(); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 5615ff55..bfacd8ef 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -4,7 +4,7 @@ import { execFileSync } from "node:child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { GitFileHistory } from "../common/file-history"; +import { GitFileHistory } from "../common/runtime/file-history"; import { type SessionMessage } from "../session-types"; import { SessionManager } from "../session"; diff --git a/src/tests/shell-utils.test.ts b/src/tests/shell-utils.test.ts index 50a71f41..9eec57b6 100644 --- a/src/tests/shell-utils.test.ts +++ b/src/tests/shell-utils.test.ts @@ -7,8 +7,8 @@ import { resolveWindowsGitBashPath, rewriteWindowsNullRedirect, windowsPathToPosixPath, -} from "../common/shell-utils"; -import { isAbsoluteFilePath, normalizeFilePath } from "../common/state"; +} from "../common/system/shell-utils"; +import { isAbsoluteFilePath, normalizeFilePath } from "../common/runtime/state"; test("Windows paths convert to Git Bash POSIX paths", () => { assert.equal(windowsPathToPosixPath("C:\\Users\\foo"), "/c/Users/foo"); diff --git a/src/tools/bash-handler.ts b/src/tools/bash-handler.ts index 42722710..fb639158 100644 --- a/src/tools/bash-handler.ts +++ b/src/tools/bash-handler.ts @@ -1,6 +1,6 @@ import { spawn } from "child_process"; -import { DEFAULT_BASH_TIMEOUT_MS, clampBashTimeoutMs } from "../common/bash-timeout"; -import { killProcessTree } from "../common/process-tree"; +import { DEFAULT_BASH_TIMEOUT_MS, clampBashTimeoutMs } from "../common/system/bash-timeout"; +import { killProcessTree } from "../common/system/process-tree"; import type { ProcessTimeoutControl, ProcessTimeoutInfo, ToolExecutionContext, ToolExecutionResult } from "./executor"; import { buildDisableExtglobCommand, @@ -9,7 +9,7 @@ import { resolveShellPath, rewriteWindowsNullRedirect, toNativeCwd, -} from "../common/shell-utils"; +} from "../common/system/shell-utils"; const MAX_OUTPUT_CHARS = 30000; const MAX_CAPTURE_CHARS = 10 * 1024 * 1024; diff --git a/src/tools/edit-handler.ts b/src/tools/edit-handler.ts index 454a673b..98afa43f 100644 --- a/src/tools/edit-handler.ts +++ b/src/tools/edit-handler.ts @@ -8,7 +8,7 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool, semanticBoolean } from "../common/runtime"; +import { executeValidatedTool, semanticBoolean } from "../common/runtime/runtime"; import { createSnippet, getFileState, @@ -18,7 +18,7 @@ import { isFullFileView, normalizeFilePath, recordFileState, -} from "../common/state"; +} from "../common/runtime/state"; const MAX_CANDIDATE_COUNT = 5; const REPLACE_ALL_MATCH_THRESHOLD = 5; diff --git a/src/tools/read-handler.ts b/src/tools/read-handler.ts index 964cdd72..606199c5 100644 --- a/src/tools/read-handler.ts +++ b/src/tools/read-handler.ts @@ -3,7 +3,7 @@ import * as path from "path"; import ignore from "ignore"; import type { ToolExecutionContext, ToolExecutionFollowUpMessage, ToolExecutionResult } from "./executor"; import { readTextFileWithMetadata } from "../common/file-utils"; -import { createSnippet, isAbsoluteFilePath, markFileRead, normalizeFilePath } from "../common/state"; +import { createSnippet, isAbsoluteFilePath, markFileRead, normalizeFilePath } from "../common/runtime/state"; const DEFAULT_LINE_LIMIT = 2000; const MAX_LINE_LENGTH = 2000; diff --git a/src/tools/update-plan-handler.ts b/src/tools/update-plan-handler.ts index 7c7198ea..a8947cfc 100644 --- a/src/tools/update-plan-handler.ts +++ b/src/tools/update-plan-handler.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { ToolExecutionContext, ToolExecutionResult } from "./executor"; -import { executeValidatedTool } from "../common/runtime"; +import { executeValidatedTool } from "../common/runtime/runtime"; const updatePlanSchema = z.strictObject({ plan: z.string().trim().min(1, "plan must not be empty."), diff --git a/src/tools/write-handler.ts b/src/tools/write-handler.ts index a4c81bf3..e91a78c7 100644 --- a/src/tools/write-handler.ts +++ b/src/tools/write-handler.ts @@ -9,8 +9,14 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool } from "../common/runtime"; -import { getFileState, isAbsoluteFilePath, isFullFileView, normalizeFilePath, recordFileState } from "../common/state"; +import { executeValidatedTool } from "../common/runtime/runtime"; +import { + getFileState, + isAbsoluteFilePath, + isFullFileView, + normalizeFilePath, + recordFileState, +} from "../common/runtime/state"; const writeSchema = z.strictObject({ file_path: z.string().min(1, "file_path is required."), diff --git a/src/ui/App.tsx b/src/ui/App.tsx index b140574d..bea96f66 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -8,8 +8,8 @@ import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptIn import { MessageView, RawModeExitPrompt } from "./components"; import { SessionList } from "./SessionList"; import { type UndoRestoreMode, UndoSelector } from "./UndoSelector"; -import { buildLoadingText } from "./loadingText"; -import { findExpandedThinkingId } from "./thinkingState"; +import { buildLoadingText } from "./core/loadingText"; +import { findExpandedThinkingId } from "./core/thinkingState"; import { WelcomeScreen } from "./WelcomeScreen"; import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; import { McpStatusList } from "./McpStatusList"; @@ -18,7 +18,7 @@ import { type AskUserQuestionAnswers, findPendingAskUserQuestion, formatAskUserQuestionAnswers, -} from "./askUserQuestion"; +} from "./core/askUserQuestion"; import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPrompt"; import { buildExitSummaryText } from "./exitSummary"; import { RawMode, useRawModeContext } from "./contexts"; @@ -32,7 +32,7 @@ import { renderRawModeMessages, } from "./utils"; import { resolveCurrentSettings, writeModelConfigSelection } from "../settings"; -import { isCollapsedThinking } from "./thinkingState"; +import { isCollapsedThinking } from "./core/thinkingState"; import { ANSI_CLEAR_SCREEN } from "./constants"; import type { LlmStreamProgress, diff --git a/src/ui/AskUserQuestionPrompt.tsx b/src/ui/AskUserQuestionPrompt.tsx index c84b6200..058de3e1 100644 --- a/src/ui/AskUserQuestionPrompt.tsx +++ b/src/ui/AskUserQuestionPrompt.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import type { AskUserQuestionAnswers, AskUserQuestionItem } from "./askUserQuestion"; -import { useTerminalInput } from "./prompt"; +import type { AskUserQuestionAnswers, AskUserQuestionItem } from "./core/askUserQuestion"; +import { useTerminalInput } from "./hooks"; type Props = { questions: AskUserQuestionItem[]; diff --git a/src/ui/PermissionPrompt.tsx b/src/ui/PermissionPrompt.tsx index dd2d8ebf..f450ac96 100644 --- a/src/ui/PermissionPrompt.tsx +++ b/src/ui/PermissionPrompt.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import { useTerminalInput } from "./prompt"; +import { useTerminalInput } from "./hooks"; import type { AskPermissionRequest, AskPermissionScope, UserToolPermission } from "../common/permissions"; import type { PermissionScope } from "../settings"; diff --git a/src/ui/ProcessStdoutView.tsx b/src/ui/ProcessStdoutView.tsx index b47e0cdd..23b230aa 100644 --- a/src/ui/ProcessStdoutView.tsx +++ b/src/ui/ProcessStdoutView.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text } from "ink"; -import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../common/bash-timeout"; +import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../common/system/bash-timeout"; import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../session-types"; -import { useTerminalInput } from "./prompt"; +import { useTerminalInput } from "./hooks"; type RunningProcesses = SessionEntry["processes"]; type ProcessStdoutViewProps = { - processStdoutRef: React.MutableRefObject>; + processStdoutRef: React.RefObject>; runningProcesses: RunningProcesses; onDismiss: () => void; onAdjustTimeout: (deltaMs: number) => BashTimeoutAdjustment | null; diff --git a/src/ui/PromptInput.tsx b/src/ui/PromptInput.tsx index ab8955ef..27af870b 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/PromptInput.tsx @@ -24,40 +24,40 @@ import { moveWordLeft, moveWordRight, moveUp, -} from "./promptBuffer"; -import type { PromptBufferState } from "./promptBuffer"; +} from "./core/promptBuffer"; +import type { PromptBufferState } from "./core/promptBuffer"; import { clearPromptUndoRedoState, createPromptUndoRedoState, recordPromptEdit, redoPromptEdit, undoPromptEdit, -} from "./promptUndoRedo"; -import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "./slashCommands"; -import type { SlashCommandItem } from "./slashCommands"; +} from "./core/promptUndoRedo"; +import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "./core/slashCommands"; +import type { SlashCommandItem } from "./core/slashCommands"; import { filterFileMentionItems, getCurrentFileMentionToken, replaceCurrentFileMentionToken, scanFileMentionItems, -} from "./fileMentions"; -import type { FileMentionItem } from "./fileMentions"; -import { readClipboardImageAsync } from "./clipboard"; +} from "./core/fileMentions"; +import type { FileMentionItem } from "./core/fileMentions"; +import { readClipboardImageAsync } from "./core/clipboard"; // Re-exported from prompt modules for backward compatibility -export { useTerminalInput, parseTerminalInput, dispatchTerminalInput } from "./prompt"; -export type { InputKey } from "./prompt"; +export { useTerminalInput, parseTerminalInput, dispatchTerminalInput } from "./hooks"; +export type { InputKey } from "./hooks"; -import { useTerminalInput } from "./prompt"; -import type { InputKey } from "./prompt"; -import { usePasteHandling } from "./prompt/paste-handling"; -import { useHistoryNavigation } from "./prompt/history-navigation"; +import { useTerminalInput } from "./hooks"; +import type { InputKey } from "./hooks"; +import { usePasteHandling } from "./hooks/paste-handling"; +import { useHistoryNavigation } from "./hooks/history-navigation"; import { useHiddenTerminalCursor, useTerminalExtendedKeys, useBracketedPaste, useTerminalFocusReporting, -} from "./prompt"; +} from "./hooks"; import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; import type { ModelConfigSelection, PermissionScope } from "../settings"; import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "./components"; diff --git a/src/ui/SlashCommandMenu.tsx b/src/ui/SlashCommandMenu.tsx index ddd79251..0ca5c696 100644 --- a/src/ui/SlashCommandMenu.tsx +++ b/src/ui/SlashCommandMenu.tsx @@ -1,5 +1,5 @@ -import { formatSlashCommandDescription, formatSlashCommandLabel } from "./slashCommands"; -import type { SlashCommandItem } from "./slashCommands"; +import { formatSlashCommandDescription, formatSlashCommandLabel } from "./core/slashCommands"; +import type { SlashCommandItem } from "./core/slashCommands"; import { ARGS_SEPARATOR } from "./constants"; import React from "react"; import { Box, Text } from "ink"; diff --git a/src/ui/WelcomeScreen.tsx b/src/ui/WelcomeScreen.tsx index 2e6b7cfa..36bb3030 100644 --- a/src/ui/WelcomeScreen.tsx +++ b/src/ui/WelcomeScreen.tsx @@ -4,7 +4,7 @@ import * as os from "node:os"; import path from "node:path"; import type { SkillInfo } from "../session-types"; import type { ResolvedDeepcodingSettings } from "../settings"; -import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "./slashCommands"; +import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "./core/slashCommands"; import { ThemedGradient } from "./ThemedGradient"; import { AsciiLogo } from "./AsciiArt"; import { useAppContext } from "./contexts"; diff --git a/src/ui/components/FileMentionMenu/index.tsx b/src/ui/components/FileMentionMenu/index.tsx index b1c77b4a..15465d42 100644 --- a/src/ui/components/FileMentionMenu/index.tsx +++ b/src/ui/components/FileMentionMenu/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Box, Text } from "ink"; import { useInput } from "ink"; import DropdownMenu from "../DropdownMenu"; -import type { FileMentionItem, FileMentionToken } from "../../fileMentions"; +import type { FileMentionItem, FileMentionToken } from "../../core/fileMentions"; type Props = { open: boolean; diff --git a/src/ui/askUserQuestion.ts b/src/ui/core/askUserQuestion.ts similarity index 98% rename from src/ui/askUserQuestion.ts rename to src/ui/core/askUserQuestion.ts index 813f7cf8..ea94342e 100644 --- a/src/ui/askUserQuestion.ts +++ b/src/ui/core/askUserQuestion.ts @@ -1,4 +1,4 @@ -import type { SessionMessage, SessionStatus } from "../session-types"; +import type { SessionMessage, SessionStatus } from "../../session-types"; export type AskUserQuestionOption = { label: string; diff --git a/src/ui/clipboard.ts b/src/ui/core/clipboard.ts similarity index 100% rename from src/ui/clipboard.ts rename to src/ui/core/clipboard.ts diff --git a/src/ui/fileMentions.ts b/src/ui/core/fileMentions.ts similarity index 100% rename from src/ui/fileMentions.ts rename to src/ui/core/fileMentions.ts diff --git a/src/ui/loadingText.ts b/src/ui/core/loadingText.ts similarity index 96% rename from src/ui/loadingText.ts rename to src/ui/core/loadingText.ts index 71304055..738b9685 100644 --- a/src/ui/loadingText.ts +++ b/src/ui/core/loadingText.ts @@ -1,4 +1,4 @@ -import type { LlmStreamProgress, SessionEntry } from "../session-types"; +import type { LlmStreamProgress, SessionEntry } from "../../session-types"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/promptBuffer.ts b/src/ui/core/promptBuffer.ts similarity index 100% rename from src/ui/promptBuffer.ts rename to src/ui/core/promptBuffer.ts diff --git a/src/ui/promptUndoRedo.ts b/src/ui/core/promptUndoRedo.ts similarity index 100% rename from src/ui/promptUndoRedo.ts rename to src/ui/core/promptUndoRedo.ts diff --git a/src/ui/slashCommands.ts b/src/ui/core/slashCommands.ts similarity index 98% rename from src/ui/slashCommands.ts rename to src/ui/core/slashCommands.ts index 2677a231..d96734b0 100644 --- a/src/ui/slashCommands.ts +++ b/src/ui/core/slashCommands.ts @@ -1,4 +1,4 @@ -import type { SkillInfo } from "../session-types"; +import type { SkillInfo } from "../../session-types"; export type SlashCommandKind = | "skill" diff --git a/src/ui/thinkingState.ts b/src/ui/core/thinkingState.ts similarity index 95% rename from src/ui/thinkingState.ts rename to src/ui/core/thinkingState.ts index aad6d212..e7a525a7 100644 --- a/src/ui/thinkingState.ts +++ b/src/ui/core/thinkingState.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../session-types"; +import type { SessionMessage } from "../../session-types"; /** * Returns the message id of the assistant "thinking" message that should stay diff --git a/src/ui/prompt/cursor.ts b/src/ui/hooks/cursor.ts similarity index 99% rename from src/ui/prompt/cursor.ts rename to src/ui/hooks/cursor.ts index aefea342..2ecbddd7 100644 --- a/src/ui/prompt/cursor.ts +++ b/src/ui/hooks/cursor.ts @@ -1,5 +1,5 @@ import { useLayoutEffect, useRef } from "react"; -import type { PromptBufferState } from "../promptBuffer"; +import type { PromptBufferState } from "../core/promptBuffer"; type CursorPlacement = { rowsUp: number; diff --git a/src/ui/prompt/history-navigation.ts b/src/ui/hooks/history-navigation.ts similarity index 96% rename from src/ui/prompt/history-navigation.ts rename to src/ui/hooks/history-navigation.ts index 22ad0f63..1f595a9c 100644 --- a/src/ui/prompt/history-navigation.ts +++ b/src/ui/hooks/history-navigation.ts @@ -1,6 +1,6 @@ import type React from "react"; import { useCallback, useState } from "react"; -import type { PromptBufferState } from "../promptBuffer"; +import type { PromptBufferState } from "../core/promptBuffer"; export type HistoryNavigationState = { historyCursor: number; diff --git a/src/ui/prompt/index.ts b/src/ui/hooks/index.ts similarity index 100% rename from src/ui/prompt/index.ts rename to src/ui/hooks/index.ts diff --git a/src/ui/prompt/paste-handling.ts b/src/ui/hooks/paste-handling.ts similarity index 97% rename from src/ui/prompt/paste-handling.ts rename to src/ui/hooks/paste-handling.ts index 63c2c547..1ecdd3d1 100644 --- a/src/ui/prompt/paste-handling.ts +++ b/src/ui/hooks/paste-handling.ts @@ -1,7 +1,7 @@ import type React from "react"; import { useRef, useState } from "react"; -import type { PromptBufferState } from "../promptBuffer"; -import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../promptBuffer"; +import type { PromptBufferState } from "../core/promptBuffer"; +import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../core/promptBuffer"; export type PasteRegion = { start: number; diff --git a/src/ui/prompt/useTerminalInput.ts b/src/ui/hooks/useTerminalInput.ts similarity index 100% rename from src/ui/prompt/useTerminalInput.ts rename to src/ui/hooks/useTerminalInput.ts diff --git a/src/ui/index.ts b/src/ui/index.ts index f3cd41a7..482e59f9 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -4,18 +4,9 @@ import { MODEL_COMMAND_THINKING_OPTIONS, } from "./components/ModelsDropdown"; -export { - readSettings, - readProjectSettings, - writeSettings, - writeProjectSettings, - writeModelConfigSelection, - resolveCurrentSettings, - DEFAULT_MODEL, - DEFAULT_BASE_URL, -} from "../settings"; +export { getThinkingOptionIndex, MODEL_COMMAND_MODELS, MODEL_COMMAND_THINKING_OPTIONS }; export { buildPromptDraftFromSessionMessage } from "./utils"; -export { createOpenAIClient } from "../common/openai-client"; +export { disableTerminalExtendedKeys, enableTerminalExtendedKeys, getPromptCursorPlacement } from "./hooks/cursor"; export { default as AppContainer } from "./AppContainer"; export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; export { MessageView } from "./components"; @@ -39,8 +30,6 @@ export { type PromptDraft, type InputKey, } from "./PromptInput"; -export { getThinkingOptionIndex, MODEL_COMMAND_MODELS, MODEL_COMMAND_THINKING_OPTIONS }; -export { disableTerminalExtendedKeys, enableTerminalExtendedKeys, getPromptCursorPlacement } from "./prompt/cursor"; export { SessionList, formatSessionTitle, filterSessions, formatSessionStatus } from "./SessionList"; export { ThemedGradient } from "./ThemedGradient"; export { UpdatePrompt, type UpdatePromptChoice } from "./UpdatePrompt"; @@ -53,9 +42,9 @@ export { type AskUserQuestionItem, type PendingAskUserQuestion, type AskUserQuestionAnswers, -} from "./askUserQuestion"; -export { readClipboardImage, type ClipboardImage } from "./clipboard"; -export { buildLoadingText, type LoadingTextInput } from "./loadingText"; +} from "./core/askUserQuestion"; +export { readClipboardImage, type ClipboardImage } from "./core/clipboard"; +export { buildLoadingText, type LoadingTextInput } from "./core/loadingText"; export { renderMarkdown, renderMarkdownSegments, type MarkdownSegment } from "./components/MessageView/markdown"; export { EMPTY_BUFFER, @@ -77,7 +66,7 @@ export { isEmpty, getCurrentSlashToken, type PromptBufferState, -} from "./promptBuffer"; +} from "./core/promptBuffer"; export { BUILTIN_SLASH_COMMANDS, buildSlashCommands, @@ -87,7 +76,7 @@ export { formatSlashCommandLabel, type SlashCommandKind, type SlashCommandItem, -} from "./slashCommands"; +} from "./core/slashCommands"; export { filterFileMentionItems, formatFileMentionPath, @@ -96,6 +85,6 @@ export { scanFileMentionItems, type FileMentionItem, type FileMentionToken, -} from "./fileMentions"; -export { findExpandedThinkingId, isCollapsedThinking } from "./thinkingState"; +} from "./core/fileMentions"; +export { findExpandedThinkingId, isCollapsedThinking } from "./core/thinkingState"; export { buildExitSummaryText } from "./exitSummary"; From 2dd9794ffa3bf3f7494c4e265c4cb1f0a1b7f083 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 20:24:27 +0800 Subject: [PATCH 05/15] =?UTF-8?q?refactor(ui):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=EF=BC=8C=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=B7=AF=E5=BE=84=E5=92=8C=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将多个 UI 组件和模块从 src/ui 目录移动到 src/ui/views 目录 - 更新所有相关文件中的导入路径,确保引用正确 - 修改多个类型导入路径,从 session-types 改为 session/types - 统一调整 hooks 和核心模块的导入路径,修正导入命名和位置 - 维护代码一致性,避免因路径更改导致的引用错误 - 重命名 session.ts 为 session/index.ts,并修改相关导入 - 更新测试用例中的类型导入路径以匹配新的文件结构 - 将多处核心或公共模块导入路径调整为模块根目录的对应位置 --- src/prompt.ts | 2 +- src/{session.ts => session/index.ts} | 34 ++++++++-------- src/{session-types.ts => session/types.ts} | 6 +-- src/session/utils.ts | 2 +- src/tests/askUserQuestion.test.ts | 2 +- src/tests/exitSummary.test.ts | 2 +- src/tests/messageView.test.ts | 2 +- src/tests/permission-prompt.test.ts | 2 +- src/tests/promptInputKeys.test.ts | 5 +-- src/tests/session.test.ts | 2 +- src/tests/sessionList.test.ts | 2 +- src/tests/slashCommands.test.ts | 2 +- src/tests/thinkingState.test.ts | 2 +- src/ui/components/MessageView/types.ts | 2 +- src/ui/components/MessageView/utils.ts | 2 +- src/ui/components/SkillsDropdown/index.tsx | 4 +- src/ui/core/askUserQuestion.ts | 2 +- src/ui/core/loadingText.ts | 2 +- src/ui/core/slashCommands.ts | 2 +- src/ui/core/thinkingState.ts | 2 +- src/ui/exitSummary.ts | 2 +- src/ui/hooks/index.ts | 8 ++-- ...-navigation.ts => useHistoryNavigation.ts} | 0 ...{paste-handling.ts => usePasteHandling.ts} | 0 src/ui/index.ts | 18 ++++----- src/ui/utils/index.ts | 4 +- src/ui/{ => views}/App.tsx | 32 +++++++-------- src/ui/{ => views}/AppContainer.tsx | 4 +- src/ui/{ => views}/AskUserQuestionPrompt.tsx | 4 +- src/ui/{ => views}/McpStatusList.tsx | 2 +- src/ui/{ => views}/PermissionPrompt.tsx | 6 +-- src/ui/{ => views}/ProcessStdoutView.tsx | 6 +-- src/ui/{ => views}/PromptInput.tsx | 39 ++++++++----------- src/ui/{ => views}/SessionList.tsx | 4 +- src/ui/{ => views}/SlashCommandMenu.tsx | 8 ++-- src/ui/{ => views}/ThemedGradient.tsx | 0 src/ui/{ => views}/UndoSelector.tsx | 2 +- src/ui/{ => views}/UpdatePrompt.tsx | 0 src/ui/{ => views}/WelcomeScreen.tsx | 10 ++--- 39 files changed, 109 insertions(+), 121 deletions(-) rename src/{session.ts => session/index.ts} (99%) rename src/{session-types.ts => session/types.ts} (96%) rename src/ui/hooks/{history-navigation.ts => useHistoryNavigation.ts} (100%) rename src/ui/hooks/{paste-handling.ts => usePasteHandling.ts} (100%) rename src/ui/{ => views}/App.tsx (97%) rename src/ui/{ => views}/AppContainer.tsx (85%) rename src/ui/{ => views}/AskUserQuestionPrompt.tsx (99%) rename src/ui/{ => views}/McpStatusList.tsx (99%) rename src/ui/{ => views}/PermissionPrompt.tsx (98%) rename src/ui/{ => views}/ProcessStdoutView.tsx (98%) rename src/ui/{ => views}/PromptInput.tsx (96%) rename src/ui/{ => views}/SessionList.tsx (98%) rename src/ui/{ => views}/SlashCommandMenu.tsx (93%) rename src/ui/{ => views}/ThemedGradient.tsx (100%) rename src/ui/{ => views}/UndoSelector.tsx (99%) rename src/ui/{ => views}/UpdatePrompt.tsx (100%) rename src/ui/{ => views}/WelcomeScreen.tsx (94%) diff --git a/src/prompt.ts b/src/prompt.ts index ce8b6a64..f4e76d9d 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -4,7 +4,7 @@ import * as os from "os"; import * as path from "path"; import { fileURLToPath } from "url"; import ejs from "ejs"; -import type { SessionMessage } from "./session-types"; +import type { SessionMessage } from "./session/types"; import { findGitBashPath, resolveShellPath } from "./common/system/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; diff --git a/src/session.ts b/src/session/index.ts similarity index 99% rename from src/session.ts rename to src/session/index.ts index fa881707..704e7692 100644 --- a/src/session.ts +++ b/src/session/index.ts @@ -6,9 +6,9 @@ import { fileURLToPath } from "url"; import matter from "gray-matter"; import ejs from "ejs"; import type { ChatCompletionContentPart, ChatCompletionMessageParam } from "openai/resources/chat/completions"; -import { launchNotifyScript } from "./common/notify"; -import { buildThinkingRequestOptions } from "./common/openai-thinking"; -import { supportsMultimodal } from "./common/model-capabilities"; +import { launchNotifyScript } from "../common/notify"; +import { buildThinkingRequestOptions } from "../common/openai-thinking"; +import { supportsMultimodal } from "../common/model-capabilities"; import { getCompactPrompt, getDefaultSkillPrompt, @@ -16,7 +16,7 @@ import { getSystemPrompt, getTools, type ToolDefinition, -} from "./prompt"; +} from "../prompt"; import { type CreateOpenAIClient, type ProcessTimeoutControl, @@ -24,14 +24,14 @@ import { type ToolCallExecution, type ToolExecutionHooks, ToolExecutor, -} from "./tools/executor"; -import { McpManager } from "./mcp/mcp-manager"; -import type { McpServerConfig, PermissionSettings } from "./settings"; -import { logApiError } from "./common/logging/error-logger"; -import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/logging/debug-logger"; -import { killProcessTree } from "./common/system/process-tree"; -import { GitFileHistory } from "./common/runtime/file-history"; -import { getSnippet } from "./common/runtime/state"; +} from "../tools/executor"; +import { McpManager } from "../mcp/mcp-manager"; +import type { McpServerConfig, PermissionSettings } from "../settings"; +import { logApiError } from "../common/logging/error-logger"; +import { logOpenAIChatCompletionDebug, normalizeDebugError } from "../common/logging/debug-logger"; +import { killProcessTree } from "../common/system/process-tree"; +import { GitFileHistory } from "../common/runtime/file-history"; +import { getSnippet } from "../common/runtime/state"; import { appendProjectPermissionAllows, buildPermissionToolExecution, @@ -42,9 +42,9 @@ import { parseToolCallForPermissions, type PermissionToolCall, type UserToolPermission, -} from "./common/permissions"; +} from "../common/permissions"; -import { getCompactPromptTokenThreshold, getTotalTokens } from "./session/utils"; +import { getCompactPromptTokenThreshold, getTotalTokens } from "./utils"; import { type BashTimeoutAdjustment, type LlmStreamProgress, @@ -59,7 +59,7 @@ import { type SkillInfo, type UndoTarget, type UserPromptContent, -} from "./session-types"; +} from "./types"; const MAX_SESSION_ENTRIES = 50; const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; @@ -135,11 +135,11 @@ function accumulateUsagePerModel( function getExtensionRoot(): string { if (typeof __dirname !== "undefined") { - return path.resolve(__dirname, ".."); + return path.resolve(__dirname, "../.."); } const currentFilePath = fileURLToPath(import.meta.url); - return path.resolve(path.dirname(currentFilePath), ".."); + return path.resolve(path.dirname(currentFilePath), "../.."); } export class SessionManager { diff --git a/src/session-types.ts b/src/session/types.ts similarity index 96% rename from src/session-types.ts rename to src/session/types.ts index ffc836f7..46639c01 100644 --- a/src/session-types.ts +++ b/src/session/types.ts @@ -1,6 +1,6 @@ -import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; -import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "./common/permissions"; -import type { CreateOpenAIClient } from "./tools/executor"; +import type { McpServerConfig, PermissionScope, PermissionSettings } from "../settings"; +import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "../common/permissions"; +import type { CreateOpenAIClient } from "../tools/executor"; export type SessionStatus = | "failed" diff --git a/src/session/utils.ts b/src/session/utils.ts index 3860a841..3b807002 100644 --- a/src/session/utils.ts +++ b/src/session/utils.ts @@ -1,5 +1,5 @@ import { DEEPSEEK_V4_MODELS } from "../common/model-capabilities"; -import type { ModelUsage } from "../session-types"; +import type { ModelUsage } from "./types"; const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; diff --git a/src/tests/askUserQuestion.test.ts b/src/tests/askUserQuestion.test.ts index 89907b07..10c9a2cb 100644 --- a/src/tests/askUserQuestion.test.ts +++ b/src/tests/askUserQuestion.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findPendingAskUserQuestion, formatAskUserQuestionAnswers, formatAskUserQuestionDecline } from "../ui"; -import type { SessionMessage } from "../session-types"; +import type { SessionMessage } from "../session/types"; function message(content: unknown): SessionMessage { const now = "2026-04-29T00:00:00.000Z"; diff --git a/src/tests/exitSummary.test.ts b/src/tests/exitSummary.test.ts index 651f8b96..e22a904c 100644 --- a/src/tests/exitSummary.test.ts +++ b/src/tests/exitSummary.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { buildExitSummaryText } from "../ui"; -import type { ModelUsage, SessionEntry } from "../session-types"; +import type { ModelUsage, SessionEntry } from "../session/types"; const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*m/g, ""); diff --git a/src/tests/messageView.test.ts b/src/tests/messageView.test.ts index c9dfa8b3..9acd01ed 100644 --- a/src/tests/messageView.test.ts +++ b/src/tests/messageView.test.ts @@ -8,7 +8,7 @@ import { parseToolPayload, } from "../ui/components/MessageView/utils"; import { RawMode } from "../ui/contexts"; -import type { SessionMessage } from "../session-types"; +import type { SessionMessage } from "../session/types"; import type { ToolSummary } from "../ui/components/MessageView/types"; test("parseDiffPreview removes headers and classifies lines", () => { diff --git a/src/tests/permission-prompt.test.ts b/src/tests/permission-prompt.test.ts index aa4f372d..4f1d87e9 100644 --- a/src/tests/permission-prompt.test.ts +++ b/src/tests/permission-prompt.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { getScopeRiskColor } from "../ui/PermissionPrompt"; +import { getScopeRiskColor } from "../ui/views/PermissionPrompt"; test("getScopeRiskColor maps permission scopes by risk", () => { assert.equal(getScopeRiskColor("read-in-cwd"), "#22c55e"); diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/promptInputKeys.test.ts index 3b24b213..6e697b65 100644 --- a/src/tests/promptInputKeys.test.ts +++ b/src/tests/promptInputKeys.test.ts @@ -14,20 +14,19 @@ import { getPromptCursorPlacement, getPromptReturnKeyAction, isClearImageAttachmentsShortcut, - parseTerminalInput, removeCurrentSlashToken, toggleSkillSelection, renderBufferWithCursor, buildInitPromptSubmission, buildPromptDraftFromSessionMessage, - dispatchTerminalInput, disableTerminalExtendedKeys, enableTerminalExtendedKeys, EMPTY_BUFFER, insertText, backspace, } from "../ui"; -import type { SessionMessage, SkillInfo } from "../session-types"; +import type { SessionMessage, SkillInfo } from "../session/types"; +import { dispatchTerminalInput, parseTerminalInput } from "../ui/hooks"; function collectDispatchedInput(data: string) { const events: ReturnType[] = []; diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index bfacd8ef..fd08c4d8 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -5,7 +5,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { GitFileHistory } from "../common/runtime/file-history"; -import { type SessionMessage } from "../session-types"; +import { type SessionMessage } from "../session/types"; import { SessionManager } from "../session"; const originalFetch = globalThis.fetch; diff --git a/src/tests/sessionList.test.ts b/src/tests/sessionList.test.ts index edae36b2..5fdda393 100644 --- a/src/tests/sessionList.test.ts +++ b/src/tests/sessionList.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { formatSessionTitle, filterSessions, formatSessionStatus } from "../ui"; -import type { SessionEntry } from "../session-types"; +import type { SessionEntry } from "../session/types"; test("formatSessionTitle replaces newlines with spaces", () => { assert.equal(formatSessionTitle("first line\nsecond line\r\nthird"), "first line second line third"); diff --git a/src/tests/slashCommands.test.ts b/src/tests/slashCommands.test.ts index d352f3a5..fa98b9f2 100644 --- a/src/tests/slashCommands.test.ts +++ b/src/tests/slashCommands.test.ts @@ -7,7 +7,7 @@ import { formatSlashCommandDescription, formatSlashCommandLabel, } from "../ui"; -import type { SkillInfo } from "../session-types"; +import type { SkillInfo } from "../session/types"; const skills: SkillInfo[] = [ { name: "skill-writer", path: "~/.agents/skills/skill-writer/SKILL.md", description: "Write a SKILL.md" }, diff --git a/src/tests/thinkingState.test.ts b/src/tests/thinkingState.test.ts index f50ab935..347ac571 100644 --- a/src/tests/thinkingState.test.ts +++ b/src/tests/thinkingState.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findExpandedThinkingId } from "../ui"; -import type { SessionMessage } from "../session-types"; +import type { SessionMessage } from "../session/types"; function buildMessage( id: string, diff --git a/src/ui/components/MessageView/types.ts b/src/ui/components/MessageView/types.ts index 3d734f89..5339513b 100644 --- a/src/ui/components/MessageView/types.ts +++ b/src/ui/components/MessageView/types.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../../session-types"; +import type { SessionMessage } from "../../../session/types"; export type MessageViewProps = { message: SessionMessage; diff --git a/src/ui/components/MessageView/utils.ts b/src/ui/components/MessageView/utils.ts index 164b5fb4..9b004dbf 100644 --- a/src/ui/components/MessageView/utils.ts +++ b/src/ui/components/MessageView/utils.ts @@ -1,5 +1,5 @@ import type { DiffPreviewLine, ToolSummary } from "./types"; -import type { SessionMessage } from "../../../session-types"; +import type { SessionMessage } from "../../../session/types"; import { RawMode } from "../../contexts"; import chalk from "chalk"; diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx index db446040..12ec226a 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -1,8 +1,8 @@ import Index from "../DropdownMenu"; import React, { useEffect, useState } from "react"; -import type { SkillInfo } from "../../../session-types"; +import type { SkillInfo } from "../../../session/types"; import { useInput } from "ink"; -import { isSkillSelected } from "../../SlashCommandMenu"; +import { isSkillSelected } from "../../views/SlashCommandMenu"; const SkillsDropdown: React.FC<{ open: boolean; diff --git a/src/ui/core/askUserQuestion.ts b/src/ui/core/askUserQuestion.ts index ea94342e..8918604a 100644 --- a/src/ui/core/askUserQuestion.ts +++ b/src/ui/core/askUserQuestion.ts @@ -1,4 +1,4 @@ -import type { SessionMessage, SessionStatus } from "../../session-types"; +import type { SessionMessage, SessionStatus } from "../../session/types"; export type AskUserQuestionOption = { label: string; diff --git a/src/ui/core/loadingText.ts b/src/ui/core/loadingText.ts index 738b9685..f74cc1ac 100644 --- a/src/ui/core/loadingText.ts +++ b/src/ui/core/loadingText.ts @@ -1,4 +1,4 @@ -import type { LlmStreamProgress, SessionEntry } from "../../session-types"; +import type { LlmStreamProgress, SessionEntry } from "../../session/types"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/core/slashCommands.ts b/src/ui/core/slashCommands.ts index d96734b0..8a7487b0 100644 --- a/src/ui/core/slashCommands.ts +++ b/src/ui/core/slashCommands.ts @@ -1,4 +1,4 @@ -import type { SkillInfo } from "../../session-types"; +import type { SkillInfo } from "../../session/types"; export type SlashCommandKind = | "skill" diff --git a/src/ui/core/thinkingState.ts b/src/ui/core/thinkingState.ts index e7a525a7..bbd8e030 100644 --- a/src/ui/core/thinkingState.ts +++ b/src/ui/core/thinkingState.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../session-types"; +import type { SessionMessage } from "../../session/types"; /** * Returns the message id of the assistant "thinking" message that should stay diff --git a/src/ui/exitSummary.ts b/src/ui/exitSummary.ts index 95e11e7b..1801bd85 100644 --- a/src/ui/exitSummary.ts +++ b/src/ui/exitSummary.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; import gradientString from "gradient-string"; -import type { ModelUsage, SessionEntry } from "../session-types"; +import type { ModelUsage, SessionEntry } from "../session/types"; type ExitSummaryInput = { session: SessionEntry | null; diff --git a/src/ui/hooks/index.ts b/src/ui/hooks/index.ts index 56e07251..86245b65 100644 --- a/src/ui/hooks/index.ts +++ b/src/ui/hooks/index.ts @@ -10,8 +10,8 @@ export { getPromptCursorPlacement, } from "./cursor"; -export { usePasteHandling } from "./paste-handling"; -export type { PasteRegion, PasteHandlingState, PasteHandlingActions } from "./paste-handling"; +export { usePasteHandling } from "./usePasteHandling"; +export type { PasteRegion, PasteHandlingState, PasteHandlingActions } from "./usePasteHandling"; -export { useHistoryNavigation } from "./history-navigation"; -export type { HistoryNavigationState, HistoryNavigationActions } from "./history-navigation"; +export { useHistoryNavigation } from "./useHistoryNavigation"; +export type { HistoryNavigationState, HistoryNavigationActions } from "./useHistoryNavigation"; diff --git a/src/ui/hooks/history-navigation.ts b/src/ui/hooks/useHistoryNavigation.ts similarity index 100% rename from src/ui/hooks/history-navigation.ts rename to src/ui/hooks/useHistoryNavigation.ts diff --git a/src/ui/hooks/paste-handling.ts b/src/ui/hooks/usePasteHandling.ts similarity index 100% rename from src/ui/hooks/paste-handling.ts rename to src/ui/hooks/usePasteHandling.ts diff --git a/src/ui/index.ts b/src/ui/index.ts index 482e59f9..d9077eee 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -7,8 +7,8 @@ import { export { getThinkingOptionIndex, MODEL_COMMAND_MODELS, MODEL_COMMAND_THINKING_OPTIONS }; export { buildPromptDraftFromSessionMessage } from "./utils"; export { disableTerminalExtendedKeys, enableTerminalExtendedKeys, getPromptCursorPlacement } from "./hooks/cursor"; -export { default as AppContainer } from "./AppContainer"; -export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; +export { default as AppContainer } from "./views/AppContainer"; +export { AskUserQuestionPrompt } from "./views/AskUserQuestionPrompt"; export { MessageView } from "./components"; export { parseDiffPreview } from "./components/MessageView/utils"; export { @@ -23,17 +23,13 @@ export { getPromptReturnKeyAction, renderBufferWithCursor, buildInitPromptSubmission, - useTerminalInput, - parseTerminalInput, - dispatchTerminalInput, type PromptSubmission, type PromptDraft, - type InputKey, -} from "./PromptInput"; -export { SessionList, formatSessionTitle, filterSessions, formatSessionStatus } from "./SessionList"; -export { ThemedGradient } from "./ThemedGradient"; -export { UpdatePrompt, type UpdatePromptChoice } from "./UpdatePrompt"; -export { WelcomeScreen, formatHomeRelativePath, buildWelcomeTips } from "./WelcomeScreen"; +} from "./views/PromptInput"; +export { SessionList, formatSessionTitle, filterSessions, formatSessionStatus } from "./views/SessionList"; +export { ThemedGradient } from "./views/ThemedGradient"; +export { UpdatePrompt, type UpdatePromptChoice } from "./views/UpdatePrompt"; +export { WelcomeScreen, formatHomeRelativePath, buildWelcomeTips } from "./views/WelcomeScreen"; export { findPendingAskUserQuestion, formatAskUserQuestionAnswers, diff --git a/src/ui/utils/index.ts b/src/ui/utils/index.ts index 4a201466..4fb2fb1f 100644 --- a/src/ui/utils/index.ts +++ b/src/ui/utils/index.ts @@ -1,9 +1,9 @@ import chalk from "chalk"; import { renderMessageToStdout } from "../components/MessageView/utils"; import type { RawMode } from "../contexts"; -import type { PromptDraft } from "../PromptInput"; +import type { PromptDraft } from "../views/PromptInput"; import type { ModelConfigSelection } from "../../settings"; -import type { SessionEntry, SessionMessage } from "../../session-types"; +import type { SessionEntry, SessionMessage } from "../../session/types"; import type { SessionManager } from "../../session"; /** diff --git a/src/ui/App.tsx b/src/ui/views/App.tsx similarity index 97% rename from src/ui/App.tsx rename to src/ui/views/App.tsx index bea96f66..6ba5b623 100644 --- a/src/ui/App.tsx +++ b/src/ui/views/App.tsx @@ -1,15 +1,15 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { Box, Static, Text, useApp, useStdout, useWindowSize } from "ink"; import chalk from "chalk"; -import { createOpenAIClient } from "../common/openai-client"; -import type { PermissionScope } from "../settings"; -import { type ModelConfigSelection } from "../settings"; +import { createOpenAIClient } from "../../common/openai-client"; +import type { PermissionScope } from "../../settings"; +import { type ModelConfigSelection } from "../../settings"; import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptInput"; -import { MessageView, RawModeExitPrompt } from "./components"; +import { MessageView, RawModeExitPrompt } from "../components"; import { SessionList } from "./SessionList"; import { type UndoRestoreMode, UndoSelector } from "./UndoSelector"; -import { buildLoadingText } from "./core/loadingText"; -import { findExpandedThinkingId } from "./core/thinkingState"; +import { buildLoadingText } from "../core/loadingText"; +import { findExpandedThinkingId } from "../core/thinkingState"; import { WelcomeScreen } from "./WelcomeScreen"; import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; import { McpStatusList } from "./McpStatusList"; @@ -18,11 +18,11 @@ import { type AskUserQuestionAnswers, findPendingAskUserQuestion, formatAskUserQuestionAnswers, -} from "./core/askUserQuestion"; +} from "../core/askUserQuestion"; import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPrompt"; -import { buildExitSummaryText } from "./exitSummary"; -import { RawMode, useRawModeContext } from "./contexts"; -import { renderMessageToStdout } from "./components/MessageView/utils"; +import { buildExitSummaryText } from "../exitSummary"; +import { RawMode, useRawModeContext } from "../contexts"; +import { renderMessageToStdout } from "../components/MessageView/utils"; import { buildPromptDraftFromSessionMessage, buildStatusLine, @@ -30,10 +30,10 @@ import { formatModelConfig, isCurrentSessionEmpty, renderRawModeMessages, -} from "./utils"; -import { resolveCurrentSettings, writeModelConfigSelection } from "../settings"; -import { isCollapsedThinking } from "./core/thinkingState"; -import { ANSI_CLEAR_SCREEN } from "./constants"; +} from "../utils"; +import { resolveCurrentSettings, writeModelConfigSelection } from "../../settings"; +import { isCollapsedThinking } from "../core/thinkingState"; +import { ANSI_CLEAR_SCREEN } from "../constants"; import type { LlmStreamProgress, MessageMeta, @@ -43,8 +43,8 @@ import type { SkillInfo, UndoTarget, UserPromptContent, -} from "../session-types"; -import { SessionManager } from "../session"; +} from "../../session/types"; +import { SessionManager } from "../../session"; type View = "chat" | "session-list" | "undo" | "mcp-status"; diff --git a/src/ui/AppContainer.tsx b/src/ui/views/AppContainer.tsx similarity index 85% rename from src/ui/AppContainer.tsx rename to src/ui/views/AppContainer.tsx index f36eb4aa..d5f6363a 100644 --- a/src/ui/AppContainer.tsx +++ b/src/ui/views/AppContainer.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { AppContext } from "./contexts"; +import { AppContext } from "../contexts"; import App from "./App"; -import { RawModeProvider } from "./contexts"; +import { RawModeProvider } from "../contexts"; const AppContainer: React.FC<{ projectRoot: string; diff --git a/src/ui/AskUserQuestionPrompt.tsx b/src/ui/views/AskUserQuestionPrompt.tsx similarity index 99% rename from src/ui/AskUserQuestionPrompt.tsx rename to src/ui/views/AskUserQuestionPrompt.tsx index 058de3e1..988215f9 100644 --- a/src/ui/AskUserQuestionPrompt.tsx +++ b/src/ui/views/AskUserQuestionPrompt.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import type { AskUserQuestionAnswers, AskUserQuestionItem } from "./core/askUserQuestion"; -import { useTerminalInput } from "./hooks"; +import type { AskUserQuestionAnswers, AskUserQuestionItem } from "../core/askUserQuestion"; +import { useTerminalInput } from "../hooks"; type Props = { questions: AskUserQuestionItem[]; diff --git a/src/ui/McpStatusList.tsx b/src/ui/views/McpStatusList.tsx similarity index 99% rename from src/ui/McpStatusList.tsx rename to src/ui/views/McpStatusList.tsx index 095612a2..4013ff81 100644 --- a/src/ui/McpStatusList.tsx +++ b/src/ui/views/McpStatusList.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { McpServerStatus } from "../mcp/mcp-manager"; +import type { McpServerStatus } from "../../mcp/mcp-manager"; type Props = { statuses: McpServerStatus[]; diff --git a/src/ui/PermissionPrompt.tsx b/src/ui/views/PermissionPrompt.tsx similarity index 98% rename from src/ui/PermissionPrompt.tsx rename to src/ui/views/PermissionPrompt.tsx index f450ac96..320dd7ab 100644 --- a/src/ui/PermissionPrompt.tsx +++ b/src/ui/views/PermissionPrompt.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import { useTerminalInput } from "./hooks"; -import type { AskPermissionRequest, AskPermissionScope, UserToolPermission } from "../common/permissions"; -import type { PermissionScope } from "../settings"; +import { useTerminalInput } from "../hooks"; +import type { AskPermissionRequest, AskPermissionScope, UserToolPermission } from "../../common/permissions"; +import type { PermissionScope } from "../../settings"; export type PermissionPromptResult = { permissions: UserToolPermission[]; diff --git a/src/ui/ProcessStdoutView.tsx b/src/ui/views/ProcessStdoutView.tsx similarity index 98% rename from src/ui/ProcessStdoutView.tsx rename to src/ui/views/ProcessStdoutView.tsx index 23b230aa..d43c39cb 100644 --- a/src/ui/ProcessStdoutView.tsx +++ b/src/ui/views/ProcessStdoutView.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text } from "ink"; -import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../common/system/bash-timeout"; -import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../session-types"; -import { useTerminalInput } from "./hooks"; +import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../../common/system/bash-timeout"; +import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../../session/types"; +import { useTerminalInput } from "../hooks"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/PromptInput.tsx b/src/ui/views/PromptInput.tsx similarity index 96% rename from src/ui/PromptInput.tsx rename to src/ui/views/PromptInput.tsx index 27af870b..824ec98e 100644 --- a/src/ui/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text, useApp, useStdout } from "ink"; import chalk from "chalk"; -import { ARGS_SEPARATOR } from "./constants"; +import { ARGS_SEPARATOR } from "../constants"; import { EMPTY_BUFFER, PASTE_MARKER_REGEX, @@ -24,45 +24,38 @@ import { moveWordLeft, moveWordRight, moveUp, -} from "./core/promptBuffer"; -import type { PromptBufferState } from "./core/promptBuffer"; +} from "../core/promptBuffer"; +import type { PromptBufferState } from "../core/promptBuffer"; import { clearPromptUndoRedoState, createPromptUndoRedoState, recordPromptEdit, redoPromptEdit, undoPromptEdit, -} from "./core/promptUndoRedo"; -import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "./core/slashCommands"; -import type { SlashCommandItem } from "./core/slashCommands"; +} from "../core/promptUndoRedo"; +import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "../core/slashCommands"; +import type { SlashCommandItem } from "../core/slashCommands"; import { filterFileMentionItems, getCurrentFileMentionToken, replaceCurrentFileMentionToken, scanFileMentionItems, -} from "./core/fileMentions"; -import type { FileMentionItem } from "./core/fileMentions"; -import { readClipboardImageAsync } from "./core/clipboard"; - -// Re-exported from prompt modules for backward compatibility -export { useTerminalInput, parseTerminalInput, dispatchTerminalInput } from "./hooks"; -export type { InputKey } from "./hooks"; - -import { useTerminalInput } from "./hooks"; -import type { InputKey } from "./hooks"; -import { usePasteHandling } from "./hooks/paste-handling"; -import { useHistoryNavigation } from "./hooks/history-navigation"; +} from "../core/fileMentions"; +import type { FileMentionItem } from "../core/fileMentions"; +import { readClipboardImageAsync } from "../core/clipboard"; +import { useTerminalInput, usePasteHandling, useHistoryNavigation } from "../hooks"; +import type { InputKey } from "../hooks"; import { useHiddenTerminalCursor, useTerminalExtendedKeys, useBracketedPaste, useTerminalFocusReporting, -} from "./hooks"; +} from "../hooks"; import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; -import type { ModelConfigSelection, PermissionScope } from "../settings"; -import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "./components"; -import type { SessionEntry, SkillInfo } from "../session-types"; -import type { UserToolPermission } from "../common/permissions"; +import type { ModelConfigSelection, PermissionScope } from "../../settings"; +import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "../components"; +import type { SessionEntry, SkillInfo } from "../../session/types"; +import type { UserToolPermission } from "../../common/permissions"; export type PromptSubmission = { text: string; diff --git a/src/ui/SessionList.tsx b/src/ui/views/SessionList.tsx similarity index 98% rename from src/ui/SessionList.tsx rename to src/ui/views/SessionList.tsx index 4ea620e7..0b81ee89 100644 --- a/src/ui/SessionList.tsx +++ b/src/ui/views/SessionList.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { SessionEntry, SessionStatus } from "../session-types"; -import { truncate } from "./components/MessageView/utils"; +import type { SessionEntry, SessionStatus } from "../../session/types"; +import { truncate } from "../components/MessageView/utils"; type Props = { sessions: SessionEntry[]; diff --git a/src/ui/SlashCommandMenu.tsx b/src/ui/views/SlashCommandMenu.tsx similarity index 93% rename from src/ui/SlashCommandMenu.tsx rename to src/ui/views/SlashCommandMenu.tsx index 0ca5c696..275cf849 100644 --- a/src/ui/SlashCommandMenu.tsx +++ b/src/ui/views/SlashCommandMenu.tsx @@ -1,9 +1,9 @@ -import { formatSlashCommandDescription, formatSlashCommandLabel } from "./core/slashCommands"; -import type { SlashCommandItem } from "./core/slashCommands"; -import { ARGS_SEPARATOR } from "./constants"; +import { formatSlashCommandDescription, formatSlashCommandLabel } from "../core/slashCommands"; +import type { SlashCommandItem } from "../core/slashCommands"; +import { ARGS_SEPARATOR } from "../constants"; import React from "react"; import { Box, Text } from "ink"; -import type { SkillInfo } from "../session-types"; +import type { SkillInfo } from "../../session/types"; type SlashCommandMenuProps = { items: SlashCommandItem[]; diff --git a/src/ui/ThemedGradient.tsx b/src/ui/views/ThemedGradient.tsx similarity index 100% rename from src/ui/ThemedGradient.tsx rename to src/ui/views/ThemedGradient.tsx diff --git a/src/ui/UndoSelector.tsx b/src/ui/views/UndoSelector.tsx similarity index 99% rename from src/ui/UndoSelector.tsx rename to src/ui/views/UndoSelector.tsx index e41993e2..1d45acb0 100644 --- a/src/ui/UndoSelector.tsx +++ b/src/ui/views/UndoSelector.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { UndoTarget } from "../session-types"; +import type { UndoTarget } from "../../session/types"; export type UndoRestoreMode = "code-and-conversation" | "conversation"; diff --git a/src/ui/UpdatePrompt.tsx b/src/ui/views/UpdatePrompt.tsx similarity index 100% rename from src/ui/UpdatePrompt.tsx rename to src/ui/views/UpdatePrompt.tsx diff --git a/src/ui/WelcomeScreen.tsx b/src/ui/views/WelcomeScreen.tsx similarity index 94% rename from src/ui/WelcomeScreen.tsx rename to src/ui/views/WelcomeScreen.tsx index 36bb3030..9bbc8f1c 100644 --- a/src/ui/WelcomeScreen.tsx +++ b/src/ui/views/WelcomeScreen.tsx @@ -2,12 +2,12 @@ import React, { useMemo, useState } from "react"; import { Box, Text } from "ink"; import * as os from "node:os"; import path from "node:path"; -import type { SkillInfo } from "../session-types"; -import type { ResolvedDeepcodingSettings } from "../settings"; -import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "./core/slashCommands"; +import type { SkillInfo } from "../../session/types"; +import type { ResolvedDeepcodingSettings } from "../../settings"; +import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "../core/slashCommands"; import { ThemedGradient } from "./ThemedGradient"; -import { AsciiLogo } from "./AsciiArt"; -import { useAppContext } from "./contexts"; +import { AsciiLogo } from "../AsciiArt"; +import { useAppContext } from "../contexts"; type WelcomeScreenProps = { projectRoot: string; From 0a1a40533883f4808cf5ca5fb291cd6e1149b0f2 Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 20:56:43 +0800 Subject: [PATCH 06/15] =?UTF-8?q?style(ui):=20=E4=BC=98=E5=8C=96=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E9=A2=9C=E8=89=B2=E5=92=8C=E6=96=87=E5=AD=97=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在App视图中取消撤销提示框的草稿状态 - 修改DropdownMenu标题颜色为#229ac3,增强视觉统一 - 精简McpStatusList的状态文本样式,去除加粗效果 - 将UndoSelector中的标题文字颜色改为#229ac3,提升界面一致性 --- src/ui/components/DropdownMenu/index.tsx | 2 +- src/ui/views/App.tsx | 1 + src/ui/views/McpStatusList.tsx | 18 ++++-------------- src/ui/views/UndoSelector.tsx | 2 +- 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/ui/components/DropdownMenu/index.tsx b/src/ui/components/DropdownMenu/index.tsx index 6593ff8d..cf323141 100644 --- a/src/ui/components/DropdownMenu/index.tsx +++ b/src/ui/components/DropdownMenu/index.tsx @@ -64,7 +64,7 @@ const DropdownMenu = React.memo(function DropdownMenu({ maxVisible = 8, width, title, - titleColor = "magenta", + titleColor = "#229ac3", activeColor = "cyanBright", helpText, emptyText = "No items found", diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index 6ba5b623..dd30cb22 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -751,6 +751,7 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl targets={undoTargets} onSelect={(target, restoreMode) => void handleUndoRestore(target, restoreMode)} onCancel={() => { + setPromptDraft(null); setView("chat"); setShowWelcome(true); }} diff --git a/src/ui/views/McpStatusList.tsx b/src/ui/views/McpStatusList.tsx index 4013ff81..40d2f3f4 100644 --- a/src/ui/views/McpStatusList.tsx +++ b/src/ui/views/McpStatusList.tsx @@ -195,20 +195,10 @@ function ServerListView({ ( - - {readyCount} ready, - - - {startingCount} starting, - - {reconnectingCount > 0 && ( - - {reconnectingCount} reconnecting, - - )} - - {failedCount} failed - + {readyCount} ready, + {startingCount} starting, + {reconnectingCount > 0 && {reconnectingCount} reconnecting,} + {failedCount} failed ) diff --git a/src/ui/views/UndoSelector.tsx b/src/ui/views/UndoSelector.tsx index 1d45acb0..613025c6 100644 --- a/src/ui/views/UndoSelector.tsx +++ b/src/ui/views/UndoSelector.tsx @@ -99,7 +99,7 @@ export function UndoSelector({ targets, onSelect, onCancel }: Props): React.Reac > - + Undo restore to the point before a prompt From 197676ec0687757d35d6940a54c31d3866e8d06c Mon Sep 17 00:00:00 2001 From: hcyang Date: Mon, 25 May 2026 21:52:30 +0800 Subject: [PATCH 07/15] =?UTF-8?q?refactor(session):=20=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E5=B9=B6=E8=BF=81=E7=A7=BB=E4=BC=9A=E8=AF=9D=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将会话相关的工具函数从 session/index.ts 中移除 - 在 session/utils.ts 中重新实现并导出这些工具函数 - 调整了导入路径,改用新的工具函数模块 - 修正了多个文件中对 executeValidatedTool 的导入路径 - 统一了 SkillsDropdown 组件中的 DropdownMenu 导入名称 --- .../runtime/{runtime.ts => validate.ts} | 0 src/session/index.ts | 81 +++---------------- src/session/utils.ts | 70 +++++++++++++++- src/tools/edit-handler.ts | 2 +- src/tools/update-plan-handler.ts | 2 +- src/tools/write-handler.ts | 2 +- src/ui/components/SkillsDropdown/index.tsx | 4 +- 7 files changed, 83 insertions(+), 78 deletions(-) rename src/common/runtime/{runtime.ts => validate.ts} (100%) diff --git a/src/common/runtime/runtime.ts b/src/common/runtime/validate.ts similarity index 100% rename from src/common/runtime/runtime.ts rename to src/common/runtime/validate.ts diff --git a/src/session/index.ts b/src/session/index.ts index 704e7692..bab18b80 100644 --- a/src/session/index.ts +++ b/src/session/index.ts @@ -2,7 +2,6 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import * as crypto from "crypto"; -import { fileURLToPath } from "url"; import matter from "gray-matter"; import ejs from "ejs"; import type { ChatCompletionContentPart, ChatCompletionMessageParam } from "openai/resources/chat/completions"; @@ -44,7 +43,15 @@ import { type UserToolPermission, } from "../common/permissions"; -import { getCompactPromptTokenThreshold, getTotalTokens } from "./utils"; +import { + accumulateUsage, + accumulateUsagePerModel, + getCompactPromptTokenThreshold, + getExtensionRoot, + getTotalTokens, + isUsageRecord, + summarizeCompletionOptions, +} from "./utils"; import { type BashTimeoutAdjustment, type LlmStreamProgress, @@ -72,76 +79,6 @@ type ChatCompletionDebugOptions = { params?: Record; }; -function isUsageRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -function summarizeCompletionOptions(options?: Record): Record | undefined { - if (!options) { - return undefined; - } - return { - ...options, - signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, - }; -} - -function addUsageValue(current: unknown, next: unknown): unknown { - if (typeof next === "number") { - return (typeof current === "number" ? current : 0) + next; - } - - if (isUsageRecord(next)) { - const currentRecord = isUsageRecord(current) ? current : {}; - const result: Record = { ...currentRecord }; - for (const [key, value] of Object.entries(next)) { - result[key] = addUsageValue(currentRecord[key], value); - } - return result; - } - - return next; -} - -function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { - if (next == null) { - return current ?? null; - } - return addUsageValue(current, next) as ModelUsage; -} - -function usageWithRequestCount(usage: ModelUsage): ModelUsage { - const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; - return { - ...usage, - total_reqs: totalReqs, - }; -} - -function accumulateUsagePerModel( - current: Record | null | undefined, - model: string, - next: ModelUsage | null | undefined -): Record | null { - if (next == null) { - return current ?? null; - } - - const usagePerModel = { ...(current ?? {}) }; - const modelName = model.trim() || "unknown"; - usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; - return usagePerModel; -} - -function getExtensionRoot(): string { - if (typeof __dirname !== "undefined") { - return path.resolve(__dirname, "../.."); - } - - const currentFilePath = fileURLToPath(import.meta.url); - return path.resolve(path.dirname(currentFilePath), "../.."); -} - export class SessionManager { private readonly projectRoot: string; private readonly createOpenAIClient: CreateOpenAIClient; diff --git a/src/session/utils.ts b/src/session/utils.ts index 3b807002..50047cdb 100644 --- a/src/session/utils.ts +++ b/src/session/utils.ts @@ -1,3 +1,5 @@ +import * as path from "path"; +import { fileURLToPath } from "url"; import { DEEPSEEK_V4_MODELS } from "../common/model-capabilities"; import type { ModelUsage } from "./types"; @@ -10,7 +12,7 @@ export function getCompactPromptTokenThreshold(model: string): number { : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; } -function isUsageRecord(value: unknown): value is Record { +export function isUsageRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } @@ -21,3 +23,69 @@ export function getTotalTokens(usage: ModelUsage | null | undefined): number { const totalTokens = (usage as Record).total_tokens; return typeof totalTokens === "number" ? totalTokens : 0; } + +export function summarizeCompletionOptions(options?: Record): Record | undefined { + if (!options) { + return undefined; + } + return { + ...options, + signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, + }; +} + +export function addUsageValue(current: unknown, next: unknown): unknown { + if (typeof next === "number") { + return (typeof current === "number" ? current : 0) + next; + } + + if (isUsageRecord(next)) { + const currentRecord = isUsageRecord(current) ? current : {}; + const result: Record = { ...currentRecord }; + for (const [key, value] of Object.entries(next)) { + result[key] = addUsageValue(currentRecord[key], value); + } + return result; + } + + return next; +} + +export function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { + if (next == null) { + return current ?? null; + } + return addUsageValue(current, next) as ModelUsage; +} + +export function usageWithRequestCount(usage: ModelUsage): ModelUsage { + const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; + return { + ...usage, + total_reqs: totalReqs, + }; +} + +export function accumulateUsagePerModel( + current: Record | null | undefined, + model: string, + next: ModelUsage | null | undefined +): Record | null { + if (next == null) { + return current ?? null; + } + + const usagePerModel = { ...(current ?? {}) }; + const modelName = model.trim() || "unknown"; + usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; + return usagePerModel; +} + +export function getExtensionRoot(): string { + if (typeof __dirname !== "undefined") { + return path.resolve(__dirname, "../.."); + } + + const currentFilePath = fileURLToPath(import.meta.url); + return path.resolve(path.dirname(currentFilePath), "../.."); +} diff --git a/src/tools/edit-handler.ts b/src/tools/edit-handler.ts index 98afa43f..6bf06112 100644 --- a/src/tools/edit-handler.ts +++ b/src/tools/edit-handler.ts @@ -8,7 +8,7 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool, semanticBoolean } from "../common/runtime/runtime"; +import { executeValidatedTool, semanticBoolean } from "../common/runtime/validate"; import { createSnippet, getFileState, diff --git a/src/tools/update-plan-handler.ts b/src/tools/update-plan-handler.ts index a8947cfc..ff848703 100644 --- a/src/tools/update-plan-handler.ts +++ b/src/tools/update-plan-handler.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { ToolExecutionContext, ToolExecutionResult } from "./executor"; -import { executeValidatedTool } from "../common/runtime/runtime"; +import { executeValidatedTool } from "../common/runtime/validate"; const updatePlanSchema = z.strictObject({ plan: z.string().trim().min(1, "plan must not be empty."), diff --git a/src/tools/write-handler.ts b/src/tools/write-handler.ts index e91a78c7..1d3fb558 100644 --- a/src/tools/write-handler.ts +++ b/src/tools/write-handler.ts @@ -9,7 +9,7 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool } from "../common/runtime/runtime"; +import { executeValidatedTool } from "../common/runtime/validate"; import { getFileState, isAbsoluteFilePath, diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx index 12ec226a..07b49de6 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -1,4 +1,4 @@ -import Index from "../DropdownMenu"; +import DropdownMenu from "../DropdownMenu"; import React, { useEffect, useState } from "react"; import type { SkillInfo } from "../../../session/types"; import { useInput } from "ink"; @@ -52,7 +52,7 @@ const SkillsDropdown: React.FC<{ } return ( - Date: Tue, 26 May 2026 09:32:44 +0800 Subject: [PATCH 08/15] =?UTF-8?q?refactor(session):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=20getExtensionRoot=20=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 getExtensionRoot 函数从 session/utils.ts 移除 - 在 session/utils.ts 中重新导出 prompt.ts 中的 getExtensionRoot 函数 - 调整 prompt.ts 中 getExtensionRoot 函数的导出为 export - 优化了 getExtensionRoot 函数的注释和环境兼容处理逻辑 --- src/prompt.ts | 4 ++-- src/session/utils.ts | 11 +---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/prompt.ts b/src/prompt.ts index f4e76d9d..e77993fc 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -2,8 +2,8 @@ import { execFileSync, execSync } from "child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { fileURLToPath } from "url"; import ejs from "ejs"; +import { fileURLToPath } from "url"; import type { SessionMessage } from "./session/types"; import { findGitBashPath, resolveShellPath } from "./common/system/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; @@ -286,7 +286,7 @@ function getUnameInfo(): string { } } -function getExtensionRoot(): string { +export function getExtensionRoot(): string { // Prefer `__dirname` which is always available in the CJS bundle output. // Fall back to `import.meta.url` for ESM test environments (tsx --test). if (typeof __dirname !== "undefined") { diff --git a/src/session/utils.ts b/src/session/utils.ts index 50047cdb..552e7725 100644 --- a/src/session/utils.ts +++ b/src/session/utils.ts @@ -1,5 +1,3 @@ -import * as path from "path"; -import { fileURLToPath } from "url"; import { DEEPSEEK_V4_MODELS } from "../common/model-capabilities"; import type { ModelUsage } from "./types"; @@ -81,11 +79,4 @@ export function accumulateUsagePerModel( return usagePerModel; } -export function getExtensionRoot(): string { - if (typeof __dirname !== "undefined") { - return path.resolve(__dirname, "../.."); - } - - const currentFilePath = fileURLToPath(import.meta.url); - return path.resolve(path.dirname(currentFilePath), "../.."); -} +export { getExtensionRoot } from "../prompt"; From 77245d8eaab3b0eff1bba718682047d844bf253c Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 10:06:52 +0800 Subject: [PATCH 09/15] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E6=AD=A3=E6=9D=83?= =?UTF-8?q?=E9=99=90=E8=AF=B7=E6=B1=82=E6=B5=81=E7=A8=8B=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=8D=89=E7=A8=BF=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在权限请求未通过时清理提示草稿,避免残留内容 - 当权限被拒绝时不再重复清空提示草稿 - 取消欢迎界面的重复显示,优化视图切换逻辑 --- src/ui/views/App.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index dd30cb22..4f614c5d 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -659,6 +659,7 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl if (!sessionId) { return; } + setPromptDraft(null); if (result.hasDeny) { setPendingPermissionReply({ sessionId, @@ -666,7 +667,6 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl alwaysAllows: result.alwaysAllows, }); setStatusLine("Permission denied. Add a reply, then press Enter to continue."); - setPromptDraft(null); sessionManager.denySessionPermission(sessionId); return; } @@ -753,7 +753,6 @@ function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.ReactEl onCancel={() => { setPromptDraft(null); setView("chat"); - setShowWelcome(true); }} /> ) : view === "mcp-status" ? ( From 39b38f31c7281e0326ba7fc3c6e919e29865cecc Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 10:57:03 +0800 Subject: [PATCH 10/15] =?UTF-8?q?refactor(core):=20=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E5=A4=9A=E4=B8=AA=E6=96=87=E4=BB=B6=E5=8F=8A=E5=85=B6?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E8=B7=AF=E5=BE=84=E4=BB=A5=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将核心模块文件名改为短横线风格(kebab-case) - 更新所有相关导入路径以匹配重命名后的文件名 - 修改测试文件名和对应引用路径,保持一致性 - 调整部分组件及钩子中的类型导入路径 - 无业务功能变更,仅代码结构及命名优化 --- src/cli.tsx | 2 +- src/common/{updateCheck.ts => update-check.ts} | 0 ...rQuestion.test.ts => ask-user-question.test.ts} | 0 ...{dropdownMenu.test.ts => dropdown-menu.test.ts} | 0 .../{exitSummary.test.ts => exit-summary.test.ts} | 0 ...{fileMentions.test.ts => file-mentions.test.ts} | 2 +- .../{loadingText.test.ts => loading-text.test.ts} | 0 .../{messageView.test.ts => message-view.test.ts} | 0 ...{promptBuffer.test.ts => prompt-buffer.test.ts} | 0 ...InputKeys.test.ts => prompt-input-keys.test.ts} | 0 ...ptUndoRedo.test.ts => prompt-undo-redo.test.ts} | 2 +- .../{sessionList.test.ts => session-list.test.ts} | 0 ...lashCommands.test.ts => slash-commands.test.ts} | 0 ...hinkingState.test.ts => thinking-state.test.ts} | 0 .../{updateCheck.test.ts => update-check.test.ts} | 2 +- ...elcomeScreen.test.ts => welcome-screen.test.ts} | 0 src/ui/{AsciiArt.ts => ascii-art.ts} | 0 src/ui/components/FileMentionMenu/index.tsx | 2 +- .../{askUserQuestion.ts => ask-user-question.ts} | 0 src/ui/core/{fileMentions.ts => file-mentions.ts} | 2 +- src/ui/core/{loadingText.ts => loading-text.ts} | 0 src/ui/core/{promptBuffer.ts => prompt-buffer.ts} | 0 .../{promptUndoRedo.ts => prompt-undo-redo.ts} | 2 +- .../core/{slashCommands.ts => slash-commands.ts} | 0 .../core/{thinkingState.ts => thinking-state.ts} | 0 src/ui/{exitSummary.ts => exit-summary.ts} | 0 src/ui/hooks/cursor.ts | 2 +- src/ui/hooks/useHistoryNavigation.ts | 2 +- src/ui/hooks/usePasteHandling.ts | 4 ++-- src/ui/index.ts | 14 +++++++------- src/ui/views/App.tsx | 10 +++++----- src/ui/views/AskUserQuestionPrompt.tsx | 2 +- src/ui/views/PromptInput.tsx | 14 +++++++------- src/ui/views/SlashCommandMenu.tsx | 4 ++-- src/ui/views/WelcomeScreen.tsx | 4 ++-- 35 files changed, 35 insertions(+), 35 deletions(-) rename src/common/{updateCheck.ts => update-check.ts} (100%) rename src/tests/{askUserQuestion.test.ts => ask-user-question.test.ts} (100%) rename src/tests/{dropdownMenu.test.ts => dropdown-menu.test.ts} (100%) rename src/tests/{exitSummary.test.ts => exit-summary.test.ts} (100%) rename src/tests/{fileMentions.test.ts => file-mentions.test.ts} (99%) rename src/tests/{loadingText.test.ts => loading-text.test.ts} (100%) rename src/tests/{messageView.test.ts => message-view.test.ts} (100%) rename src/tests/{promptBuffer.test.ts => prompt-buffer.test.ts} (100%) rename src/tests/{promptInputKeys.test.ts => prompt-input-keys.test.ts} (100%) rename src/tests/{promptUndoRedo.test.ts => prompt-undo-redo.test.ts} (98%) rename src/tests/{sessionList.test.ts => session-list.test.ts} (100%) rename src/tests/{slashCommands.test.ts => slash-commands.test.ts} (100%) rename src/tests/{thinkingState.test.ts => thinking-state.test.ts} (100%) rename src/tests/{updateCheck.test.ts => update-check.test.ts} (97%) rename src/tests/{welcomeScreen.test.ts => welcome-screen.test.ts} (100%) rename src/ui/{AsciiArt.ts => ascii-art.ts} (100%) rename src/ui/core/{askUserQuestion.ts => ask-user-question.ts} (100%) rename src/ui/core/{fileMentions.ts => file-mentions.ts} (99%) rename src/ui/core/{loadingText.ts => loading-text.ts} (100%) rename src/ui/core/{promptBuffer.ts => prompt-buffer.ts} (100%) rename src/ui/core/{promptUndoRedo.ts => prompt-undo-redo.ts} (95%) rename src/ui/core/{slashCommands.ts => slash-commands.ts} (100%) rename src/ui/core/{thinkingState.ts => thinking-state.ts} (100%) rename src/ui/{exitSummary.ts => exit-summary.ts} (100%) diff --git a/src/cli.tsx b/src/cli.tsx index de26bf2c..d851a911 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render } from "ink"; import { setShellIfWindows } from "./common/system/shell-utils"; -import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/updateCheck"; +import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/update-check"; import { AppContainer } from "./ui"; const args = process.argv.slice(2); diff --git a/src/common/updateCheck.ts b/src/common/update-check.ts similarity index 100% rename from src/common/updateCheck.ts rename to src/common/update-check.ts diff --git a/src/tests/askUserQuestion.test.ts b/src/tests/ask-user-question.test.ts similarity index 100% rename from src/tests/askUserQuestion.test.ts rename to src/tests/ask-user-question.test.ts diff --git a/src/tests/dropdownMenu.test.ts b/src/tests/dropdown-menu.test.ts similarity index 100% rename from src/tests/dropdownMenu.test.ts rename to src/tests/dropdown-menu.test.ts diff --git a/src/tests/exitSummary.test.ts b/src/tests/exit-summary.test.ts similarity index 100% rename from src/tests/exitSummary.test.ts rename to src/tests/exit-summary.test.ts diff --git a/src/tests/fileMentions.test.ts b/src/tests/file-mentions.test.ts similarity index 99% rename from src/tests/fileMentions.test.ts rename to src/tests/file-mentions.test.ts index 50a6dc41..93d9bccc 100644 --- a/src/tests/fileMentions.test.ts +++ b/src/tests/file-mentions.test.ts @@ -10,7 +10,7 @@ import { replaceCurrentFileMentionToken, scanFileMentionItems, type FileMentionItem, -} from "../ui/core/fileMentions"; +} from "../ui/core/file-mentions"; test("getCurrentFileMentionToken detects bare @file tokens under the cursor", () => { assert.deepEqual(getCurrentFileMentionToken({ text: "review @src/app.ts please", cursor: 10 }), { diff --git a/src/tests/loadingText.test.ts b/src/tests/loading-text.test.ts similarity index 100% rename from src/tests/loadingText.test.ts rename to src/tests/loading-text.test.ts diff --git a/src/tests/messageView.test.ts b/src/tests/message-view.test.ts similarity index 100% rename from src/tests/messageView.test.ts rename to src/tests/message-view.test.ts diff --git a/src/tests/promptBuffer.test.ts b/src/tests/prompt-buffer.test.ts similarity index 100% rename from src/tests/promptBuffer.test.ts rename to src/tests/prompt-buffer.test.ts diff --git a/src/tests/promptInputKeys.test.ts b/src/tests/prompt-input-keys.test.ts similarity index 100% rename from src/tests/promptInputKeys.test.ts rename to src/tests/prompt-input-keys.test.ts diff --git a/src/tests/promptUndoRedo.test.ts b/src/tests/prompt-undo-redo.test.ts similarity index 98% rename from src/tests/promptUndoRedo.test.ts rename to src/tests/prompt-undo-redo.test.ts index 26360c04..d4590fb6 100644 --- a/src/tests/promptUndoRedo.test.ts +++ b/src/tests/prompt-undo-redo.test.ts @@ -8,7 +8,7 @@ import { recordPromptEdit, redoPromptEdit, undoPromptEdit, -} from "../ui/core/promptUndoRedo"; +} from "../ui/core/prompt-undo-redo"; test("prompt undo and redo restore edited buffer states", () => { const history = createPromptUndoRedoState(); diff --git a/src/tests/sessionList.test.ts b/src/tests/session-list.test.ts similarity index 100% rename from src/tests/sessionList.test.ts rename to src/tests/session-list.test.ts diff --git a/src/tests/slashCommands.test.ts b/src/tests/slash-commands.test.ts similarity index 100% rename from src/tests/slashCommands.test.ts rename to src/tests/slash-commands.test.ts diff --git a/src/tests/thinkingState.test.ts b/src/tests/thinking-state.test.ts similarity index 100% rename from src/tests/thinkingState.test.ts rename to src/tests/thinking-state.test.ts diff --git a/src/tests/updateCheck.test.ts b/src/tests/update-check.test.ts similarity index 97% rename from src/tests/updateCheck.test.ts rename to src/tests/update-check.test.ts index 23682de2..93b30360 100644 --- a/src/tests/updateCheck.test.ts +++ b/src/tests/update-check.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { compareVersions, parseNpmViewVersion } from "../common/updateCheck"; +import { compareVersions, parseNpmViewVersion } from "../common/update-check"; test("compareVersions orders semantic versions", () => { assert.equal(compareVersions("0.1.4", "0.1.3"), 1); diff --git a/src/tests/welcomeScreen.test.ts b/src/tests/welcome-screen.test.ts similarity index 100% rename from src/tests/welcomeScreen.test.ts rename to src/tests/welcome-screen.test.ts diff --git a/src/ui/AsciiArt.ts b/src/ui/ascii-art.ts similarity index 100% rename from src/ui/AsciiArt.ts rename to src/ui/ascii-art.ts diff --git a/src/ui/components/FileMentionMenu/index.tsx b/src/ui/components/FileMentionMenu/index.tsx index 15465d42..f00b367e 100644 --- a/src/ui/components/FileMentionMenu/index.tsx +++ b/src/ui/components/FileMentionMenu/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Box, Text } from "ink"; import { useInput } from "ink"; import DropdownMenu from "../DropdownMenu"; -import type { FileMentionItem, FileMentionToken } from "../../core/fileMentions"; +import type { FileMentionItem, FileMentionToken } from "../../core/file-mentions"; type Props = { open: boolean; diff --git a/src/ui/core/askUserQuestion.ts b/src/ui/core/ask-user-question.ts similarity index 100% rename from src/ui/core/askUserQuestion.ts rename to src/ui/core/ask-user-question.ts diff --git a/src/ui/core/fileMentions.ts b/src/ui/core/file-mentions.ts similarity index 99% rename from src/ui/core/fileMentions.ts rename to src/ui/core/file-mentions.ts index cbacbe6d..ae9c8b99 100644 --- a/src/ui/core/fileMentions.ts +++ b/src/ui/core/file-mentions.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import ignore from "ignore"; -import type { PromptBufferState } from "./promptBuffer"; +import type { PromptBufferState } from "./prompt-buffer"; export type FileMentionItem = { path: string; diff --git a/src/ui/core/loadingText.ts b/src/ui/core/loading-text.ts similarity index 100% rename from src/ui/core/loadingText.ts rename to src/ui/core/loading-text.ts diff --git a/src/ui/core/promptBuffer.ts b/src/ui/core/prompt-buffer.ts similarity index 100% rename from src/ui/core/promptBuffer.ts rename to src/ui/core/prompt-buffer.ts diff --git a/src/ui/core/promptUndoRedo.ts b/src/ui/core/prompt-undo-redo.ts similarity index 95% rename from src/ui/core/promptUndoRedo.ts rename to src/ui/core/prompt-undo-redo.ts index 9d30f57b..fd2870a6 100644 --- a/src/ui/core/promptUndoRedo.ts +++ b/src/ui/core/prompt-undo-redo.ts @@ -1,4 +1,4 @@ -import type { PromptBufferState } from "./promptBuffer"; +import type { PromptBufferState } from "./prompt-buffer"; export type PromptUndoRedoState = { undoStack: PromptBufferState[]; diff --git a/src/ui/core/slashCommands.ts b/src/ui/core/slash-commands.ts similarity index 100% rename from src/ui/core/slashCommands.ts rename to src/ui/core/slash-commands.ts diff --git a/src/ui/core/thinkingState.ts b/src/ui/core/thinking-state.ts similarity index 100% rename from src/ui/core/thinkingState.ts rename to src/ui/core/thinking-state.ts diff --git a/src/ui/exitSummary.ts b/src/ui/exit-summary.ts similarity index 100% rename from src/ui/exitSummary.ts rename to src/ui/exit-summary.ts diff --git a/src/ui/hooks/cursor.ts b/src/ui/hooks/cursor.ts index 2ecbddd7..07cc5779 100644 --- a/src/ui/hooks/cursor.ts +++ b/src/ui/hooks/cursor.ts @@ -1,5 +1,5 @@ import { useLayoutEffect, useRef } from "react"; -import type { PromptBufferState } from "../core/promptBuffer"; +import type { PromptBufferState } from "../core/prompt-buffer"; type CursorPlacement = { rowsUp: number; diff --git a/src/ui/hooks/useHistoryNavigation.ts b/src/ui/hooks/useHistoryNavigation.ts index 1f595a9c..433d493c 100644 --- a/src/ui/hooks/useHistoryNavigation.ts +++ b/src/ui/hooks/useHistoryNavigation.ts @@ -1,6 +1,6 @@ import type React from "react"; import { useCallback, useState } from "react"; -import type { PromptBufferState } from "../core/promptBuffer"; +import type { PromptBufferState } from "../core/prompt-buffer"; export type HistoryNavigationState = { historyCursor: number; diff --git a/src/ui/hooks/usePasteHandling.ts b/src/ui/hooks/usePasteHandling.ts index 1ecdd3d1..50cae754 100644 --- a/src/ui/hooks/usePasteHandling.ts +++ b/src/ui/hooks/usePasteHandling.ts @@ -1,7 +1,7 @@ import type React from "react"; import { useRef, useState } from "react"; -import type { PromptBufferState } from "../core/promptBuffer"; -import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../core/promptBuffer"; +import type { PromptBufferState } from "../core/prompt-buffer"; +import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../core/prompt-buffer"; export type PasteRegion = { start: number; diff --git a/src/ui/index.ts b/src/ui/index.ts index d9077eee..ae1109ad 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -38,9 +38,9 @@ export { type AskUserQuestionItem, type PendingAskUserQuestion, type AskUserQuestionAnswers, -} from "./core/askUserQuestion"; +} from "./core/ask-user-question"; export { readClipboardImage, type ClipboardImage } from "./core/clipboard"; -export { buildLoadingText, type LoadingTextInput } from "./core/loadingText"; +export { buildLoadingText, type LoadingTextInput } from "./core/loading-text"; export { renderMarkdown, renderMarkdownSegments, type MarkdownSegment } from "./components/MessageView/markdown"; export { EMPTY_BUFFER, @@ -62,7 +62,7 @@ export { isEmpty, getCurrentSlashToken, type PromptBufferState, -} from "./core/promptBuffer"; +} from "./core/prompt-buffer"; export { BUILTIN_SLASH_COMMANDS, buildSlashCommands, @@ -72,7 +72,7 @@ export { formatSlashCommandLabel, type SlashCommandKind, type SlashCommandItem, -} from "./core/slashCommands"; +} from "./core/slash-commands"; export { filterFileMentionItems, formatFileMentionPath, @@ -81,6 +81,6 @@ export { scanFileMentionItems, type FileMentionItem, type FileMentionToken, -} from "./core/fileMentions"; -export { findExpandedThinkingId, isCollapsedThinking } from "./core/thinkingState"; -export { buildExitSummaryText } from "./exitSummary"; +} from "./core/file-mentions"; +export { findExpandedThinkingId, isCollapsedThinking } from "./core/thinking-state"; +export { buildExitSummaryText } from "./exit-summary"; diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index 4f614c5d..e8c41537 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -8,8 +8,8 @@ import { type PromptDraft, PromptInput, type PromptSubmission } from "./PromptIn import { MessageView, RawModeExitPrompt } from "../components"; import { SessionList } from "./SessionList"; import { type UndoRestoreMode, UndoSelector } from "./UndoSelector"; -import { buildLoadingText } from "../core/loadingText"; -import { findExpandedThinkingId } from "../core/thinkingState"; +import { buildLoadingText } from "../core/loading-text"; +import { findExpandedThinkingId } from "../core/thinking-state"; import { WelcomeScreen } from "./WelcomeScreen"; import { AskUserQuestionPrompt } from "./AskUserQuestionPrompt"; import { McpStatusList } from "./McpStatusList"; @@ -18,9 +18,9 @@ import { type AskUserQuestionAnswers, findPendingAskUserQuestion, formatAskUserQuestionAnswers, -} from "../core/askUserQuestion"; +} from "../core/ask-user-question"; import { PermissionPrompt, type PermissionPromptResult } from "./PermissionPrompt"; -import { buildExitSummaryText } from "../exitSummary"; +import { buildExitSummaryText } from "../exit-summary"; import { RawMode, useRawModeContext } from "../contexts"; import { renderMessageToStdout } from "../components/MessageView/utils"; import { @@ -32,7 +32,7 @@ import { renderRawModeMessages, } from "../utils"; import { resolveCurrentSettings, writeModelConfigSelection } from "../../settings"; -import { isCollapsedThinking } from "../core/thinkingState"; +import { isCollapsedThinking } from "../core/thinking-state"; import { ANSI_CLEAR_SCREEN } from "../constants"; import type { LlmStreamProgress, diff --git a/src/ui/views/AskUserQuestionPrompt.tsx b/src/ui/views/AskUserQuestionPrompt.tsx index 988215f9..a2f91adb 100644 --- a/src/ui/views/AskUserQuestionPrompt.tsx +++ b/src/ui/views/AskUserQuestionPrompt.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, Text } from "ink"; -import type { AskUserQuestionAnswers, AskUserQuestionItem } from "../core/askUserQuestion"; +import type { AskUserQuestionAnswers, AskUserQuestionItem } from "../core/ask-user-question"; import { useTerminalInput } from "../hooks"; type Props = { diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index 824ec98e..9067e71c 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -24,24 +24,24 @@ import { moveWordLeft, moveWordRight, moveUp, -} from "../core/promptBuffer"; -import type { PromptBufferState } from "../core/promptBuffer"; +} from "../core/prompt-buffer"; +import type { PromptBufferState } from "../core/prompt-buffer"; import { clearPromptUndoRedoState, createPromptUndoRedoState, recordPromptEdit, redoPromptEdit, undoPromptEdit, -} from "../core/promptUndoRedo"; -import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "../core/slashCommands"; -import type { SlashCommandItem } from "../core/slashCommands"; +} from "../core/prompt-undo-redo"; +import { buildSlashCommands, filterSlashCommands, findExactSlashCommand } from "../core/slash-commands"; +import type { SlashCommandItem } from "../core/slash-commands"; import { filterFileMentionItems, getCurrentFileMentionToken, replaceCurrentFileMentionToken, scanFileMentionItems, -} from "../core/fileMentions"; -import type { FileMentionItem } from "../core/fileMentions"; +} from "../core/file-mentions"; +import type { FileMentionItem } from "../core/file-mentions"; import { readClipboardImageAsync } from "../core/clipboard"; import { useTerminalInput, usePasteHandling, useHistoryNavigation } from "../hooks"; import type { InputKey } from "../hooks"; diff --git a/src/ui/views/SlashCommandMenu.tsx b/src/ui/views/SlashCommandMenu.tsx index 275cf849..5b6eb762 100644 --- a/src/ui/views/SlashCommandMenu.tsx +++ b/src/ui/views/SlashCommandMenu.tsx @@ -1,5 +1,5 @@ -import { formatSlashCommandDescription, formatSlashCommandLabel } from "../core/slashCommands"; -import type { SlashCommandItem } from "../core/slashCommands"; +import { formatSlashCommandDescription, formatSlashCommandLabel } from "../core/slash-commands"; +import type { SlashCommandItem } from "../core/slash-commands"; import { ARGS_SEPARATOR } from "../constants"; import React from "react"; import { Box, Text } from "ink"; diff --git a/src/ui/views/WelcomeScreen.tsx b/src/ui/views/WelcomeScreen.tsx index 9bbc8f1c..7cae2889 100644 --- a/src/ui/views/WelcomeScreen.tsx +++ b/src/ui/views/WelcomeScreen.tsx @@ -4,9 +4,9 @@ import * as os from "node:os"; import path from "node:path"; import type { SkillInfo } from "../../session/types"; import type { ResolvedDeepcodingSettings } from "../../settings"; -import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "../core/slashCommands"; +import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "../core/slash-commands"; import { ThemedGradient } from "./ThemedGradient"; -import { AsciiLogo } from "../AsciiArt"; +import { AsciiLogo } from "../ascii-art"; import { useAppContext } from "../contexts"; type WelcomeScreenProps = { From 192d02df326a8fa3b69c3e87406ceb82f761090d Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 11:27:22 +0800 Subject: [PATCH 11/15] =?UTF-8?q?refactor(session):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=20session=20=E6=96=87=E4=BB=B6=E5=A4=B9=E5=8F=8A=E4=B8=8B?= =?UTF-8?q?=E9=9D=A2=E7=9A=84=E5=85=A8=E9=83=A8=E6=96=87=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=20=E5=9B=9E=E6=BB=9A=E4=B8=BA=E7=BB=9F=E4=B8=80=E5=BC=95?= =?UTF-8?q?=E7=94=A8=20session=20=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除了整个 session 文件夹及相关类型定义 - 统一所有模块中对 session 相关类型的导入,改为从 session.ts 模块直接导入 --- src/prompt.ts | 2 +- src/{session/index.ts => session.ts} | 276 ++++++++++++++++++--- src/session/types.ts | 140 ----------- src/session/utils.ts | 82 ------ src/tests/ask-user-question.test.ts | 2 +- src/tests/exit-summary.test.ts | 2 +- src/tests/message-view.test.ts | 2 +- src/tests/prompt-input-keys.test.ts | 2 +- src/tests/session-list.test.ts | 2 +- src/tests/session.test.ts | 2 +- src/tests/slash-commands.test.ts | 2 +- src/tests/thinking-state.test.ts | 2 +- src/ui/components/MessageView/types.ts | 2 +- src/ui/components/MessageView/utils.ts | 2 +- src/ui/components/SkillsDropdown/index.tsx | 2 +- src/ui/core/ask-user-question.ts | 2 +- src/ui/core/loading-text.ts | 2 +- src/ui/core/slash-commands.ts | 2 +- src/ui/core/thinking-state.ts | 2 +- src/ui/exit-summary.ts | 2 +- src/ui/utils/index.ts | 2 +- src/ui/views/App.tsx | 2 +- src/ui/views/ProcessStdoutView.tsx | 2 +- src/ui/views/PromptInput.tsx | 2 +- src/ui/views/SessionList.tsx | 2 +- src/ui/views/SlashCommandMenu.tsx | 2 +- src/ui/views/UndoSelector.tsx | 2 +- src/ui/views/WelcomeScreen.tsx | 2 +- 28 files changed, 260 insertions(+), 288 deletions(-) rename src/{session/index.ts => session.ts} (92%) delete mode 100644 src/session/types.ts delete mode 100644 src/session/utils.ts diff --git a/src/prompt.ts b/src/prompt.ts index e77993fc..9daea588 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -4,7 +4,7 @@ import * as os from "os"; import * as path from "path"; import ejs from "ejs"; import { fileURLToPath } from "url"; -import type { SessionMessage } from "./session/types"; +import type { SessionMessage } from "./session"; import { findGitBashPath, resolveShellPath } from "./common/system/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; diff --git a/src/session/index.ts b/src/session.ts similarity index 92% rename from src/session/index.ts rename to src/session.ts index bab18b80..c9e11b7d 100644 --- a/src/session/index.ts +++ b/src/session.ts @@ -1,3 +1,224 @@ +import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; +import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "./common/permissions"; +import type { CreateOpenAIClient } from "./tools/executor"; + +export type SessionStatus = + | "failed" + | "pending" + | "processing" + | "waiting_for_user" + | "completed" + | "interrupted" + | "ask_permission" + | "permission_denied"; + +export type ModelUsage = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + completion_tokens_details?: Record; + prompt_tokens_details?: Record; + prompt_cache_hit_tokens?: number; + prompt_cache_miss_tokens?: number; + total_reqs?: number; +}; + +export type SessionProcessEntry = { + startTime: string; + command: string; + timeoutMs?: number; + deadlineAt?: string; + timedOut?: boolean; +}; + +export type BashTimeoutAdjustment = { + processId: string; + timeoutMs: number; + deadlineAt: string; + timedOut: boolean; +}; + +export type SessionEntry = { + id: string; + summary: string | null; + assistantReply: string | null; + assistantThinking: string | null; + assistantRefusal: string | null; + toolCalls: unknown[] | null; + status: SessionStatus; + failReason: string | null; + usage: ModelUsage | null; + usagePerModel: Record | null; + activeTokens: number; + createTime: string; + updateTime: string; + processes: Map | null; + askPermissions?: AskPermissionRequest[]; +}; + +export type SessionsIndex = { + version: 1; + entries: SessionEntry[]; + originalPath: string; +}; + +export type SessionMessageRole = "system" | "user" | "assistant" | "tool"; + +export type MessageMeta = { + function?: unknown; + paramsMd?: string; + resultMd?: string; + asThinking?: boolean; + isSummary?: boolean; + isModelChange?: boolean; + skill?: SkillInfo; + permissions?: MessageToolPermission[]; + userPrompt?: UserPromptContent; +}; + +export type SessionMessage = { + id: string; + sessionId: string; + role: SessionMessageRole; + content: string | null; + contentParams: unknown | null; + messageParams: unknown | null; + compacted: boolean; + visible: boolean; + createTime: string; + updateTime: string; + meta?: MessageMeta; + html?: string; + checkpointHash?: string; +}; + +export type UndoTarget = { + message: SessionMessage; + index: number; + canRestoreCode: boolean; +}; + +export type UserPromptContent = { + text?: string; + imageUrls?: string[]; + skills?: SkillInfo[]; + permissions?: UserToolPermission[]; + alwaysAllows?: PermissionScope[]; +}; + +export type SkillInfo = { + name: string; + path: string; + description: string; + isLoaded?: boolean; +}; + +export type SessionManagerOptions = { + projectRoot: string; + createOpenAIClient: CreateOpenAIClient; + getResolvedSettings: () => { + model: string; + webSearchTool?: string; + mcpServers?: Record; + permissions?: Required; + }; + renderMarkdown: (text: string) => string; + onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; + onSessionEntryUpdated?: (entry: SessionEntry) => void; + onLlmStreamProgress?: (progress: LlmStreamProgress) => void; + onMcpStatusChanged?: () => void; + onProcessStdout?: (pid: number, chunk: string) => void; +}; + +export type LlmStreamProgress = { + requestId: string; + sessionId?: string; + startedAt: string; + estimatedTokens: number; + formattedTokens: string; + phase: "start" | "update" | "end"; +}; +import { DEEPSEEK_V4_MODELS } from "./common/model-capabilities"; + +const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; +const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; + +export function getCompactPromptTokenThreshold(model: string): number { + return DEEPSEEK_V4_MODELS.has(model) + ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD + : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; +} + +export function isUsageRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +export function getTotalTokens(usage: ModelUsage | null | undefined): number { + if (!isUsageRecord(usage)) { + return 0; + } + const totalTokens = (usage as Record).total_tokens; + return typeof totalTokens === "number" ? totalTokens : 0; +} + +export function summarizeCompletionOptions(options?: Record): Record | undefined { + if (!options) { + return undefined; + } + return { + ...options, + signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, + }; +} + +export function addUsageValue(current: unknown, next: unknown): unknown { + if (typeof next === "number") { + return (typeof current === "number" ? current : 0) + next; + } + + if (isUsageRecord(next)) { + const currentRecord = isUsageRecord(current) ? current : {}; + const result: Record = { ...currentRecord }; + for (const [key, value] of Object.entries(next)) { + result[key] = addUsageValue(currentRecord[key], value); + } + return result; + } + + return next; +} + +export function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { + if (next == null) { + return current ?? null; + } + return addUsageValue(current, next) as ModelUsage; +} + +export function usageWithRequestCount(usage: ModelUsage): ModelUsage { + const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; + return { + ...usage, + total_reqs: totalReqs, + }; +} + +export function accumulateUsagePerModel( + current: Record | null | undefined, + model: string, + next: ModelUsage | null | undefined +): Record | null { + if (next == null) { + return current ?? null; + } + + const usagePerModel = { ...(current ?? {}) }; + const modelName = model.trim() || "unknown"; + usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; + return usagePerModel; +} + +export { getExtensionRoot } from "./prompt"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; @@ -5,68 +226,41 @@ import * as crypto from "crypto"; import matter from "gray-matter"; import ejs from "ejs"; import type { ChatCompletionContentPart, ChatCompletionMessageParam } from "openai/resources/chat/completions"; -import { launchNotifyScript } from "../common/notify"; -import { buildThinkingRequestOptions } from "../common/openai-thinking"; -import { supportsMultimodal } from "../common/model-capabilities"; +import { launchNotifyScript } from "./common/notify"; +import { buildThinkingRequestOptions } from "./common/openai-thinking"; +import { supportsMultimodal } from "./common/model-capabilities"; import { getCompactPrompt, getDefaultSkillPrompt, + getExtensionRoot, getRuntimeContext, getSystemPrompt, getTools, type ToolDefinition, -} from "../prompt"; +} from "./prompt"; import { - type CreateOpenAIClient, type ProcessTimeoutControl, type ProcessTimeoutInfo, type ToolCallExecution, type ToolExecutionHooks, ToolExecutor, -} from "../tools/executor"; -import { McpManager } from "../mcp/mcp-manager"; -import type { McpServerConfig, PermissionSettings } from "../settings"; -import { logApiError } from "../common/logging/error-logger"; -import { logOpenAIChatCompletionDebug, normalizeDebugError } from "../common/logging/debug-logger"; -import { killProcessTree } from "../common/system/process-tree"; -import { GitFileHistory } from "../common/runtime/file-history"; -import { getSnippet } from "../common/runtime/state"; +} from "./tools/executor"; +import { McpManager } from "./mcp/mcp-manager"; + +import { logApiError } from "./common/logging/error-logger"; +import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/logging/debug-logger"; +import { killProcessTree } from "./common/system/process-tree"; +import { GitFileHistory } from "./common/runtime/file-history"; +import { getSnippet } from "./common/runtime/state"; import { appendProjectPermissionAllows, buildPermissionToolExecution, computeToolCallPermissions, hasUserPermissionReplies, - type MessageToolPermission, normalizeAskPermissions, parseToolCallForPermissions, type PermissionToolCall, - type UserToolPermission, -} from "../common/permissions"; - -import { - accumulateUsage, - accumulateUsagePerModel, - getCompactPromptTokenThreshold, - getExtensionRoot, - getTotalTokens, - isUsageRecord, - summarizeCompletionOptions, -} from "./utils"; -import { - type BashTimeoutAdjustment, - type LlmStreamProgress, - type MessageMeta, - type ModelUsage, - type SessionEntry, - type SessionManagerOptions, - type SessionMessage, - type SessionProcessEntry, - type SessionsIndex, - type SessionStatus, - type SkillInfo, - type UndoTarget, - type UserPromptContent, -} from "./types"; +} from "./common/permissions"; const MAX_SESSION_ENTRIES = 50; const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; diff --git a/src/session/types.ts b/src/session/types.ts deleted file mode 100644 index 46639c01..00000000 --- a/src/session/types.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { McpServerConfig, PermissionScope, PermissionSettings } from "../settings"; -import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "../common/permissions"; -import type { CreateOpenAIClient } from "../tools/executor"; - -export type SessionStatus = - | "failed" - | "pending" - | "processing" - | "waiting_for_user" - | "completed" - | "interrupted" - | "ask_permission" - | "permission_denied"; - -export type ModelUsage = { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - completion_tokens_details?: Record; - prompt_tokens_details?: Record; - prompt_cache_hit_tokens?: number; - prompt_cache_miss_tokens?: number; - total_reqs?: number; -}; - -export type SessionProcessEntry = { - startTime: string; - command: string; - timeoutMs?: number; - deadlineAt?: string; - timedOut?: boolean; -}; - -export type BashTimeoutAdjustment = { - processId: string; - timeoutMs: number; - deadlineAt: string; - timedOut: boolean; -}; - -export type SessionEntry = { - id: string; - summary: string | null; - assistantReply: string | null; - assistantThinking: string | null; - assistantRefusal: string | null; - toolCalls: unknown[] | null; - status: SessionStatus; - failReason: string | null; - usage: ModelUsage | null; - usagePerModel: Record | null; - activeTokens: number; - createTime: string; - updateTime: string; - processes: Map | null; - askPermissions?: AskPermissionRequest[]; -}; - -export type SessionsIndex = { - version: 1; - entries: SessionEntry[]; - originalPath: string; -}; - -export type SessionMessageRole = "system" | "user" | "assistant" | "tool"; - -export type MessageMeta = { - function?: unknown; - paramsMd?: string; - resultMd?: string; - asThinking?: boolean; - isSummary?: boolean; - isModelChange?: boolean; - skill?: SkillInfo; - permissions?: MessageToolPermission[]; - userPrompt?: UserPromptContent; -}; - -export type SessionMessage = { - id: string; - sessionId: string; - role: SessionMessageRole; - content: string | null; - contentParams: unknown | null; - messageParams: unknown | null; - compacted: boolean; - visible: boolean; - createTime: string; - updateTime: string; - meta?: MessageMeta; - html?: string; - checkpointHash?: string; -}; - -export type UndoTarget = { - message: SessionMessage; - index: number; - canRestoreCode: boolean; -}; - -export type UserPromptContent = { - text?: string; - imageUrls?: string[]; - skills?: SkillInfo[]; - permissions?: UserToolPermission[]; - alwaysAllows?: PermissionScope[]; -}; - -export type SkillInfo = { - name: string; - path: string; - description: string; - isLoaded?: boolean; -}; - -export type SessionManagerOptions = { - projectRoot: string; - createOpenAIClient: CreateOpenAIClient; - getResolvedSettings: () => { - model: string; - webSearchTool?: string; - mcpServers?: Record; - permissions?: Required; - }; - renderMarkdown: (text: string) => string; - onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void; - onSessionEntryUpdated?: (entry: SessionEntry) => void; - onLlmStreamProgress?: (progress: LlmStreamProgress) => void; - onMcpStatusChanged?: () => void; - onProcessStdout?: (pid: number, chunk: string) => void; -}; - -export type LlmStreamProgress = { - requestId: string; - sessionId?: string; - startedAt: string; - estimatedTokens: number; - formattedTokens: string; - phase: "start" | "update" | "end"; -}; diff --git a/src/session/utils.ts b/src/session/utils.ts deleted file mode 100644 index 552e7725..00000000 --- a/src/session/utils.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { DEEPSEEK_V4_MODELS } from "../common/model-capabilities"; -import type { ModelUsage } from "./types"; - -const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; -const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; - -export function getCompactPromptTokenThreshold(model: string): number { - return DEEPSEEK_V4_MODELS.has(model) - ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD - : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; -} - -export function isUsageRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -export function getTotalTokens(usage: ModelUsage | null | undefined): number { - if (!isUsageRecord(usage)) { - return 0; - } - const totalTokens = (usage as Record).total_tokens; - return typeof totalTokens === "number" ? totalTokens : 0; -} - -export function summarizeCompletionOptions(options?: Record): Record | undefined { - if (!options) { - return undefined; - } - return { - ...options, - signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, - }; -} - -export function addUsageValue(current: unknown, next: unknown): unknown { - if (typeof next === "number") { - return (typeof current === "number" ? current : 0) + next; - } - - if (isUsageRecord(next)) { - const currentRecord = isUsageRecord(current) ? current : {}; - const result: Record = { ...currentRecord }; - for (const [key, value] of Object.entries(next)) { - result[key] = addUsageValue(currentRecord[key], value); - } - return result; - } - - return next; -} - -export function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { - if (next == null) { - return current ?? null; - } - return addUsageValue(current, next) as ModelUsage; -} - -export function usageWithRequestCount(usage: ModelUsage): ModelUsage { - const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; - return { - ...usage, - total_reqs: totalReqs, - }; -} - -export function accumulateUsagePerModel( - current: Record | null | undefined, - model: string, - next: ModelUsage | null | undefined -): Record | null { - if (next == null) { - return current ?? null; - } - - const usagePerModel = { ...(current ?? {}) }; - const modelName = model.trim() || "unknown"; - usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; - return usagePerModel; -} - -export { getExtensionRoot } from "../prompt"; diff --git a/src/tests/ask-user-question.test.ts b/src/tests/ask-user-question.test.ts index 10c9a2cb..f7543512 100644 --- a/src/tests/ask-user-question.test.ts +++ b/src/tests/ask-user-question.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findPendingAskUserQuestion, formatAskUserQuestionAnswers, formatAskUserQuestionDecline } from "../ui"; -import type { SessionMessage } from "../session/types"; +import type { SessionMessage } from "../session"; function message(content: unknown): SessionMessage { const now = "2026-04-29T00:00:00.000Z"; diff --git a/src/tests/exit-summary.test.ts b/src/tests/exit-summary.test.ts index e22a904c..5ea4b579 100644 --- a/src/tests/exit-summary.test.ts +++ b/src/tests/exit-summary.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { buildExitSummaryText } from "../ui"; -import type { ModelUsage, SessionEntry } from "../session/types"; +import type { ModelUsage, SessionEntry } from "../session"; const stripAnsi = (text: string): string => text.replace(/\u001b\[[0-9;]*m/g, ""); diff --git a/src/tests/message-view.test.ts b/src/tests/message-view.test.ts index 9acd01ed..b806dbd1 100644 --- a/src/tests/message-view.test.ts +++ b/src/tests/message-view.test.ts @@ -8,7 +8,7 @@ import { parseToolPayload, } from "../ui/components/MessageView/utils"; import { RawMode } from "../ui/contexts"; -import type { SessionMessage } from "../session/types"; +import type { SessionMessage } from "../session"; import type { ToolSummary } from "../ui/components/MessageView/types"; test("parseDiffPreview removes headers and classifies lines", () => { diff --git a/src/tests/prompt-input-keys.test.ts b/src/tests/prompt-input-keys.test.ts index 6e697b65..4ca564f9 100644 --- a/src/tests/prompt-input-keys.test.ts +++ b/src/tests/prompt-input-keys.test.ts @@ -25,7 +25,7 @@ import { insertText, backspace, } from "../ui"; -import type { SessionMessage, SkillInfo } from "../session/types"; +import type { SessionMessage, SkillInfo } from "../session"; import { dispatchTerminalInput, parseTerminalInput } from "../ui/hooks"; function collectDispatchedInput(data: string) { diff --git a/src/tests/session-list.test.ts b/src/tests/session-list.test.ts index 5fdda393..6fe41c70 100644 --- a/src/tests/session-list.test.ts +++ b/src/tests/session-list.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { formatSessionTitle, filterSessions, formatSessionStatus } from "../ui"; -import type { SessionEntry } from "../session/types"; +import type { SessionEntry } from "../session"; test("formatSessionTitle replaces newlines with spaces", () => { assert.equal(formatSessionTitle("first line\nsecond line\r\nthird"), "first line second line third"); diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index fd08c4d8..87ddf558 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -5,7 +5,7 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { GitFileHistory } from "../common/runtime/file-history"; -import { type SessionMessage } from "../session/types"; +import { type SessionMessage } from "../session"; import { SessionManager } from "../session"; const originalFetch = globalThis.fetch; diff --git a/src/tests/slash-commands.test.ts b/src/tests/slash-commands.test.ts index fa98b9f2..30d77eeb 100644 --- a/src/tests/slash-commands.test.ts +++ b/src/tests/slash-commands.test.ts @@ -7,7 +7,7 @@ import { formatSlashCommandDescription, formatSlashCommandLabel, } from "../ui"; -import type { SkillInfo } from "../session/types"; +import type { SkillInfo } from "../session"; const skills: SkillInfo[] = [ { name: "skill-writer", path: "~/.agents/skills/skill-writer/SKILL.md", description: "Write a SKILL.md" }, diff --git a/src/tests/thinking-state.test.ts b/src/tests/thinking-state.test.ts index 347ac571..8f2a0e30 100644 --- a/src/tests/thinking-state.test.ts +++ b/src/tests/thinking-state.test.ts @@ -1,7 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { findExpandedThinkingId } from "../ui"; -import type { SessionMessage } from "../session/types"; +import type { SessionMessage } from "../session"; function buildMessage( id: string, diff --git a/src/ui/components/MessageView/types.ts b/src/ui/components/MessageView/types.ts index 5339513b..743eb2dc 100644 --- a/src/ui/components/MessageView/types.ts +++ b/src/ui/components/MessageView/types.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../../session/types"; +import type { SessionMessage } from "../../../session"; export type MessageViewProps = { message: SessionMessage; diff --git a/src/ui/components/MessageView/utils.ts b/src/ui/components/MessageView/utils.ts index 9b004dbf..af5391d8 100644 --- a/src/ui/components/MessageView/utils.ts +++ b/src/ui/components/MessageView/utils.ts @@ -1,5 +1,5 @@ import type { DiffPreviewLine, ToolSummary } from "./types"; -import type { SessionMessage } from "../../../session/types"; +import type { SessionMessage } from "../../../session"; import { RawMode } from "../../contexts"; import chalk from "chalk"; diff --git a/src/ui/components/SkillsDropdown/index.tsx b/src/ui/components/SkillsDropdown/index.tsx index 07b49de6..4ec53397 100644 --- a/src/ui/components/SkillsDropdown/index.tsx +++ b/src/ui/components/SkillsDropdown/index.tsx @@ -1,6 +1,6 @@ import DropdownMenu from "../DropdownMenu"; import React, { useEffect, useState } from "react"; -import type { SkillInfo } from "../../../session/types"; +import type { SkillInfo } from "../../../session"; import { useInput } from "ink"; import { isSkillSelected } from "../../views/SlashCommandMenu"; diff --git a/src/ui/core/ask-user-question.ts b/src/ui/core/ask-user-question.ts index 8918604a..8a07e400 100644 --- a/src/ui/core/ask-user-question.ts +++ b/src/ui/core/ask-user-question.ts @@ -1,4 +1,4 @@ -import type { SessionMessage, SessionStatus } from "../../session/types"; +import type { SessionMessage, SessionStatus } from "../../session"; export type AskUserQuestionOption = { label: string; diff --git a/src/ui/core/loading-text.ts b/src/ui/core/loading-text.ts index f74cc1ac..2c965ea3 100644 --- a/src/ui/core/loading-text.ts +++ b/src/ui/core/loading-text.ts @@ -1,4 +1,4 @@ -import type { LlmStreamProgress, SessionEntry } from "../../session/types"; +import type { LlmStreamProgress, SessionEntry } from "../../session"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/core/slash-commands.ts b/src/ui/core/slash-commands.ts index 8a7487b0..04840baa 100644 --- a/src/ui/core/slash-commands.ts +++ b/src/ui/core/slash-commands.ts @@ -1,4 +1,4 @@ -import type { SkillInfo } from "../../session/types"; +import type { SkillInfo } from "../../session"; export type SlashCommandKind = | "skill" diff --git a/src/ui/core/thinking-state.ts b/src/ui/core/thinking-state.ts index bbd8e030..02245091 100644 --- a/src/ui/core/thinking-state.ts +++ b/src/ui/core/thinking-state.ts @@ -1,4 +1,4 @@ -import type { SessionMessage } from "../../session/types"; +import type { SessionMessage } from "../../session"; /** * Returns the message id of the assistant "thinking" message that should stay diff --git a/src/ui/exit-summary.ts b/src/ui/exit-summary.ts index 1801bd85..c55d9ce8 100644 --- a/src/ui/exit-summary.ts +++ b/src/ui/exit-summary.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; import gradientString from "gradient-string"; -import type { ModelUsage, SessionEntry } from "../session/types"; +import type { ModelUsage, SessionEntry } from "../session"; type ExitSummaryInput = { session: SessionEntry | null; diff --git a/src/ui/utils/index.ts b/src/ui/utils/index.ts index 4fb2fb1f..b9b61ec4 100644 --- a/src/ui/utils/index.ts +++ b/src/ui/utils/index.ts @@ -3,7 +3,7 @@ import { renderMessageToStdout } from "../components/MessageView/utils"; import type { RawMode } from "../contexts"; import type { PromptDraft } from "../views/PromptInput"; import type { ModelConfigSelection } from "../../settings"; -import type { SessionEntry, SessionMessage } from "../../session/types"; +import type { SessionEntry, SessionMessage } from "../../session"; import type { SessionManager } from "../../session"; /** diff --git a/src/ui/views/App.tsx b/src/ui/views/App.tsx index e8c41537..bef803e3 100644 --- a/src/ui/views/App.tsx +++ b/src/ui/views/App.tsx @@ -43,7 +43,7 @@ import type { SkillInfo, UndoTarget, UserPromptContent, -} from "../../session/types"; +} from "../../session"; import { SessionManager } from "../../session"; type View = "chat" | "session-list" | "undo" | "mcp-status"; diff --git a/src/ui/views/ProcessStdoutView.tsx b/src/ui/views/ProcessStdoutView.tsx index d43c39cb..39299599 100644 --- a/src/ui/views/ProcessStdoutView.tsx +++ b/src/ui/views/ProcessStdoutView.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text } from "ink"; import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../../common/system/bash-timeout"; -import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../../session/types"; +import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../../session"; import { useTerminalInput } from "../hooks"; type RunningProcesses = SessionEntry["processes"]; diff --git a/src/ui/views/PromptInput.tsx b/src/ui/views/PromptInput.tsx index 9067e71c..b812a73d 100644 --- a/src/ui/views/PromptInput.tsx +++ b/src/ui/views/PromptInput.tsx @@ -54,7 +54,7 @@ import { import SlashCommandMenu, { isSkillSelected } from "./SlashCommandMenu"; import type { ModelConfigSelection, PermissionScope } from "../../settings"; import { FileMentionMenu, ModelsDropdown, RawModelDropdown, SkillsDropdown } from "../components"; -import type { SessionEntry, SkillInfo } from "../../session/types"; +import type { SessionEntry, SkillInfo } from "../../session"; import type { UserToolPermission } from "../../common/permissions"; export type PromptSubmission = { diff --git a/src/ui/views/SessionList.tsx b/src/ui/views/SessionList.tsx index 0b81ee89..ac53f218 100644 --- a/src/ui/views/SessionList.tsx +++ b/src/ui/views/SessionList.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useCallback } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { SessionEntry, SessionStatus } from "../../session/types"; +import type { SessionEntry, SessionStatus } from "../../session"; import { truncate } from "../components/MessageView/utils"; type Props = { diff --git a/src/ui/views/SlashCommandMenu.tsx b/src/ui/views/SlashCommandMenu.tsx index 5b6eb762..d93446de 100644 --- a/src/ui/views/SlashCommandMenu.tsx +++ b/src/ui/views/SlashCommandMenu.tsx @@ -3,7 +3,7 @@ import type { SlashCommandItem } from "../core/slash-commands"; import { ARGS_SEPARATOR } from "../constants"; import React from "react"; import { Box, Text } from "ink"; -import type { SkillInfo } from "../../session/types"; +import type { SkillInfo } from "../../session"; type SlashCommandMenuProps = { items: SlashCommandItem[]; diff --git a/src/ui/views/UndoSelector.tsx b/src/ui/views/UndoSelector.tsx index 613025c6..977bca26 100644 --- a/src/ui/views/UndoSelector.tsx +++ b/src/ui/views/UndoSelector.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; -import type { UndoTarget } from "../../session/types"; +import type { UndoTarget } from "../../session"; export type UndoRestoreMode = "code-and-conversation" | "conversation"; diff --git a/src/ui/views/WelcomeScreen.tsx b/src/ui/views/WelcomeScreen.tsx index 7cae2889..96aef71f 100644 --- a/src/ui/views/WelcomeScreen.tsx +++ b/src/ui/views/WelcomeScreen.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useState } from "react"; import { Box, Text } from "ink"; import * as os from "node:os"; import path from "node:path"; -import type { SkillInfo } from "../../session/types"; +import type { SkillInfo } from "../../session"; import type { ResolvedDeepcodingSettings } from "../../settings"; import { buildSlashCommands, BUILTIN_SLASH_COMMANDS, formatSlashCommandDescription } from "../core/slash-commands"; import { ThemedGradient } from "./ThemedGradient"; From 5fd54b981f0fa1fcd371ea8e0016edd3de73af35 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 11:34:11 +0800 Subject: [PATCH 12/15] =?UTF-8?q?refactor(src):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=BC=95=E7=94=A8=E5=B9=B6=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E9=83=A8=E5=88=86=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一调整内部模块导入路径,移除多余的 system 子目录 - 重命名 common/runtime/state.ts 为 common/state.ts 并同步更新相关引用 - 重命名 common/runtime/validate.ts 为 common/validate.ts 并同步更新相关引用 - 重命名 common/runtime/file-history.ts 为 common/file-history 并同步更新相关引用 - 更新测试文件和工具模块中对应的导入路径,确保一致性 - 保持代码逻辑不变,仅调整代码结构和模块路径优化维护性 --- src/cli.tsx | 2 +- src/common/{system => }/bash-timeout.ts | 0 src/common/{logging => }/debug-logger.ts | 0 src/common/{logging => }/error-logger.ts | 0 src/common/{runtime => }/file-history.ts | 0 src/common/file-utils.ts | 2 +- src/common/permissions.ts | 2 +- src/common/{system => }/process-tree.ts | 0 src/common/{system => }/shell-utils.ts | 0 src/common/{runtime => }/state.ts | 2 +- src/common/update-check.ts | 2 +- src/common/{runtime => }/validate.ts | 2 +- src/mcp/mcp-client.ts | 2 +- src/prompt.ts | 2 +- src/session.ts | 10 +++++----- src/tests/debug-logger.test.ts | 2 +- src/tests/process-tree.test.ts | 2 +- src/tests/session.test.ts | 2 +- src/tests/shell-utils.test.ts | 4 ++-- src/tools/bash-handler.ts | 6 +++--- src/tools/edit-handler.ts | 4 ++-- src/tools/read-handler.ts | 2 +- src/tools/update-plan-handler.ts | 2 +- src/tools/write-handler.ts | 10 ++-------- src/ui/views/ProcessStdoutView.tsx | 2 +- 25 files changed, 28 insertions(+), 34 deletions(-) rename src/common/{system => }/bash-timeout.ts (100%) rename src/common/{logging => }/debug-logger.ts (100%) rename src/common/{logging => }/error-logger.ts (100%) rename src/common/{runtime => }/file-history.ts (100%) rename src/common/{system => }/process-tree.ts (100%) rename src/common/{system => }/shell-utils.ts (100%) rename src/common/{runtime => }/state.ts (98%) rename src/common/{runtime => }/validate.ts (98%) diff --git a/src/cli.tsx b/src/cli.tsx index d851a911..87fb9fb5 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,6 +1,6 @@ import React from "react"; import { render } from "ink"; -import { setShellIfWindows } from "./common/system/shell-utils"; +import { setShellIfWindows } from "./common/shell-utils"; import { checkForNpmUpdate, promptForPendingUpdate, type PackageInfo } from "./common/update-check"; import { AppContainer } from "./ui"; diff --git a/src/common/system/bash-timeout.ts b/src/common/bash-timeout.ts similarity index 100% rename from src/common/system/bash-timeout.ts rename to src/common/bash-timeout.ts diff --git a/src/common/logging/debug-logger.ts b/src/common/debug-logger.ts similarity index 100% rename from src/common/logging/debug-logger.ts rename to src/common/debug-logger.ts diff --git a/src/common/logging/error-logger.ts b/src/common/error-logger.ts similarity index 100% rename from src/common/logging/error-logger.ts rename to src/common/error-logger.ts diff --git a/src/common/runtime/file-history.ts b/src/common/file-history.ts similarity index 100% rename from src/common/runtime/file-history.ts rename to src/common/file-history.ts diff --git a/src/common/file-utils.ts b/src/common/file-utils.ts index 72c83c0a..6656172e 100644 --- a/src/common/file-utils.ts +++ b/src/common/file-utils.ts @@ -1,6 +1,6 @@ import * as fs from "fs"; import * as path from "path"; -import type { FileState, FileLineEnding } from "./runtime/state"; +import type { FileState, FileLineEnding } from "./state"; export type FileReadMetadata = { content: string; diff --git a/src/common/permissions.ts b/src/common/permissions.ts index 1ebca8c2..564bfeb8 100644 --- a/src/common/permissions.ts +++ b/src/common/permissions.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; import type { DeepcodingSettings, PermissionScope, PermissionSettings } from "../settings"; -import { isAbsoluteFilePath, normalizeFilePath } from "./runtime/state"; +import { isAbsoluteFilePath, normalizeFilePath } from "./state"; export type BashPermissionScope = Exclude | "unknown"; diff --git a/src/common/system/process-tree.ts b/src/common/process-tree.ts similarity index 100% rename from src/common/system/process-tree.ts rename to src/common/process-tree.ts diff --git a/src/common/system/shell-utils.ts b/src/common/shell-utils.ts similarity index 100% rename from src/common/system/shell-utils.ts rename to src/common/shell-utils.ts diff --git a/src/common/runtime/state.ts b/src/common/state.ts similarity index 98% rename from src/common/runtime/state.ts rename to src/common/state.ts index 122a1aca..add27f35 100644 --- a/src/common/runtime/state.ts +++ b/src/common/state.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { posixPathToWindowsPath } from "../system/shell-utils"; +import { posixPathToWindowsPath } from "./shell-utils"; export type FileLineEnding = "LF" | "CRLF"; diff --git a/src/common/update-check.ts b/src/common/update-check.ts index 6baa58f7..09c0273c 100644 --- a/src/common/update-check.ts +++ b/src/common/update-check.ts @@ -6,7 +6,7 @@ import * as path from "path"; import { render, type Instance } from "ink"; import chalk from "chalk"; import { UpdatePrompt, type UpdatePromptChoice } from "../ui"; -import { killProcessTree } from "./system/process-tree"; +import { killProcessTree } from "./process-tree"; export type PackageInfo = { name: string; diff --git a/src/common/runtime/validate.ts b/src/common/validate.ts similarity index 98% rename from src/common/runtime/validate.ts rename to src/common/validate.ts index 756dc819..b1195d8d 100644 --- a/src/common/runtime/validate.ts +++ b/src/common/validate.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { ToolExecutionContext, ToolExecutionResult } from "../../tools/executor"; +import type { ToolExecutionContext, ToolExecutionResult } from "../tools/executor"; export type ValidationResult = { ok: true; input: Record } | { ok: false; error: string }; diff --git a/src/mcp/mcp-client.ts b/src/mcp/mcp-client.ts index 4ea0eca4..26a7a321 100644 --- a/src/mcp/mcp-client.ts +++ b/src/mcp/mcp-client.ts @@ -1,7 +1,7 @@ import { spawn, type ChildProcess } from "child_process"; import { createInterface, type Interface } from "readline"; import * as path from "path"; -import { killProcessTree } from "../common/system/process-tree"; +import { killProcessTree } from "../common/process-tree"; type JsonRpcRequest = { jsonrpc: "2.0"; diff --git a/src/prompt.ts b/src/prompt.ts index 9daea588..669e5759 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -5,7 +5,7 @@ import * as path from "path"; import ejs from "ejs"; import { fileURLToPath } from "url"; import type { SessionMessage } from "./session"; -import { findGitBashPath, resolveShellPath } from "./common/system/shell-utils"; +import { findGitBashPath, resolveShellPath } from "./common/shell-utils"; import { supportsMultimodal } from "./common/model-capabilities"; const COMPACT_PROMPT_BASE = `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions. diff --git a/src/session.ts b/src/session.ts index c9e11b7d..54e31368 100644 --- a/src/session.ts +++ b/src/session.ts @@ -247,11 +247,11 @@ import { } from "./tools/executor"; import { McpManager } from "./mcp/mcp-manager"; -import { logApiError } from "./common/logging/error-logger"; -import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/logging/debug-logger"; -import { killProcessTree } from "./common/system/process-tree"; -import { GitFileHistory } from "./common/runtime/file-history"; -import { getSnippet } from "./common/runtime/state"; +import { logApiError } from "./common/error-logger"; +import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; +import { killProcessTree } from "./common/process-tree"; +import { GitFileHistory } from "./common/file-history"; +import { getSnippet } from "./common/state"; import { appendProjectPermissionAllows, buildPermissionToolExecution, diff --git a/src/tests/debug-logger.test.ts b/src/tests/debug-logger.test.ts index 374da743..7b1aad40 100644 --- a/src/tests/debug-logger.test.ts +++ b/src/tests/debug-logger.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { getDebugLogPath, logOpenAIChatCompletionDebug } from "../common/logging/debug-logger"; +import { getDebugLogPath, logOpenAIChatCompletionDebug } from "../common/debug-logger"; test("debug logger appends full entries without rotation", () => { const originalHome = process.env.HOME; diff --git a/src/tests/process-tree.test.ts b/src/tests/process-tree.test.ts index 97c68248..1dd08a1e 100644 --- a/src/tests/process-tree.test.ts +++ b/src/tests/process-tree.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { killProcessTree, runWindowsTaskkill } from "../common/system/process-tree"; +import { killProcessTree, runWindowsTaskkill } from "../common/process-tree"; test("runWindowsTaskkill invokes taskkill for the full process tree", () => { const calls: Array<{ command: string; args: string[]; options: { stdio: "ignore"; windowsHide: true } }> = []; diff --git a/src/tests/session.test.ts b/src/tests/session.test.ts index 87ddf558..6af3cb2d 100644 --- a/src/tests/session.test.ts +++ b/src/tests/session.test.ts @@ -4,7 +4,7 @@ import { execFileSync } from "node:child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { GitFileHistory } from "../common/runtime/file-history"; +import { GitFileHistory } from "../common/file-history"; import { type SessionMessage } from "../session"; import { SessionManager } from "../session"; diff --git a/src/tests/shell-utils.test.ts b/src/tests/shell-utils.test.ts index 9eec57b6..50a71f41 100644 --- a/src/tests/shell-utils.test.ts +++ b/src/tests/shell-utils.test.ts @@ -7,8 +7,8 @@ import { resolveWindowsGitBashPath, rewriteWindowsNullRedirect, windowsPathToPosixPath, -} from "../common/system/shell-utils"; -import { isAbsoluteFilePath, normalizeFilePath } from "../common/runtime/state"; +} from "../common/shell-utils"; +import { isAbsoluteFilePath, normalizeFilePath } from "../common/state"; test("Windows paths convert to Git Bash POSIX paths", () => { assert.equal(windowsPathToPosixPath("C:\\Users\\foo"), "/c/Users/foo"); diff --git a/src/tools/bash-handler.ts b/src/tools/bash-handler.ts index fb639158..42722710 100644 --- a/src/tools/bash-handler.ts +++ b/src/tools/bash-handler.ts @@ -1,6 +1,6 @@ import { spawn } from "child_process"; -import { DEFAULT_BASH_TIMEOUT_MS, clampBashTimeoutMs } from "../common/system/bash-timeout"; -import { killProcessTree } from "../common/system/process-tree"; +import { DEFAULT_BASH_TIMEOUT_MS, clampBashTimeoutMs } from "../common/bash-timeout"; +import { killProcessTree } from "../common/process-tree"; import type { ProcessTimeoutControl, ProcessTimeoutInfo, ToolExecutionContext, ToolExecutionResult } from "./executor"; import { buildDisableExtglobCommand, @@ -9,7 +9,7 @@ import { resolveShellPath, rewriteWindowsNullRedirect, toNativeCwd, -} from "../common/system/shell-utils"; +} from "../common/shell-utils"; const MAX_OUTPUT_CHARS = 30000; const MAX_CAPTURE_CHARS = 10 * 1024 * 1024; diff --git a/src/tools/edit-handler.ts b/src/tools/edit-handler.ts index 6bf06112..6460d611 100644 --- a/src/tools/edit-handler.ts +++ b/src/tools/edit-handler.ts @@ -8,7 +8,7 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool, semanticBoolean } from "../common/runtime/validate"; +import { executeValidatedTool, semanticBoolean } from "../common/validate"; import { createSnippet, getFileState, @@ -18,7 +18,7 @@ import { isFullFileView, normalizeFilePath, recordFileState, -} from "../common/runtime/state"; +} from "../common/state"; const MAX_CANDIDATE_COUNT = 5; const REPLACE_ALL_MATCH_THRESHOLD = 5; diff --git a/src/tools/read-handler.ts b/src/tools/read-handler.ts index 606199c5..964cdd72 100644 --- a/src/tools/read-handler.ts +++ b/src/tools/read-handler.ts @@ -3,7 +3,7 @@ import * as path from "path"; import ignore from "ignore"; import type { ToolExecutionContext, ToolExecutionFollowUpMessage, ToolExecutionResult } from "./executor"; import { readTextFileWithMetadata } from "../common/file-utils"; -import { createSnippet, isAbsoluteFilePath, markFileRead, normalizeFilePath } from "../common/runtime/state"; +import { createSnippet, isAbsoluteFilePath, markFileRead, normalizeFilePath } from "../common/state"; const DEFAULT_LINE_LIMIT = 2000; const MAX_LINE_LENGTH = 2000; diff --git a/src/tools/update-plan-handler.ts b/src/tools/update-plan-handler.ts index ff848703..11439784 100644 --- a/src/tools/update-plan-handler.ts +++ b/src/tools/update-plan-handler.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { ToolExecutionContext, ToolExecutionResult } from "./executor"; -import { executeValidatedTool } from "../common/runtime/validate"; +import { executeValidatedTool } from "../common/validate"; const updatePlanSchema = z.strictObject({ plan: z.string().trim().min(1, "plan must not be empty."), diff --git a/src/tools/write-handler.ts b/src/tools/write-handler.ts index 1d3fb558..35ecdb2d 100644 --- a/src/tools/write-handler.ts +++ b/src/tools/write-handler.ts @@ -9,14 +9,8 @@ import { readTextFileWithMetadata, writeTextFile, } from "../common/file-utils"; -import { executeValidatedTool } from "../common/runtime/validate"; -import { - getFileState, - isAbsoluteFilePath, - isFullFileView, - normalizeFilePath, - recordFileState, -} from "../common/runtime/state"; +import { executeValidatedTool } from "../common/validate"; +import { getFileState, isAbsoluteFilePath, isFullFileView, normalizeFilePath, recordFileState } from "../common/state"; const writeSchema = z.strictObject({ file_path: z.string().min(1, "file_path is required."), diff --git a/src/ui/views/ProcessStdoutView.tsx b/src/ui/views/ProcessStdoutView.tsx index 39299599..bd5e6363 100644 --- a/src/ui/views/ProcessStdoutView.tsx +++ b/src/ui/views/ProcessStdoutView.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Text } from "ink"; -import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../../common/system/bash-timeout"; +import { BASH_TIMEOUT_DECREMENT_MS, BASH_TIMEOUT_INCREMENT_MS } from "../../common/bash-timeout"; import type { BashTimeoutAdjustment, SessionEntry, SessionProcessEntry } from "../../session"; import { useTerminalInput } from "../hooks"; From a10917825f02881db9af34ded9a2dfc0e25a2ea9 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 16:43:49 +0800 Subject: [PATCH 13/15] =?UTF-8?q?refactor(session):=20=E5=9B=9E=E6=BB=9A?= =?UTF-8?q?=20session.ts=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/session.ts | 292 ++++++++++++++++++++++++++----------------------- 1 file changed, 154 insertions(+), 138 deletions(-) diff --git a/src/session.ts b/src/session.ts index 54e31368..a9fc39e8 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,6 +1,156 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import * as crypto from "crypto"; +import { fileURLToPath } from "url"; +import matter from "gray-matter"; +import ejs from "ejs"; +import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "openai/resources/chat/completions"; +import { launchNotifyScript } from "./common/notify"; +import { buildThinkingRequestOptions } from "./common/openai-thinking"; +import { DEEPSEEK_V4_MODELS, supportsMultimodal } from "./common/model-capabilities"; +import { + getCompactPrompt, + getDefaultSkillPrompt, + getRuntimeContext, + getSystemPrompt, + getTools, + type ToolDefinition, +} from "./prompt"; +import { + ToolExecutor, + type CreateOpenAIClient, + type ProcessTimeoutControl, + type ProcessTimeoutInfo, + type ToolCallExecution, + type ToolExecutionHooks, +} from "./tools/executor"; +import { McpManager } from "./mcp/mcp-manager"; import type { McpServerConfig, PermissionScope, PermissionSettings } from "./settings"; -import type { AskPermissionRequest, MessageToolPermission, UserToolPermission } from "./common/permissions"; -import type { CreateOpenAIClient } from "./tools/executor"; +import { logApiError } from "./common/error-logger"; +import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; +import { killProcessTree } from "./common/process-tree"; +import { GitFileHistory } from "./common/file-history"; +import { getSnippet } from "./common/state"; +import { + appendProjectPermissionAllows, + buildPermissionToolExecution, + computeToolCallPermissions, + hasUserPermissionReplies, + normalizeAskPermissions, + parseToolCallForPermissions, + type AskPermissionRequest, + type MessageToolPermission, + type PermissionToolCall, + type UserToolPermission, +} from "./common/permissions"; + +export type { PermissionScope } from "./settings"; +export type { + AskPermissionRequest, + AskPermissionScope, + BashPermissionScope, + MessageToolPermission, + PermissionDecision, + UserToolPermission, +} from "./common/permissions"; + +const MAX_SESSION_ENTRIES = 50; +const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; +const NEW_PROMPT_REPORT_TIMEOUT_MS = 3000; +const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; +const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; + +type ChatCompletionDebugOptions = { + enabled?: boolean; + location: string; + baseURL?: string; + params?: Record; +}; + +export function getCompactPromptTokenThreshold(model: string): number { + return DEEPSEEK_V4_MODELS.has(model) + ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD + : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; +} + +function isUsageRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function summarizeCompletionOptions(options?: Record): Record | undefined { + if (!options) { + return undefined; + } + return { + ...options, + signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, + }; +} + +function addUsageValue(current: unknown, next: unknown): unknown { + if (typeof next === "number") { + return (typeof current === "number" ? current : 0) + next; + } + + if (isUsageRecord(next)) { + const currentRecord = isUsageRecord(current) ? current : {}; + const result: Record = { ...currentRecord }; + for (const [key, value] of Object.entries(next)) { + result[key] = addUsageValue(currentRecord[key], value); + } + return result; + } + + return next; +} + +function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { + if (next == null) { + return current ?? null; + } + return addUsageValue(current, next) as ModelUsage; +} + +function usageWithRequestCount(usage: ModelUsage): ModelUsage { + const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; + return { + ...usage, + total_reqs: totalReqs, + }; +} + +function accumulateUsagePerModel( + current: Record | null | undefined, + model: string, + next: ModelUsage | null | undefined +): Record | null { + if (next == null) { + return current ?? null; + } + + const usagePerModel = { ...(current ?? {}) }; + const modelName = model.trim() || "unknown"; + usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; + return usagePerModel; +} + +function getExtensionRoot(): string { + if (typeof __dirname !== "undefined") { + return path.resolve(__dirname, ".."); + } + + const currentFilePath = fileURLToPath(import.meta.url); + return path.resolve(path.dirname(currentFilePath), ".."); +} + +function getTotalTokens(usage: ModelUsage | null | undefined): number { + if (!isUsageRecord(usage)) { + return 0; + } + const totalTokens = usage.total_tokens; + return typeof totalTokens === "number" ? totalTokens : 0; +} export type SessionStatus = | "failed" @@ -52,7 +202,7 @@ export type SessionEntry = { activeTokens: number; createTime: string; updateTime: string; - processes: Map | null; + processes: Map | null; // {pid: process info} askPermissions?: AskPermissionRequest[]; }; @@ -113,7 +263,7 @@ export type SkillInfo = { isLoaded?: boolean; }; -export type SessionManagerOptions = { +type SessionManagerOptions = { projectRoot: string; createOpenAIClient: CreateOpenAIClient; getResolvedSettings: () => { @@ -138,140 +288,6 @@ export type LlmStreamProgress = { formattedTokens: string; phase: "start" | "update" | "end"; }; -import { DEEPSEEK_V4_MODELS } from "./common/model-capabilities"; - -const DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD = 128 * 1024; -const DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD = 512 * 1024; - -export function getCompactPromptTokenThreshold(model: string): number { - return DEEPSEEK_V4_MODELS.has(model) - ? DEEPSEEK_V4_COMPACT_PROMPT_TOKEN_THRESHOLD - : DEFAULT_COMPACT_PROMPT_TOKEN_THRESHOLD; -} - -export function isUsageRecord(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - -export function getTotalTokens(usage: ModelUsage | null | undefined): number { - if (!isUsageRecord(usage)) { - return 0; - } - const totalTokens = (usage as Record).total_tokens; - return typeof totalTokens === "number" ? totalTokens : 0; -} - -export function summarizeCompletionOptions(options?: Record): Record | undefined { - if (!options) { - return undefined; - } - return { - ...options, - signal: options.signal instanceof AbortSignal ? { aborted: options.signal.aborted } : options.signal, - }; -} - -export function addUsageValue(current: unknown, next: unknown): unknown { - if (typeof next === "number") { - return (typeof current === "number" ? current : 0) + next; - } - - if (isUsageRecord(next)) { - const currentRecord = isUsageRecord(current) ? current : {}; - const result: Record = { ...currentRecord }; - for (const [key, value] of Object.entries(next)) { - result[key] = addUsageValue(currentRecord[key], value); - } - return result; - } - - return next; -} - -export function accumulateUsage(current: ModelUsage | null, next: unknown | null | undefined): ModelUsage | null { - if (next == null) { - return current ?? null; - } - return addUsageValue(current, next) as ModelUsage; -} - -export function usageWithRequestCount(usage: ModelUsage): ModelUsage { - const totalReqs = typeof usage.total_reqs === "number" ? usage.total_reqs + 1 : 1; - return { - ...usage, - total_reqs: totalReqs, - }; -} - -export function accumulateUsagePerModel( - current: Record | null | undefined, - model: string, - next: ModelUsage | null | undefined -): Record | null { - if (next == null) { - return current ?? null; - } - - const usagePerModel = { ...(current ?? {}) }; - const modelName = model.trim() || "unknown"; - usagePerModel[modelName] = accumulateUsage(usagePerModel[modelName] ?? null, usageWithRequestCount(next))!; - return usagePerModel; -} - -export { getExtensionRoot } from "./prompt"; -import * as fs from "fs"; -import * as path from "path"; -import * as os from "os"; -import * as crypto from "crypto"; -import matter from "gray-matter"; -import ejs from "ejs"; -import type { ChatCompletionContentPart, ChatCompletionMessageParam } from "openai/resources/chat/completions"; -import { launchNotifyScript } from "./common/notify"; -import { buildThinkingRequestOptions } from "./common/openai-thinking"; -import { supportsMultimodal } from "./common/model-capabilities"; -import { - getCompactPrompt, - getDefaultSkillPrompt, - getExtensionRoot, - getRuntimeContext, - getSystemPrompt, - getTools, - type ToolDefinition, -} from "./prompt"; -import { - type ProcessTimeoutControl, - type ProcessTimeoutInfo, - type ToolCallExecution, - type ToolExecutionHooks, - ToolExecutor, -} from "./tools/executor"; -import { McpManager } from "./mcp/mcp-manager"; - -import { logApiError } from "./common/error-logger"; -import { logOpenAIChatCompletionDebug, normalizeDebugError } from "./common/debug-logger"; -import { killProcessTree } from "./common/process-tree"; -import { GitFileHistory } from "./common/file-history"; -import { getSnippet } from "./common/state"; -import { - appendProjectPermissionAllows, - buildPermissionToolExecution, - computeToolCallPermissions, - hasUserPermissionReplies, - normalizeAskPermissions, - parseToolCallForPermissions, - type PermissionToolCall, -} from "./common/permissions"; - -const MAX_SESSION_ENTRIES = 50; -const DEFAULT_NEW_PROMPT_API_URL = "https://deepcode.vegamo.cn/api/plugin/new"; -const NEW_PROMPT_REPORT_TIMEOUT_MS = 3000; - -type ChatCompletionDebugOptions = { - enabled?: boolean; - location: string; - baseURL?: string; - params?: Record; -}; export class SessionManager { private readonly projectRoot: string; From d65260e2419c85bb4bc78c58deb75d6f7eaeb828 Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 17:03:23 +0800 Subject: [PATCH 14/15] =?UTF-8?q?refactor(session):=20=E5=9B=9E=E6=BB=9A?= =?UTF-8?q?=20session.ts=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/session.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/session.ts b/src/session.ts index a9fc39e8..876d2561 100644 --- a/src/session.ts +++ b/src/session.ts @@ -2,7 +2,6 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import * as crypto from "crypto"; -import { fileURLToPath } from "url"; import matter from "gray-matter"; import ejs from "ejs"; import type { ChatCompletionMessageParam, ChatCompletionContentPart } from "openai/resources/chat/completions"; @@ -12,6 +11,7 @@ import { DEEPSEEK_V4_MODELS, supportsMultimodal } from "./common/model-capabilit import { getCompactPrompt, getDefaultSkillPrompt, + getExtensionRoot, getRuntimeContext, getSystemPrompt, getTools, @@ -135,15 +135,6 @@ function accumulateUsagePerModel( return usagePerModel; } -function getExtensionRoot(): string { - if (typeof __dirname !== "undefined") { - return path.resolve(__dirname, ".."); - } - - const currentFilePath = fileURLToPath(import.meta.url); - return path.resolve(path.dirname(currentFilePath), ".."); -} - function getTotalTokens(usage: ModelUsage | null | undefined): number { if (!isUsageRecord(usage)) { return 0; From e25cb9eca1ce5b6b07049003271bd4df400c9ffc Mon Sep 17 00:00:00 2001 From: hcyang Date: Tue, 26 May 2026 17:49:39 +0800 Subject: [PATCH 15/15] =?UTF-8?q?fix(ui):=20=E4=BC=98=E5=8C=96=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E5=AF=BC=E8=88=AA=E5=92=8C=E7=B2=98=E8=B4=B4=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在历史导航中同步捕获当前草稿,避免异步状态不一致 - 修正使用最新捕获草稿替代旧状态以设置缓冲区文本 - 在粘贴处理钩子中引入 useEffect 钩子以响应缓冲区文本变化 - 通过副作用保持衍生标志状态与缓冲区文本同步 - 移除潜在的异步更新问题,提升用户交互体验 --- src/ui/hooks/useHistoryNavigation.ts | 4 +++- src/ui/hooks/usePasteHandling.ts | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ui/hooks/useHistoryNavigation.ts b/src/ui/hooks/useHistoryNavigation.ts index 433d493c..54ccabfd 100644 --- a/src/ui/hooks/useHistoryNavigation.ts +++ b/src/ui/hooks/useHistoryNavigation.ts @@ -38,13 +38,15 @@ export function useHistoryNavigation( const previousCursor = historyCursor === -1 ? promptHistory.length : historyCursor; const nextCursor = Math.max(0, Math.min(promptHistory.length, previousCursor + direction)); + // Capture the current draft synchronously before `setDraftBeforeHistory`. + const draft = historyCursor === -1 ? buffer.text : draftBeforeHistory; if (historyCursor === -1) { setDraftBeforeHistory(buffer.text); } if (nextCursor === promptHistory.length) { - const text = draftBeforeHistory ?? ""; + const text = draft ?? ""; setBuffer({ text, cursor: text.length }); setHistoryCursor(-1); setDraftBeforeHistory(null); diff --git a/src/ui/hooks/usePasteHandling.ts b/src/ui/hooks/usePasteHandling.ts index 50cae754..beaf0859 100644 --- a/src/ui/hooks/usePasteHandling.ts +++ b/src/ui/hooks/usePasteHandling.ts @@ -1,5 +1,5 @@ import type React from "react"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import type { PromptBufferState } from "../core/prompt-buffer"; import { cleanPasteContent, findPasteMarkerContaining, hasActivePasteMarkers, insertText } from "../core/prompt-buffer"; @@ -51,6 +51,13 @@ export function usePasteHandling( setHasExpandedRegions(expandedRegionsRef.current.size > 0); } + // Recompute derived flags whenever the buffer text changes, so they stay + // in sync after any state update (e.g. large paste, expand/collapse, undo). + useEffect(() => { + refreshDerivedFlags(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [buffer.text]); + function handlePaste(pastedText: string): void { const totalChars = pastedText.length;