From 64f4efe31df82eeaafc8bd1e144d7522061f0cd5 Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 23 Oct 2026 15:14:06 +0800 Subject: [PATCH] fix(ui-conversation): decorate only whole folder tokens in the composer A pasted file path such as @src/a.ts or @"dir/a b.md" styled its directory prefix as a folder reference. --- .../src/client/input/decorations.ts | 36 ++++++++++++++----- .../tests/draft-document.client.spec.ts | 7 ++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/client/ui-conversation/src/client/input/decorations.ts b/packages/client/ui-conversation/src/client/input/decorations.ts index d71c4fb252..83be8dec9c 100644 --- a/packages/client/ui-conversation/src/client/input/decorations.ts +++ b/packages/client/ui-conversation/src/client/input/decorations.ts @@ -22,7 +22,28 @@ export interface TextRefRange { /** Token matcher: a trigger char at line start or after whitespace, then a word-ish name (never crosses \n). */ const TEXT_REF_RE = /(^|\s)([/@])([\w-]+)/g -const FOLDER_REF_RE = /(^|\s)(@(?:"[^"\n]*\/|[^\s"]+\/))/g +/** One `@` path token: a quoted path through its closing quote or line end, or an unquoted run. */ +const PATH_TOKEN_RE = /(^|\s)(@(?:"[^"\n]*"?|[^\s"]+))/g +/** What may follow a folder token: the trailing punctuation the sent-message path rule strips, then whitespace or the draft end. */ +const FOLDER_END_RE = /[.,;:!?,。;:!?]*(?:\s|$)/y + +/** + * End of the whole `@dir/` folder token at `start`, so `@src/a.ts` and + * `@"a/b c.md"` never match their directory prefix. A closed quote must + * directly follow the slash; an unquoted or open-quoted token (a drill in + * progress) ends at its last slash. + * @param draft - draft text. + * @param start - offset of the token's `@`. + * @param token - the path token at `start`. + * @returns the folder token's end offset, or undefined for a non-folder token. + */ +function folderEnd(draft: string, start: number, token: string): number | undefined { + const closed = token.length > 2 && token.startsWith('@"') && token.endsWith('"') + const end = closed ? start + token.length : start + token.lastIndexOf('/') + 1 + if (end === start || (closed && !token.endsWith('/"'))) return undefined + FOLDER_END_RE.lastIndex = end + return FOLDER_END_RE.test(draft) ? end : undefined +} /** * What may follow a `/name` token: whitespace or the draft end, the boundary * the host skill gesture (`dsh-tool-skill`) requires, so `/nfs-hg/xxx`, @@ -56,13 +77,12 @@ export function scanTextRefs( out.push({ start, end: start + 1 + name.length, trigger }) } } - FOLDER_REF_RE.lastIndex = 0 - let folder: RegExpExecArray | null - while ((folder = FOLDER_REF_RE.exec(draft)) !== null) { - const token = folder[2] ?? '' - const start = folder.index + (folder[1]?.length ?? 0) - const end = start + token.length - if (!out.some(range => range.start < end && range.end > start)) { + PATH_TOKEN_RE.lastIndex = 0 + let path: RegExpExecArray | null + while ((path = PATH_TOKEN_RE.exec(draft)) !== null) { + const start = path.index + (path[1]?.length ?? 0) + const end = folderEnd(draft, start, path[2] ?? '') + if (end !== undefined && !out.some(range => range.start < end && range.end > start)) { out.push({ start, end, trigger: '@' }) } } diff --git a/packages/client/ui-conversation/tests/draft-document.client.spec.ts b/packages/client/ui-conversation/tests/draft-document.client.spec.ts index ef737eec06..c7b15cff18 100644 --- a/packages/client/ui-conversation/tests/draft-document.client.spec.ts +++ b/packages/client/ui-conversation/tests/draft-document.client.spec.ts @@ -811,4 +811,11 @@ describe('plain-text reference catalogs', () => { { start: 0, end: 14, trigger: '@' }, ]) }) + + it('matches whole folder tokens, never the directory prefix of a pasted file path', () => { + expect(scanTextRefs('@src/deep/file.txt @"/a/nv/SM100 ISA.html" 看 @"a/ b.md" @src/.env @src/de', new Map())).toEqual([]) + const text = '@"dir a/" @src/deep/。 @dir/ @"open dir/ x' + expect(scanTextRefs(text, new Map()).map(range => text.slice(range.start, range.end))) + .toEqual(['@"dir a/"', '@src/deep/', '@dir/', '@"open dir/']) + }) })