Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0d8b483
refactor(ui): 移动并重构设置相关工具函数至utils目录
hqwlkj May 25, 2026
425baa3
Merge branch 'main' into refactor/tidy-up
hqwlkj May 25, 2026
74a85e9
refactor(settings): 移动配置文件读写及默认常量至settings模块
hqwlkj May 25, 2026
89a42e1
Merge branch 'main' into refactor/tidy-up
hqwlkj May 25, 2026
33fdcd6
refactor(prompt): 重构粘贴和历史导航处理逻辑
hqwlkj May 25, 2026
febcd93
refactor(core): 重构代码结构,移动多个模块至 core 文件夹
hqwlkj May 25, 2026
2dd9794
refactor(ui): 重构代码结构,调整文件路径和导入引用
hqwlkj May 25, 2026
0a1a405
style(ui): 优化界面颜色和文字样式
hqwlkj May 25, 2026
197676e
refactor(session): 提取并迁移会话相关工具函数
hqwlkj May 25, 2026
509434c
refactor(session): 统一导出 getExtensionRoot 函数
hqwlkj May 26, 2026
77245d8
fix(ui): 修正权限请求流程中的提示草稿状态
hqwlkj May 26, 2026
39b38f3
refactor(core): 重命名多个文件及其引用路径以统一命名规范
hqwlkj May 26, 2026
192d02d
refactor(session): 移除 session 文件夹及下面的全部文件, 回滚为统一引用 session 模块
hqwlkj May 26, 2026
5fd54b9
refactor(src): 重构路径引用并重命名部分模块
hqwlkj May 26, 2026
a109178
refactor(session): 回滚 session.ts 文件
hqwlkj May 26, 2026
d65260e
refactor(session): 回滚 session.ts 文件
hqwlkj May 26, 2026
e25cb9e
fix(ui): 优化历史导航和粘贴处理逻辑
hqwlkj May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cli.tsx
Original file line number Diff line number Diff line change
@@ -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/update-check";
import { AppContainer } from "./ui";

const args = process.argv.slice(2);
Expand Down
2 changes: 1 addition & 1 deletion src/common/openai-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "../settings";

// Custom undici Agent with a 180-second keepAlive timeout. The default
// global fetch (undici) only keeps connections alive for 4 seconds, which
Expand Down
4 changes: 2 additions & 2 deletions src/updateCheck.ts → src/common/update-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
File renamed without changes.
7 changes: 3 additions & 4 deletions src/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
import { findGitBashPath, resolveShellPath } from "./common/shell-utils";
import { supportsMultimodal } from "./common/model-capabilities";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -287,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") {
Expand Down
11 changes: 1 addition & 10 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -12,6 +11,7 @@ import { DEEPSEEK_V4_MODELS, supportsMultimodal } from "./common/model-capabilit
import {
getCompactPrompt,
getDefaultSkillPrompt,
getExtensionRoot,
getRuntimeContext,
getSystemPrompt,
getTools,
Expand Down Expand Up @@ -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;
Expand Down
88 changes: 88 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
@@ -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<string, string | undefined> & {
MODEL?: string;
Expand Down Expand Up @@ -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
);
}
File renamed without changes.
6 changes: 3 additions & 3 deletions src/tests/clipboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,7 +30,7 @@ function withPlatform<T>(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);
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
replaceCurrentFileMentionToken,
scanFileMentionItems,
type FileMentionItem,
} from "../ui/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 }), {
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/tests/permission-prompt.test.ts
Original file line number Diff line number Diff line change
@@ -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");
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
import { dispatchTerminalInput, parseTerminalInput } from "../ui/hooks";

function collectDispatchedInput(data: string) {
const events: ReturnType<typeof parseTerminalInput>[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
recordPromptEdit,
redoPromptEdit,
undoPromptEdit,
} from "../ui/promptUndoRedo";
} from "../ui/core/prompt-undo-redo";

test("prompt undo and redo restore edited buffer states", () => {
const history = createPromptUndoRedoState();
Expand Down
File renamed without changes.
3 changes: 2 additions & 1 deletion src/tests/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
import { SessionManager } from "../session";

const originalFetch = globalThis.fetch;
const originalConsoleWarn = console.warn;
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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/update-check";

test("compareVersions orders semantic versions", () => {
assert.equal(compareVersions("0.1.4", "0.1.3"), 1);
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/tools/edit-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
readTextFileWithMetadata,
writeTextFile,
} from "../common/file-utils";
import { executeValidatedTool, semanticBoolean } from "../common/runtime";
import { executeValidatedTool, semanticBoolean } from "../common/validate";
import {
createSnippet,
getFileState,
Expand Down
2 changes: 1 addition & 1 deletion src/tools/update-plan-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";
import type { ToolExecutionContext, ToolExecutionResult } from "./executor";
import { executeValidatedTool } from "../common/runtime";
import { executeValidatedTool } from "../common/validate";

const updatePlanSchema = z.strictObject({
plan: z.string().trim().min(1, "plan must not be empty."),
Expand Down
2 changes: 1 addition & 1 deletion src/tools/write-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
readTextFileWithMetadata,
writeTextFile,
} from "../common/file-utils";
import { executeValidatedTool } from "../common/runtime";
import { executeValidatedTool } from "../common/validate";
import { getFileState, isAbsoluteFilePath, isFullFileView, normalizeFilePath, recordFileState } from "../common/state";

const writeSchema = z.strictObject({
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/ui/components/FileMentionMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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 DropdownMenu from "../DropdownMenu";
import type { FileMentionItem, FileMentionToken } from "../../core/file-mentions";

type Props = {
open: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/ModelsDropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/RawModelDropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
4 changes: 2 additions & 2 deletions src/ui/components/SkillsDropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import DropdownMenu from "../../DropdownMenu";
import DropdownMenu from "../DropdownMenu";
import React, { useEffect, useState } from "react";
import type { SkillInfo } from "../../../session";
import { useInput } from "ink";
import { isSkillSelected } from "../../SlashCommandMenu";
import { isSkillSelected } from "../../views/SlashCommandMenu";

const SkillsDropdown: React.FC<{
open: boolean;
Expand Down
1 change: 1 addition & 0 deletions src/ui/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { RawModeExitPrompt } from "./RawModeExitPrompt";
export { default as SkillsDropdown } from "./SkillsDropdown";
export { default as ModelsDropdown } from "./ModelsDropdown";
export { default as FileMentionMenu } from "./FileMentionMenu";
export { default as DropdownMenu } from "./DropdownMenu";
2 changes: 0 additions & 2 deletions src/ui/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// UI-level shared constants used across components.

/** Separator used when rendering command arguments inline (e.g., `arg1 | arg2 | arg3`). */
export const ARGS_SEPARATOR = " | ";

Expand Down
2 changes: 1 addition & 1 deletion src/ui/contexts/RawModeContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { createContext, useCallback, useContext, useRef, useState } from "react";
import type { DropdownMenuItem } from "../DropdownMenu";
import type { DropdownMenuItem } from "../components/DropdownMenu";

export enum RawMode {
None = "Normal mode",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SessionMessage, SessionStatus } from "../session";
import type { SessionMessage, SessionStatus } from "../../session";

export type AskUserQuestionOption = {
label: string;
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/ui/fileMentions.ts → src/ui/core/file-mentions.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/ui/loadingText.ts → src/ui/core/loading-text.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { LlmStreamProgress, SessionEntry } from "../session";
import type { LlmStreamProgress, SessionEntry } from "../../session";

type RunningProcesses = SessionEntry["processes"];

Expand Down
File renamed without changes.
Loading
Loading