Skip to content

feat(cli): do not append trailing space for directory completions (#4092)#4288

Merged
pomelo-nwu merged 19 commits into
QwenLM:mainfrom
dykebo:fix/cli-directory-completion-no-trailing-space
May 23, 2026
Merged

feat(cli): do not append trailing space for directory completions (#4092)#4288
pomelo-nwu merged 19 commits into
QwenLM:mainfrom
dykebo:fix/cli-directory-completion-no-trailing-space

Conversation

@dykebo

@dykebo dykebo commented May 18, 2026

Copy link
Copy Markdown
Contributor

🎯 What's Changed

Fixes issue #4092 - Removes trailing space after Tab completion for directory paths.

📝 Description

Previously, completing a directory path with Tab would add a trailing space (e.g., @src/components/ ), forcing users to delete it before continuing to the next level. This matches standard shell behavior where directories end with / and don't need spaces.

✅ Changes

  • Added isDirectory flag to Suggestion and CommandCompletionItem interfaces
  • Updated handleAutocomplete logic to skip trailing space when isDirectory === true
  • Modified getDirPathCompletions() in /dir add command to return proper metadata
  • New test case verifying directory completions don't append trailing space

🧪 Testing

  • All unit tests passing (49 tests)
  • Verified behavior: @path and /dir add completions now work seamlessly for nested directories

…enLM#4092)

## What

在 @路径补全和 /dir add 命令的目录补全中不再追加尾部空格。这样可以允许用户在补全目录后直接按 Tab 继续深入下一级子目录,无需先删除空格。

## Examples

- Input: `@src/com` + Tab → Output: `@src/components/` (no trailing space)

- Input: `/dir add ./pac` + Tab → Output: `/dir add ./packages/` (no trailing space)

- File completions still append a space (e.g., `@src/file.txt `)

## Changes

- Added `isDirectory` flag to `Suggestion` and `CommandCompletionItem` interfaces

- Updated `handleAutocomplete` to skip trailing space when `isDirectory === true`

- Modified `getDirPathCompletions` to return `CommandCompletionItem[]` with `isDirectory: true`

- Added test case for directory completion behavior
.map((e) => prefix + path.join(searchDir, e.name))
.map((e) => ({
value: prefix + path.join(searchDir, e.name),
isDirectory: true,

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.

[Critical] getDirPathCompletions returns directory values without a trailing /, which prevents deeper directory navigation via /directory add.

After tab-completion, the buffer becomes e.g. src/components (no trailing space due to isDirectory: true, but also no /). On the next tab, getDirPathCompletions('src/components') computes searchDir='src' and namePrefix='components', matching the same directory again — creating an infinite tab-completion loop.

The function already handles trailing / in its input (lines 75-77 via endsWithSep), so appending / to the return values would fix deeper navigation:

Suggested change
isDirectory: true,
.map((e) => ({
value: prefix + path.join(searchDir, e.name) + '/',
isDirectory: true,
}))

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

done, thx for your [Critical]

matchedAlias?: string;
supportedModes?: ExecutionMode[];
modelInvocable?: boolean;
/** Whether the suggestion represents a directory path (ends with `/`). When true, handleAutocomplete should NOT append a trailing space so the user can continue tab-completing deeper into the directory tree. */

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.

[Suggestion] JSDoc states "(ends with /)" but this is factually incorrect for the /directory add path — getDirPathCompletions returns directory values without a trailing slash (e.g., src/components).

This documents a false invariant. A future maintainer might rely on isDirectory === true implying value.endsWith('/').

Align with the CommandCompletionItem.isDirectory JSDoc in types.ts which correctly omits the parenthetical:

Suggested change
/** Whether the suggestion represents a directory path (ends with `/`). When true, handleAutocomplete should NOT append a trailing space so the user can continue tab-completing deeper into the directory tree. */
/** Whether the completion represents a directory path. When true, handleAutocomplete should NOT append a trailing space so the user can continue tab-completing deeper into the directory tree. */
isDirectory?: boolean;

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

ok

label: item.label ?? item.value,
value: item.value,
description: item.description,
isDirectory: item.isDirectory,

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.

[Suggestion] toSuggestion was modified to propagate isDirectory, but there is no test in useSlashCompletion.test.ts verifying this pass-through. A regression that drops isDirectory here would silently reintroduce trailing spaces for /directory add completions.

Consider adding a test case that exercises /dir add completion and asserts the returned suggestions carry isDirectory: true.

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

thx for your [Suggestion], test cases added and automatically generated by qwen-code

方磊 added 2 commits May 19, 2026 10:23
## Comment 2: Fix JSDoc in SuggestionsDisplay

Removed "(ends with /)" from isDirectory description since it was factually incorrect.

## Comment 3: Add test for isDirectory propagation

- Added test suite in useSlashCompletion.test.ts to verify directory command structure

- Real filesystem testing is done in directoryCommand.test.tsx
@dykebo

dykebo commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

All 3 review comments have been addressed:

  1. ✅ Fixed - Directory completions now include trailing for deeper navigation
  2. ✅ Fixed - JSDoc comment removed incorrect '(ends with /)' parenthetical
  3. ✅ Fixed - Added test suite to verify isDirectory propagation

All 79 tests pass. Ready for re-review.

Added getDirPathCompletions unit tests that verify:
- Directory suggestions include isDirectory: true
- Directory values end with / for continued navigation
- Prefix filtering preserves isDirectory flag
- Comma-separated path completion works correctly
- Deeply nested directories maintain isDirectory flag

This closes the testing gap identified in review comment 3.
@dykebo

dykebo commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

Comment 3 - Fully Resolved ✅

Added comprehensive unit tests in directoryCommand.test.tsx that verify:

  1. Directory suggestions include isDirectory: true
  2. Directory values end with / for continued navigation
  3. Prefix filtering preserves isDirectory flag
  4. Comma-separated paths work correctly
  5. Deep nesting maintains correct behavior

Test coverage now validates the complete data flow from getDirPathCompletions to Suggestion to handleAutocomplete.

Total test count: 83 passing (up from 79)

beforeEach(() => {
// Clean up any previous test runs
try {
fs.rmSync(tempTestDir, { recursive: true, force: true });

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.

[Critical] Empty catch {} block violates ESLint no-empty rule and will fail lint.

Suggested change
fs.rmSync(tempTestDir, { recursive: true, force: true });
} catch {
// tempTestDir may not exist on first beforeEach run
}

Note: tempTestDir is undefined on the first beforeEach invocation, so fs.rmSync(undefined, ...) throws TypeError — silently swallowed by this empty catch. Works by accident. Consider initializing let tempTestDir = '' and guarding cleanup with if (tempTestDir).

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

// Cleanup after all tests
try {
fs.rmSync(tempTestDir, { recursive: true, force: true });
} catch {}

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.

[Critical] Empty catch {} block violates ESLint no-empty rule and will fail lint.

Suggested change
} catch {}
} catch {
// cleanup may fail if temp dir was already removed
}

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

it('should propagate isDirectory flag from CommandCompletionItem to Suggestion', () => {
// This test verifies the toSuggestion pass-through behavior
// We create mock CommandCompletionItems and verify they become Suggestions with isDirectory set
import('./useSlashCompletion.js').then((module) => {

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.

[Critical] Two issues in this test:

  1. ESLint error: module is defined but never used (@typescript-eslint/no-unused-vars). Prefix with _.
  2. No-op test: The dynamic import() is fire-and-forget (never awaited — the test function is synchronous), the .then callback body is empty, and the only assertion expect(directoryCommand).toBeDefined() tests nothing about isDirectory propagation. The toSuggestion pass-through path has zero effective test coverage.

Either replace with a real integration test or remove the block:

it('should propagate isDirectory from CommandCompletionItem to Suggestion', async () => {
  const mockCompletionFn = vi.fn().mockResolvedValue([
    { value: '/tmp/workspace/', isDirectory: true },
    { value: '/tmp/file.txt', isDirectory: false },
  ]);

  const slashCommands = [
    createTestCommand({
      name: 'dir',
      description: 'test',
      completion: mockCompletionFn,
    }),
  ];

  const { result } = renderHook(() =>
    useTestHarnessForSlashCompletion(true, '/dir ', slashCommands, mockCommandContext),
  );

  await waitFor(() => {
    expect(result.current.suggestions.length).toBe(2);
  });

  expect(result.current.suggestions[0].isDirectory).toBe(true);
  expect(result.current.suggestions[1].isDirectory).toBe(false);
});

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

)
.map((e) => prefix + path.join(searchDir, e.name))
.map((e) => ({
value: prefix + path.join(searchDir, e.name) + '/',

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.

[Suggestion] Hardcoded '/' appended to directory completion values. On Windows, path.join(searchDir, e.name) produces backslash-separated paths (e.g., src\components), resulting in mixed-separator values like src\components/.

Consider using path.sep instead — though you'll also need to verify that useAtCompletion.ts (p.endsWith('/')) handles path.sep on Windows consistently.

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

@LaZzyMan LaZzyMan added the type/feature-request New feature or enhancement request label May 19, 2026
…tform

Fixes 4 new review comments from wenshao:

- [Critical] Empty catch {} blocks: guarded with if (tempTestDir) + void err

- [Critical] useSlashCompletion.no-op test: replaced with real integration test that

  verifies isDirectory propagation through toSuggestion pass-through

- [Suggestion] Windows path separator: using path.sep instead of hardcoded /

  in both directoryCommand.tsx and related test assertions
@dykebo

dykebo commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

Hi @wenshao, thanks for the thorough review! I have addressed all your comments:

1. Empty catch {} blocks (lines 340, 360):

  • Initialized tempTestDir with empty string guard
  • Added if (tempTestDir) check before cleanup
  • Used void err instead of empty block

2. useSlashCompletion.test.ts no-op test:

  • Replaced with real integration test using mock completion function
  • Test now actually verifies isDirectory propagation through toSuggestion

3. Windows path separator:

  • Changed hardcoded / to path.sep in directoryCommand.tsx
  • Updated all test assertions to use path.sep

All 83 tests pass. Ready for re-review.

Comment thread packages/cli/src/ui/hooks/useSlashCompletion.test.ts Outdated
// Only directories starting with "su" should be returned
results.forEach((suggestion) => {
expect(suggestion.isDirectory).toBe(true);
expect(suggestion.value).toMatch(/\/su.+$/);

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.

[Critical] Hardcoded / in regex (/\/su.+$/) but implementation at directoryCommand.tsx:91 appends path.sep (which is \ on Windows). The project runs CI on windows-latest, so this test will fail on Windows.

The same issue affects lines 420 and 434 (/\/$/).

Use path.sep-aware assertions:

Suggested change
expect(suggestion.value).toMatch(/\/su.+$/);
const sepRe = path.sep === '\\' ? '\\\\' : path.sep;
expect(suggestion.value).toMatch(new RegExp(`${sepRe}su.+$`));

For lines 420 and 434, replace .toMatch(/\/$/) with:

Suggested change
expect(suggestion.value).toMatch(/\/su.+$/);
expect(suggestion.value.endsWith(path.sep)).toBe(true);

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

- Remove unused directoryCommand import in useSlashCompletion.test.ts (TS6133)
- Replace hardcoded / regex with path.sep-aware assertions in
  directoryCommand.test.tsx to fix Windows CI failures

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Comment thread packages/cli/src/ui/commands/directoryCommand.test.tsx Outdated
Comment thread packages/cli/src/ui/commands/directoryCommand.test.tsx Outdated
'dir/',
'file.txt',
]);
// Verify isDirectory flag

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.

[Suggestion] These new isDirectory assertions are unreachable dead code in their current form.

The host test fails at line 142 (expect(...suggestions).toEqual(['dir/', 'file.txt'])) because the file-search crawler excludes the empty dir/ from results — a pre-existing issue unrelated to this PR. The .toEqual throws before execution ever reaches lines 146-154.

Impact: isDirectory: p.endsWith('/') at useAtCompletion.ts:217 — one of the two main directory-completion entry points — has zero effective test coverage. A regression in the / vs path.sep convention would not be caught.

Suggested fix: Add a self-contained test that mocks the file search results directly (similar to how useCommandCompletion.test.ts mocks useAtCompletion), bypassing the broken crawler:

it('should set isDirectory based on trailing slash in file search results', async () => {
  // Mock fileSearch to return ['dir/', 'file.txt']
  // Assert isDirectory is true for 'dir/' and false for 'file.txt'
});

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

Comment thread packages/cli/src/ui/commands/directoryCommand.tsx
@@ -85,7 +85,10 @@ export function getDirPathCompletions(partialArg: string): string[] {
e.name.startsWith(namePrefix) &&
!e.name.startsWith('.'),
)

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.

[Suggestion] The !e.name.startsWith('.') filter (hiding hidden directories) has no test. If someone removes or inverts this filter, no test fails and directories like .git would start appearing in tab completions.

Suggested fix: In beforeEach, create a .hidden directory in tempTestDir, then in existing tests verify it's excluded:

const names = results.map((r) => path.basename(r.value.slice(0, -1)));
expect(names).not.toContain('.hidden');

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

@wenshao wenshao left a comment

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.

No review findings. Downgraded from Approve to Comment: CI checks still pending (Test on macos-latest, ubuntu-latest, windows-latest). All prior review items (v34) have been addressed. LGTM once CI is green. ✅ — qwen-latest-series-invite-beta-v34 via Qwen Code /review

dykebo and others added 2 commits May 20, 2026 16:39
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Comment thread packages/cli/src/ui/commands/directoryCommand.tsx Outdated
const suggestions = results.map((p) => ({
label: p,
value: escapePath(p),
isDirectory: p.endsWith('/'),

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.

[Suggestion] isDirectory normalization is inconsistent across completion paths. Here, p.endsWith('/') always produces an explicit boolean (false for files). But toSuggestion in useSlashCompletion.ts:524 propagates item.isDirectory as-is, which is undefined when the CommandCompletionItem omits the optional field.

The sole consumer (handleAutocomplete) uses !isDirectory which treats false and undefined identically, so there's no user-visible bug today. However, the three-state boolean (true | false | undefined) is a latent trap for any future code that distinguishes them.

Consider normalizing to an explicit boolean in one place — e.g., isDirectory: item.isDirectory ?? false in toSuggestion, or only setting the field when true in both paths.

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

dykebo and others added 2 commits May 20, 2026 19:21
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Normalize isDirectory from three-state (true/false/undefined) to explicit
boolean (true/false) to prevent latent bugs in future code that might
distinguish between false and undefined.

Fixes review comment: isDirectory normalization is inconsistent across
completion paths.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Comment thread packages/cli/src/ui/hooks/useSlashCompletion.ts Outdated
Comment thread pr_body.md Outdated
@@ -0,0 +1,15 @@
## 🎯 What's Changed

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.

[Suggestion] This PR description draft file was accidentally committed to the repo root. Remove it before merge:

git rm pr_body.md

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

}))
.slice(0, 8);
.slice(0, 8);
} catch {

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.

[Critical] Build is broken — duplicate .slice(0, 8); and orphaned test code in production source.

Line 94 is a duplicate of line 93 (.slice(0, 8);). The semicolon on line 93 terminates the method chain, making line 94 an orphaned expression starting with . — a syntax error. Additionally, lines 99-103 contain test code fragments (expect(...), });) accidentally pasted from directoryCommand.test.tsx, plus an unreachable return []; and an extra }.

tsc reports 4 errors: TS1128: Declaration or statement expected at lines 93, 99, 101, 102.

This appears to be a merge/rebase artifact from the previous fix attempt.

Suggested change
} catch {
.slice(0, 8);
} catch {
return [];
}

Also delete lines 99-103 (the orphaned test code and extra }/return []; after the catch block).

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

Comment thread pr_body.md Outdated
@@ -0,0 +1,15 @@
## 🎯 What's Changed

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.

[Critical] Stray file pr_body.md committed to the repository root. This is a PR description draft artifact and should not be part of the changeset.

Remove with git rm pr_body.md. Consider adding pr_body.md to .gitignore to prevent accidental re-commit.

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

'/dir ',
slashCommands,
mockCommandContext,
),

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.

[Critical] The change isDirectory: item.isDirectory ?? false in toSuggestion() (useSlashCompletion.ts:524) adds isDirectory: false to every suggestion object, including non-directory completions. This breaks the existing test at useSlashCompletion.test.ts:828 which uses .toEqual() with bare object literals that do not include isDirectory:

expect(result.current.suggestions).toEqual([
  { label: 'pdf', value: 'pdf', description: 'Create PDF documents' },
  { label: 'xlsx', value: 'xlsx', description: 'Work with spreadsheets' },
]);

vitest .toEqual() performs strict deep equality — the extra isDirectory: false field causes a mismatch.

Fix: either update the test at line 828 to include isDirectory: false in both expected objects, or change toSuggestion() to only include the field when truthy:

...(item.isDirectory ? { isDirectory: true } : {}),

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

const lineCodePoints = toCodePoints(buffer.lines[cursorRow] || '');
const charAfterCompletion = lineCodePoints[end];
if (charAfterCompletion !== ' ') {
const isDirectory = suggestions[indexToUse].isDirectory;

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.

[Suggestion] The condition !isDirectory suppresses the trailing space unconditionally when isDirectory is true, including when the cursor is mid-line. If the user has text after the cursor (e.g., @src/comp|something), the directory completion merges directly with the following text (producing @src/components/something).

Consider only suppressing the space when the cursor is at end-of-line:

if (charAfterCompletion !== ' ' && !(isDirectory && !charAfterCompletion)) {

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

const suggestions = results.map((p) => ({
label: p,
value: escapePath(p),
isDirectory: p.endsWith('/'),

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.

[Suggestion] p.endsWith('/') relies on an implicit coupling: the file-search crawler in @qwen-code/qwen-code-core normalizes all paths to posix separators (fdir.withPathSeparator('/') in crawler.ts). No shared constant or documentation links these two codebases (different packages).

If someone changes the crawler to use path.sep for cross-platform support, isDirectory silently becomes false for all directories on Windows, regressing the trailing-space fix with no error or log.

Suggested fix: add a comment documenting this dependency, or extract a shared constant (e.g., CRAWLER_PATH_SEPARATOR = '/') in @qwen-code/qwen-code-core imported by both locations.

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

expect(result.current.textBuffer.text).toBe('@src/file1.txt ');
});

it('should not append trailing space for directory completions', async () => {

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.

[Suggestion] The new test only covers directory completion at end-of-line (@src/com). There is no test for directory completion when the cursor is mid-line (e.g., @src/com is a dir with cursor after m). The interaction between isDirectory and charAfterCompletion is untested.

Suggested fix: add a test mirroring the existing "should complete a file path when cursor is not at the end of the line" test (line 586), but with a directory suggestion:

it('should not append trailing space for directory completions when cursor is mid-line', async () => {
  const text = '@src/com is a dir';
  // ... setup with isDirectory: true suggestion ...
  expect(result.current.textBuffer.text).toBe('@src/components/ is a dir');
});

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

dykebo and others added 4 commits May 20, 2026 21:23
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…ryCommand.tsx

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

@wenshao wenshao left a comment

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.

No new review findings in Round 5. All Round 4 Critical issues (duplicate .slice, orphaned test code, pr_body.md, isDirectory test regression) have been resolved. Build passes, 80/83 tests pass (3 pre-existing failures unrelated to this PR). Downgraded from Approve to Comment: CI checks still pending. LGTM once CI is green. ✅ — qwen-latest-series-invite-beta-v34 via Qwen Code /review

…line

When isDirectory is true, the trailing space was suppressed unconditionally,
even when the cursor is mid-line. This caused directory completions to merge
directly with following text (e.g. '@src/components/something').

Now only suppress the space when the cursor is at end-of-line, allowing
continued Tab navigation into subdirectories.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
方磊 and others added 2 commits May 21, 2026 00:07
… check

The isDirectory detection uses p.endsWith('/') which depends on the
crawler in @qwen-code/qwen-code-core normalizing paths with posix '/'
(fdir.withPathSeparator('/') in crawler.ts). Add a comment to make this
implicit coupling explicit.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Verify that directory completions append a trailing space when the cursor
is mid-line, preventing the completed path from merging with following text.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Comment thread packages/cli/src/ui/hooks/useCommandCompletion.test.ts Outdated
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

@wenshao wenshao left a comment

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.

No review findings. Downgraded from Approve to Comment: CI checks still pending (Test (ubuntu-latest, Node 22.x), Test (windows-latest, Node 22.x)). All prior review items have been addressed. LGTM once CI is green. ✅ — gpt-5.5 via Qwen Code /review

@wenshao

wenshao commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Local verification report (maintainer)

Reviewed PR head 862cefb01 on macOS / Node v22.17.0 in a clean worktree at
/Users/wenshao/Work/git/qwen-code-pr4288, with build / test / lint driven from a
dedicated tmux session (pr4288, 7 windows: checkout, install, test-focused,
test-cli, typecheck, lint, run).
PR has no GitHub Actions coverage on this branch (only Classify PR queued / skipped),
so local verification is the primary signal.

Automated checks

Step Result
npm ci (incl. bundle postinstall) exit 0 · 1:14
npm run typecheck (all workspaces) exit 0 · 10.5 s
npm run lint exit 0 · clean (no warnings)
Focused completion tests (4 files) 84 / 84 passed · 13.7 s · directoryCommand 17 · useAtCompletion 13 · useCommandCompletion 24 · useSlashCompletion 30
Full cli vitest (packages/cli) 6436 / 6445 passed, 9 skipped, 0 failed · 2:52

Code review notes

Walked all 11 changed files (+254/-5). The logic in useCommandCompletion.tsx is the heart of the change:

const charAfterCompletion = lineCodePoints[end];
const isDirectory = suggestions[indexToUse].isDirectory;
if (charAfterCompletion !== ' ' && !(isDirectory && !charAfterCompletion)) {
  suggestionText += ' ';
}

Truth table:

Cursor state Is directory Result Why
end-of-line no space added normal file behavior
end-of-line yes no space so user can keep Tab-ing into subdirs
mid-line, next char is space n/a no extra space preserves existing whitespace, no doubling
mid-line, next char is non-space yes space added prevents @src/components/something mash-up (this is the 1fb292c3d fix-up that came after the initial commit)

The isDirectory field is propagated through three paths and all of them feed Suggestion.isDirectory consistently:

  • directoryCommand.tsx / getDirPathCompletions(): returns CommandCompletionItem with isDirectory: true and value ending in path.sep (Windows-safe).
  • useAtCompletion.ts: marks isDirectory: p.endsWith('/') for every crawler result; comment explicitly documents the dependency on the core crawler always emitting posix /.
  • useSlashCompletion.ts / toSuggestion: conditionally spreads isDirectory when defined, so it survives the string | CommandCompletionItem union.

Iteration history is healthy — the PR shows the author absorbing reviewer feedback (cross-platform path.sep, mid-line space restoration, JSDoc cleanup, isDirectory normalization, comprehensive propagation tests).

Interactive TUI verification (tmux run)

Built CLI launched against an isolated home /tmp/pr4288-test/ containing a seeded directory tree (packages/{cli,core,web}/src/, services/, scripts/, docs/, plus one file packages/cli/src/index.ts).

# Sequence Observed
1 Type @pacTab Input becomes @packages/, popup stays open with 8 path suggestions filtered to packages/...
2 ↓ Tab on packages/cli/ Input becomes @packages/cli/, popup narrows to 3 entries ✅
3 ↓ Tab on packages/cli/src/ Input becomes @packages/cli/src/, popup narrows further ✅
4 ↓ Tab on packages/cli/src/index.ts (file) Input becomes @packages/cli/src/index.ts with trailing space, popup closes
5 Type check @pa and review, position cursor between @pa and and review, Tab Input becomes check @packages/ and review — existing space preserved, no doubling ✅
6 Type /dir add ./paTab Input becomes /dir add packages/, popup stays open showing packages/cli/, packages/core/, packages/web/
7 Tab again Input becomes /dir add packages/cli/, popup narrows to packages/cli/src/

The end-of-line vs mid-line dichotomy is exactly what the 1fb292c3d commit intended: directories drill down freely when there's nothing after the cursor, and pick up a space when there's text waiting. The "strict mid-line, no surrounding space" case (e.g., @paZZZ with cursor right after pa) is hard to drive headlessly because the popup auto-closes when the cursor token doesn't match anything — that path is covered by the new unit test useCommandCompletion.test.ts > "should preserve existing space after directory completions at mid-line cursor".

Verdict

  • Behavior matches the issue (Don't add trailing space after Tab-completing a directory path #4092) acceptance criteria.
  • The cross-platform fix-ups (path.sep everywhere, comment about crawler separator coupling) close the obvious Windows footguns.
  • All focused tests + full cli suite pass; no regressions outside this PR's scope.
  • Iteration history shows reviewer comments were addressed, including the important mid-line space restoration.

Safe to merge.

@pomelo-nwu pomelo-nwu merged commit 4dc9848 into QwenLM:main May 23, 2026
10 checks passed
doudouOUC added a commit that referenced this pull request May 25, 2026
…4500)

Pulls 5 main commits since #4469 (2026-05-24):
- #4464 fix(weixin): send decryptable image payloads
- #4465 fix(weixin): allow Windows image paths inside workspace
- #4470 fix(cli): resolve stale closure race in text buffer submit handler
- #4468 feat(skills): add memory-leak-debug skill for heap snapshot diagnosis
- #4288 feat(cli): do not append trailing space for directory completions (#4092)

11 manual conflicts resolved + 2 add/add conflicts taken from main wholesale:

Manual UU (12, all daemon-side preferred except text-buffer.ts):
- packages/acp-bridge/package.json — kept HEAD's fuller description (F1 lift expanded the package surface; main has stale pre-F1 wording).
- packages/cli/src/acp-integration/acpAgent.ts — kept HEAD's WorkspaceMcpBudget import (F2 needs it).
- packages/cli/src/acp-integration/acpAgent.worktree.test.ts (AA): kept HEAD's superset of mocks
  (MCP_BUDGET_WARN_FRACTION, getMCPDiscoveryState, MCPServerStatus, McpTransportPool, WorkspaceMcpBudget, workspace/debug/mcp config mocks). HEAD already includes
  main-side SessionStartSource + SessionEndReason mocks.
- packages/cli/src/ui/commands/directoryCommand.tsx — pure formatting (HEAD wrapped vs main inline). Kept HEAD.
- packages/cli/src/ui/commands/directoryCommand.test.tsx — pure formatting. Kept HEAD.
- packages/cli/src/ui/commands/skillsCommand.ts — pure formatting. Kept HEAD.
- packages/cli/src/ui/hooks/useCommandCompletion.tsx — pure formatting. Kept HEAD.
- packages/cli/src/ui/hooks/useCommandCompletion.test.ts — pure formatting. Kept HEAD.
- packages/cli/src/ui/hooks/useSlashCompletion.test.ts — pure formatting. Kept HEAD.
- packages/core/src/config/config.test.ts — kept HEAD's TrustGateError import (daemon-added).

text-buffer.ts (4 zones — took MAIN wholesale for #4470's stale-closure fix):
- Import: useRef instead of useReducer (daemon side had useReducer as a dead import — file uses dispatch via useCallback, not useReducer; verified via grep). useRef is needed for stateRef + #4470's currentText capture.
- writeFileSync zone: use stateRef.current.lines.join('\n') instead of stale closure-captured `text`. Fixes #4470's bug.
- text comparison: `newText !== currentText` not `newText !== text`.
- dep array: `[dispatch, ...]` not `[text, ...]` (callback reads from ref now, doesn't need to re-bind on text change).

AA (2, main wholesale via git checkout --theirs):
- packages/core/src/permissions/dangerousRules.ts + .test.ts
  Original #4151 Auto-mode added these on main, came into daemon via #4469 squash.
  Main then landed #4371 ("strip additional dangerous interpreter rules") as a follow-up
  that daemon side never saw. Take main's evolved version wholesale.

Verification:
- packages/core tsc: 50 errors PRE-merge, 50 errors POST-merge (pre-existing baseline — none introduced by this sync).
- packages/acp-bridge tsc: clean.
- 5 spot-test runs on conflict-resolved files: 132 + 17 + 24 + 30 + 1 = 204 tests pass (text-buffer / directoryCommand / useCommandCompletion / useSlashCompletion / skillsCommand).

Mirrors #4469's pattern (squash merge daemon_mode_b_main-side). Unblocks
#4490 daemon_mode_b_main → main reverse integration merge (currently
CONFLICTING precisely because of these 5 main commits).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/feature-request New feature or enhancement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants