fix(cli): auto-prepend @ when pasting or dropping multiple file paths#4544
Conversation
|
Hi @MikeWang0316tw, thanks for this fix! The UX inconsistency with multi-file paste is a real issue and worth fixing. Quick observation on the diff: this PR includes version bumps from Could you check and remove those version-related changes? This PR should only contain the I also have a few other findings on the code changes — will share once this is sorted. 中文说明PR 里混入了从 0.16.0 到 0.16.1 的全量版本号变更(14 个文件),看起来像是 merge/rebase 时带进来的。麻烦检查下,只保留 — Qwen Code |
When pasting or drag-and-dropping a single file path, Qwen Code automatically prepends @ to convert it into an @file reference. However, pasting or dragging multiple file paths (separated by newlines or whitespace) did not receive the same treatment, requiring users to manually add @ to each path. This change extends the paste detection logic in text-buffer.ts to detect and prepend @ to multiple file paths, matching the behavior of single-file paste/drag. The fix also strips surrounding quotes (common in drag-and-drop input) before path validation. Before: - Drag & drop 1 file → @file ✅ - Drag & drop 3 files → 'file1' 'file2' 'file3' ❌ - Paste 1 file → @file ✅ - Paste 3 files → file1 file2 file3 ❌ After: - Drag & drop 1 file → @file ✅ (unchanged) - Drag & drop 3 files → @file1 @FILE2 @file3 ✅ (fixed) - Paste 1 file → @file ✅ (unchanged) - Paste 3 files → @file1 @FILE2 @file3 ✅ (fixed)
94bba10 to
6d8ae66
Compare
|
Hi @pomelo-nwu, thanks for the quick review! I've rebased the PR to the latest upstream/main and removed all the version-related changes. Now the PR only contains the Thanks for looking into this! I'll wait for your other findings. |
E2E Test Report: Multi-File Paste Auto-@ PrefixTested on Result: ✅ ALL PASS — 8 scenarios, 18 assertions, 138 unit tests Unit Tests
Test 1 — Single file paste (baseline regression) ✅Paste input: TUI input box after paste: Test 2 — Multi-file paste, space-separated, quoted ✅Paste input: TUI input box after paste: Quotes stripped, all 3 paths got Test 3 — Multi-file paste, space-separated, unquoted ✅Paste input: TUI input box after paste: ✅ (3/3 assertions) Test 4 — Multi-file paste, newline-separated ✅Paste input: TUI input box after paste: Newlines normalized to spaces, all paths got Test 5 — Multi-file paste, newline-separated, quoted ✅Paste input: TUI input box after paste: ✅ (3/3 assertions) Test 6 — Mixed quoted and unquoted ✅Paste input: TUI input box after paste: ✅ (3/3 assertions) Test 7 — Single quoted file (regression) ✅Paste input: TUI input box after paste: Single-file path still works correctly through the new code path. ✅ Test 8 — Non-path text (negative test) ✅Paste input: TUI input box after paste: No Remaining Review ItemsThe code-level concerns from the earlier comment still apply:
But functionally, the paste behavior is verified correct across all tested scenarios. 中文说明在 PR 分支上通过 tmux 真实 TUI 测试验证了多文件粘贴自动添加 8 个测试场景全部通过:
138 个单元测试也全部通过。 每个场景上方都附了 TUI 输入框的实际截图(来自 tmux capture-pane),可以直接看到粘贴后输入框里显示的内容。 功能层面验证通过,但代码层面仍有待处理项: 换行分支缺少 — Qwen Code |
Refactor multi-file paste @-path detection: - Extract tryExtractFilePaths() and extractPathsFromSegment() helpers to eliminate ~100 lines of duplicated path-extraction logic - Add shellModeActive guard to newline paste branch for consistency - Remove redundant isPaste variable - Escape spaces in paths with backslash so downstream parseAllAtCommands parser correctly handles filenames with spaces (e.g. @/path/to/my\ file.txt)
|
The macOS CI failure ( Verification:
This appears to be a pre-existing race condition in the TUI dialog test, not caused by the multi-file paste changes. |
Verification Report — local tmux real-CLI testingReproduced and verified on Automated checks
Interactive tmux paste scenariosFiles used:
Code-level notes
RecommendationLGTM for merge. The fix is well-scoped to |
| potentialPath = potentialPath.trim(); | ||
| if (isValidPath(unescapePath(potentialPath))) { | ||
| ch = `@${potentialPath} `; | ||
| const validPaths = tryExtractFilePaths(ch.trim(), isValidPath); |
There was a problem hiding this comment.
[Critical] Silent data loss regression: tryExtractFilePaths extracts paths from within arbitrary text and replaces the entire paste with only @-prefixed paths. Non-path text is silently discarded.
The old code was all-or-nothing: it only prepended @ if the entire pasted string was a single valid path, otherwise it kept the text unchanged. The new code breaks this — pasting "Check out /src/index.ts and /src/utils.ts" produces "@/src/index.ts @/src/utils.ts " (words "Check out" and "and" are lost).
This affects both the single-line branch (here) and the multi-line branch above. The existing test "should only prepend @ to valid paths among mixed content" actually demonstrates this data loss by treating /path/to/invalid.txt being dropped as correct behavior — but prose text between valid paths is equally dropped.
Suggested fix: Only transform the paste when the content looks like a pure list of paths (every token is either a valid path or whitespace/newline). If any non-path, non-separator token exists, fall back to inserting the raw paste unchanged. This preserves the original all-or-nothing semantics while still supporting the multi-file use case.
— qwen3.7-max via Qwen Code /review
| let i = 0; | ||
| while (i < tokens.length) { | ||
| let found = false; | ||
| for (let j = i + 1; j <= tokens.length; j++) { |
There was a problem hiding this comment.
[Critical] O(n²) synchronous filesystem calls on the UI thread. The inner loop tries every j from i+1 to tokens.length, calling isValidPath (which does fs.existsSync + fs.statSync) on each candidate. For N tokens of non-path text, this is N*(N+1)/2 iterations with ~2 sync syscalls each.
Pasting a 40-word paragraph of non-path text produces ~820 isValidPath calls (~1640 sync fs operations), all blocking the React/Ink render thread. This can cause a visible UI freeze.
Suggested fix: Pre-filter tokens that look like path prefixes (start with /, ./, ~/, or a drive letter) before entering the inner loop. For tokens that can't plausibly be paths, skip directly to i++. This reduces the worst case dramatically for typical prose pastes.
— qwen3.7-max via Qwen Code /review
| segment: string, | ||
| isValidPath: (p: string) => boolean, | ||
| ): string[] { | ||
| const tokens = segment.split(/\s+/).filter(Boolean); |
There was a problem hiding this comment.
[Critical] No test exercises the greedy multi-token matching or the escapePath integration. All 6 new tests use paths without spaces (/path/to/file1.txt), yet the greedy for (let j = i + 1; ...) loop and escapePath (which backslash-escapes spaces) exist specifically to handle paths with spaces.
If the greedy matching or escaping has a bug, it would go completely undetected. Users dragging files like My Document.txt from Finder would get broken @ references.
Suggested fix: Add tests like:
it('should handle unquoted paths with spaces via greedy matching', () => {
const { result } = renderHook(() =>
useTextBuffer({
viewport,
isValidPath: (p: string) => p === '/path/to/my file.txt',
}),
);
act(() => result.current.insert('/path/to/my file.txt', { paste: true }));
expect(getBufferState(result).text).toBe('@/path/to/my\\ file.txt ');
});— qwen3.7-max via Qwen Code /review
| for (const line of lines) { | ||
| const quotedPathRegex = /'([^']*)'/g; | ||
| let lastIndex = 0; | ||
| let match; |
There was a problem hiding this comment.
[Suggestion] The regex /'([^']*)'/g matches text between any two apostrophes, including English contractions. Input like don't /src/file.ts it's done produces a false match 't /src/file.ts it' (capturing everything between the apostrophe in "don't" and the one in "it's"). This sets hasQuotedPaths = true, which changes how the line is processed — the actual path /src/file.ts gets trapped inside the false quoted match and fails isValidPath, then is silently dropped.
Suggested fix: Require the quoted content to look path-like before treating it as a quoted path:
const quotedPathRegex = /'([\/~.][^']*)'/g;Or only set hasQuotedPaths = true when at least one quoted match is actually a valid path.
— qwen3.7-max via Qwen Code /review
| // We need to process these before the newline check below, which would | ||
| // otherwise cause an early return and skip the @-path detection. | ||
| if (paste && /[\n\r]/.test(ch) && !shellModeActive) { | ||
| const validPaths = tryExtractFilePaths(ch, isValidPath); |
There was a problem hiding this comment.
[Suggestion] Both the if (validPaths) branch and the else branch below dispatch the identical action dispatch({ type: 'insert', payload: ch }) followed by return. This can be flattened:
if (paste && /[\n\r]/.test(ch) && !shellModeActive) {
const validPaths = tryExtractFilePaths(ch, isValidPath);
if (validPaths) {
ch = `${validPaths.join(' ')} `;
}
dispatch({ type: 'insert', payload: ch });
return;
}— qwen3.7-max via Qwen Code /review
| isValidPath: (p: string) => boolean, | ||
| ): string[] | null { | ||
| const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); | ||
| const lines = normalized.split(/\n/).filter((s) => s.trim().length > 0); |
There was a problem hiding this comment.
[Suggestion] Several branches in the new code lack test coverage:
- CRLF normalization (this line):
content.replace(/\r\n/g, '\n').replace(/\r/g, '\n')is never tested with\r\nor bare\rinput. - Shell mode + newline paste: the
!shellModeActiveguard above means newline pastes in shell mode skip path extraction — no test verifies this fallthrough. - Null return from
tryExtractFilePaths: when no valid paths are found in a newline paste, the raw content is dispatched — no test covers this branch.
Suggested fix: Add tests for each branch:
// CRLF
act(() => result.current.insert('/a.txt\r\n/b.txt', { paste: true }));
// Shell mode + newline
useTextBuffer({ viewport, isValidPath: () => true, shellModeActive: true });
act(() => result.current.insert('/a.txt\n/b.txt', { paste: true }));
// No valid paths
useTextBuffer({ viewport, isValidPath: () => false });
act(() => result.current.insert('line one\nline two', { paste: true }));— qwen3.7-max via Qwen Code /review
| * IMPORTANT: Escapes spaces in paths with backslash so that the downstream | ||
| * `parseAllAtCommands` parser (which terminates at unescaped whitespace) | ||
| * correctly includes the entire path. | ||
| */ |
There was a problem hiding this comment.
[Suggestion] The JSDoc says escapePath escapes for parseAllAtCommands, but the two functions have a mismatched character set. The parser terminates at /[,\s;!?()[\]{}]/ (line 94 of atCommandProcessor.ts) — note the comma. But SHELL_SPECIAL_CHARS used by escapePath is /[ \t()[\]{};|*?$'"#&<>!~]/` — no comma.
A file like /path/to/report,v2.txt will be output as @/path/to/report,v2.txt (comma unescaped), and the parser will truncate at the comma, reading only @/path/to/report.
Suggested fix: Add , to SHELL_SPECIAL_CHARS in packages/core/src/utils/paths.ts, or additionally escape commas in the output after escapePath.
— qwen3.7-max via Qwen Code /review
Address 3 critical and 4 suggestion issues from wenshao's review: Critical fixes: - Prevent silent data loss: only transform paste when all tokens are valid paths. If any non-path token exists, preserve original content (hadNonPathToken tracking) - Fix O(n²) synchronous fs calls: pre-filter tokens with looksLikePath() to skip non-path tokens before expensive fs.existsSync calls - Add tests for paths with spaces and escapePath integration (6 new tests) Suggestion fixes: - Fix quoted path regex: require path-like prefix to avoid false matches on English contractions (don't → /'([~/.][^']*)'/g) - Flatten if/else in newline paste branch - Add CRLF, shell mode, and null-return tests - Escape commas in paths for parseAllAtCommands compatibility
Use escapePath() for whitespace escaping instead of manual regex to avoid CodeQL's incomplete-string-escaping warning, then additionally escape commas.
- Add comma to SHELL_SPECIAL_CHARS in core so escapePath handles it - Simplify escapePathAndCommas to just call escapePath(path) - This avoids CodeQL's incomplete-string-escaping warning
| * Used to pre-filter tokens before expensive fs calls. | ||
| */ | ||
| function looksLikePath(str: string): boolean { | ||
| // Strip surrounding quotes first to handle quoted paths |
There was a problem hiding this comment.
[Suggestion] looksLikePath is too restrictive compared to the old single-path paste logic it replaces. The old code (on main) passed any token directly to isValidPath with no prefix check. This new pre-filter rejects bare relative paths like package.json, .gitignore, and src/index.ts — none of which start with /, ./, ../, ~/, or a drive letter.
This is a silent regression: pasting package.json (which exists on disk) used to produce @package.json but now preserves the original text unchanged.
| // Strip surrounding quotes first to handle quoted paths | |
| function looksLikePath(str: string): boolean { | |
| // Strip surrounding quotes first to handle quoted paths | |
| const unquoted = str.replace(/^'(.*)'$/, '$1'); | |
| return ( | |
| unquoted.startsWith('/') || | |
| unquoted.startsWith('./') || | |
| unquoted.startsWith('../') || | |
| unquoted.startsWith('~/') || | |
| unquoted.startsWith('.') || | |
| /^[A-Za-z]:/.test(unquoted) || | |
| unquoted.includes('/') | |
| ); | |
| } |
Adding unquoted.startsWith('.') covers dotfiles (.gitignore, .env). Adding unquoted.includes('/') covers bare relative paths (src/index.ts, packages/core/src/utils/paths.ts) while still rejecting plain words.
— qwen3.7-max via Qwen Code /review
| let found = false; | ||
| for (let j = i + 1; j <= tokens.length; j++) { | ||
| const candidate = tokens.slice(i, j).join(' '); | ||
| let unquoted = candidate; |
There was a problem hiding this comment.
[Suggestion] Shortest-match-first greedy matching can miss transformations for unquoted paths with spaces. If /tmp/a exists as a file and the user pastes /tmp/a b.txt, the algorithm matches /tmp/a at j = i + 1 (shortest), then b.txt fails looksLikePath, setting hadNonPathToken = true. The entire paste falls back to original content — the valid longer path /tmp/a b.txt is never tried.
Consider reversing the loop to longest-match-first:
| let unquoted = candidate; | |
| for (let j = tokens.length; j >= i + 1; j--) { |
This tries the longest candidate first and falls back to shorter ones, matching user intent more accurately for unquoted paths with spaces. Trade-off: more isValidPath calls in the worst case, but looksLikePath pre-filtering limits this to path-like tokens only.
— qwen3.7-max via Qwen Code /review
| * asterisks, question marks, dollar signs, backticks, quotes, hash, and other shell metacharacters. | ||
| */ | ||
| export const SHELL_SPECIAL_CHARS = /[ \t()[\]{};|*?$`'"#&<>!~]/; | ||
| export const SHELL_SPECIAL_CHARS = /[ \t()[\]{};|*?$`'"#&<>!~,]/; |
There was a problem hiding this comment.
[Suggestion] Adding , to SHELL_SPECIAL_CHARS has global side effects on all 15+ callers of escapePath()/unescapePath() across every tool (edit, read-file, write-file, glob, grep, etc.). Comma is not a POSIX shell metacharacter — it's included here because parseAllAtCommands treats it as a path terminator, but the JSDoc still says "shell metacharacters."
Two follow-up items:
- The
vscode-ide-companionpackage has its own copy ofSHELL_SPECIAL_CHARSatpackages/vscode-ide-companion/src/utils/imageSupport.ts:31that does NOT include comma. If a comma-escaped path from core flows into the extension'sunescapePath, the\,won't be unescaped, leaving a stray backslash. - Consider adding a test in
paths.test.tsfor comma-escaping to prevent silent regression.
— qwen3.7-max via Qwen Code /review
| act(() => result.current.insert(shortText, { paste: true })); | ||
| expect(getBufferState(result).text).toBe(shortText); | ||
| }); | ||
|
|
There was a problem hiding this comment.
[Suggestion] Several code paths in the new helpers lack test coverage:
- Relative and home-dir paths:
looksLikePathhas explicit branches for./,../, and~/but all 11 tests use absolute paths starting with/. A test like'./src/index.ts ../lib/utils.ts ~/notes.md'would cover these. - Unquoted path that looks like a path but is invalid: The
!foundbranch inextractPathsFromSegment(line 2048) is never exercised. Existing non-path tests use quoted invalid paths which are handled byquotedPathRegex, not this branch. A test like'/valid/file.txt /nonexistent/path'(where only the first is valid) would cover it. paths.test.tscomma test:SHELL_SPECIAL_CHARSwas modified to include comma but the exhaustive special-characters test inpaths.test.tswas not updated.
— qwen3.7-max via Qwen Code /review
- looksLikePath: add support for relative paths (./, ../, ~/) and dotfiles - Greedy matching: reverse to longest-match-first for paths with spaces - Add tests for relative paths, longest-match-first, and invalid paths
|
Hi @wenshao, thanks for the detailed review! I've addressed all the remaining findings: 1. looksLikePath too restrictive (line 1911) ✅ 2. Greedy matching order (line 2035) ✅ 3. Comma escaping global side effects (line 47, paths.ts) ✅ 4. Missing tests (line 581, text-buffer.test.ts) ✅
All 148 tests pass. Thanks for the thorough review! |
本地真实测试报告(merge 参考)按 PR body 的 Testing 列出的 6 类场景 + 一个关键 negative case(含 prose 不能被误覆盖),通过真 tmux + bracketed-paste 直接灌进 测试环境
1. 静态检查与单测
2. Diff 对账
3. Manual smoke — 真 tmux + bracketed paste 端到端Setupmkdir -p '/tmp/pr4544-test-paths/with comma,name'
echo alpha > /tmp/pr4544-test-paths/file1.txt
echo beta > /tmp/pr4544-test-paths/file2.txt
echo gamma > /tmp/pr4544-test-paths/file3.txt
echo with-comma-content > '/tmp/pr4544-test-paths/with comma,name/file.txt'
tmux new-session -d -s pr4544 -x 220 -y 50 -c <worktree>
tmux send-keys -t pr4544 'npm run dev' Enter每个 case 通过 6/6 端到端结果
完整 stdout 摘录(tmux paste-buffer 实测,未删改)4. 与 PR body 的 Before/After 表对账
PR body 的 Before/After 表的所有 4 行都在真 CLI 上复现到了对应的 After 状态。 5. 结论
从本地真实测试角度,该 PR 可以合并。 修复了多文件 paste / drag-and-drop 与单文件不一致的体验问题;引入的 |
|
Thanks @wenshao for the thorough review and detailed testing reports! Both tmux verification reports are very helpful. I appreciate the time you put into verifying all the edge cases. |
|
|
||
| /** | ||
| * Extract file paths from a whitespace-separated segment. | ||
| * Tries shortest possible path first to handle paths with spaces. |
There was a problem hiding this comment.
[Suggestion] Stale JSDoc: says "Tries shortest possible path first" but the implementation at line 2037 iterates for (let j = tokens.length; j >= i + 1; j--) — longest-match-first. The inline comment three lines below correctly says "Try longest-match-first". A future reader relying on this JSDoc will misunderstand the algorithm and may "fix" the code to match the docstring, silently breaking paths with spaces.
| * Tries shortest possible path first to handle paths with spaces. | |
| * Tries longest possible path first (greedy) so paths with spaces are matched before shorter prefixes. |
— qwen3.7-max via Qwen Code /review
| unquoted.startsWith('~/') || | ||
| unquoted.startsWith('.') || | ||
| /^[A-Za-z]:/.test(unquoted) || | ||
| unquoted.includes('/') |
There was a problem hiding this comment.
[Suggestion] unquoted.includes('/') is overly broad as a path pre-filter — it matches URLs (https://example.com/foo), dates (2026/05/27), MIME types (text/plain), and git diff lines (+++ b/src/foo.ts). These all pass through to the greedy matching loop where each candidate triggers synchronous fs.existsSync + fs.statSync calls.
The preceding checks (startsWith('/'), ./, ../, ~/, drive letter) already cover all standard path formats. The new test for relative paths only covers ./, ../, and ~/ prefixed paths — no test exercises bare src/foo.ts paths (without prefix), which was presumably the reason for adding this catch-all.
Consider removing unquoted.includes('/'). If bare relative paths like src/foo.ts need support, add a test for them and use a more targeted check (e.g., require a known file extension).
— qwen3.7-max via Qwen Code /review
| */ | ||
| export const SHELL_SPECIAL_CHARS = /[ \t()[\]{};|*?$`'"#&<>!~]/; | ||
| export const SHELL_SPECIAL_CHARS = /[ \t()[\]{};|*?$`'"#&<>!~,]/; | ||
|
|
There was a problem hiding this comment.
[Suggestion] vscode-ide-companion has its own copy of SHELL_SPECIAL_CHARS at packages/vscode-ide-companion/src/utils/imageSupport.ts:31 that does NOT include ,:
// imageSupport.ts:31 (unchanged by this PR):
export const SHELL_SPECIAL_CHARS = /[ \t()[\]{};|*?$`'"#&<>!~]/;
// paths.ts:47 (this PR):
export const SHELL_SPECIAL_CHARS = /[ \t()[\]{};|*?$`'"#&<>!~,]/;The two packages' escape/unescape behavior now diverges — core's escapePath escapes commas, vscode's does not. If the vscode extension generates @-paths consumed by core's parseAllAtCommands, paths with commas will be truncated.
Consider syncing the vscode copy, or better: eliminating the duplication by importing from core.
— qwen3.7-max via Qwen Code /review
| result.current.insert("'/path/to/report,v2.txt'", { paste: true }), | ||
| ); | ||
| // Comma should be escaped so parseAllAtCommands doesn't truncate | ||
| expect(getBufferState(result).text).toBe('@/path/to/report\\,v2.txt '); |
There was a problem hiding this comment.
[Suggestion] Test coverage gap for shell metacharacters: only spaces and commas are tested. Real-world filenames commonly contain parentheses (file(1).txt, macOS downloads), semicolons (report;v2.txt), or brackets (data[2024].csv). If escapePath fails to escape any of these, parseAllAtCommands (which terminates at /[,\s;!?()[\]{}]/) would silently truncate the @-path with no user-visible error.
Also: the single-file paste path (line 2185) now goes through escapePath — the old code did NOT escape, so this silently fixes a pre-existing truncation bug for paths like /Downloads/report(v2).txt. No test validates this fix.
Suggest adding at least one test:
it('should escape shell metacharacters in paths', () => {
const { result } = renderHook(() =>
useTextBuffer({
viewport,
isValidPath: (p: string) => p === '/Downloads/report(v2).txt',
}),
);
act(() => result.current.insert("'/Downloads/report(v2).txt'", { paste: true }));
expect(getBufferState(result).text).toBe('@/Downloads/report\\(v2\\).txt ');
});— qwen3.7-max via Qwen Code /review
| const tokens = segment.split(/\s+/).filter(Boolean); | ||
| const paths: string[] = []; | ||
| let i = 0; | ||
| while (i < tokens.length) { |
There was a problem hiding this comment.
[Suggestion] Missing short-circuit when hadNonPathToken is already set. Once any token is flagged as a non-path, the all-or-nothing check at the end of tryExtractFilePaths guarantees the result will be null — but both loops continue processing all remaining tokens, making unnecessary isValidPath (fs) calls.
| while (i < tokens.length) { | |
| while (i < tokens.length) { | |
| if (hadNonPathToken.value) break; |
And in the outer tryExtractFilePaths line loop:
for (const line of lines) {
if (hadNonPathToken.value) break;
// ... existing logic
}This eliminates wasted fs calls when the paste contains mixed path/non-path content.
— qwen3.7-max via Qwen Code /review
|
|
||
| /** | ||
| * Escape spaces and commas so that downstream @-path parsing doesn't truncate. | ||
| * parseAllAtCommands terminates at unescaped whitespace and commas. |
There was a problem hiding this comment.
[Suggestion] escapePathAndCommas is a trivial pass-through to escapePath — the comma escaping only works because , was added to the global SHELL_SPECIAL_CHARS in this PR. Two follow-ups:
-
Missing test in
paths.test.ts: theescapePathdescribe block (line 68) has no test for comma escaping. The special-char test string at line 189 omits,. A future change could revert the comma fromSHELL_SPECIAL_CHARSand no core-level test would catch it. Add:expect(escapePath('file,v2.txt')).toBe('file\\,v2.txt'); -
Consider either inlining
escapePath(path)at call sites (removing the misleading wrapper name) or making the comma logic explicit here:return escapePath(path).replace(/,/g, '\\,')— this decouples from the global regex change.
— qwen3.7-max via Qwen Code /review
- Fix stale JSDoc: change 'shortest' to 'longest' match-first
- Remove overly broad includes('/') from looksLikePath
- Sync SHELL_SPECIAL_CHARS in vscode-ide-companion (add comma)
- Add tests for shell metacharacters (parentheses, brackets, semicolons)
- Add short-circuit when hadNonPathToken is set
- Inline escapePathAndCommas (now redundant with comma in SHELL_SPECIAL_CHARS)
| let lastIndex = 0; | ||
| let match; | ||
| let hasQuotedPaths = false; | ||
|
|
There was a problem hiding this comment.
[Suggestion] quotedPathRegex = /'([~/.][^']*)'/g does not match Windows drive-letter paths. The character class [~/.] requires the quoted content to start with ~, /, or ., so paths like 'C:/Users/My Documents/file.txt' are never matched by this regex. They fall through to extractPathsFromSegment, which splits by whitespace — 'C:/Users/My fails looksLikePath (quote-strip requires both quotes, drive-letter test sees ' not C), setting hadNonPathToken = true. The paste silently preserves the original text unchanged — a regression from the old code which handled 'C:/Users/My Documents/file.txt' via potentialPath.match(/^'(.*)'$/).
| const quotedPathRegex = /'((?:[~/.]|[A-Za-z]:)[^']*)'/g; |
Also add a test for quoted Windows paths with spaces, and for the drive-letter branch of looksLikePath (/^[A-Za-z]:/) which currently has zero test coverage.
— qwen3.7-max via Qwen Code /review
| function looksLikePath(str: string): boolean { | ||
| // Strip surrounding quotes first to handle quoted paths | ||
| const unquoted = str.replace(/^'(.*)'$/, '$1'); | ||
| return ( |
There was a problem hiding this comment.
[Suggestion] looksLikePath drops bare filenames — behavioral regression for single-file paste. The old code on main called isValidPath(unescapePath(potentialPath)) directly on any pasted text ≥ 3 chars with no prefix check. The new code routes through tryExtractFilePaths → extractPathsFromSegment → looksLikePath, which rejects tokens not starting with /, ./, ../, ~/, ., or a drive letter. A paste of README.md (when it exists as a file in CWD) previously produced @README.md ; it now produces README.md unchanged.
| return ( | |
| // Pre-filter: skip tokens that can't possibly be paths. | |
| // For single-token segments, let isValidPath decide (preserves | |
| // old behavior for bare filenames like README.md). | |
| if (tokens.length > 1 && !looksLikePath(tokens[i])) { |
— qwen3.7-max via Qwen Code /review
| function looksLikePath(str: string): boolean { | ||
| // Strip surrounding quotes first to handle quoted paths | ||
| const unquoted = str.replace(/^'(.*)'$/, '$1'); | ||
| return ( |
There was a problem hiding this comment.
[Critical] looksLikePath silently rejects quoted Windows drive-letter paths with spaces (e.g., 'C:\Users\my file.txt').
When extractPathsFromSegment splits by whitespace, the first token becomes 'C:\Users\my (with leading '). looksLikePath strips quotes via str.replace(/^'(.*)'$/, '$1') which requires both quotes — so the leading ' survives. Then ^[A-Za-z]: fails because the first char is ', not a letter. looksLikePath returns false → hadNonPathToken = true → entire paste preserved unchanged.
The greedy loop (line 2033) would correctly reconstruct the full quoted string and strip quotes — but it is unreachable because looksLikePath rejects the first token before the loop is entered.
Regression: the old code stripped quotes from the whole string before validating, so 'C:\Users\my file.txt' worked correctly.
| return ( | |
| function looksLikePath(str: string): boolean { | |
| // Strip surrounding quotes first to handle quoted paths | |
| const unquoted = str.replace(/^'(.*)'$/, '$1'); | |
| // Also handle tokens that are the start of a quoted path split by whitespace | |
| const inner = unquoted.startsWith("'") ? unquoted.slice(1) : unquoted; | |
| return ( | |
| inner.startsWith('/') || | |
| inner.startsWith('./') || | |
| inner.startsWith('../') || | |
| inner.startsWith('~/') || | |
| inner.startsWith('.') || | |
| /^[A-Za-z]:/.test(inner) | |
| ); | |
| } |
— qwen3.7-max via Qwen Code /review
| expect(getBufferState(result).text).toBe('@/report\\;v2.txt '); | ||
| }); | ||
|
|
||
| it('should handle relative paths like ./src/index.ts', () => { |
There was a problem hiding this comment.
[Suggestion] Missing test for Windows drive-letter paths. looksLikePath has an explicit /^[A-Za-z]:/ branch (line 1919) but no test exercises it. All 16+ new tests use Unix-style paths (/, ./, ../, ~/). A regression in the drive-letter branch would go undetected.
it('should handle Windows drive-letter paths', () => {
const { result } = renderHook(() =>
useTextBuffer({
viewport,
isValidPath: (p: string) =>
p === 'C:\\Users\\file.txt' || p === 'D:\\data\\report.csv',
}),
);
act(() =>
result.current.insert('C:\\Users\\file.txt D:\\data\\report.csv', {
paste: true,
}),
);
expect(getBufferState(result).text).toBe(
'@C:\\Users\\file.txt @D:\\data\\report.csv ',
);
});— qwen3.7-max via Qwen Code /review
| * asterisks, question marks, dollar signs, backticks, quotes, hash, and other shell metacharacters. | ||
| */ | ||
| export const SHELL_SPECIAL_CHARS = /[ \t()[\]{};|*?$`'"#&<>!~]/; | ||
| export const SHELL_SPECIAL_CHARS = /[ \t()[\]{};|*?$`'"#&<>!~,]/; |
There was a problem hiding this comment.
[Suggestion] The exhaustive escapePath test (paths.test.ts:188, "should handle paths with only special characters") does not include , in its input or expected output. Comma has zero unit-test coverage in escapePath/unescapePath despite being added to SHELL_SPECIAL_CHARS. The text-buffer integration test (comma escaping) covers it indirectly, but if escapePath itself regresses on comma, only that one integration test would catch it.
Update paths.test.ts:188 to include comma:
expect(escapePath(' ()[]{};&|*?$`\'"#!~<>,')).toBe(
'\\ \\(\\)\\[\\]\\{\\}\\;\\&\\|\\*\\?\\$\\`\\\'\\"\\#\\!\\~\\<\\>\\,',
);— qwen3.7-max via Qwen Code /review
- Fix quotedPathRegex to support Windows drive-letter paths - Fix looksLikePath to handle quoted Windows paths split by whitespace - Allow bare filenames for single-token segments - Add tests for Windows drive-letter and quoted Windows paths - Add comma to paths.test.ts escapePath test
| // --- Path extraction helpers (pure functions, outside useTextBuffer) --- | ||
|
|
||
| /** | ||
| * Check if a string looks like a path prefix (starts with /, ./, ~/, or drive letter). |
There was a problem hiding this comment.
[Suggestion] JSDoc summary is incomplete — lists only /, ./, ~/, and drive letter, but the implementation also checks ../ (line 1919) and bare ./.. (line 1921).
| * Check if a string looks like a path prefix (starts with /, ./, ~/, or drive letter). | |
| * Check if a string looks like a path prefix (starts with /, ./, ../, ~/, ., .., or a drive letter). |
— qwen3.7-max via Qwen Code /review
| * Handles quoted paths, unquoted paths, whitespace-separated, and newline-separated. | ||
| * Supports file paths with spaces using greedy matching. | ||
| * IMPORTANT: Escapes spaces and commas in paths with backslash so that the | ||
| * downstream `parseAllAtCommands` parser (which terminates at unescaped |
There was a problem hiding this comment.
[Suggestion] JSDoc says "Escapes spaces and commas" but escapePath actually escapes all SHELL_SPECIAL_CHARS (22 characters including parentheses, brackets, semicolons, pipes, etc.). The tests at lines 768-797 verify parentheses and brackets are escaped, contradicting this doc.
| * downstream `parseAllAtCommands` parser (which terminates at unescaped | |
| * IMPORTANT: Escapes shell-special characters (spaces, commas, parentheses, | |
| * brackets, semicolons, etc.) with backslash so that the downstream | |
| * `parseAllAtCommands` parser correctly includes the entire path. |
— qwen3.7-max via Qwen Code /review
| // Pre-filter: skip tokens that can't possibly be paths. | ||
| // For single-token segments, let isValidPath decide (preserves | ||
| // old behavior for bare filenames like README.md). | ||
| if (tokens.length > 1 && !looksLikePath(tokens[i])) { |
There was a problem hiding this comment.
[Suggestion] The tokens.length > 1 guard intentionally bypasses looksLikePath for single-token segments to preserve bare-filename behavior (e.g., pasting README.md). The comment documents this intent, but no test exercises it. A regression that removes this guard would silently break bare-filename paste with no test failure.
Consider adding:
it('should prepend @ to a bare filename when isValidPath returns true', () => {
const { result } = renderHook(() =>
useTextBuffer({ viewport, isValidPath: (p) => p === 'README.md' }),
);
act(() => result.current.insert('README.md', { paste: true }));
expect(getBufferState(result).text).toBe('@README.md ');
});— qwen3.7-max via Qwen Code /review
- Fix looksLikePath JSDoc to include ../ and .. - Fix tryExtractFilePaths JSDoc to reflect all shell-special chars - Add test for bare filename (README.md) single-token segment
本地真实测试报告(追加,针对最新 commit
|
| 检查 | 结果 |
|---|---|
npm ci |
OK |
npm run build (full repo) |
OK, packages/cli/dist/index.js 正常生成 |
npx tsc --noEmit -p packages/cli |
clean |
npx tsc --noEmit -p packages/core |
clean |
npx eslint <5 touched files> |
clean (0 problems) |
vitest run packages/cli/src/ui/components/shared/text-buffer.test.ts |
152/152 pass (较 PR body 138 多了 14 个新增 case) |
vitest run packages/core/src/utils/paths.test.ts |
115/115 pass + 1 skipped(含逗号 escape 校验) |
vitest run packages/cli/src/ui/components/shared/ (regression) |
337/337 pass + 1 skipped 跨 10 个文件,无回归 |
152/152 = 138(初稿)+ 6(critical/suggestion 第一轮)+ 3(greedy / relative / 非法路径)+ 5(Windows / parens / brackets / 分号 / bare README)
2. tmux 真实 paste 端到端 — 24/24 通过
环境:Linux x86_64 (Debian 13),Node v22.22.2,tmux 3.5a。
驱动方式:tmux load-buffer 灌进 paste buffer,tmux paste-buffer -p 用 bracketed paste(ESC[200~ … ESC[201~,即终端 emit 的真实粘贴序列)送进 node packages/cli/dist/index.js。每个 case 之间用 Esc + C-u × 8 + BSpace × 80 把输入框清空,避免串扰。
测试目录:/tmp/pr4544-rerun/ 下有 file1/2/3.txt、README.md、space file.txt、with comma,name/file.txt、dir(parens)/x.txt、dir[brackets]/y.txt、dir;semi/z.txt。
多文件 paste 基础矩阵(沿用 PR body Before/After 表)
| # | Case | Payload | 输入框实际显示 | ✓ |
|---|---|---|---|---|
| T01 | 单 path(regression) | /tmp/.../file1.txt |
@/tmp/.../file1.txt |
✅ |
| T02 | 3 paths whitespace-sep | …/file1.txt …/file2.txt …/file3.txt |
@…/file1.txt @…/file2.txt @…/file3.txt |
✅ |
| T03 | 3 paths newline-sep | …/file1.txt\n…/file2.txt\n…/file3.txt |
@…/file1.txt @…/file2.txt @…/file3.txt |
✅ |
| T04 | 3 quoted paths(drag-drop) | '…/file1.txt' '…/file2.txt' '…/file3.txt' |
@…/file1.txt @…/file2.txt @…/file3.txt |
✅ |
| T05 | mixed quoted/unquoted | '…/file1.txt' …/file2.txt '…/file3.txt' |
@…/file1.txt @…/file2.txt @…/file3.txt |
✅ |
| T08 | quoted + newline-sep | '…/f1' \n '…/f2' \n '…/f3' |
@…/file1 @…/file2 @…/file3 |
✅ |
Silent-data-loss 防御(关键 invariant)
| # | Case | Payload | 实际显示 | 期望 |
|---|---|---|---|---|
| T09 | 纯 prose | please review my code |
please review my code |
不加 @ ✅ |
| T10 | prose 夹一个 path | Please summarize /tmp/.../file1.txt for me |
原文照旧 | 整段保留 ✅ |
| T11 | 有效 + 无效 path 混合 | …/file1.txt …/nonexistent.txt |
原文照旧 | 任一非 path → 整段保留 ✅ |
| T12 | 多行 prose(无 path) | line one\nline two of prose |
原文照旧(保留换行) | 落入 newline fallback ✅ |
| T13 | 极短 paste | hi |
hi |
< 3 字符短路守卫 ✅ |
| T19 | 含 contraction (don't) |
I don't think this needs the @ prefix |
原文照旧 | quotedPathRegex 不误匹配 ✅ |
| T16 | 已经 @-prefixed |
@/tmp/.../file1.txt |
@/tmp/.../file1.txt |
无双前缀 ✅ |
Shell 转义(escapePath 集成验证)
| # | Case | Payload | 实际显示 | ✓ |
|---|---|---|---|---|
| T06 | quoted 含空格 | '…/space file.txt' |
@…/space\ file.txt |
✅ |
| T07 | quoted 含空格 + 逗号 | '…/with comma,name/file.txt' |
@…/with\ comma\,name/file.txt |
✅ |
| T20 | 含括号 | '…/dir(parens)/x.txt' |
@…/dir\(parens\)/x.txt |
✅ |
| T21 | 含方括号 | '…/dir[brackets]/y.txt' |
@…/dir\[brackets\]/y.txt |
✅ |
| T22 | 含分号 | '…/dir;semi/z.txt' |
@…/dir\;semi/z.txt |
✅ |
| T23 | 三 quoted 混含空格+逗号 | '…/f1' '…/with comma,name/file.txt' '…/f2' |
@…/f1 @…/with\ comma\,name/file.txt @…/f2 |
✅ |
| T24 | unquoted 含空格(greedy) | /tmp/.../space file.txt |
@/tmp/.../space\ file.txt |
✅ |
T24 直接验证了
extractPathsFromSegment的 longest-match-first:单纯按空格切会得到/tmp/.../space+file.txt两个非法 token,但贪婪先试/tmp/.../space file.txt,匹中后整段当作单 path 处理。
新 commit 关注点(58a85887 + 7f068731)
| # | Case | Payload | 实际显示 | ✓ |
|---|---|---|---|---|
| T14 | bare filename 单 token | README.md |
@README.md |
✅ — tokens.length > 1 pre-filter 跳过,保留旧行为 |
| T17 | dotfile 形态 .env 不存在 |
.env |
.env |
✅ — looksLikePath ok 但 isValidPath 失败 → silent-loss 守卫保留原文 |
| T18 | relative ./path/in/cwd |
./packages/cli/dist/index.js |
@./packages/cli/dist/index.js |
✅ — relative 路径支持生效 |
| T15 | Windows drive-letter on Linux | C:\Users\file.txt D:\data\report.csv |
C:\Users\file.txt D:\data\report.csv |
✅ — looksLikePath 认它像 path,但 Linux fs 上无效 → silent-loss 守卫保留 |
T15 验证了在 Linux 上
C:\...形态的 paste 不会被错误地转换或丢失,恰恰是因为 silent-loss invariant 起作用——Windows 上同一个 payload 才会触发@C:\Users\file.txt @D:\data\report.csv(unit testshould handle Windows drive-letter paths覆盖)。
3. 与 PR body Before/After 表对账
PR body Before/After 表 4 行全部在真 CLI 上复现到 After 状态:
| Action | After(PR 声称) | tmux 实测 | 一致 |
|---|---|---|---|
Drag & drop 1 file → @file |
unchanged | T01 ✅ | |
Drag & drop 3 files → @file1 @file2 @file3 |
fixed | T04 ✅ | |
Paste 1 file → @file |
unchanged | T01 ✅ | |
Paste 3 files → @file1 @file2 @file3 |
fixed | T02 (whitespace) / T03 (newline) ✅ |
4. tmux paste 摘录(按场景节选)
=== T02 3 paths whitespace-sep ===
rendered: > @/tmp/pr4544-rerun/file1.txt @/tmp/pr4544-rerun/file2.txt @/tmp/pr4544-rerun/file3.txt
=== T03 3 paths newline-sep ===
rendered: > @/tmp/pr4544-rerun/file1.txt @/tmp/pr4544-rerun/file2.txt @/tmp/pr4544-rerun/file3.txt
=== T07 path with comma+space ===
payload : '/tmp/pr4544-rerun/with comma,name/file.txt'
rendered: > @/tmp/pr4544-rerun/with\ comma\,name/file.txt
=== T10 prose mixed with path (must NOT @-prefix) ===
payload : Please summarize /tmp/pr4544-rerun/file1.txt for me
rendered: > Please summarize /tmp/pr4544-rerun/file1.txt for me
=== T14 bare README (single-token) ===
payload : README.md
rendered: > @README.md
=== T15 Windows path on Linux (preserved) ===
payload : C:\Users\file.txt D:\data\report.csv
rendered: > C:\Users\file.txt D:\data\report.csv
=== T20 path with parentheses ===
payload : '/tmp/pr4544-rerun/dir(parens)/x.txt'
rendered: > @/tmp/pr4544-rerun/dir\(parens\)/x.txt
=== T24 unquoted path-with-space (greedy) ===
payload : /tmp/pr4544-rerun/space file.txt
rendered: > @/tmp/pr4544-rerun/space\ file.txt
5. 结论
- ✅ 152/152 单测 + 115/115 paths 单测 + 337 broader shared tests 全绿,无回归
- ✅ tsc / eslint 全 clean,CI 也全 SUCCESS
- ✅ 24 个 tmux + bracketed paste 端到端场景全部对得上:多文件 paste 基础矩阵、silent-loss 守卫、shell-meta escape(空格/逗号/括号/方括号/分号)、bare filename、relative path、Windows path 在 Linux 上的安全降级,全部行为符合预期
- ✅ PR body Before/After 表 4 行全部复现到 After 状态
- ✅
tryExtractFilePaths的关键 invariant —— "只有所有非空白 token 都是 valid path 才转换" —— 在 T10/T11/T15 三个不同形态的混入场景中都生效,没有 silent data loss 风险
LGTM for merge. 从最新 commit 7f068731 的本地真实测试角度看,这个 PR 已经经过两轮 wenshao review + 1 轮 pomelo-nwu review + 多轮 tmux 真实端到端验证,覆盖范围远超 PR body 所述。修复的 UX 不一致问题(多文件 paste/drag-drop 与单文件不一致)已彻底解决;新引入的 tryExtractFilePaths + silent-loss invariant 是非常保守的设计;SHELL_SPECIAL_CHARS 同步加 , 也已在 core 与 vscode-ide-companion 两边对齐。
| let match; | ||
| let hasQuotedPaths = false; | ||
|
|
||
| while ((match = quotedPathRegex.exec(line)) !== null) { |
There was a problem hiding this comment.
[Suggestion] The quotedPathRegex while-loop is missing the hadNonPathToken short-circuit check that was added to the outer for loop (line 1949) and extractPathsFromSegment (line 2021). When a gap between two quoted paths sets hadNonPathToken = true via extractPathsFromSegment, the while-loop continues calling isValidPath() on subsequent quoted matches — making unnecessary sync fs calls that will be discarded by the final !hadNonPathToken.value guard.
| while ((match = quotedPathRegex.exec(line)) !== null) { | |
| while ((match = quotedPathRegex.exec(line)) !== null) { | |
| if (hadNonPathToken.value) break; | |
| const gap = line.slice(lastIndex, match.index).trim(); |
— qwen3.7-max via Qwen Code /review
| // Handle pastes that contain newlines (e.g., file paths separated by newlines). | ||
| // We need to process these before the newline check below, which would | ||
| // otherwise cause an early return and skip the @-path detection. | ||
| if (paste && /[\n\r]/.test(ch) && !shellModeActive) { |
There was a problem hiding this comment.
[Suggestion] No size guard before calling tryExtractFilePaths on newline-containing pastes. A large paste (e.g., a 100KB log snippet that happens to contain newlines) would run the full extraction pipeline (normalize + split + regex + greedy fs loop) on the UI thread, causing a visible freeze.
Consider adding an early size check:
| if (paste && /[\n\r]/.test(ch) && !shellModeActive) { | |
| if (paste && /[\n\r]/.test(ch) && !shellModeActive && ch.length <= 4096) { |
— qwen3.7-max via Qwen Code /review
| ch = `@${potentialPath} `; | ||
| const validPaths = tryExtractFilePaths(ch.trim(), isValidPath); | ||
| if (validPaths) { | ||
| ch = `${validPaths.join(' ')} `; |
There was a problem hiding this comment.
[Suggestion] Single-line paste now routes through the full tryExtractFilePaths pipeline (CRLF normalize, split, filter, per-line regex exec, greedy token loop) instead of the previous O(1) direct check (trim → quote strip → isValidPath). For the most common case — pasting a single file path — this adds unnecessary allocations and sync fs overhead.
Consider a fast-path for single-token input:
const trimmed = ch.trim();
if (!/\s/.test(trimmed)) {
let p = trimmed;
const q = p.match(/^'(.*)'$/);
if (q) p = q[1];
const unescaped = unescapePath(p);
if (isValidPath(unescaped)) {
ch = `@${escapePath(unescaped)} `;
}
} else {
const validPaths = tryExtractFilePaths(trimmed, isValidPath);
if (validPaths) ch = `${validPaths.join(' ')} `;
}— qwen3.7-max via Qwen Code /review
|
|
||
| // --- Path extraction helpers (pure functions, outside useTextBuffer) --- | ||
|
|
||
| /** |
There was a problem hiding this comment.
[Suggestion] Zero debugLogger calls in ~160 lines of new path-extraction code, while the rest of this file uses debugLogger extensively (14 call sites). When the all-or-nothing transformation silently falls back to raw paste, there is no diagnostic trail — no log of which token triggered hadNonPathToken, no count of tokens processed, no indication that tryExtractFilePaths was even called.
At minimum, add logging when:
tryExtractFilePathsis entered (log token count)hadNonPathTokenis set (log the failing token and reason —looksLikePathrejection vsisValidPathfalse)- Paths are successfully extracted (log count)
— qwen3.7-max via Qwen Code /review
| inner.startsWith('./') || | ||
| inner.startsWith('../') || | ||
| inner.startsWith('~/') || | ||
| inner.startsWith('.') || |
There was a problem hiding this comment.
[Suggestion] inner.startsWith('.') is overly broad — it matches .5, .env, .bashrc, and any dot-prefixed string. In multi-token mode, these pass looksLikePath but fail isValidPath, setting hadNonPathToken and aborting the entire multi-file paste. For example, pasting /valid/path.txt .5 silently preserves the original content because .5 is treated as a failed path.
Also, inner.startsWith('./') and inner.startsWith('../') above are dead code — inner.startsWith('.') subsumes them via short-circuit OR.
Tighten to avoid false positives:
| inner.startsWith('.') || | |
| return ( | |
| inner.startsWith('/') || | |
| inner.startsWith('./') || | |
| inner.startsWith('../') || | |
| inner.startsWith('~/') || | |
| inner === '.' || | |
| inner === '..' || | |
| /^[A-Za-z]:/.test(inner) | |
| ); |
— qwen3.7-max via Qwen Code /review
| function extractPathsFromSegment( | ||
| segment: string, | ||
| isValidPath: (p: string) => boolean, | ||
| hadNonPathToken: { value: boolean }, |
There was a problem hiding this comment.
[Suggestion] split(/\s+/) collapses consecutive whitespace, then tokens.slice(i, j).join(' ') reconstructs with single spaces. A filename with consecutive spaces (e.g., my file.txt) pasted unquoted would be split into ["my", "file.txt"], joined as my file.txt (one space), and isValidPath called on the wrong string.
Quoted paths handle this correctly (the regex preserves inner content). For unquoted paths, this is a silent misreference edge case. Consider documenting this limitation in a code comment so future maintainers know quoted paths are the safe path for filenames with unusual whitespace.
— qwen3.7-max via Qwen Code /review
| // Short-circuit: once any token is flagged as non-path, the result will be null | ||
| if (hadNonPathToken.value) break; | ||
|
|
||
| // Pre-filter: skip tokens that can't possibly be paths. |
There was a problem hiding this comment.
[Suggestion] looksLikePath rejects bare filenames like README.md or file.txt (no path prefix). In multi-token mode (tokens.length > 1), any such token sets hadNonPathToken, aborting the entire paste. But single-file paste of README.md works because the pre-filter is skipped when tokens.length === 1.
This means README.md alone gets @-prefixed, but README.md src/index.ts does not — both are valid files, with no feedback to the user about why the transformation was skipped.
Consider: when tokens.length is small (e.g., ≤ 10), still try isValidPath as a fallback before setting hadNonPathToken.
— qwen3.7-max via Qwen Code /review
| const { result } = renderHook(() => | ||
| useTextBuffer({ viewport, isValidPath: () => true }), | ||
| ); | ||
| const filePaths = |
There was a problem hiding this comment.
[Suggestion] The "trailing" branch in tryExtractFilePaths (content after the last quoted-path regex match, line 1979-1987) is never exercised by any test. All existing mixed quoted/unquoted tests place unquoted paths between quoted paths (the "gap" branch) or end with a quoted path. For example, this test's input '/path/to/file1.txt' /path/to/file2.txt '/path/to/file3.txt' ends with a quoted path, so trailing is always empty.
Consider adding a test with trailing unquoted content after the last quoted path:
it('should handle trailing unquoted paths after quoted paths', () => {
const { result } = renderHook(() =>
useTextBuffer({
viewport,
isValidPath: (p: string) =>
p === '/path/to/a.txt' || p === '/path/to/b.txt',
}),
);
act(() =>
result.current.insert("'/path/to/a.txt' /path/to/b.txt", {
paste: true,
}),
);
expect(getBufferState(result).text).toBe(
'@/path/to/a.txt @/path/to/b.txt ',
);
});— qwen3.7-max via Qwen Code /review
What does this PR do?
Fixes an inconsistency where pasting or drag-and-dropping multiple file
paths did not auto-prepend
@, while single-file paste/drag workedcorrectly.
Before
@file'file1' 'file2' 'file3'@filefile1 file2 file3After
@file@file1 @file2 @file3@file@file1 @file2 @file3Changes
packages/cli/src/ui/components/shared/text-buffer.ts@to each detected file pathTesting
🤖 Generated with Qwen Code (35-shotted by Qwen-Coder)