Skip to content

feat(chat): accept OS file-manager drops as external context - #64

Merged
luoxuanzao merged 11 commits into
mainfrom
feat/external-file-drop
Sep 15, 2026
Merged

luoxuanzao merged 11 commits into
mainfrom
feat/external-file-drop

Conversation

@luoxuanzao

Copy link
Copy Markdown
Member

Summary

  • Drag files and folders from the OS file manager (Finder/Explorer) onto the composer: directories and non-image files become external context roots — file paths resolve through Electron webUtils.getPathForFile (the replacement for File.path, removed in Electron 32) with a text/uri-list fallback. Images keep attaching through the image manager, and every OS drag is claimed (window-capture listeners) so the dropped file's content is never pasted into the input as text.
  • External context roots behave like vault folders for @ mentions: selecting one inserts @root/ with a folder chip and settles (typing / still drills into its files), and a bare @root/ resolves to the root's absolute path at send time.
  • Sent user-message mentions of external contexts render as the same chips (now carrying the absolute path), and clicking one reveals the item in Finder/Explorer; vault chips keep Obsidian's reveal.
  • Single-file external contexts can be persisted (the lock) across sessions; path validation is file-tolerant end to end.
  • Message render failures now log the underlying error instead of failing silently.
  • Adds a protocol-driven real-app e2e (npm run test:e2e:file-drop) plus a CONTRIBUTING note.

Verification

  • npm run typecheck
  • npm run lint
  • npm run test — 164 suites / 3536 tests pass
  • npm run test:e2e:file-drop — 4/4 against a running Obsidian (folder drop → external context, notice + badge, dragover accepted before simulated host interception, dropped content not pasted)
  • Live checks: external chip renders with the absolute path and clicking it invokes shell.showItemInFolder(<path>); single-file persistence round-trips through settings
  • Idle-cost check (the fix(chat): stop the context row re-laying out on every frame #59 lesson): 5s idle CPU profile shows the renderer ~90% idle and the plugin at ~0.05%; zero idle DOM mutations

Safety

  • No credentials, private vault content, internal URLs, or personal paths are included
  • User-visible changes are documented in CHANGELOG.md

Co-authored-by: QoderAI (Qwen 3.8 Max) qoder_ai@qoder.com

luoxuanzao and others added 11 commits September 15, 2026 20:43
Finder/Explorer drags previously fell through to the browser's default drop
and pasted the file's content into the composer. Claim these drags in the
composer drop zone: directories and non-image files become external context
roots (the scanner now accepts a single file as a context root), images keep
attaching via the image manager, and the drop is always preventDefaulted so
no text is inserted.

Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
Round out OS drag-and-drop support: resolve dropped files through Electron's
webUtils.getPathForFile (File.path was removed in Electron 32), claim the
drag lifecycle on the window capture phase so host-level interceptors cannot
swallow real Finder/Explorer drops, and scope claiming to drags that end
inside the composer. Drops now report the added path or the rejection
reason, and the external-context badge shows the count even for a single
path.

Add a protocol-driven e2e check (npm run test:e2e:file-drop) that drives a
folder drop through Chromium's input protocol against a running Obsidian.

Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
`@root/` previously stayed unresolved in outgoing prompts because the
external mention resolver required a file path after the root. Resolve the
bare root to the context root's absolute path so external folders behave
like vault folder mentions.

Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
Selecting an external context root inserted `@root/` without a chip and then
immediately reopened the dropdown drilled into that root's files, so the
whole-folder reference never settled. Register a folder chip for the root and
insert `@root/ ` with a trailing space; typing `/` still drills into the
root's files.

Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
User-message bubbles only chipped `@path` tokens that resolved inside the
vault, so an external context root or file mention stayed raw text after
sending. Recognize external context roots by name (`@root/` chips as a
folder, `@root/relative/path` as a file), which needs no scan on the render
path, and pass the tab's external contexts into the renderer.

Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
External chips now carry their absolute path, and clicking one opens it in
Finder/Explorer through Electron's shell instead of silently failing the
vault lookup. Vault chips keep Obsidian's reveal behavior.

Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
The render catch replaced content with a generic message and swallowed the
cause, so a render regression looked like a plain text failure with no
diagnostic trail.

Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
Persistence validated paths with the directory-only check, so a file root
added by an OS drop could not be kept across sessions and the rejection
message was misleading. Validate with the file-tolerant check and filter
persisted settings paths the same way.

Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
filterValidPaths and isValidDirectoryPath lost their last production caller
when persistence switched to the file-tolerant validators.

Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
dragenter fires for every child element under the pointer, and Obsidian's
class helpers rewrite the attribute even when the value is unchanged.

Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
Windows normalizes the path separators, so asserting the exact POSIX
literal failed there.

Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
@luoxuanzao
luoxuanzao merged commit 80c1a12 into main Sep 15, 2026
10 checks passed
@luoxuanzao
luoxuanzao deleted the feat/external-file-drop branch September 15, 2026 14:22
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.

1 participant