Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
120 changes: 120 additions & 0 deletions docs/specs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# 架构设计 (Architecture Specification)

> **功能名称:** 文件树拖拽生成 ContextMention(文件 + 目录)
> **版本:** v1.1
> **状态:** 审查中
> **关联需求:** `docs/specs/product.md`
> **最后更新:** 2026-03-05

---

## 1. 系统概览

本功能涉及 FileTree 与 MessageInput 的拖拽链路。FileTree 在拖拽开始时注入自定义 MIME payload,
MessageInput 作为 drop 目标解析 payload,文件拖拽触发附件桥接事件并添加 ContextMention,
目录拖拽仅添加 ContextMention。附件桥接失败时回退插入 `@path` 文本;发送前对 ContextMention
与输入文本的 `@path` 做去重。

### 架构决策记录 (ADR)

| 决策 | 选择方案 | 被否定方案 | 理由 |
|:---|:---|:---|:---|
| 拖拽数据格式 | 自定义 MIME + JSON payload | 仅使用 `text/plain` | 便于区分 FileTree 拖拽与外部拖拽 |
| 附件桥接方式 | `attach-file-to-chat` 事件 + `usePromptInputAttachments` | 直接在 MessageInput 内调用 PromptInput 私有 API | 保持与现有 FileTree “+” 入口一致 |
| 去重策略 | 发送前过滤已存在 `@path` | 发送时全部拼接 | 避免重复路径 |

---

## 2. 组件拓扑图

```mermaid
graph TD
FT["FileTree 拖拽源"] --> DT["DataTransfer JSON payload"]
DT --> MI["MessageInput drop zone"]
MI --> CM["ContextMention 状态 + Chip 渲染"]
MI --> EV["CustomEvent: attach-file-to-chat"]
EV --> BR["FileTreeAttachmentBridge"]
BR --> AT["PromptInput attachments"]
CM --> SEND["MessageInput 提交:mention 去重"]
```

---

## 3. 数据模型

### 3.1 实体定义

#### ContextMention (前端内存态)

| 字段名 | 类型 | 约束 | 描述 |
|:---|:---|:---|:---|
| `id` | string | 唯一 | chip 标识 |
| `path` | string | 必填 | 绝对路径 |
| `name` | string | 必填 | 显示名称 |
| `type` | "file" \| "directory" | 必填 | chip 类型 |

#### FileTreeDragPayload (拖拽 payload)

| 字段名 | 类型 | 约束 | 描述 |
|:---|:---|:---|:---|
| `path` | string | 必填 | 节点路径 |
| `name` | string | 必填 | 节点名称 |
| `type` | "file" \| "directory" | 必填 | 节点类型 |

### 3.2 实体关系图

无持久化实体关系;仅在 MessageInput 内部维护数组状态。

---

## 4. API / 接口签名

### 4.1 复用既有 API

本迭代不新增 API 端点,仅复用:
- `GET /api/files/raw?path=...`(附件读取)
- 自定义事件 `attach-file-to-chat`

---

## 5. 依赖白名单

| 依赖名 | 版本 | 用途 | 是否新增 |
|:---|:---|:---|:---|
| — | — | 无新增依赖 | 否 |

---

## 6. 错误处理策略

| 错误场景 | 处理方式 | 用户感知 |
|:---|:---|:---|
| `/api/files/raw` 失败 | 触发 `onAttachFailed`,插入 `@path` 文本 | 输入框出现回退路径 |
| 拖拽 payload 解析失败 | 忽略 drop | 无感知 |

---

## 7. 安全策略

- **输入验证:** 仅处理 FileTree 自定义 MIME payload。
- **身份认证:** 沿用现有 API 权限。
- **数据访问控制:** 仍由 `/api/files/raw` 处理路径读取。
- **敏感数据处理:** 不新增 PII 处理。
- **审计日志:** 不新增。

---

## 8. 性能考量

| 指标 | 目标值 | 测量方式 |
|:---|:---|:---|
| 拖拽响应 | 交互无明显卡顿 | 手动交互验证 |
| 额外状态更新 | O(mention 数量) | 代码审查 |

---

## 9. 审批记录

| 日期 | 审批人 | 决定 | 备注 |
|:---|:---|:---|:---|
| 2026-03-05 | 待定 | 待修改 | 规范需更新以反映缺失的拖拽链路 |
102 changes: 102 additions & 0 deletions docs/specs/product.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 需求规范 (Product Specification)

> **功能名称:** 文件树拖拽生成 ContextMention(文件 + 目录)
> **版本:** v1.1
> **状态:** 审查中
> **作者:** AI Architect + Human Engineer
> **最后更新:** 2026-03-05

---

## 1. 概述

补齐 FileTree → MessageInput 的拖拽体验:拖拽文件时创建附件并生成文件 ContextMention,
拖拽目录时生成目录 ContextMention。附件拉取失败时插入 `@path` 文本作为回退,并在发送时对
ContextMention 前缀进行去重,避免重复路径。

---

## 2. 用户故事与验收标准

### US-001: 文件拖拽同时生成附件与 ContextMention

**作为** 聊天用户,**我希望** 将文件从 FileTree 拖到 MessageInput 时同时获得附件与 ContextMention,
**以便** 发送时既包含文件内容,又显式标注上下文路径。

#### 验收标准

- **AC-001.1:**
- **GIVEN** 用户从 FileTree 拖拽文件节点
- **WHEN** 在 MessageInput 区域松开
- **THEN** PromptInput 附件列表新增该文件
- **AND** 同时生成文件类型的 ContextMention chip(显示文件名、可移除)

- **AC-001.2:**
- **GIVEN** 文件拖拽触发附件拉取失败
- **WHEN** 失败回调触发
- **THEN** 在输入框插入 `@path` 文本作为回退

### US-002: 目录拖拽生成 ContextMention

**作为** 聊天用户,**我希望** 拖拽目录时生成目录 ContextMention,
**以便** 发送时能显式标注目录上下文。

#### 验收标准

- **AC-002.1:**
- **GIVEN** 用户从 FileTree 拖拽目录节点
- **WHEN** 在 MessageInput 区域松开
- **THEN** 生成目录类型的 ContextMention chip
- **AND** 不创建附件

### US-003: 发送内容的 ContextMention 去重

**作为** 聊天用户,**我希望** 当输入框已包含相同 `@path` 文本时,
**以便** 发送内容里不会重复拼接 ContextMention 前缀。

#### 验收标准

- **AC-003.1:**
- **GIVEN** ContextMention 列表包含某个 `path`
- **WHEN** 输入内容已包含 `@{path}`
- **THEN** 发送前不再重复追加该 `path` 的前缀

---

## 3. 非功能性需求

| ID | 类别 | 描述 | 目标指标 |
|:---|:---|:---|:---|
| NFR-001 | 质量 | 不引入 TypeScript/ESLint 错误 | `npm run test` 通过 |
| NFR-002 | 可用性 | 拖拽交互无浏览器默认文本插入 | 交互无异常 |
| NFR-003 | 体验 | UI 改动需用 CDP 验证、console 无报错 | 手动验证通过 |
| NFR-004 | 兼容性 | 不新增依赖,保持既有事件与 API | 无新依赖 |

---

## 4. 约束与假设

### 约束
- 仅处理 FileTree 内部拖拽,不支持 OS 文件拖拽到 MessageInput 的额外规则。
- 不修改数据库或 API Schema。

### 假设
- FileTree 拖拽 payload 包含 `path` 与 `name`。
- MessageInput 使用 ContextMention 在发送时前置 `@path`。

---

## 5. 超出范围 (Out of Scope)

以下内容**明确不在**本次迭代的范围内:
- 批量拖拽多个文件或目录。
- 调整 ContextMention 的视觉设计方案。
- 变更文件预览或文件树搜索逻辑。

---

## 6. 审批记录

| 日期 | 审批人 | 决定 | 备注 |
|:---|:---|:---|:---|
| 2026-03-05 | 待定 | 待修改 | 规范需更新以反映缺失的拖拽链路 |
97 changes: 97 additions & 0 deletions docs/specs/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 任务清单 (Task Breakdown)

> **功能名称:** 文件树拖拽生成 ContextMention(文件 + 目录)
> **关联规范:** `docs/specs/product.md` · `docs/specs/architecture.md`
> **最后更新:** 2026-03-05
> **进度:** 8 / 8 已完成

---

## 执行规则

1. **严格顺序执行:** 从上到下,一次只处理一个 `- [ ]` 复选框
2. **单任务约束:** 每个复选框完成后必须经过验证,才可标记为 `- [x]`
3. **禁止跳跃:** 不得跳过任何任务,除非人类明确指示 "跳过"(标记为 `- [~]`)
4. **退回机制:** 如发现需要修改架构设计,必须暂停并退回到 `architecture.md` 修改

---

## 阶段 1:基础设施 (Foundation)

- [x] **T-001:** 在 FileTree 节点加入拖拽 payload(文件 + 目录)
- 📁 涉及文件:`src/components/ai-elements/file-tree.tsx`
- ✅ 验证标准:拖拽文件/目录时 DataTransfer 带有自定义 MIME payload
- ⏱️ 预估工程量:0.5-1 小时
- 🔗 依赖:无

- [x] **T-002:** 在 MessageInput 添加 ContextMention 状态与 chip 渲染
- 📁 涉及文件:`src/components/chat/MessageInput.tsx`
- ✅ 验证标准:可添加/移除文件与目录 chip,样式可见
- ⏱️ 预估工程量:1 小时
- 🔗 依赖:T-001

---

## 阶段 2:核心逻辑 (Core Logic)

- [x] **T-003:** 实现 FileTreeAttachmentBridge(监听 `attach-file-to-chat` 并添加附件)
- 📁 涉及文件:`src/components/chat/MessageInput.tsx`
- ✅ 验证标准:触发事件后附件 capsule 出现;失败时插入 `@path`
- ⏱️ 预估工程量:1 小时
- 🔗 依赖:T-002

- [x] **T-004:** 在 MessageInput 实现拖拽 drop 处理(文件 -> 附件 + chip;目录 -> chip)
- 📁 涉及文件:`src/components/chat/MessageInput.tsx`
- ✅ 验证标准:拖拽文件/目录符合 US-001/US-002
- ⏱️ 预估工程量:1 小时
- 🔗 依赖:T-003

- [x] **T-005:** 发送前去重 ContextMention 前缀与输入中的 `@path`
- 📁 涉及文件:`src/components/chat/MessageInput.tsx`
- ✅ 验证标准:当输入包含 `@path` 时发送内容只出现一次该路径
- ⏱️ 预估工程量:0.5 小时
- 🔗 依赖:T-004

---

## 阶段 3:接口层 (Interface Layer)

- [x] **T-006:** 使用 CDP 验证拖拽交互与 console 无报错
- 📁 涉及文件:`src/components/chat/MessageInput.tsx`
- ✅ 验证标准:拖拽文件/目录行为符合 US-001~US-003
- ⏱️ 预估工程量:0.5-1 小时
- 🔗 依赖:T-005

---

## 阶段 4:测试与集成 (Testing & Integration)

- [x] **T-007:** 运行 `npm run test`
- 📁 涉及文件:`package.json`
- ✅ 验证标准:命令零退出码
- ⏱️ 预估工程量:0.2 小时
- 🔗 依赖:T-006

- [x] **T-008:** 创建分支、提交修改并使用 `gh pr create` 提交 PR
- 📁 涉及文件:`.git/`
- ✅ 验证标准:PR 指向原作者仓库 `main` 且包含本次变更
- ⏱️ 预估工程量:0.2 小时
- 🔗 依赖:T-007

---

## 风险标记

> 以下任务涉及高风险系统变更,必须请求人类深度审查。

| 任务 ID | 风险类别 | 风险描述 |
|:---|:---|:---|
| — | — | 无高风险变更 |

---

## 完成日志

| 任务 ID | 完成时间 | Commit Hash | 备注 |
|:---|:---|:---|:---|
| — | — | — | 暂无完成任务 |
22 changes: 18 additions & 4 deletions src/__tests__/unit/claude-session-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@ import assert from 'node:assert/strict';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { pathToFileURL } from 'node:url';

// We test the parser functions by creating temporary JSONL files
// that mimic Claude Code's session storage format.

const TEST_DIR = path.join(os.tmpdir(), `codepilot-test-sessions-${Date.now()}`);
const PROJECTS_DIR = path.join(TEST_DIR, '.claude', 'projects');
const originalEnv = {
HOME: process.env.HOME,
USERPROFILE: process.env.USERPROFILE,
HOMEDRIVE: process.env.HOMEDRIVE,
HOMEPATH: process.env.HOMEPATH,
};

// Helper to create a JSONL session file
function createSessionFile(
Expand Down Expand Up @@ -121,18 +128,25 @@ describe('claude-session-parser', () => {
let parser: typeof import('../../lib/claude-session-parser');

before(async () => {
// Set HOME to our test directory so the parser looks for sessions there
// Point all common home env vars to test dir so os.homedir() is deterministic on Windows/macOS/Linux.
const parsed = path.parse(TEST_DIR);
process.env.HOME = TEST_DIR;
process.env.USERPROFILE = TEST_DIR;
process.env.HOMEDRIVE = parsed.root.replace(/[\\\/]$/, '');
process.env.HOMEPATH = TEST_DIR.slice(parsed.root.length - 1);

// Dynamic import - tsx handles the TypeScript + path alias resolution
parser = await import(parserPath);
parser = await import(pathToFileURL(parserPath).href);
});

after(() => {
// Clean up test directory
fs.rmSync(TEST_DIR, { recursive: true, force: true });
// Restore HOME
process.env.HOME = os.homedir();
// Restore env
process.env.HOME = originalEnv.HOME;
process.env.USERPROFILE = originalEnv.USERPROFILE;
process.env.HOMEDRIVE = originalEnv.HOMEDRIVE;
process.env.HOMEPATH = originalEnv.HOMEPATH;
});

describe('decodeProjectPath', () => {
Expand Down
Loading
Loading