Skip to content

fix: convert dragged file paths to @mentions for attachment#382

Merged
kevincodex1 merged 9 commits intoGitlawb:mainfrom
paulovitin:fix/drag-drop-file-attachment
Apr 6, 2026
Merged

fix: convert dragged file paths to @mentions for attachment#382
kevincodex1 merged 9 commits intoGitlawb:mainfrom
paulovitin:fix/drag-drop-file-attachment

Conversation

@paulovitin
Copy link
Copy Markdown
Contributor

Summary

  • When non-image files are dragged into the terminal, the file path was inserted as plain text and never attached to the conversation
  • Added detection of absolute file paths in pasted text and automatic conversion to @mentions, so the existing attachment system picks them up on submit

Impact

  • user-facing impact: dragging files (.ts, .json, .txt, etc.) into the terminal now attaches them as @mentions instead of inserting raw paths as inert text. Image files are unaffected (already handled upstream by the image paste handler).
  • developer/maintainer impact: new utility src/utils/dragDropPaths.ts with extractDraggedFilePaths() — splits pasted text using the same logic as usePasteHandler, validates each segment is an absolute path to an existing non-image file, and returns cleaned paths. Called from onTextPaste in PromptInput.tsx.

Testing

  • bun run build
  • bun run smoke
  • focused tests: bun test src/utils/dragDropPaths.test.ts (12 tests — single path, newline-separated, space-separated, quoted paths, shell-escaped paths, image exclusion, nonexistent files, empty input)
  • bun test full suite (368 pass, 0 fail)
  • manual: dragged .ts and .json files from Finder into the terminal — paths converted to @mentions correctly

Notes

  • provider/model path tested: n/a (input handling, provider-independent)
  • screenshots attached (if UI changed): n/a (terminal input behavior, no visual change beyond the @ prefix)
  • follow-up work or known limitations: paths with spaces work via quoted @"path" format. Files must exist on disk to be converted (prevents false positives on pasted text that happens to start with /).

When non-image files are dragged into the terminal, the file path was
inserted as plain text and never attached. Now detected absolute paths
are converted to @mentions so they get picked up by the attachment system.
insertTextAtCursor read input and cursorOffset from the React closure,
which is stale when called in a synchronous loop (e.g. onImagePaste for
multiple dragged images). Now uses refs so each insertion chains on the
previous one.
kevincodex1
kevincodex1 previously approved these changes Apr 5, 2026
Copy link
Copy Markdown
Contributor

@kevincodex1 kevincodex1 left a comment

Choose a reason for hiding this comment

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

LGTM

Comment thread src/components/PromptInput/PromptInput.tsx Outdated
Copy link
Copy Markdown
Collaborator

@Vasanthdev2004 Vasanthdev2004 left a comment

Choose a reason for hiding this comment

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

Rechecked the latest head 2b3dd004d411e803f485ededc197139cb4a3e3ef.

I still can't approve this because there is one real Windows-path regression in the new @mention conversion:

  1. src/components/PromptInput/PromptInput.tsx emits unquoted Windows absolute paths like @C:\Users\me\file.txt whenever the dragged path has no spaces.
    That overlaps with the existing MCP mention syntax, because attachments.ts treats any unquoted @name:rest token as an MCP resource mention too.

    Direct repro on this head:

    • extractAtMentionedFiles('@C:\Users\me\file.txt') -> ['C:\Users\me\file.txt']
    • extractMcpResourceMentions('@C:\Users\me\file.txt') -> ['C:\Users\me\file.txt']

    So the new Windows drag/drop path format is currently ambiguous: it is treated as both a file mention and an MCP resource mention. The fix is to emit Windows absolute paths in quoted form too (for example whenever the path contains :), not only when the path contains spaces.

Fresh verification on this head:

  • bun test src/utils/dragDropPaths.test.ts -> 12 pass
  • bun run build -> success
  • bun run smoke -> success

The Unix-style drag/drop parsing and the build/smoke checks look fine, but I wouldn't merge this until the Windows mention format no longer collides with MCP resource parsing.

Paths containing ':' (e.g. Windows drive letters) are now emitted in
quoted @"..." form so they don't match the MCP resource mention regex.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Vasanthdev2004
Copy link
Copy Markdown
Collaborator

@paulovitin ready for review ?

@paulovitin
Copy link
Copy Markdown
Contributor Author

@paulovitin ready for review ?

Not yet!

- Check image extension against the cleaned path (post quote/escape
  stripping) so quoted or backslash-escaped image drops are reliably
  routed to the image paste handler.
- Inline the image extension regex and drop the imagePaste/fsOperations
  imports so the module (and its tests) no longer pull in `bun:bundle`
  and the heavier fs wrapper chain. Use plain `fs.existsSync` for the
  on-disk check.
- Add tests covering quoted image paths, uppercase extensions,
  backslash-escaped image paths, escaped real files with spaces, mixed
  segments containing an image, quoted-nonexistent paths, and leading
  or trailing whitespace.
Adds a fixture under a scoped-package-style subdir (`@types/index.d.ts`)
so we exercise the realistic `node_modules/@types/...` drag case and
lock in that `extractDraggedFilePaths` returns the raw path unchanged —
the `@` inside the path must not collide with the mention prefix the
caller prepends downstream.
Groups the 21 scenarios into four table-driven describes
(empty-result, single-path, multi-path, backslash-escaped) so that
adding a new case is a one-line row instead of a new `test()` block.
Fixture directories are now created synchronously at describe-load
time so their paths are available to the test.each tables, which are
built before any hook runs.
@Vasanthdev2004
Copy link
Copy Markdown
Collaborator

ping me when you need review @paulovitin

@paulovitin
Copy link
Copy Markdown
Contributor Author

@Vasanthdev2004, it's good to me now. Your review is welcome

const draggedPaths = extractDraggedFilePaths(text);
if (draggedPaths.length > 0) {
const mentions = draggedPaths
.map(p => (p.includes(' ') || p.includes(':') ? `@"${p}"` : `@${p}`))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This still does not avoid the MCP collision on Windows. On the current head, @\C:\Users\me\file.txt\ is still parsed by both extractors: extractAtMentionedFiles() returns C:\Users\me\file.txt, and extractMcpResourceMentions() returns the same string because its regex (/@([^\\s]+:[^\\s]+)\b/) still matches inside the quoted form. So quoting the Windows path here does not actually fix the ambiguity the previous review called out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right — I verified the collision empirically. The \b at the end of the MCP regex backtracks past the trailing " and produces a ghost match like "C:\Users\me\file.txt (leading quote included, trailing quote dropped), so the quoting in 55276b1 only hides the symptom.

I pushed the failing contract tests in 04998d5 so the bug is reproducible in the suite — 3 of the 11 cases fail on current HEAD, exactly at the boundary you described:

  • @"C:\Users\me\file.txt" — quoted Windows drive-letter path
  • @C:\Users\me\file.txt — unquoted equivalent
  • @"/tmp/weird:name.txt" — POSIX path with a : in the filename (same family)

My plan for the fix, before I push it — want your take:

  1. In extractMcpResourceMentions, tighten the regex to /(^|\s)@(?!")([^\s"]+:[^\s"]+)\b/g. The negative lookahead (?!") drops quoted tokens entirely, and adding " to the character classes prevents any partial match via backtracking even if the lookahead were ever removed.
  2. On top of that, a post-match filter that discards anything matching /^[A-Za-z]:[\\/]/ — a single-letter "server" followed by :\ or :/ is always a Windows drive-letter prefix, never a real MCP resource. This covers the unquoted case that regex alone can't disambiguate.

The legitimate MCP cases (@server:resource/path, plugin-scoped like @asana-plugin:project-status/123, inline prose mentions) stay green — the test file pins them explicitly so the fix can't over-correct.

Does this approach land where you'd want it, or would you rather see the disambiguation somewhere else (e.g. at the call site in attachments.ts by subtracting file mentions from MCP matches)? Happy to adjust.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@Vasanthdev2004 pushed the fix in 2951eb0. Both guards from the plan landed:

  1. extractMcpResourceMentions regex is now /(^|\s)@(?!")([^\s"]+:[^\s"]+)\b/g — the (?!") drops quoted tokens outright, and " in the character classes blocks any residual backtracking across the closing quote.
  2. Post-match filter strips anything matching ^[A-Za-z]:[\\/] — covers the unquoted @C:\Users\... case the regex alone can't disambiguate.

All 11 contract tests in attachments.extractors.test.ts are green, including the 3 that were failing on the previous head (@"C:\Users\me\file.txt", @C:\Users\me\file.txt, @"/tmp/weird:name.txt"). Full suite: 388/388. Legitimate MCP mentions (@server:resource/path, @asana-plugin:project-status/123, inline prose) stay matched — pinned explicitly so the fix can't over-correct.

Mind taking another look when you have a minute?

Copy link
Copy Markdown
Collaborator

@Vasanthdev2004 Vasanthdev2004 left a comment

Choose a reason for hiding this comment

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

Rechecked the latest head c2e32b6f37392786bfbaaa2664d468af74f830fa.

The newer cleanup in this branch does improve a few real edge cases:

  • quoted and escaped image paths are now correctly excluded from file-mention conversion
  • the drag/drop parser tests are broader and all pass on this head
  • build and smoke still pass

But the previous Windows-path blocker is still present on the latest head, so I still can't approve it.

Current blocker:

  1. Quoting Windows absolute paths in PromptInput does NOT actually avoid the MCP resource collision.
    Direct repro on this head:

    • extractAtMentionedFiles('@C:\Users\me\file.txt') -> ['C:\Users\me\file.txt']
    • extractMcpResourceMentions('@C:\Users\me\file.txt') -> ['C:\Users\me\file.txt']

    So @C:\... is still ambiguous: it is parsed as both a file mention and an MCP resource mention. The quoted form does not fix the underlying overlap because extractMcpResourceMentions() still matches the quoted token.

Fresh verification on this head:

  • direct repro: quoted Windows path still matches both extractors
  • direct repro: quoted image paths and escaped image paths now correctly return [] from extractDraggedFilePaths()
  • bun test src/utils/dragDropPaths.test.ts -> 19 pass
  • bun run build -> success
  • bun run smoke -> success

So the new parser hardening is good, but I still wouldn't merge it until Windows absolute-path mentions no longer collide with MCP mention parsing.

@Vasanthdev2004
Copy link
Copy Markdown
Collaborator

Clarification on my latest review: the exact current-head repro string is:

@C:\Users\me\file.txt

On c2e32b6, that quoted form still matches both extractors:

  • extractAtMentionedFiles(...)
  • extractMcpResourceMentions(...)

So the new quoting change does not yet eliminate the MCP overlap.

Pins the contract between `extractAtMentionedFiles` and
`extractMcpResourceMentions` so the MCP regex can't silently swallow
quoted file-path mentions.

These tests fail on current HEAD — 3 of 11 cases expose the regression
pointed out in the review on Gitlawb#382: `extractMcpResourceMentions`'s
trailing `\b` backtracks past the closing `"` of a quoted mention and
produces a ghost match for `@"C:\Users\..."`, `@C:\Users\...`, and
`@"/tmp/weird:name.txt"`. The remaining 8 cases lock in the behaviour
that must not change (legitimate `server:resource` mentions and plain
file-path mentions).

Committed failing on purpose as the first half of a test-then-fix
pair; the regex fix follows in a subsequent commit.
The MCP resource regex used `\b` as a trailing anchor with `[^\s]+`
character classes. On any quoted file mention containing a colon
(`@"C:\Users\me\file.txt"`, `@"/tmp/weird:name.txt"`), the engine
backtracked past the closing `"` to satisfy `\b`, producing a ghost
match that collided with `extractAtMentionedFiles`. Unquoted Windows
drive-letter paths (`@C:\Users\me\file.txt`) also matched because a
drive letter is structurally identical to an MCP `server:resource`
token.

Two guards:

1. `(?!")` right after `@` drops quoted tokens entirely, and adding
   `"` to the character classes blocks any mid-match backtracking.
2. A post-match filter discards `^[A-Za-z]:[\\/]` — a single-letter
   server followed by a path separator is always a Windows drive
   prefix, never a real MCP resource.

Legitimate MCP forms (`@server:resource/path`, plugin-scoped like
`@asana-plugin:project-status/123`, inline prose mentions) remain
matched and are pinned by the contract tests added in 04998d5.
Copy link
Copy Markdown
Collaborator

@Vasanthdev2004 Vasanthdev2004 left a comment

Choose a reason for hiding this comment

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

Rechecked the latest head 2951eb0487a65450ec8f886825df49197bb1cced.

The previous blocker is fixed on this revision.

What I verified on this head:

  • the drag/drop mention formatter in PromptInput now quotes paths containing : as well as spaces, so Windows absolute paths and POSIX filenames containing : are routed through the quoted form
  • the MCP extractor now has two guards against the old collision:
    • it skips quoted tokens entirely via (?!")
    • it filters out ^[A-Za-z]:[\\/] so bare Windows drive-letter paths are not treated as MCP resources
  • direct repro on this head:
    • extractAtMentionedFiles('@C:\Users\me\file.txt') -> ['C:\Users\me\file.txt']
    • extractMcpResourceMentions('@C:\Users\me\file.txt') -> []
  • the new extractor contract tests pass:
    • bun test src/utils/attachments.extractors.test.ts --filter extractor -> 11 pass
  • broader attachment/drag-drop checks also pass:
    • bun test src/utils/attachments.extractors.test.ts src/utils/dragDropPaths.test.ts -> 30 pass
  • bun run build -> success
  • bun run smoke -> success

I didn't find a remaining blocker on the current head.

Residual risk is small and pre-existing rather than introduced here: legitimate MCP mentions like @server:resource/path are still also visible to the generic file-mention extractor, but the branch-specific Windows/quoted-path collision that blocked this PR is addressed.

@kevincodex1 kevincodex1 merged commit 112df59 into Gitlawb:main Apr 6, 2026
1 check passed
euxaristia pushed a commit to euxaristia/openclaude that referenced this pull request Apr 13, 2026
)

* fix: convert dragged file paths to @mentions for attachment

When non-image files are dragged into the terminal, the file path was
inserted as plain text and never attached. Now detected absolute paths
are converted to @mentions so they get picked up by the attachment system.

* test: add tests for drag-and-drop file path detection

* fix: multi-image drag-and-drop only showing last image

insertTextAtCursor read input and cursorOffset from the React closure,
which is stale when called in a synchronous loop (e.g. onImagePaste for
multiple dragged images). Now uses refs so each insertion chains on the
previous one.

* fix: quote Windows absolute paths to avoid MCP mention collision

Paths containing ':' (e.g. Windows drive letters) are now emitted in
quoted @"..." form so they don't match the MCP resource mention regex.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: decouple dragDropPaths from imagePaste and harden image checks

- Check image extension against the cleaned path (post quote/escape
  stripping) so quoted or backslash-escaped image drops are reliably
  routed to the image paste handler.
- Inline the image extension regex and drop the imagePaste/fsOperations
  imports so the module (and its tests) no longer pull in `bun:bundle`
  and the heavier fs wrapper chain. Use plain `fs.existsSync` for the
  on-disk check.
- Add tests covering quoted image paths, uppercase extensions,
  backslash-escaped image paths, escaped real files with spaces, mixed
  segments containing an image, quoted-nonexistent paths, and leading
  or trailing whitespace.

* test: verify dragged paths with an `@` segment are preserved

Adds a fixture under a scoped-package-style subdir (`@types/index.d.ts`)
so we exercise the realistic `node_modules/@types/...` drag case and
lock in that `extractDraggedFilePaths` returns the raw path unchanged —
the `@` inside the path must not collide with the mention prefix the
caller prepends downstream.

* test: parametrize dragDropPaths cases with test.each

Groups the 21 scenarios into four table-driven describes
(empty-result, single-path, multi-path, backslash-escaped) so that
adding a new case is a one-line row instead of a new `test()` block.
Fixture directories are now created synchronously at describe-load
time so their paths are available to the test.each tables, which are
built before any hook runs.

* test: add contract tests for @-mention extractor boundary

Pins the contract between `extractAtMentionedFiles` and
`extractMcpResourceMentions` so the MCP regex can't silently swallow
quoted file-path mentions.

These tests fail on current HEAD — 3 of 11 cases expose the regression
pointed out in the review on Gitlawb#382: `extractMcpResourceMentions`'s
trailing `\b` backtracks past the closing `"` of a quoted mention and
produces a ghost match for `@"C:\Users\..."`, `@C:\Users\...`, and
`@"/tmp/weird:name.txt"`. The remaining 8 cases lock in the behaviour
that must not change (legitimate `server:resource` mentions and plain
file-path mentions).

Committed failing on purpose as the first half of a test-then-fix
pair; the regex fix follows in a subsequent commit.

* fix: prevent MCP extractor from ghost-matching quoted/Windows paths

The MCP resource regex used `\b` as a trailing anchor with `[^\s]+`
character classes. On any quoted file mention containing a colon
(`@"C:\Users\me\file.txt"`, `@"/tmp/weird:name.txt"`), the engine
backtracked past the closing `"` to satisfy `\b`, producing a ghost
match that collided with `extractAtMentionedFiles`. Unquoted Windows
drive-letter paths (`@C:\Users\me\file.txt`) also matched because a
drive letter is structurally identical to an MCP `server:resource`
token.

Two guards:

1. `(?!")` right after `@` drops quoted tokens entirely, and adding
   `"` to the character classes blocks any mid-match backtracking.
2. A post-match filter discards `^[A-Za-z]:[\\/]` — a single-letter
   server followed by a path separator is always a Windows drive
   prefix, never a real MCP resource.

Legitimate MCP forms (`@server:resource/path`, plugin-scoped like
`@asana-plugin:project-status/123`, inline prose mentions) remain
matched and are pinned by the contract tests added in 04998d5.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <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.

3 participants