mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-23 14:33:36 +08:00
Fix four root causes in the desktop preview pipeline, surfaced when the model writes the files the user pointed it at: - Output chips guessed paths from prose and could point at a missing file. They are now reconciled against the turn's real changed files: a bare `index.html` resolves to the `todo-app/index.html` actually written, and mentions the turn never changed are dropped. - A standalone single-page index.html got no browser preview (mistaken for a Vite template). It is now only routed to the source view when a package.json/vite.config ships in the same change-set. - Files written outside the session workdir (another folder, or another drive on Windows) failed to preview with 'Path is outside workspace'. The turn's changed-file directories are registered as filesystem access roots; html serves via /local-file and other files via a workdir-relaxed read. - The visual-selection prompt leaked as a raw bubble on Windows because the server-appended '[Image source: ...]' line broke replay dedupe. Replay text is now metadata-normalized before comparison (affects any image message). Adds unit tests for each: htmlPreviewPolicy, assistantOutputTargets reconciliation, replay dedupe + stripGeneratedImageMetadataLines, filesystem access roots, and workspace outside-workdir reads.
72 lines
3.2 KiB
TypeScript
72 lines
3.2 KiB
TypeScript
import { classifyPreviewLink } from './previewLinkRouter'
|
|
import { shouldOfferStaticHtmlPreview } from './htmlPreviewPolicy'
|
|
import { isAbsoluteLocalPath, localFileUrl, previewFsUrl } from './handlePreviewLink'
|
|
import type { OpenWithContext } from './openWithItems'
|
|
|
|
/**
|
|
* Build an open-with context for a workspace file (we have both its relative +
|
|
* absolute path).
|
|
*
|
|
* `siblingFiles` is the rest of the same change-set (the turn's changed files).
|
|
* It lets {@link shouldOfferStaticHtmlPreview} tell a hand-authored single-page
|
|
* `index.html` (→ offer a static browser preview) from a framework template
|
|
* that ships with a `package.json` / `vite.config.*` (→ source view only).
|
|
*/
|
|
export function openWithContextForWorkspaceFile(
|
|
relPath: string,
|
|
absolutePath: string,
|
|
opts: { sessionId: string; serverBaseUrl: string; siblingFiles?: string[] },
|
|
): OpenWithContext {
|
|
// A changed file that could not be relativized against the workdir arrives with
|
|
// an absolute `relPath` — it lives outside the session workspace (e.g. another
|
|
// drive). Such a file is served by the $HOME/registered-root /local-file route,
|
|
// not the workdir-sandboxed /preview-fs route.
|
|
const outsideWorkspace = isAbsoluteLocalPath(relPath)
|
|
const inAppBrowserUrl = shouldOfferStaticHtmlPreview(relPath, { siblingFiles: opts.siblingFiles })
|
|
? outsideWorkspace
|
|
? localFileUrl(opts.serverBaseUrl, absolutePath)
|
|
: previewFsUrl(opts.serverBaseUrl, opts.sessionId, relPath)
|
|
: undefined
|
|
return {
|
|
kind: 'file',
|
|
absolutePath,
|
|
relPath,
|
|
previewable: true,
|
|
inAppBrowserUrl,
|
|
}
|
|
}
|
|
|
|
function resolveAbsolute(workDir: string | undefined, p: string): string {
|
|
// Tilde paths are home-relative, not workspace-relative — pass them through
|
|
// for the backend (which knows the home dir and platform) to expand.
|
|
if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) return p
|
|
if (!workDir || p.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(p)) return p
|
|
return `${workDir.replace(/[\\/]+$/, '')}/${p.replace(/^[/\\]+/, '')}`
|
|
}
|
|
|
|
export function openWithContextForHref(
|
|
href: string,
|
|
opts: { sessionId: string; serverBaseUrl: string; workDir?: string },
|
|
): OpenWithContext | null {
|
|
const c = classifyPreviewLink(href)
|
|
if ((c.kind === 'browser-localhost' || c.kind === 'remote') && c.url) {
|
|
return { kind: 'url', url: c.url }
|
|
}
|
|
if (c.kind === 'file-preview' && c.path) {
|
|
return { kind: 'file', absolutePath: resolveAbsolute(opts.workDir, c.path), relPath: c.path, previewable: true }
|
|
}
|
|
if (c.kind === 'browser-file' && c.path) {
|
|
// Absolute paths may be outside the session workspace → serve via the
|
|
// $HOME-sandboxed /local-file route; relative paths stay workspace-scoped.
|
|
const absolutePath = resolveAbsolute(opts.workDir, c.path)
|
|
if (isAbsoluteLocalPath(c.path)) {
|
|
return { kind: 'file', absolutePath, inAppBrowserUrl: localFileUrl(opts.serverBaseUrl, c.path) }
|
|
}
|
|
if (shouldOfferStaticHtmlPreview(c.path)) {
|
|
return { kind: 'file', absolutePath, inAppBrowserUrl: previewFsUrl(opts.serverBaseUrl, opts.sessionId, c.path) }
|
|
}
|
|
return { kind: 'file', absolutePath, relPath: c.path, previewable: true }
|
|
}
|
|
return null
|
|
}
|