Skip to content

feat: 从文件栏拖拽文件到会话上下文#384

Open
binison wants to merge 3 commits intoYishenTu:mainfrom
binison:feature/dragfile2context
Open

feat: 从文件栏拖拽文件到会话上下文#384
binison wants to merge 3 commits intoYishenTu:mainfrom
binison:feature/dragfile2context

Conversation

@binison
Copy link
Copy Markdown

@binison binison commented Mar 20, 2026

Summary

  • 支持从 Obsidian 左侧文件栏拖拽文件到聊天输入框,自动附加为会话上下文
  • 拖拽的文件路径在发送时以 <context_files> 形式注入 prompt
  • 修复 dragEnterCount 可能为负的 bug,改进外部拖拽(URL/书签)的误判检测
  • vault.getFiles() 替换低效的递归文件夹搜索

Test plan

  • 从文件栏拖拽单个文件到输入框,确认出现文件 chip
  • 发送消息后确认 prompt 中包含 <context_files> 及文件路径
  • 拖拽同一文件两次,确认不会重复附加
  • 从外部拖拽 URL/书签,确认不触发文件附加逻辑
  • 新建/加载对话时确认 pendingContextFiles 被清空
  • 运行单元测试 npm run test -- --selectProjects unit

🤖 Generated with Claude Code

binison and others added 2 commits March 20, 2026 12:20
- 用 vault.getFiles() 替换递归文件夹搜索,简化 basename 查找
- 修复 dragEnterCount 可能为负的问题(改用 Math.max)
- 改进 isObsidianFileDrag 检测,排除 URL/书签/外部纯文本拖拽
- 引入 pendingContextFiles 队列,实现发送时消费并附加 <context_files>
- 修复重复附加同一文件的 no-op 处理
- 更新测试 mock 以包含 consumeContextFiles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 20, 2026 16:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

该 PR 为聊天输入框增加了从 Obsidian 左侧文件栏拖拽文件的能力:拖拽后以 chip 形式附加到会话上下文,并在发送时将文件路径以 <context_files> 注入 prompt;同时对跨平台路径解析/拼接做了多处健壮性改进并补充了单元测试覆盖。

Changes:

  • 新增 FileDragDropManager:识别 Obsidian 文件树拖拽并将文件路径回调给 FileContextManager 进行附加与提示。
  • FileContextManager/InputController 增加 pending context files 队列:发送时注入 <context_files>,并在会话切换时清空。
  • 强化跨平台路径处理(PATH 解析、posix/win32 join、MSYS 路径转换等)并更新/新增相关单测与样式、i18n 文案。

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/unit/utils/path.test.ts 调整 normalizePathForFilesystem 环境变量展开断言以匹配新行为
tests/unit/features/chat/ui/FileDragDropManager.test.ts 新增拖拽管理器单测覆盖 overlay、解析与去重逻辑
tests/unit/features/chat/ui/FileContextManager.test.ts 增加 chip 渲染与移除行为测试
tests/unit/features/chat/tabs/Tab.test.ts 为新增 UI 组件 FileDragDropManager 补齐 mock
tests/unit/features/chat/controllers/InputController.test.ts consumeContextFiles 补齐 mock 以适配发送逻辑变更
tests/unit/core/agents/AgentManager.test.ts 修复测试中 Windows 路径分隔符导致的匹配问题
src/utils/sdkSession.ts 统一用 posix join + 斜杠规范化生成 SDK session 路径
src/utils/path.ts 重构 PATH 解析/探测与多处跨平台 join/normalize 行为
src/utils/externalContext.ts child 冲突检测改为“优先 parent 冲突,否则返回首个 child 冲突”
src/utils/env.ts 平台路径 API 统一化、增强 PATH 拼接与 node 发现逻辑
src/style/index.css 引入新增拖拽样式文件
src/style/features/file-drag-drop.css 新增拖拽 overlay 与动画样式
src/i18n/types.ts 添加 chat.fileDrop.* 翻译 key 类型
src/i18n/locales/*.json 各语言补齐 file drop notice/overlay 文案
src/features/chat/ui/index.ts 导出 FileDragDropManager
src/features/chat/ui/file-context/view/FileChipsView.ts renderCurrentNote 支持渲染当前文件 + 已附加文件 chips
src/features/chat/ui/FileDragDropManager.ts 新增拖拽解析与 overlay 展示、vault 路径解析逻辑
src/features/chat/ui/FileContext.ts 增加 pendingContextFiles + attach/consume API,并在会话切换时清空
src/features/chat/tabs/types.ts TabUIComponents 增加 fileDragDropManager
src/features/chat/tabs/Tab.ts 初始化/销毁 FileDragDropManager,drop 后 attach 并 Notice 提示
src/features/chat/controllers/InputController.ts 发送前注入 <context_files>(来自 consumeContextFiles()
src/core/agents/AgentManager.ts agents 目录扫描路径规范化;允许文件 agent 覆盖 builtin 同名 ID
scripts/run-jest.js 固定 jest 运行 cwd 为项目根目录

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

this.refreshCurrentNoteChip();
}

this.state.detachFile(filePath);
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pendingContextFiles is not updated when an attachment chip is removed. After a drag-drop attach, clicking the remove button calls state.detachFile(...) but the removed path remains in pendingContextFiles, so it can still be injected into the next prompt via consumeContextFiles() even though the UI shows it removed. Ensure detach/removal also deletes the path from pendingContextFiles (and consider updating the set on rename/delete as well so injected paths reflect current attachments).

Suggested change
this.state.detachFile(filePath);
this.state.detachFile(filePath);
this.pendingContextFiles.delete(filePath);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c94ddf5. Added this.pendingContextFiles.delete(filePath) in the onRemoveAttachment callback so that removing a chip also removes it from the pending queue, preventing it from being injected into the next prompt.

const url = new URL(trimmed);
if (url.hostname !== 'open') return null;
const file = url.searchParams.get('file');
return file ? decodeURIComponent(file) : null;
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URLSearchParams.get('file') already returns a decoded string. Calling decodeURIComponent again can throw for legitimate filenames containing % (e.g. 100% done.md), causing obsidian://open links to be ignored. Return the file value directly (or only decode when it still contains percent-escapes) to avoid double-decoding.

Suggested change
return file ? decodeURIComponent(file) : null;
return file || null;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c94ddf5. Removed the redundant decodeURIComponent call — URLSearchParams.get('file') already returns a decoded string, so double-decoding would throw for filenames containing literal % characters. Now returning file || null directly.

display: flex;
animation: claudian-drop-pulse 0.5s ease-in-out infinite;
}

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overlay uses an infinite pulse animation when visible. Consider respecting prefers-reduced-motion by disabling the animation under @media (prefers-reduced-motion: reduce) so users who reduce motion aren't forced to see continuous animation.

Suggested change
@media (prefers-reduced-motion: reduce) {
.claudian-file-drop-overlay.visible {
animation: none;
}
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c94ddf5. Added a @media (prefers-reduced-motion: reduce) block that disables the pulse animation for users who have requested reduced motion.

Comment thread src/utils/path.ts
Comment on lines +132 to +138
if (process.platform !== 'win32') {
rawEntries = pathValue.split(':');
} else if (/^[A-Za-z]:[\\/]/.test(pathValue)) {
// Single Windows absolute path entry (e.g., C:\custom\bin)
rawEntries = [pathValue];
} else if (pathValue.includes(';')) {
rawEntries = pathValue
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parsePathEntries will mis-parse a normal Windows PATH that starts with a drive letter (e.g. C:\Windows\System32;...). The /^[A-Za-z]:[\\/]/ branch treats the entire PATH string as a single entry, so subsequent ;-separated segments are never split. This can break CLI/node discovery and enhanced PATH construction on Windows. Reorder the conditions (split on ; first) or additionally require that the value does not contain ; before treating it as a single entry, and add a unit test covering a typical Windows PATH with multiple entries.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

@binison binison Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c94ddf5. Added !pathValue.includes(';') guard to the drive-letter branch so that a Windows PATH like C:\Windows\System32;D:\tools falls through to the semicolon-split branch instead of being treated as a single entry. Also added a unit test covering this case.

- FileContext: delete from pendingContextFiles on chip remove to prevent
  removed files being injected into next prompt
- FileDragDropManager: remove double decodeURIComponent (URLSearchParams
  already decodes, double-decode throws for filenames containing %)
- file-drag-drop.css: respect prefers-reduced-motion by disabling pulse animation
- path.ts: fix parsePathEntries treating multi-entry Windows PATH as single
  entry when starting with drive letter; add unit test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants