From d7be928c385a03ae155f6fc33863fac232fe124b Mon Sep 17 00:00:00 2001 From: gy212 <2124065319@qq.com> Date: Thu, 5 Mar 2026 19:52:29 +0800 Subject: [PATCH 1/4] feat(chat): support file tree drag to context mentions Implement file/folder drag payloads from FileTree and handle drops in MessageInput. File drops now add both attachment and context mention chip; folder drops add directory chip. Keep attachment failure fallback by inserting @path and dedupe mention prefixes on send. Fix claude-session-parser unit tests on Windows by using file URL import and deterministic home env overrides. Add spec-coding artifacts under docs/specs for this feature. --- docs/specs/architecture.md | 120 +++++++++ docs/specs/product.md | 102 ++++++++ docs/specs/tasks.md | 97 +++++++ .../unit/claude-session-parser.test.ts | 22 +- src/components/ai-elements/file-tree.tsx | 29 ++ src/components/chat/MessageInput.tsx | 247 ++++++++++++++++-- 6 files changed, 596 insertions(+), 21 deletions(-) create mode 100644 docs/specs/architecture.md create mode 100644 docs/specs/product.md create mode 100644 docs/specs/tasks.md diff --git a/docs/specs/architecture.md b/docs/specs/architecture.md new file mode 100644 index 00000000..c87f4ca1 --- /dev/null +++ b/docs/specs/architecture.md @@ -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 | 待定 | 待修改 | 规范需更新以反映缺失的拖拽链路 | diff --git a/docs/specs/product.md b/docs/specs/product.md new file mode 100644 index 00000000..9f2b9df6 --- /dev/null +++ b/docs/specs/product.md @@ -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 | 待定 | 待修改 | 规范需更新以反映缺失的拖拽链路 | diff --git a/docs/specs/tasks.md b/docs/specs/tasks.md new file mode 100644 index 00000000..96887c4b --- /dev/null +++ b/docs/specs/tasks.md @@ -0,0 +1,97 @@ +# 任务清单 (Task Breakdown) + +> **功能名称:** 文件树拖拽生成 ContextMention(文件 + 目录) +> **关联规范:** `docs/specs/product.md` · `docs/specs/architecture.md` +> **最后更新:** 2026-03-05 +> **进度:** 7 / 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 + +- [ ] **T-008:** 创建分支、提交修改并使用 `gh pr create` 提交 PR + - 📁 涉及文件:`.git/` + - ✅ 验证标准:PR 指向原作者仓库 `main` 且包含本次变更 + - ⏱️ 预估工程量:0.2 小时 + - 🔗 依赖:T-007 + +--- + +## 风险标记 + +> 以下任务涉及高风险系统变更,必须请求人类深度审查。 + +| 任务 ID | 风险类别 | 风险描述 | +|:---|:---|:---| +| — | — | 无高风险变更 | + +--- + +## 完成日志 + +| 任务 ID | 完成时间 | Commit Hash | 备注 | +|:---|:---|:---|:---| +| — | — | — | 暂无完成任务 | diff --git a/src/__tests__/unit/claude-session-parser.test.ts b/src/__tests__/unit/claude-session-parser.test.ts index b3d94b00..51a5a296 100644 --- a/src/__tests__/unit/claude-session-parser.test.ts +++ b/src/__tests__/unit/claude-session-parser.test.ts @@ -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( @@ -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', () => { diff --git a/src/components/ai-elements/file-tree.tsx b/src/components/ai-elements/file-tree.tsx index b2b7015b..e864f17d 100644 --- a/src/components/ai-elements/file-tree.tsx +++ b/src/components/ai-elements/file-tree.tsx @@ -24,6 +24,9 @@ import { useState, } from "react"; +const FILE_TREE_DRAG_MIME = "application/x-codepilot-path"; +const FILE_TREE_DRAG_FALLBACK_MIME = "text/x-codepilot-path"; + interface FileTreeContextType { expandedPaths: Set; togglePath: (path: string) => void; @@ -132,6 +135,17 @@ export const FileTreeFolder = ({ togglePath(path); }, [togglePath, path]); + const handleDragStart = useCallback( + (e: React.DragEvent) => { + const payload = JSON.stringify({ path, name, type: "directory" }); + e.dataTransfer.setData(FILE_TREE_DRAG_MIME, payload); + e.dataTransfer.setData(FILE_TREE_DRAG_FALLBACK_MIME, payload); + e.dataTransfer.setData("text/plain", path); + e.dataTransfer.effectAllowed = "copy"; + }, + [name, path] + ); + const folderContextValue = useMemo( () => ({ isExpanded, name, path }), [isExpanded, name, path] @@ -148,6 +162,8 @@ export const FileTreeFolder = ({ >
+ + ); + })} +
+ ); +} + export function MessageInput({ onSend, onImageGenerate, @@ -380,6 +508,7 @@ export function MessageInput({ const popoverRef = useRef(null); const searchInputRef = useRef(null); const modelMenuRef = useRef(null); + const dropZoneRef = useRef(null); const [popoverMode, setPopoverMode] = useState(null); const [popoverItems, setPopoverItems] = useState([]); @@ -395,6 +524,76 @@ export function MessageInput({ const [aiSearchLoading, setAiSearchLoading] = useState(false); const aiSearchAbortRef = useRef(null); const aiSearchTimerRef = useRef | null>(null); + const [isDragOver, setIsDragOver] = useState(false); + const [contextMentions, setContextMentions] = useState([]); + + const removeContextMention = useCallback((id: string) => { + setContextMentions((prev) => prev.filter((m) => m.id !== id)); + }, []); + + const addContextMention = useCallback((path: string, name: string, type: 'file' | 'directory') => { + setContextMentions((prev) => { + if (prev.some((m) => m.path === path)) return prev; + return [...prev, { id: nanoid(), path, name, type }]; + }); + setTimeout(() => textareaRef.current?.focus(), 0); + }, []); + + const appendPathMention = useCallback((path: string) => { + setInputValue((prev) => { + const suffix = `@${path} `; + return prev ? `${prev}${suffix}` : suffix; + }); + setTimeout(() => textareaRef.current?.focus(), 0); + }, []); + + useEffect(() => { + const el = dropZoneRef.current; + if (!el) return; + + const onDragOver = (e: DragEvent) => { + const isTreeDrag = hasDragType(e.dataTransfer, FILE_TREE_DRAG_MIME) + || hasDragType(e.dataTransfer, FILE_TREE_DRAG_FALLBACK_MIME); + + if (isTreeDrag) { + e.preventDefault(); + e.stopPropagation(); + if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; + setIsDragOver(true); + } + }; + + const onDragLeave = (e: DragEvent) => { + if (el.contains(e.relatedTarget as Node)) return; + setIsDragOver(false); + }; + + const onDrop = (e: DragEvent) => { + setIsDragOver(false); + const data = readFileTreeDropData(e.dataTransfer); + if (!data) return; + e.preventDefault(); + e.stopPropagation(); + if (data.type === 'file') { + window.dispatchEvent( + new CustomEvent('attach-file-to-chat', { detail: { path: data.path } }) + ); + const mentionName = data.name || data.path.split(/[/\\]/).pop() || data.path; + addContextMention(data.path, mentionName, 'file'); + } else { + addContextMention(data.path, data.name || data.path, 'directory'); + } + }; + + el.addEventListener('dragover', onDragOver); + el.addEventListener('dragleave', onDragLeave); + el.addEventListener('drop', onDrop); + return () => { + el.removeEventListener('dragover', onDragOver); + el.removeEventListener('dragleave', onDragLeave); + el.removeEventListener('drop', onDrop); + }; + }, [addContextMention]); // Fetch provider groups from API const fetchProviderModels = useCallback(() => { @@ -602,7 +801,14 @@ export function MessageInput({ const handleSubmit = useCallback(async (msg: { text: string; files: Array<{ type: string; url: string; filename?: string; mediaType?: string }> }, e: FormEvent) => { e.preventDefault(); - const content = inputValue.trim(); + const rawContent = inputValue.trim(); + const mentionPrefix = contextMentions.length > 0 + ? contextMentions + .filter((m) => !rawContent.includes(`@${m.path}`)) + .map((m) => `@${m.path}`) + .join(' ') + : ''; + const content = [mentionPrefix, rawContent].filter(Boolean).join(' '); closePopover(); @@ -640,6 +846,7 @@ export function MessageInput({ deleteRefImages(PENDING_KEY); } + setContextMentions([]); setInputValue(''); if (onSend) { onSend(content, files.length > 0 ? files : undefined, IMAGE_AGENT_SYSTEM_PROMPT); @@ -679,6 +886,7 @@ export function MessageInput({ const files = await convertFiles(); setBadge(null); + setContextMentions([]); setInputValue(''); onSend(finalPrompt, files.length > 0 ? files : undefined); return; @@ -724,8 +932,9 @@ export function MessageInput({ } onSend(content || 'Please review the attached file(s).', hasFiles ? files : undefined); + setContextMentions([]); setInputValue(''); - }, [inputValue, onSend, onImageGenerate, onCommand, disabled, isStreaming, closePopover, badge, imageGen]); + }, [inputValue, onSend, onImageGenerate, onCommand, disabled, isStreaming, closePopover, badge, imageGen, contextMentions]); const filteredItems = popoverItems.filter((item) => { const q = popoverFilter.toLowerCase(); @@ -908,7 +1117,10 @@ export function MessageInput({ return (
-
+
{/* Popover */} {popoverMode && (allDisplayedItems.length > 0 || aiSearchLoading) && (() => { const builtInItems = filteredItems.filter(item => item.builtIn); @@ -1068,8 +1280,7 @@ export function MessageInput({ accept="" multiple > - {/* Bridge: listens for file tree "+" button events */} - + {/* Command badge */} {badge && (
@@ -1092,6 +1303,7 @@ export function MessageInput({ )} {/* File attachment capsules */} + - - - + 0} + /> + +
From a73e5d551d6c47cfa2bcec168fbdcc257b06b1d0 Mon Sep 17 00:00:00 2001 From: gy212 <2124065319@qq.com> Date: Thu, 5 Mar 2026 19:55:14 +0800 Subject: [PATCH 2/4] docs(spec): mark drag feature tasks complete --- docs/specs/tasks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/specs/tasks.md b/docs/specs/tasks.md index 96887c4b..f6f8a20f 100644 --- a/docs/specs/tasks.md +++ b/docs/specs/tasks.md @@ -3,7 +3,7 @@ > **功能名称:** 文件树拖拽生成 ContextMention(文件 + 目录) > **关联规范:** `docs/specs/product.md` · `docs/specs/architecture.md` > **最后更新:** 2026-03-05 -> **进度:** 7 / 8 已完成 +> **进度:** 8 / 8 已完成 --- @@ -72,7 +72,7 @@ - ⏱️ 预估工程量:0.2 小时 - 🔗 依赖:T-006 -- [ ] **T-008:** 创建分支、提交修改并使用 `gh pr create` 提交 PR +- [x] **T-008:** 创建分支、提交修改并使用 `gh pr create` 提交 PR - 📁 涉及文件:`.git/` - ✅ 验证标准:PR 指向原作者仓库 `main` 且包含本次变更 - ⏱️ 预估工程量:0.2 小时 From 56a02eb784fe0d4529b109b960fc1686205efaf8 Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Tue, 10 Mar 2026 15:48:18 +0800 Subject: [PATCH 3/4] fix: address PR 171 review feedback --- docs/specs/architecture.md | 120 ------------------------------------- docs/specs/product.md | 102 ------------------------------- docs/specs/tasks.md | 97 ------------------------------ 3 files changed, 319 deletions(-) delete mode 100644 docs/specs/architecture.md delete mode 100644 docs/specs/product.md delete mode 100644 docs/specs/tasks.md diff --git a/docs/specs/architecture.md b/docs/specs/architecture.md deleted file mode 100644 index c87f4ca1..00000000 --- a/docs/specs/architecture.md +++ /dev/null @@ -1,120 +0,0 @@ -# 架构设计 (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 | 待定 | 待修改 | 规范需更新以反映缺失的拖拽链路 | diff --git a/docs/specs/product.md b/docs/specs/product.md deleted file mode 100644 index 9f2b9df6..00000000 --- a/docs/specs/product.md +++ /dev/null @@ -1,102 +0,0 @@ -# 需求规范 (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 | 待定 | 待修改 | 规范需更新以反映缺失的拖拽链路 | diff --git a/docs/specs/tasks.md b/docs/specs/tasks.md deleted file mode 100644 index f6f8a20f..00000000 --- a/docs/specs/tasks.md +++ /dev/null @@ -1,97 +0,0 @@ -# 任务清单 (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 | 备注 | -|:---|:---|:---|:---| -| — | — | — | 暂无完成任务 | From 377814a97c16c180d8dce3aae4548f9d9f5ace1e Mon Sep 17 00:00:00 2001 From: Codex Automation Date: Tue, 10 Mar 2026 15:49:17 +0800 Subject: [PATCH 4/4] fix: improve drag mention UX and i18n --- src/components/ai-elements/file-tree.tsx | 1 + src/components/chat/MessageInput.tsx | 49 ++++-------------------- src/i18n/en.ts | 1 + src/i18n/zh.ts | 1 + 4 files changed, 11 insertions(+), 41 deletions(-) diff --git a/src/components/ai-elements/file-tree.tsx b/src/components/ai-elements/file-tree.tsx index e864f17d..9d56ebc0 100644 --- a/src/components/ai-elements/file-tree.tsx +++ b/src/components/ai-elements/file-tree.tsx @@ -25,6 +25,7 @@ import { } from "react"; const FILE_TREE_DRAG_MIME = "application/x-codepilot-path"; +// Keep a text/* fallback because some drag-and-drop consumers strip unknown custom MIME types. const FILE_TREE_DRAG_FALLBACK_MIME = "text/x-codepilot-path"; interface FileTreeContextType { diff --git a/src/components/chat/MessageInput.tsx b/src/components/chat/MessageInput.tsx index 45682314..8588b6ce 100644 --- a/src/components/chat/MessageInput.tsx +++ b/src/components/chat/MessageInput.tsx @@ -84,23 +84,12 @@ const IMAGE_AGENT_SYSTEM_PROMPT = `你是一个图像生成助手。当用户请 - 在输出结构化块之前,可以先简要说明你的理解和计划`; const FILE_TREE_DRAG_MIME = 'application/x-codepilot-path'; +// Keep a text/* fallback because some drag-and-drop bridges drop unknown custom MIME types. const FILE_TREE_DRAG_FALLBACK_MIME = 'text/x-codepilot-path'; function hasDragType(dataTransfer: DataTransfer | null | undefined, type: string): boolean { if (!dataTransfer) return false; - - const types = dataTransfer.types as unknown; - if (!types) return false; - - if (typeof (types as { includes?: unknown }).includes === 'function') { - return (types as { includes: (value: string) => boolean }).includes(type); - } - - if (typeof (types as { contains?: unknown }).contains === 'function') { - return (types as { contains: (value: string) => boolean }).contains(type); - } - - return Array.from(types as ArrayLike).includes(type); + return Array.from(dataTransfer.types).includes(type); } function readFileTreeDropData(dataTransfer: DataTransfer | null | undefined) { @@ -419,34 +408,9 @@ function getMentionColorClasses(mention: ContextMention): { chip: string; hover: hover: "hover:bg-amber-500/20", }; } - const ext = mention.name.split('.').pop()?.toLowerCase() || ''; - if (['doc', 'docx'].includes(ext)) { - return { - chip: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20", - hover: "hover:bg-blue-500/20", - }; - } - if (['xls', 'xlsx', 'csv'].includes(ext)) { - return { - chip: "bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20", - hover: "hover:bg-green-500/20", - }; - } - if (ext === 'pdf') { - return { - chip: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20", - hover: "hover:bg-red-500/20", - }; - } - if (ext === 'txt') { - return { - chip: "bg-gray-500/10 text-gray-600 dark:text-gray-400 border-gray-500/20", - hover: "hover:bg-gray-500/20", - }; - } return { - chip: "bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20", - hover: "hover:bg-violet-500/20", + chip: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20", + hover: "hover:bg-blue-500/20", }; } @@ -457,9 +421,10 @@ function ContextMentionChips({ mentions: ContextMention[]; onRemove: (id: string) => void; }) { + const { t } = useTranslation(); if (mentions.length === 0) return null; return ( -
+
{mentions.map((m) => { const colors = getMentionColorClasses(m); return ( @@ -475,6 +440,8 @@ function ContextMentionChips({