fix: address Copilot review (9 comments)
#1 GitView: hide/disable 'open in editor' for external linked worktrees (path outside session workspace is rejected by the fence); add isWithinWorkspace client helper for the gate. #2 DiffTab: untracked diff read resolves against diff.repoRoot (child-repo paths are repo-root-relative, not cwd-relative); fs.read route threads selectedRepoOf into resolveGitPath so the host resolves the right base. #3 GitView: chooseRepo handler mirrors chooseWorktree — clears status/ history/log and bumps generation before async refresh, preventing stale rows from acting on the newly selected repository. #4 git.ts: repoRoots/repoRoot compare roots via pathIdentity() (platform- aware) instead of raw includes(), fixing win32 forward/backslash mismatch that silently fell back to roots[0]. #5 markdown-images: mask fenced code blocks and inline code spans before rewriting; only rewrite reference definitions whose label is actually referenced by image syntax (not plain links). +3 tests. #6 package.json: downgrade unrun ^0.3.1 → ^0.2.39 (0.3.x requires Node ^22.13, incompatible with declared engines >=20). #7 market-manifest.spec: shell:true on win32 so execFileSync resolves the pnpm .cmd shim. #8 smoke.spec: build html-route URL via encodeHtmlUrl() (production encoder) so the Windows drive separator is correct. #9 index.ts git.worktrees route: thread selectedRepoOf so a workspace container's selected child repo's linked worktrees appear.
This commit is contained in:
@@ -50,6 +50,14 @@ function normalizeLocalPath(path: string): string {
|
||||
* local paths pass through. Remote (http/https/data/mailto) and `#`-anchor
|
||||
* destinations are left untouched for `MarkdownText`. Reference-style images
|
||||
* (`![x][id]` + `[id]: url`) are covered by rewriting their definition lines.
|
||||
*
|
||||
* Code spans (`` `...` ``) and fenced code blocks (``` ```...``` ```) are
|
||||
* masked before rewriting so documentation that demonstrates ``
|
||||
* is not mutated into a `/sidebar/file` URL. Reference definitions are only
|
||||
* rewritten when their label is actually referenced by an image (collapsed
|
||||
* `[![][id]]`, full `![alt][id]`, or shortcut `![]` referencing the next
|
||||
* definition) — a plain link `[text][id]` must not have its destination
|
||||
* redirected to the media route.
|
||||
* @param text - The raw markdown source (inline + reference images).
|
||||
* @param scope - The session scope (sessionId + cwd) for the media route.
|
||||
* @param filePath - The absolute path of the opened `.md` file.
|
||||
@@ -79,11 +87,38 @@ export function rewriteLocalImageUrls(
|
||||
const candidate = isAbsolutePath(trimmed) ? trimmed : directory + trimmed
|
||||
return mediaUrl(normalizeLocalPath(candidate))
|
||||
}
|
||||
const inline = text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_match, alt, dest) => {
|
||||
|
||||
// Mask fenced code blocks and inline code spans so image-looking text
|
||||
// inside documentation examples is never rewritten. The sentinel uses a
|
||||
// character unlikely to appear in prose; the original spans are restored
|
||||
// after the image rewrite.
|
||||
const masks: string[] = []
|
||||
const masked = text
|
||||
.replace(/```[\s\S]*?```/g, (block) => { masks.push(block); return `\u0000${masks.length - 1}\u0000` })
|
||||
.replace(/`[^`\n]*`/g, (span) => { masks.push(span); return `\u0000${masks.length - 1}\u0000` })
|
||||
|
||||
const inline = masked.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_match, alt, dest) => {
|
||||
return `})`
|
||||
})
|
||||
// Reference-style images: rewrite their `[id]: <url>` definitions.
|
||||
return inline.replace(/^(\s*\[[^\]]+\]:\s*)(<[^>]+>|[^\s]+)/gm, (_match, head, dest) => {
|
||||
|
||||
// Collect labels referenced by image syntax (full `![alt][id]` and
|
||||
// collapsed `![][id]`) so only those reference definitions are rewritten.
|
||||
// A shortcut reference (`![alt]` with no `[id]`) resolves to the label
|
||||
// text `alt` itself; include it too. Plain links `[text][id]` never match
|
||||
// the leading `!` and are left untouched.
|
||||
const imageLabels = new Set<string>()
|
||||
const labelRe = /!\[([^\]]*)\](?:\[((?:[^\][]|\[[^\]]*\])*)\])?/g
|
||||
let labelMatch: RegExpExecArray | null
|
||||
while ((labelMatch = labelRe.exec(inline)) !== null) {
|
||||
const alt = labelMatch[1] ?? ''
|
||||
const ref = labelMatch[2]
|
||||
imageLabels.add(ref !== undefined && ref !== '' ? ref.toLowerCase() : alt.toLowerCase())
|
||||
}
|
||||
|
||||
const refsRewritten = inline.replace(/^(\s*\[([^\]]+)\]:\s*)(<[^>]+>|[^\s]+)/gm, (match, head: string, label: string, dest: string) => {
|
||||
if (!imageLabels.has(label.toLowerCase())) return match
|
||||
return `${head}${resolve(dest.replace(/^<|>$/g, ''))}`
|
||||
})
|
||||
|
||||
return refsRewritten.replace(/\u0000(\d+)\u0000/g, (_m, index: string) => masks[Number(index)] ?? '')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user