Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ LLM_BASE_URL=
# 撤回会改磁盘。本地试用先跑 scripts/git-sandbox.sh,再把这里指到 tmp/git-sandbox。
# 要操作本仓时显式写成仓根。pnpm dev:api 在沙箱存在时默认用沙箱。
GIT_REPO=

# 本机 Codex CLI 可执行文件(默认 codex)。未安装时主服务仍可启动,/codex/status 会说明原因。
CODEX_BIN=codex

10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ jobs:
- name: Test
run: go test ./...

- name: Codex coverage
run: |
go test -covermode=atomic -coverprofile=/tmp/codex.out \
-coverpkg=codedock/pkg/codex,codedock/internal/codex,codedock/internal/handler/codex \
./pkg/codex ./internal/codex ./internal/handler/codex
total=$(go tool cover -func=/tmp/codex.out | awk '/^total:/ {print $3}' | tr -d '%')
echo "codex coverage: ${total}%"
awk -v t="$total" 'BEGIN { if (t+0 < 85) { print "codex coverage below 85%"; exit 1 } }'


- name: Build
run: go build -o /tmp/codedock ./cmd/server

Expand Down
14 changes: 8 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,27 @@

修改 CodeDock 代码前,先阅读 [`docs/architecture.md`](docs/architecture.md)。该文档是当前目录归属和模块边界的依据。

Agent Loop 已闭环:用户发文本、装上下文、调模型、产出文字或 Tool、事件落库并由 SSE 消费。默认注册 `ping`、记忆工具、编码八工具与 `plan_*`。Git 用户操作走 HTTP + `pkg/git`,不经过 Agent Tool。仓库根是当前会话冻结的工作目录(请求带 `session_id`);未带会话才回落 `GIT_REPO` / cwd。前端 Git 在 `packages/core/git`、`packages/views/git` 与 `apps/web` 的 `/git`,不扩 `AgentClient`。
Agent Loop 已闭环:用户发文本、装上下文、调模型、产出文字或 Tool、事件落库并由 SSE 消费。默认注册 `ping`、记忆工具、编码八工具与 `plan_*`。Git 用户操作走 HTTP + `pkg/git`,不经过 Agent Tool。仓库根是当前会话冻结的工作目录(请求带 `session_id`);未带会话才回落 `GIT_REPO` / cwd。前端 Git 在 `packages/core/git`、`packages/views/git` 与 `apps/web` 的 `/git`,不扩 `AgentClient`。Codex 对话复用 Agent 页,新建会话时选择模式;HTTP 走独立 `/codex`,前端用 `CodexClient`,不扩 `AgentClient`。

## 目录放置规则

- 服务启动、配置读取、Router 和依赖装配放在 `server/cmd/server`。
- 大部分 HTTP 逻辑放在 `server/internal/handler`:Session / Message / Usage / Approval 的 CRUD,SSE,Run 的 Start / Continue / Cancel,审批裁决,用户侧记忆查看/删除,以及 Git(直接调 `pkg/git`)。创建 Session 时在本包冻结 `workspace_id`。不 import `internal/agent/tools`。新对话选目录由 web 弹出目录浏览框(`apps/web` 列本机目录),不走 Agent Tool。
- 大部分 HTTP 逻辑放在 `server/internal/handler`:Session / Message / Usage / Approval 的 CRUD,SSE,Run 的 Start / Continue / Cancel,审批裁决,用户侧记忆查看/删除,以及 Git(直接调 `pkg/git`)。创建 Session 时在本包冻结 `workspace_id`。不 import `internal/agent/tools`。新对话选目录由 web 弹出系统目录选择框,不走 Agent Tool。Codex 的独立 `/codex` HTTP 放在 `server/internal/handler/codex`。
- Agent 运行时编排和 sqlc 持久化放在 `server/internal/agent`。
- 本机 Codex app-server 生命周期、内存排队/问票/SSE 放在 `server/internal/codex`。不新增 Codex 业务表;凡官方 API 能读到的都不入库。
- Markdown 记忆(热层目录+专题)与 context message 索引(冷层按工作区 FTS)放在 `server/internal/agent/memory`;不放 `pkg/memory`。memory 不 import 父包 `internal/agent`,不定义 Tool。
- 具体工具定义放在 `server/internal/agent/tools`。工具名、入参/出参、schema、权限和编排都在本包;Execute 若要调外部能力,只通过 `Ports` 里的接口。Runtime `New` 时由 `cmd/server` 注入 `Ports` 的具体实现,再 `Register`。每个工具只定义入参/出参结构体,执行用 `encoding/json`,schema 从类型推断。`tools` 可 import `memory`,不 import 父包 `internal/agent`。
- Agent 通用无状态逻辑放在 `server/pkg/agent`:类型、token 统计、提示词、上下文、Tool 抽象(不含具体工具定义)、Agent 配置、模型调用。
- Git CLI 操作放在 `server/pkg/git`:无状态,不写产品流程;Handler 直接调用。不进 `pkg/agent`。
- Codex 协议与领域类型放在 `server/pkg/codex`:看板的子模块,JSONL 客户端给 `internal/codex` 调用;不查库、不 spawn CLI。不进 `pkg/agent`。
- 进程内事件总线放在 `server/internal/events`。
- 数据库入口和 sqlc 生成代码放在 `server/pkg/db`。
- 数据库结构演进放在 `server/migrations`。
- 运行时产生的文件(SQLite 等)放在仓根 `data/`,不要写进 `server/`。该目录 gitignore。
- 无头业务放在 `packages/core`(`@codedock/core`):按业务域拆(现有 `chat/`、`git/`),文件直接在域目录下,不要 `src/`。不依赖 React、Next、DOM、`process.env`。`baseUrl` / `userId` 由调用方注入。Git 用独立 `GitClient`。
- 无头业务放在 `packages/core`(`@codedock/core`):按业务域拆(现有 `chat/`、`git/`、`codex/`),文件直接在域目录下,不要 `src/`。不依赖 React、Next、DOM、`process.env`。`baseUrl` / `userId` 由调用方注入。Git 用独立 `GitClient`。Codex 用独立 `CodexClient`,不扩 `AgentClient`。
- 无业务 UI 放在 `packages/ui`(`@codedock/ui`):`components/`、`lib/`、`styles/`,不要 `src/`,不按业务域拆。不依赖 core,不知道 Session / Run / TimelineItem。
- 组合层放在 `packages/views`(`@codedock/views`):按业务域拆,与 core 对齐(现有 `chat/`、`git/`)。包根 `provider.tsx` 注入 Agent client;Git 用 `views/git` 的 `GitProvider`。不 import `next/*`;导航用回调。不要 `src/`,不预建空业务域。
- Web 路由和平台装配放在 `apps/web`:读 `NEXT_PUBLIC_*`、创建 `AgentClient` / `GitClient`、包对应 Provider、`router.push`。`/git` 放在 `(chat)` 组外。开发态切页顶栏只放 web。不解析 SSE。
- 组合层放在 `packages/views`(`@codedock/views`):按业务域拆,与 core 对齐(现有 `chat/`、`git/`、`codex/`)。包根 `provider.tsx` 注入 Agent client;Git 用 `views/git` 的 `GitProvider`;Codex 用 `views/codex` 的 `CodexProvider`,由 `ChatPage` 在 Codex 模式下组合,不单独做 Codex 页。不 import `next/*`;导航用回调。不要 `src/`,不预建空业务域。
- Web 路由和平台装配放在 `apps/web`:读 `NEXT_PUBLIC_*`、创建 `AgentClient` / `GitClient` / `CodexClient`、包对应 Provider、`router.push`。`/git` 放在 `(chat)` 组外。Codex 不单独路由,走 `/` 与 `/s/c/:id`。开发态切页顶栏只放 web。不解析 SSE。
- 依赖方向:`apps/web` → `packages/views` → `packages/core`;`packages/views` → `packages/ui`。`ui` 不依赖 `core`。未来 CLI 只依赖 `core`。
- 不要创建 `server/pkg/ai`。大模型调用属于 `pkg/agent`。

Expand All @@ -38,6 +40,6 @@ Agent Loop 已闭环:用户发文本、装上下文、调模型、产出文字
- 不要预先创建 Issue、Task、Review、Workspace 或其他具体业务域目录。
- 不要把产品工作流放入 `server/pkg`。
- 前端三层不得反依赖:`core` 不依赖 React / Next / DOM / `process.env`;`ui` 不依赖 `core`;`views` 不 import `next/*`;`apps/web` 只做路由与平台装配。
- 前端按业务域拆模块,不要 `src/`:`core` / `views` 用同名域目录(现有 `chat`);`ui` 只用 `components` / `lib` / `styles`。新业务再建目录,不预建空文件夹。
- 前端按业务域拆模块,不要 `src/`:`core` / `views` 用同名域目录(现有 `chat` / `git` / `codex`);`ui` 只用 `components` / `lib` / `styles`。新业务再建目录,不预建空文件夹。

当需求变更没有明显的代码归属时,先依据 `docs/architecture.md` 对其分类,再开始编写代码。
3 changes: 3 additions & 0 deletions apps/web/app/(chat)/s/c/[sessionId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function CodexSessionPage() {
return null;
}
18 changes: 18 additions & 0 deletions apps/web/app/api/directories/pick/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";

import { pickDirectoryNative } from "@/lib/native-file-pick";

export const maxDuration = 300;

export async function POST(req: Request) {
const body = (await req.json().catch(() => ({}))) as { start?: string };
try {
const path = await pickDirectoryNative({ start: body.start });
return NextResponse.json({ path });
} catch (err) {
return NextResponse.json(
{ error: err instanceof Error ? err.message : "无法打开系统目录选择框" },
{ status: 500 },
);
}
}
24 changes: 0 additions & 24 deletions apps/web/app/api/directories/route.ts

This file was deleted.

26 changes: 26 additions & 0 deletions apps/web/app/api/files/pick/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextResponse } from "next/server";

import { pickFilesNative } from "@/lib/native-file-pick";

export const maxDuration = 300;

export async function POST(req: Request) {
const body = (await req.json().catch(() => ({}))) as {
images?: boolean;
multiple?: boolean;
start?: string;
};
try {
const paths = await pickFilesNative({
images: Boolean(body.images),
multiple: body.multiple !== false,
start: body.start,
});
return NextResponse.json({ paths });
} catch (err) {
return NextResponse.json(
{ error: err instanceof Error ? err.message : "无法打开系统文件选择框" },
{ status: 500 },
);
}
}
32 changes: 23 additions & 9 deletions apps/web/app/chat-host.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
"use client";

import { ChatPage } from "@codedock/views/chat";
import { ChatPage, type SessionEngine } from "@codedock/views/chat";
import { usePathname, useRouter } from "next/navigation";

import { rememberSession } from "@/lib/session";

export function ChatHost() {
const pathname = usePathname();
const router = useRouter();
const match = pathname.match(/^\/s\/([^/]+)/);
const sessionId = match?.[1] ? decodeURIComponent(match[1]) : undefined;
if (sessionId) {
rememberSession(sessionId);
const parsed = parseChatPath(pathname);
if (parsed.sessionId && parsed.engine === "agent") {
rememberSession(parsed.sessionId);
}

return (
<ChatPage
sessionId={sessionId}
sessionId={parsed.sessionId}
engine={parsed.engine}
brandSrc="/brand/codedock-berth-mark.svg"
onOpenSession={(id) => {
if (id !== sessionId) {
router.push(`/s/${id}`);
codexIconSrc="/brand/codex-app-icon.png"
onOpenSession={(id, engine = parsed.engine ?? "agent") => {
const path = engine === "codex" ? `/s/c/${id}` : `/s/${id}`;
if (pathname !== path) {
router.push(path);
}
}}
onNewConversation={() => {
Expand All @@ -31,3 +33,15 @@ export function ChatHost() {
/>
);
}

function parseChatPath(pathname: string): { sessionId?: string; engine?: SessionEngine } {
const codex = pathname.match(/^\/s\/c\/([^/]+)/);
if (codex?.[1]) {
return { sessionId: decodeURIComponent(codex[1]), engine: "codex" };
}
const agent = pathname.match(/^\/s\/([^/]+)/);
if (agent?.[1] && agent[1] !== "c") {
return { sessionId: decodeURIComponent(agent[1]), engine: "agent" };
}
return {};
}
2 changes: 1 addition & 1 deletion apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const geistMono = Geist_Mono({

export const metadata: Metadata = {
title: "CodeDock",
description: "Agent 对话工作台",
description: "Local 对话工作台",
};

export default function RootLayout({ children }: LayoutProps<"/">) {
Expand Down
15 changes: 12 additions & 3 deletions apps/web/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
"use client";

import { AgentClient } from "@codedock/core/chat";
import { CodexClient } from "@codedock/core/codex";
import { CodexProvider } from "@codedock/views/codex";
import { AgentProvider } from "@codedock/views";
import { useMemo, type ReactNode } from "react";

import { listDirectories } from "@/lib/directories";
import { apiBase, defaultUserId } from "@/lib/env";
import { pickDirectory } from "@/lib/pick-directory";
import { pickFiles } from "@/lib/pick-files";

export function Providers({ children }: { children: ReactNode }) {
const client = useMemo(() => new AgentClient({ baseUrl: apiBase }), []);
const codex = useMemo(() => new CodexClient({ baseUrl: apiBase }), []);
return (
<AgentProvider client={client} userId={defaultUserId} listDirectories={listDirectories}>
{children}
<AgentProvider
client={client}
userId={defaultUserId}
pickDirectory={pickDirectory}
pickFiles={pickFiles}
>
<CodexProvider client={codex}>{children}</CodexProvider>
</AgentProvider>
);
}
15 changes: 0 additions & 15 deletions apps/web/lib/directories.ts

This file was deleted.

Loading
Loading