DSH 0.1.6-alpha.2 made two plugin-facing contracts stricter, and both broke
this plugin silently:
- `SidebarRightGuideEntry.id` is now required and unique per provider. The
native tab registrations sent guide entries without one, so the second kind
collided on `undefined` and `SidebarRightTabRegistry.register` threw
'duplicate guide entry id'. The throw happens inside `ctx.inject`'s callback
body, which cordis swallows, so the whole native surface stayed empty with no
symptom beyond a missing guide row.
- `conversation.chat.turnTail` changed from a `chain` to an additive `list`.
The chain's `select` made one entry render and let this plugin REPLACE the
host's produced-files row; a list cannot express that, and the registration
now throws 'list slot ... requires options.id'. Upstream chose the list
deliberately ('An additive list lets both plugins contribute without either
knowing the other's data or rendering'), so the plugin's row can only ever
duplicate the host's deliverables row -- which itself opens files through the
host's openFile, i.e. into this plugin's editor. The contribution is removed
and the row is the host's alone.
The file-opening half survives as `sidebar-file.ts` (the changes tab and the
editor host still use it), and `resolveSidebarPath` moves to the `paths.ts`
path-helper module beside its siblings. `registerNativeSurface` grows an
optional `reportFailure` so a future contract break reaches the visible
diagnostic strip instead of a silently empty guide, and one bad descriptor can
no longer take the remaining registrations down with it.
99 lines
4.2 KiB
TypeScript
99 lines
4.2 KiB
TypeScript
/**
|
|
* Path projection helpers shared by the explorer rows: a path relative to
|
|
* the session cwd (for the @-reference button and "copy relative path").
|
|
* The fs-tree joins with '/' even on Windows, so both separators normalize
|
|
* to '/' before comparison.
|
|
*
|
|
* This module is dependency-free (no node:path in the client bundle): the
|
|
* host is the authority for path semantics, so this mirror deliberately
|
|
* accepts a SUPERSET of absolute forms — anything a Windows host would emit
|
|
* (drive letters, UNC) plus POSIX roots. A form the host would reject
|
|
* (e.g. a backslash UNC path on a POSIX host) passes through here and then
|
|
* fails loudly in the host's requireAbsolute instead of being silently
|
|
* joined onto the cwd.
|
|
*/
|
|
|
|
/**
|
|
* Mirror of the host's absolute-path notion (see fs-tree.requireAbsolute):
|
|
* POSIX roots, Windows drive letters, and Windows UNC network shares in
|
|
* both backslash (`\\server\share\...`) and forward-slash
|
|
* (`//server/share/...`) form. Deliberately a superset — see the module
|
|
* comment — so a produced UNC path is never joined onto the cwd.
|
|
*/
|
|
export function isAbsolutePath(path: string): boolean {
|
|
return path.startsWith('/') || /^[A-Za-z]:[\\/]/.test(path) || /^[\\/]{2}[^\\/]/.test(path)
|
|
}
|
|
|
|
/**
|
|
* The path relative to the session's working directory.
|
|
* @param cwd - the explorer root (absolute).
|
|
* @param path - an absolute entry path from the fs-tree.
|
|
* @returns the relative path with '/' separators ('.' for the cwd itself),
|
|
* or `path` unchanged when it lies outside the cwd.
|
|
*
|
|
* The prefix test is case-insensitive: Windows paths (and macOS's
|
|
* case-insensitive volumes) may arrive with different casing than the cwd
|
|
* row, and the containment decision must not depend on it. The returned
|
|
* relative text keeps the caller's own casing.
|
|
*/
|
|
export function relativeTo(cwd: string, path: string): string {
|
|
const base = cwd.replace(/[\\/]+$/, '')
|
|
const norm = (value: string): string => value.replace(/\\/g, '/')
|
|
const nBase = norm(base)
|
|
const nPath = norm(path)
|
|
if (nPath === nBase) return '.'
|
|
if (nPath.toLowerCase().startsWith(`${nBase.toLowerCase()}/`)) return nPath.slice(nBase.length + 1)
|
|
return path
|
|
}
|
|
|
|
/**
|
|
* Whether `target` lies under `base` (or equals it), tolerant of separator
|
|
* style and — on Windows-style drive paths — of letter case. A client-side
|
|
* mirror of the host's `isWithin` (fs-tree.ts) used to decide whether a
|
|
* git-derived path can be opened in the editor (a linked worktree outside
|
|
* the session workspace cannot: the host's workspace fence would reject it).
|
|
*/
|
|
export function isWithinWorkspace(base: string, target: string): boolean {
|
|
const norm = (value: string): string => value.replace(/[\\/]+/g, '/').replace(/\/$/, '')
|
|
const b = norm(base)
|
|
const t = norm(target)
|
|
const lb = b.toLowerCase()
|
|
const lt = t.toLowerCase()
|
|
return lt === lb || lt.startsWith(`${lb}/`)
|
|
}
|
|
|
|
/**
|
|
* The last path segment of a '/'- or '\'-separated path (a diff tab title,
|
|
* a worktree label). Returns the whole string when no separator is present.
|
|
*/
|
|
export function baseName(path: string): string {
|
|
const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
|
|
return at === -1 ? path : path.slice(at + 1)
|
|
}
|
|
|
|
/**
|
|
* The lowercased file extension of a path ('' when none). The dot must sit
|
|
* inside the last segment — a dot in a directory name is not an extension.
|
|
* Shared by the editor language mapping (lang.ts) and the viewer registry's
|
|
* extension matching (service.ts), which both live in the core bundle.
|
|
*/
|
|
export function extOf(path: string): string {
|
|
const at = path.lastIndexOf('.')
|
|
if (at === -1) return ''
|
|
const base = path.slice(at + 1).toLowerCase()
|
|
return base.includes('/') || base.includes('\\') ? '' : base
|
|
}
|
|
|
|
/**
|
|
* Resolve a (possibly relative) path against the session cwd for the sidebar.
|
|
* Absolute detection mirrors the host (see {@link isAbsolutePath}): POSIX
|
|
* roots, drive letters and UNC shares must not be joined onto the cwd.
|
|
*/
|
|
export function resolveSidebarPath(cwd: string | undefined, path: string): string {
|
|
if (isAbsolutePath(path)) return path
|
|
const base = cwd ?? ''
|
|
if (base === '') return path
|
|
const separator = base.includes('\\') ? '\\' : '/'
|
|
return `${base.replace(/[\\/]+$/, '')}${separator}${path}`
|
|
}
|