From 74c2b4fc3f28e41adad4df50dd3b36db3deb9f17 Mon Sep 17 00:00:00 2001 From: lswang9 <194203888+lswang9@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:34:25 +0800 Subject: [PATCH] fix(deliverables): keep scoped packages and emails out of file links `localPathReference` treated any inline-code token containing a slash as a local path, and any `name.suffix` token as a filename. Scoped package names (`@deepseek-ai/cordis`) and email addresses (`user@example.com`) matched both rules, so assistant prose rendered them as clickable produced-file chips that opened a path which never existed. Reject those two exact shapes after the absolute/relative prefix check, so an explicit `./@scope/pkg` or `node_modules/@foo/bar/lib/client.js` still resolves as a path. Only a bare `@scope/name` with a single slash, or a bare `local@domain.tld`, goes inert. Fixes #85 --- ...sh-client-ui-deliverables+0.1.2-rc.1.patch | 9 +- test/local-path-links.test.ts | 92 ++++++++++++++++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/patches/@deepseek-ai+dsh-client-ui-deliverables+0.1.2-rc.1.patch b/patches/@deepseek-ai+dsh-client-ui-deliverables+0.1.2-rc.1.patch index cacdb1fcd..556bd7604 100644 --- a/patches/@deepseek-ai+dsh-client-ui-deliverables+0.1.2-rc.1.patch +++ b/patches/@deepseek-ai+dsh-client-ui-deliverables+0.1.2-rc.1.patch @@ -10,7 +10,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/client.js if (path === void 0) return void 0; return { open: () => { -@@ -218,6 +218,21 @@ +@@ -218,6 +218,28 @@ title: path }; } }; @@ -20,12 +20,19 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/client.js + * in the current turn. Keep bare identifiers inert: without a slash, an + * absolute-path prefix, or a conventional filename suffix, inline code is + * much more likely to be a command, package, or symbol than a local path. ++ * ++ * A leading `@` plus a single slash is a scoped package name, and a bare ++ * `local@domain.tld` is an email address; both are far more common in ++ * assistant prose than files with those exact shapes, and neither is ++ * openable. An explicit `./@scope/pkg` still resolves as a path above. + */ + function localPathReference(value) { + const candidate = value.trim(); + if (candidate === "" || /[\r\n]/.test(candidate) || /^(?:https?|data|javascript):/i.test(candidate)) return void 0; + const path = candidate.replace(/(?:#L\d+(?:C\d+)?|:\d+(?::\d+)?)$/, ""); + if (/^(?:\/|~[\\/]|\.{1,2}[\\/]|[A-Za-z]:[\\/]|\\\\)/.test(path)) return path; ++ if (/^@[^\\/@\s]+\/[^\\/@\s]+$/.test(path)) return void 0; ++ if (/^[^@\s\\/]+@[^@\s\\/]+\.[A-Za-z]{2,}$/.test(path)) return void 0; + if (/[\\/]/.test(path) || /[\\/]$/.test(path)) return path; + if (/^[^\\/]+\.[A-Za-z0-9][A-Za-z0-9._-]{0,15}$/.test(path)) return path; + return void 0; diff --git a/test/local-path-links.test.ts b/test/local-path-links.test.ts index 03f789a6e..03a6421d4 100644 --- a/test/local-path-links.test.ts +++ b/test/local-path-links.test.ts @@ -1,6 +1,35 @@ import { readFile } from 'node:fs/promises' import { describe, expect, it } from 'vitest' -import { patchPath, projectRoot } from './patch-path' +import { patchPath } from './patch-path' + +/** + * The patch is the source of truth for this helper: `node_modules` may hold a + * different Harness build than the one the patch targets, so read the added + * lines straight out of the patch and evaluate them. + */ +async function loadLocalPathReference(): Promise< + (value: string) => string | undefined +> { + const patch = await readFile( + patchPath('@deepseek-ai/dsh-client-ui-deliverables'), + 'utf8' + ) + const added = patch + .split('\n') + .filter((line) => line.startsWith('+') && !line.startsWith('+++')) + .map((line) => line.slice(1)) + .join('\n') + // The function's closing brace is a context line, not an added one, so the + // extracted body stops at the last `return` and is closed here. + const source = added.match( + /function localPathReference\(value\) \{[\s\S]*?\n\t\t\treturn void 0;/ + )?.[0] + + expect(source).toBeDefined() + return new Function( + `${source}\n}; return localPathReference` + )() as (value: string) => string | undefined +} describe('assistant local path links', () => { it('links Codex-style path references even when they are not turn deliverables', async () => { @@ -15,4 +44,65 @@ describe('assistant local path links', () => { expect(patch).toContain('[A-Za-z]:[\\\\/]') expect(patch).toContain('owner.openFile') }) + + it('resolves real local paths', async () => { + const localPathReference = await loadLocalPathReference() + + for (const value of [ + 'src/main.ts', + './scripts/build.mjs', + '../sibling/file.txt', + 'C:\\Users\\me\\file.txt', + '/etc/hosts', + '~/notes.md', + 'package.json', + 'vitest.config.ts', + 'docs/', + ]) { + expect(localPathReference(value), value).toBe(value) + } + }) + + it('keeps scoped package names and email addresses inert', async () => { + const localPathReference = await loadLocalPathReference() + + for (const value of [ + '@deepseek-ai/cordis', + '@deepseek-ai/dsh-client-ui-deliverables', + '@foo/bar', + '@plugin/name', + 'user@example.com', + 'first.last@sub.example.co', + ]) { + expect(localPathReference(value), value).toBeUndefined() + } + }) + + it('still resolves paths that merely contain an @ segment', async () => { + const localPathReference = await loadLocalPathReference() + + for (const value of [ + './@scope/pkg', + '/tmp/@scope/pkg/index.js', + 'patches/@deepseek-ai+dsh-client-ui-deliverables+0.1.2-rc.1.patch', + 'node_modules/@foo/bar/lib/client.js', + ]) { + expect(localPathReference(value), value).toBe(value) + } + }) + + it('keeps bare identifiers and commands inert', async () => { + const localPathReference = await loadLocalPathReference() + + for (const value of ['npm install', 'someFunction', '', ' ']) { + expect(localPathReference(value), value).toBeUndefined() + } + }) + + it('strips line and column suffixes from resolved paths', async () => { + const localPathReference = await loadLocalPathReference() + + expect(localPathReference('src/main.ts#L42')).toBe('src/main.ts') + expect(localPathReference('src/main.ts:42:7')).toBe('src/main.ts') + }) })