mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
* fix(mobile): open wrapped terminal urls and paths with desktop's link detection Desktop's terminal URL and file-link detectors move to src/shared and the phone's WebView document calls them, replacing its single-row copies. File taps carry every reading of the tapped text so the phone opens the longest one the host finds, as desktop does. * fix(mobile): keep the unc host of osc 8 file links for the host to resolve * fix(mobile): resolve terminal file-tap readings a few at a time A tap inside a run of path rows yields hundreds of hard-wrap guesses; firing them all at once overflowed the page bridge's pending-request cap and the real path was never checked. * refactor(terminal): share the osc 8 link classifier and trim file-tap plumbing Desktop and the phone now read OSC 8 targets through one shared classifier. Terminal file taps carry only their candidate list, deduped by path, and desktop checks OSC overlap only for candidates it can resolve. * refactor(terminal): keep shared link files under the line cap The xterm buffer subset gets its own module, and the text-only OSC resolver goes now that the router uses findExactTerminalFileLink.
131 lines
3.6 KiB
TypeScript
131 lines
3.6 KiB
TypeScript
type NormalizedAbsolutePath = {
|
||
normalized: string
|
||
comparisonKey: string
|
||
rootKind: 'posix' | 'windows' | 'unc'
|
||
}
|
||
|
||
function normalizeSegments(pathValue: string): string[] {
|
||
const segments = pathValue.split(/[\\/]+/)
|
||
const stack: string[] = []
|
||
for (const segment of segments) {
|
||
if (!segment || segment === '.') {
|
||
continue
|
||
}
|
||
if (segment === '..') {
|
||
if (stack.length > 0) {
|
||
stack.pop()
|
||
}
|
||
continue
|
||
}
|
||
stack.push(segment)
|
||
}
|
||
|
||
return stack
|
||
}
|
||
|
||
export function normalizeAbsolutePath(pathValue: string): NormalizedAbsolutePath | null {
|
||
const windowsDriveMatch = /^([A-Za-z]):[\\/]*(.*)$/.exec(pathValue)
|
||
if (windowsDriveMatch) {
|
||
const driveLetter = windowsDriveMatch[1].toUpperCase()
|
||
const suffix = normalizeSegments(windowsDriveMatch[2]).join('/')
|
||
const normalized = suffix ? `${driveLetter}:/${suffix}` : `${driveLetter}:/`
|
||
return {
|
||
normalized,
|
||
comparisonKey: normalized.toLowerCase(),
|
||
rootKind: 'windows'
|
||
}
|
||
}
|
||
|
||
const uncMatch = /^(?:\\\\|\/\/)([^\\/]+)[\\/]+([^\\/]+)(?:[\\/]*(.*))?$/.exec(pathValue)
|
||
if (uncMatch) {
|
||
const server = uncMatch[1]
|
||
const share = uncMatch[2]
|
||
const suffix = normalizeSegments(uncMatch[3] ?? '').join('/')
|
||
const normalizedRoot = `//${server}/${share}`
|
||
const normalized = suffix ? `${normalizedRoot}/${suffix}` : normalizedRoot
|
||
return {
|
||
normalized,
|
||
comparisonKey: normalized.toLowerCase(),
|
||
rootKind: 'unc'
|
||
}
|
||
}
|
||
|
||
if (pathValue.startsWith('/')) {
|
||
const normalized = `/${normalizeSegments(pathValue).join('/')}`.replace(/\/+$/, '') || '/'
|
||
return {
|
||
normalized,
|
||
comparisonKey: normalized,
|
||
rootKind: 'posix'
|
||
}
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
function inferHomePathFromCwd(cwd: string): string | null {
|
||
const normalizedCwd = normalizeAbsolutePath(cwd)
|
||
if (!normalizedCwd) {
|
||
return null
|
||
}
|
||
|
||
const segments = normalizeSegments(normalizedCwd.normalized)
|
||
if (normalizedCwd.rootKind === 'windows') {
|
||
const [drive, usersSegment, userSegment] = segments
|
||
if (!drive || !usersSegment || !userSegment || usersSegment.toLowerCase() !== 'users') {
|
||
return null
|
||
}
|
||
return `${drive}/${usersSegment}/${userSegment}`
|
||
}
|
||
|
||
if (normalizedCwd.rootKind === 'posix') {
|
||
const [rootParent, userSegment] = segments
|
||
if ((rootParent === 'Users' || rootParent === 'home') && userSegment) {
|
||
return `/${rootParent}/${userSegment}`
|
||
}
|
||
if (rootParent === 'root') {
|
||
return '/root'
|
||
}
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
function normalizeExplicitHomePath(homePath: string | null | undefined): string | null {
|
||
const trimmedHomePath = homePath?.trim()
|
||
if (!trimmedHomePath) {
|
||
return null
|
||
}
|
||
|
||
return normalizeAbsolutePath(trimmedHomePath)?.normalized ?? null
|
||
}
|
||
|
||
export function resolveTildePath(
|
||
pathValue: string,
|
||
cwd: string,
|
||
homePath?: string | null
|
||
): string | null {
|
||
if (!/^~[\\/]/.test(pathValue)) {
|
||
return null
|
||
}
|
||
|
||
// Prefer explicit terminal home; remote/devcontainer cwd need not sit under the user’s home.
|
||
const resolvedHomePath = normalizeExplicitHomePath(homePath) ?? inferHomePathFromCwd(cwd)
|
||
if (!resolvedHomePath) {
|
||
return null
|
||
}
|
||
|
||
return joinAbsolutePath(resolvedHomePath, pathValue.slice(2))
|
||
}
|
||
|
||
export function joinAbsolutePath(basePath: string, relativePath: string): string | null {
|
||
const normalizedBase = normalizeAbsolutePath(basePath)
|
||
if (!normalizedBase) {
|
||
return null
|
||
}
|
||
|
||
const base = normalizedBase.normalized.replace(/\/$/, '')
|
||
const suffix = relativePath.replace(/^[\\/]+/, '')
|
||
// Resolve parent segments against the base while its drive or UNC share remains the root.
|
||
return normalizeAbsolutePath(`${base}/${suffix}`)?.normalized ?? null
|
||
}
|