# Phase 6: 在输入框输入@时，可引用已生成文件和已上传附件 - Research

**Researched:** 2026-04-15  
**Domain:** 聊天输入框 `@` 文件引用（thread 内 artifacts + uploads）  
**Confidence:** HIGH

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- **D-01:** 引用来源限定为“当前线程”的 `artifacts + uploads`，不做跨线程或全局文件池。
- **D-02:** 输入 `@` 即刻弹出候选面板；继续输入即进行过滤。
- **D-03:** 选中文件后，在输入框内展示为可删除标签（chip），而非纯文本 `@文件名`。
- **D-04:** 同名文件场景下，候选项展示“文件名 + 类型徽标 + 路径尾段”，避免歧义。
- **D-09:** `@` 触发后的文件选择面板必须使用 dropdown 组件实现（不使用自定义浮层替代）。
- **D-05:** 复用 `additional_kwargs.files` 作为提交数据结构，不新增并行主结构。
- **D-06:** 在 `files` 项内增加来源/类型元信息（如 `ref_kind` / `ref_source`），用于区分“引用文件”与“上传文件”，保持与现有渲染链路兼容。
- **D-07:** 采用软失败：引用项失效时自动剔除并给出 toast，不阻止整条消息发送。
- **D-08:** 每条消息最多允许 10 个引用文件，超限时给出提示并阻止继续添加。

### Claude's Discretion
- `@` 候选面板的具体键盘交互细节（上下选择、回车确认、Esc 关闭）的实现方式。
- chip 的具体视觉样式与动画，不改变已确认交互语义。
- `ref_kind` / `ref_source` 的精确字段命名（前提是语义清晰且不破坏现有消费逻辑）。

### Deferred Ideas (OUT OF SCOPE)
- 跨线程/全局文件引用能力（可作为后续独立 phase）。
- 基于语义检索或标签检索的高级文件查找（超出本阶段范围）。
</user_constraints>

## Project Constraints (from CLAUDE.md)

- 仓库根目录未发现 `CLAUDE.md`，无额外项目级强制约束可继承。[VERIFIED: codebase grep]
- 仓库根目录未发现 `AGENTS.md`，无额外项目级指令文件可继承。[VERIFIED: codebase grep]
- 未发现 `.claude/skills/` 或 `.agents/skills/` 项目技能目录。[VERIFIED: codebase grep]

## Summary

本阶段最稳妥方案是“仅在现有输入与提交链路上加一层 thread-scoped 引用状态”，不改后端主契约、不引入新存储：`InputBox/PromptInputTextarea` 负责 `@` 触发与候选选择，`useThreadStream.sendMessage` 继续作为唯一提交汇总点，把“上传文件 + 引用文件”统一写入 `additional_kwargs.files`。[VERIFIED: codebase grep]

当前代码已具备三块可复用能力：1) 输入框附件管理与提交 (`PromptInput`/`PromptInputMessage`)，2) 当前线程 artifacts 来源 (`thread.values.artifacts`)，3) 当前线程 uploads 查询 API (`/api/threads/{threadId}/uploads/list`)；因此本 phase 核心是“状态拼接与交互补全”，而不是基础设施建设。[VERIFIED: codebase grep]

约束上最关键的是 D-09 与 D-05：候选面板必须基于现有 dropdown（Radix 封装）实现，且最终协议必须落到 `additional_kwargs.files`，这意味着应避免“独立 mention payload”或“自绘浮层”两类分叉实现。[VERIFIED: codebase grep][CITED: https://www.radix-ui.com/primitives/docs/components/dropdown-menu]

**Primary recommendation:** 在 `InputBox` 增加 `referencedFiles`（chip 状态）+ dropdown 候选层，在 `useThreadStream` 合并为单一 `additional_kwargs.files` 提交，并为失效引用执行发送前软剔除。[VERIFIED: codebase grep]

## Standard Stack

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| `@radix-ui/react-dropdown-menu` | `2.1.16` (project) / `2.1.16` (latest) | `@` 候选弹层、焦点管理、键盘导航 | 仓库已封装 `components/ui/dropdown-menu.tsx`，且官方支持完整键盘导航与焦点管理。[VERIFIED: npm registry][VERIFIED: codebase grep][CITED: https://www.radix-ui.com/primitives/docs/components/dropdown-menu] |
| `@tanstack/react-query` | `5.90.17` (project) / `5.99.0` (latest) | 复用 uploads 列表查询缓存与失效机制 | 现有 `useUploadedFiles` 已标准化 thread 级文件查询，不应手写请求状态机。[VERIFIED: npm registry][VERIFIED: codebase grep] |
| `sonner` | `2.0.7` (project) / `2.0.7` (latest) | 软失败 toast（引用失效/超限） | 现有错误提示链路已统一使用 `toast.error`，保持一致性最小回归。[VERIFIED: npm registry][VERIFIED: codebase grep] |

### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| `react` | `19.0.0` (project) / `19.2.5` (latest) | 输入态、候选态、chip 态管理 | 本 phase 只做组件内状态扩展，不做 React 升级。[VERIFIED: npm registry][VERIFIED: codebase grep] |
| Internal: `PromptInput` + `useThreadStream` | current repo | 输入与提交主链路 | 所有 `@` 行为应挂接在该链路，避免并行提交路径。[VERIFIED: codebase grep] |

### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Dropdown 组件 | 自定义绝对定位浮层 | 违背 D-09，且会重复处理焦点/键盘/关闭行为。[VERIFIED: codebase grep] |
| `additional_kwargs.files` 统一提交 | 新增 `mentions` 顶层字段 | 违背 D-05，增加后端与渲染兼容风险。[VERIFIED: codebase grep] |
| thread 范围候选 | 全局文件池检索 | 违背 D-01，范围失控并引入权限语义。[VERIFIED: codebase grep] |

**Installation:**
```bash
# 本 phase 无需新增依赖
```

**Version verification:**  
- `npm view @radix-ui/react-dropdown-menu version time --json` → latest `2.1.16`。[VERIFIED: npm registry]  
- `npm view @tanstack/react-query version time --json` → latest `5.99.0`（项目当前 `5.90.17`）。[VERIFIED: npm registry]  
- `npm view sonner version time --json` → latest `2.0.7`。[VERIFIED: npm registry]  
- `npm view react version time --json` → latest stable `19.2.5`（项目当前 `19.0.0`）。[VERIFIED: npm registry]

## Architecture Patterns

### Recommended Project Structure
```text
frontend/src/components/workspace/
├── input-box.tsx                  # @ 触发、候选 dropdown、chip 交互
frontend/src/components/ai-elements/
├── prompt-input.tsx               # 输入事件钩子（onChange/onKeyDown）扩展点
frontend/src/core/threads/
├── hooks.ts                       # 发送前合并 uploads + refs -> additional_kwargs.files
frontend/src/core/messages/
├── utils.ts                       # FileInMessage 类型扩展与兼容解析
```

### Pattern 1: Thread-Scoped Candidate Aggregation
**What:** 候选集合 = `thread.values.artifacts` + `useUploadedFiles(threadId)`，在前端归一为统一候选结构（含 `displayName/type/pathTail/source`）。[VERIFIED: codebase grep]  
**When to use:** 每次输入框出现 `@` 触发态时。  
**Example:**
```typescript
// Source: frontend/src/components/workspace/chats/chat-box.tsx
// Source: frontend/src/core/uploads/hooks.ts
const artifactPaths = thread.values.artifacts ?? [];
const { data: uploads } = useUploadedFiles(threadId);
const candidates = normalizeCandidates(artifactPaths, uploads?.files ?? []);
```

### Pattern 2: Chip State Separate from Raw Text
**What:** `@` 选择结果保存在独立 `referencedFiles` 状态，不把 `@xxx` 文本作为真实提交依据。  
**When to use:** 处理删除、去重、同名文件 disambiguation、上限控制。  
**Example:**
```typescript
type ReferencedFile = {
  key: string; // source + path
  filename: string;
  path: string;
  ref_source: "artifact" | "upload";
  ref_kind: "mention";
};
```

### Pattern 3: Single Submit Envelope
**What:** 发送前把“已上传附件 + 引用文件”统一组装为 `additional_kwargs.files`。  
**When to use:** `useThreadStream.sendMessage` 的 `thread.submit` 前。  
**Example:**
```typescript
// Source: frontend/src/core/threads/hooks.ts
const filesForSubmit = [...uploadedFiles, ...referencedFiles].slice(0, 10);
await thread.submit({
  messages: [{ type: "human", content, additional_kwargs: { files: filesForSubmit } }],
});
```

### Pattern 4: Soft-Fail on Stale References
**What:** 提交前校验引用项是否仍存在；失效则自动移除并 toast，不中断文本发送。  
**When to use:** 后端提交前最后一步校验。  
**Example:**
```typescript
const { validRefs, staleRefs } = validateRefs(referencedFiles, latestCandidates);
if (staleRefs.length) toast.error("部分引用已失效，已自动移除");
```

### Anti-Patterns to Avoid
- **自定义浮层替代 dropdown:** 违反 D-09，并引入焦点逃逸/关闭行为缺陷风险。[VERIFIED: codebase grep]
- **把引用仅编码进纯文本 `@文件名`:** 无法稳定区分同名文件，且删除/失效处理困难。[VERIFIED: codebase grep]
- **新增并行提交结构（如 `mentions`）:** 与当前渲染和兼容链路分叉，违反 D-05。[VERIFIED: codebase grep]

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| 候选面板交互 | 自写键盘导航/焦点环 | `DropdownMenu` (Radix) | 官方能力已覆盖焦点与键盘导航，重造成本高且易出无障碍缺陷。[CITED: https://www.radix-ui.com/primitives/docs/components/dropdown-menu] |
| 线程文件查询缓存 | 手写 `fetch + useEffect` 缓存层 | `useUploadedFiles` + React Query | 现有 query key 与失效逻辑已稳定使用于 uploads 领域。[VERIFIED: codebase grep] |
| 文件渲染协议 | 新建消息文件协议 | 复用 `additional_kwargs.files` | 现有 `message-list-item` 与 `messages/utils` 已消费该结构。[VERIFIED: codebase grep] |

**Key insight:** 本 phase 的复杂度主要来自“交互状态一致性”，不是“API 能力缺失”；复用现有协议可显著降低回归面。[VERIFIED: codebase grep]

## Common Pitfalls

### Pitfall 1: IME 输入法与 `@` 触发冲突
**What goes wrong:** 中文输入组合态误触发候选面板。  
**Why it happens:** 仅监听按键，不区分 `isComposing`。  
**How to avoid:** 与现有 Enter 逻辑一致，基于 `isComposing` / `nativeEvent.isComposing` 保护 `@` 触发。[VERIFIED: codebase grep]  
**Warning signs:** 中文拼写期间面板闪烁或误选。

### Pitfall 2: 同名文件引用歧义
**What goes wrong:** `report.md` 来自 artifact 还是 upload 无法区分。  
**Why it happens:** 候选展示缺少 path/source。  
**How to avoid:** 候选项固定显示“文件名 + 类型 + 路径尾段（或来源标签）”。[VERIFIED: codebase grep]  
**Warning signs:** 选中后 chip 文案无法回溯来源。

### Pitfall 3: 发送时覆盖已有上传文件
**What goes wrong:** 引用文件写入后把上传文件挤掉。  
**Why it happens:** 覆盖赋值而非合并数组。  
**How to avoid:** 在 `hooks.ts` 保持统一 merge（uploads first, refs append, 统一上限）。[VERIFIED: codebase grep]  
**Warning signs:** 上传成功但消息只显示引用 chip。

### Pitfall 4: 失效引用阻断发送
**What goes wrong:** 单个引用失效导致整条消息失败。  
**Why it happens:** 抛异常中断提交。  
**How to avoid:** 执行 D-07 软失败策略：剔除失效项 + toast + 继续发送文本。[VERIFIED: codebase grep]
**Warning signs:** 用户可复现“删了附件后消息无法发送”。

### Pitfall 5: Backspace 删除行为冲突
**What goes wrong:** 空输入框按退格时，附件与引用 chip 删除顺序混乱。  
**Why it happens:** 当前 `Backspace` 已绑定附件删除，需要定义 chip 优先级。[VERIFIED: codebase grep]  
**How to avoid:** 统一规则（建议：先删引用 chip，再删附件）。[ASSUMED]  
**Warning signs:** 用户感觉“按一次退格删错对象”。

## Code Examples

Verified patterns from official sources:

### 1) Dropdown 基础结构（用于 @ 候选）
```tsx
// Source: https://www.radix-ui.com/primitives/docs/components/dropdown-menu
<DropdownMenu open={open} onOpenChange={setOpen}>
  <DropdownMenuTrigger asChild>
    <button type="button">Trigger</button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start" sideOffset={4}>
    {items.map((item) => (
      <DropdownMenuItem key={item.key} onSelect={() => select(item)}>
        {item.label}
      </DropdownMenuItem>
    ))}
  </DropdownMenuContent>
</DropdownMenu>
```

### 2) 现有提交结构（需保持兼容）
```typescript
// Source: frontend/src/core/threads/hooks.ts
await thread.submit({
  messages: [
    {
      type: "human",
      content: [{ type: "text", text }],
      additional_kwargs: filesForSubmit.length > 0 ? { files: filesForSubmit } : {},
    },
  ],
});
```

### 3) 现有消息文件消费（需兼容）
```typescript
// Source: frontend/src/components/workspace/messages/message-list-item.tsx
const files = message.additional_kwargs?.files;
if (Array.isArray(files) && files.length > 0) {
  return <RichFilesList files={files as FileInMessage[]} threadId={threadId} />;
}
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| 从消息正文解析 `<uploaded_files>` 标签 | 优先使用 `additional_kwargs.files` 结构化字段；仅保留正文解析作为兼容回退 | 精确时间未知（代码中已存在回退逻辑） | 新功能应继续写结构化字段，避免文本协议漂移。[VERIFIED: codebase grep] |

**Deprecated/outdated:**
- 仅依赖 `<uploaded_files>` 文本标签作为主数据源：当前属于兼容路径，不应作为新功能主路径。[VERIFIED: codebase grep]

## Assumptions Log

| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | 后端对 `additional_kwargs.files` 中新增 `ref_kind/ref_source` 字段是前向兼容（忽略或透传） | Architecture Patterns / Standard Stack | 若不兼容，将导致提交失败或渲染异常 |
| A2 | 空输入框 Backspace 的“先删引用 chip 再删附件”顺序是更符合用户预期的规则 | Common Pitfalls | 若预期相反，会造成交互争议，需要产品确认 |

## Resolved Questions

1. **`ref_kind/ref_source` 的最终字段名与枚举值**
   - Resolution: 保持 `ref_kind: "mention"` 与 `ref_source: "artifact" | "upload"`，不再改名。
   - Why resolved: Phase 6 已有计划与验证链路都围绕这两个字段展开，且提交契约仍固定落在 `additional_kwargs.files`，符合 D-05/D-06。[VERIFIED: 06-01-PLAN, 06-VERIFICATION]
   - Planning impact: gap-closure 只允许补强验证与 UI 去歧义，不再重新设计字段名。

2. **同名同路径尾段时的最终去歧义显示**
   - Resolution: 固定为“文件名 + 类型徽标 + 路径尾段”，若路径尾段仍冲突，再附加 `source` 徽标作为第四层提示，但不替代“类型”维度。
   - Why resolved: 这与锁定决策 D-04 完全对齐，也正是 06-05 要关闭的 verification gap。
   - Planning impact: 06-05 必须在候选与已选引用预览中都兑现该展示合同，不允许回退为仅 `pathTail/ref_source`。

## Environment Availability

| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Node.js | 前端构建/测试 | ✓ | `v24.14.0` | — |
| npm | registry 校验/脚本 | ✓ | `11.9.0` | — |
| pnpm | 项目脚本执行 | ✓ | `10.32.1` | `npm`（不推荐，锁文件不同） |
| Playwright CLI | E2E 验证 | ✓ | `1.59.1` | 仅做单测/静态检查（覆盖不足） |
| Frontend dev server (`127.0.0.1:3000`) | 本地 E2E 运行 | ✗ | — | 启动 `pnpm --dir frontend dev` |
| Backend API (`127.0.0.1:8000`) | uploads/artifacts 联调 | ✗ | — | 启动后端服务或使用 mock 断言 |

**Missing dependencies with no fallback:**
- 无（CLI 工具均可用）。[VERIFIED: local command]

**Missing dependencies with fallback:**
- 本地前后端服务当前未运行，可通过启动命令补齐。[VERIFIED: local command]

## Validation Architecture

### Test Framework
| Property | Value |
|----------|-------|
| Framework | Playwright `1.59.1` + existing unit tests (`*.test.ts/.mjs`) |
| Config file | `frontend/playwright.config.ts` |
| Quick run command | `pnpm --dir frontend playwright test frontend/tests/e2e/input-and-compose.spec.ts` |
| Full suite command | `pnpm --dir frontend test:e2e` |

### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| D-01/D-02 | `@` 仅展示当前线程候选并可过滤 | e2e | `pnpm --dir frontend playwright test frontend/tests/e2e/input-and-compose.spec.ts -g "@候选"` | ❌ Wave 0 |
| D-03/D-08 | 选中后显示 chip；最多 10 个 | e2e | `pnpm --dir frontend playwright test frontend/tests/e2e/input-and-compose.spec.ts -g "DF-INPUT-009"` | ✅ 由 06-05 落地 |
| D-05/D-06 | 提交落入 `additional_kwargs.files` 且含来源元信息 | unit/integration | `pnpm --dir frontend node --test frontend/src/core/threads/hooks.test.ts` | ✅（需扩展用例） |
| D-07 | 失效引用软失败，不阻断发送 | e2e | `pnpm --dir frontend playwright test frontend/tests/e2e/input-and-compose.spec.ts -g "stale ref"` | ❌ Wave 0 |

### Sampling Rate
- **Per task commit:** `pnpm --dir frontend playwright test frontend/tests/e2e/input-and-compose.spec.ts`
- **Per wave merge:** `pnpm --dir frontend test:e2e`
- **Phase gate:** Full suite green before `/gsd-verify-work`

### Wave 0 Gaps
- [x] `frontend/src/core/threads/hooks.test.ts` — 已覆盖 uploads+refs 合并与 soft-fail 场景断言（06-01 / 06-03）。
- [x] `frontend/tests/e2e/input-and-compose.spec.ts` — 已作为主 E2E 文件承接 D-01~D-08；06-05 继续补稳 DF-INPUT-008/009。
- [x] `frontend/src/core/messages/utils.ts` 契约验证 — 由 06-01 类型契约与 hooks 单测共同覆盖，不再拆独立测试文件。

## Security Domain

### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | no | 由现有会话体系负责（本 phase 不新增认证机制） |
| V3 Session Management | no | 复用现有线程会话 |
| V4 Access Control | yes | 严格 thread 范围候选来源（artifacts/uploads with threadId） |
| V5 Input Validation | yes | 前端仅提交候选池中的受控文件元数据，不信任自由文本路径 |
| V6 Cryptography | no | 本 phase 不引入加密实现 |

### Known Threat Patterns for frontend mention-reference flow
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| 跨线程文件枚举（IDOR） | Information Disclosure | 候选源仅取当前 `threadId` 的 artifacts/uploads，禁止全局检索 |
| 客户端伪造文件路径 | Tampering | 提交前按候选池二次校验，失效项软剔除 |
| 文件名注入 UI（异常字符） | Tampering | 渲染时只做文本展示，不执行 HTML；沿用现有 React 转义 |
| 超量引用导致 UI/消息膨胀 | Denial of Service | 强制上限 10 并阻止继续添加 |

## Sources

### Primary (HIGH confidence)
- `frontend/src/components/workspace/input-box.tsx` - 输入框组合、提交入口、附件 UI。[VERIFIED: codebase grep]
- `frontend/src/components/ai-elements/prompt-input.tsx` - 文本/附件状态、键盘行为、`PromptInputMessage`。[VERIFIED: codebase grep]
- `frontend/src/core/threads/hooks.ts` - `additional_kwargs.files` 提交与上传流程。[VERIFIED: codebase grep]
- `frontend/src/components/workspace/messages/message-list-item.tsx` - `additional_kwargs.files` 渲染消费。[VERIFIED: codebase grep]
- `frontend/src/core/messages/utils.ts` - `FileInMessage` 与兼容解析（含 `<uploaded_files>` 回退）。[VERIFIED: codebase grep]
- `frontend/src/core/uploads/api.ts` / `frontend/src/core/uploads/hooks.ts` - 当前线程 uploads API 与 query 封装。[VERIFIED: codebase grep]
- npm registry (`npm view ...`) - 版本与发布时间校验。[VERIFIED: npm registry]

### Secondary (MEDIUM confidence)
- Radix Dropdown Menu official docs: https://www.radix-ui.com/primitives/docs/components/dropdown-menu  （能力说明：focus management / keyboard navigation）。[CITED: https://www.radix-ui.com/primitives/docs/components/dropdown-menu]
- TanStack Query official docs (React v5): https://tanstack.com/query/latest/docs/framework/react/overview  （现有 query 模型一致性参考）。[CITED: https://tanstack.com/query/latest/docs/framework/react/overview]

### Tertiary (LOW confidence)
- 无。

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH - 主要基于仓库现有依赖与 npm registry 实时校验。
- Architecture: HIGH - 关键链路（输入->提交->渲染）均在代码中可直接定位。
- Pitfalls: MEDIUM - 大部分可由现有行为推导，个别交互优先级仍需产品确认。

**Research date:** 2026-04-15  
**Valid until:** 2026-05-15（30 天）
