diff --git a/apps/desktop/src/app/artifacts/artifact-utils.ts b/apps/desktop/src/app/artifacts/artifact-utils.ts index db2d0a9f26..4609150adf 100644 --- a/apps/desktop/src/app/artifacts/artifact-utils.ts +++ b/apps/desktop/src/app/artifacts/artifact-utils.ts @@ -56,7 +56,40 @@ const SCREENSHOT_PATH_RE = /Screenshot path:\s*([^\r\n<>]+)/gi // scraped heuristically out of prose or a tool payload. type PushValue = (value: string, explicit?: boolean) => void +// pip's own traffic, not session output (#52972): terminal output logs every +// download (`Downloading https:///packages/…/pkg-1.0-py3-none-any.whl`, +// plus its `.whl.metadata`) and cached files live under pip's cache dir. +const PIP_CACHE_DIR_RE = /\/(?:\.cache\/pip|library\/caches\/pip|appdata\/local\/pip\/cache)\// +const WHEEL_RE = /\.whl(?:\.metadata)?$/ +const PACKAGE_INDEX_DIST_RE = /\/packages\/.+\.(?:tar\.gz|tar\.bz2|zip|egg)(?:\.metadata)?$/ + +function artifactPathForFiltering(value: string): string { + if (/^(?:https?|file):\/\//i.test(value)) { + try { + return decodeURIComponent(new URL(value).pathname).toLowerCase() + } catch { + // Malformed URL: fall through to plain string normalization. + } + } + + return value.replace(/\\/g, '/').toLowerCase() +} + +function isPythonPackageDownload(value: string): boolean { + const path = artifactPathForFiltering(value) + + return ( + PIP_CACHE_DIR_RE.test(path) || + WHEEL_RE.test(path) || + (/^https?:\/\//i.test(value) && PACKAGE_INDEX_DIST_RE.test(path)) + ) +} + function looksLikeArtifact(value: string, explicit = false): boolean { + if (!explicit && isPythonPackageDownload(value)) { + return false + } + if (/^(?:https?:\/\/|data:image\/)/.test(value)) { return true } diff --git a/apps/desktop/src/app/artifacts/index.test.ts b/apps/desktop/src/app/artifacts/index.test.ts index 943b5ccf74..f6ef646c50 100644 --- a/apps/desktop/src/app/artifacts/index.test.ts +++ b/apps/desktop/src/app/artifacts/index.test.ts @@ -251,6 +251,36 @@ ${payload} ]) }) + // #52972: pip logs every download with its full URL when the index is not + // files.pythonhosted.org (a mirror), and on Windows reports sdists under + // its cache dir. None of that is something the session produced. + it('does not index pip downloads or cache files from terminal output', () => { + const mirror = 'https://mirror.example.com/pypi/packages/7a/1b/0f3c' + const report = '/home/example/project/report.pdf' + const release = 'https://github.com/example/tool/archive/refs/tags/v1.0.tar.gz' + + const artifacts = collectArtifactsForSession(makeSession({ id: 'pip-session' }), [ + { + content: JSON.stringify({ + output: [ + 'Collecting anthropic', + ` Downloading ${mirror}/anthropic-0.46.0-py3-none-any.whl.metadata (23 kB)`, + ` Downloading ${mirror}/anthropic-0.46.0-py3-none-any.whl (223 kB)`, + ` Downloading ${mirror}/jiter-0.8.2.tar.gz (163 kB)`, + ' Saved C:\\Users\\Alice\\AppData\\Local\\pip\\Cache\\http-v2\\a\\b\\docstring_parser-0.16.tar.gz', + `Wrote ${report}; upstream release: ${release}` + ].join('\n'), + exit_code: 0 + }), + role: 'tool', + timestamp: 1_781_774_001, + tool_name: 'terminal' + } + ]) + + expect(artifacts.map(artifact => artifact.value).sort()).toEqual([report, release].sort()) + }) + it('does not treat an arbitrary dotted absolute path as an artifact', () => { const artifacts = collectArtifactsForSession(makeSession(), [ {