feat(coding-agent): implemented git worktree detection and rendering
- Added `git.repo.linkedWorktreeSync` to identify and resolve git worktree metadata without spawning subprocesses. - Updated `StatusLineComponent` to detect linked worktrees and resolve project/worktree context names. - Modified path segment rendering to collapse nested git worktree paths and display the worktree name when it diverges from the active branch. - Introduced `icon.worktree` symbol across themes to visually distinguish git worktree paths.
This commit is contained in:
@@ -160,6 +160,29 @@ interface ActiveRepoCache {
|
||||
projectDir: string;
|
||||
activeRepo: ActiveRepoContext | null;
|
||||
effectiveGitCwd: string;
|
||||
/** Project + worktree dir name when `projectDir` is a linked worktree, else null. */
|
||||
worktree: WorktreeContext | null;
|
||||
}
|
||||
|
||||
interface WorktreeContext {
|
||||
/** Primary-checkout (project) name shown by the path segment. */
|
||||
projectName: string;
|
||||
/** Worktree directory name — suppressed from the path when it equals the branch. */
|
||||
worktreeName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project + worktree-dir names when `cwd` is a linked git worktree, else null.
|
||||
* The project name comes from the shared primary checkout; bare-repo worktrees
|
||||
* resolve to the shared `foo.git` dir, so a trailing `.git` is stripped.
|
||||
*/
|
||||
function resolveWorktreeContext(cwd: string): WorktreeContext | null {
|
||||
const worktree = git.repo.linkedWorktreeSync(cwd);
|
||||
if (!worktree) return null;
|
||||
const base = path.basename(worktree.primaryRoot);
|
||||
const projectName = base.endsWith(".git") ? base.slice(0, -4) : base;
|
||||
if (!projectName) return null;
|
||||
return { projectName, worktreeName: path.basename(worktree.root) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,7 +335,10 @@ export class StatusLineComponent implements Component {
|
||||
|
||||
const activeRepo = resolveActiveRepoContextSync(projectDir);
|
||||
const effectiveGitCwd = activeRepo?.repoRoot ?? projectDir;
|
||||
this.#activeRepoCache = { projectDir, activeRepo, effectiveGitCwd };
|
||||
// Only collapse the bare-cwd case: a single-direct-child-repo context
|
||||
// (activeRepo set) renders `<parent> ↳ <child>`, which we leave intact.
|
||||
const worktree = activeRepo ? null : resolveWorktreeContext(effectiveGitCwd);
|
||||
this.#activeRepoCache = { projectDir, activeRepo, effectiveGitCwd, worktree };
|
||||
return this.#activeRepoCache;
|
||||
}
|
||||
|
||||
@@ -988,7 +1014,7 @@ export class StatusLineComponent implements Component {
|
||||
const projectDir = getProjectDir();
|
||||
const activeRepoCache = shouldResolveActiveRepo
|
||||
? this.#resolveActiveRepoCache()
|
||||
: { projectDir, activeRepo: null, effectiveGitCwd: projectDir };
|
||||
: { projectDir, activeRepo: null, effectiveGitCwd: projectDir, worktree: null };
|
||||
const gitBranch = includeGit || includePr ? this.#getCurrentBranch(activeRepoCache.effectiveGitCwd) : null;
|
||||
const gitStatus = includeGit ? this.#getGitStatus(activeRepoCache.effectiveGitCwd) : null;
|
||||
const gitPr = includePr ? this.#lookupPr(activeRepoCache.effectiveGitCwd) : null;
|
||||
@@ -1015,6 +1041,7 @@ export class StatusLineComponent implements Component {
|
||||
status: gitStatus,
|
||||
pr: gitPr,
|
||||
},
|
||||
worktree: activeRepoCache.worktree,
|
||||
usage: this.#cachedUsage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,13 @@ function withIcon(icon: string, text: string): string {
|
||||
return icon ? `${icon} ${text}` : text;
|
||||
}
|
||||
|
||||
/** Left-truncate a path/label to `maxLen`, prefixing an ellipsis when clipped. */
|
||||
function clampPathLength(pwd: string, maxLen: number): string {
|
||||
if (pwd.length <= maxLen) return pwd;
|
||||
const ellipsis = "…";
|
||||
return `${ellipsis}${pwd.slice(-Math.max(0, maxLen - ellipsis.length))}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Leading glyph of a thinking-level display string (e.g. "◉ xhigh" → "◉").
|
||||
* Compact mode promotes this glyph to the model-segment icon so the level
|
||||
@@ -220,12 +227,24 @@ const pathSegment: StatusLineSegment = {
|
||||
id: "path",
|
||||
render(ctx) {
|
||||
const opts = ctx.options.path ?? {};
|
||||
const stripPrefix = opts.stripWorkPrefix !== false;
|
||||
|
||||
// Linked git worktree: the on-disk path nests the worktree base, the
|
||||
// project, and a worktree dir that usually duplicates the branch (already
|
||||
// shown by the git segment). Collapse to the project name, appending the
|
||||
// worktree dir only when it diverges from the branch.
|
||||
if (stripPrefix && ctx.worktree) {
|
||||
const { projectName, worktreeName } = ctx.worktree;
|
||||
const label = ctx.git.branch === worktreeName ? projectName : `${projectName}/${worktreeName}`;
|
||||
const content = withIcon(theme.icon.worktree, clampPathLength(label, opts.maxLength ?? 40));
|
||||
return { content: theme.fg("statusLinePath", content), visible: true };
|
||||
}
|
||||
|
||||
const projectDir = ctx.activeRepo?.cwd ?? getProjectDir();
|
||||
const { scratch, relative } = classifyProjectDir(projectDir);
|
||||
let pwd = projectDir;
|
||||
|
||||
if (opts.stripWorkPrefix !== false) {
|
||||
if (stripPrefix) {
|
||||
if (scratch) {
|
||||
if (relative) pwd = relative;
|
||||
} else {
|
||||
@@ -237,17 +256,12 @@ const pathSegment: StatusLineSegment = {
|
||||
pwd = shortenPath(pwd);
|
||||
}
|
||||
|
||||
const maxLen = opts.maxLength ?? 40;
|
||||
if (pwd.length > maxLen) {
|
||||
const ellipsis = "…";
|
||||
const sliceLen = Math.max(0, maxLen - ellipsis.length);
|
||||
pwd = `${ellipsis}${pwd.slice(-sliceLen)}`;
|
||||
}
|
||||
pwd = clampPathLength(pwd, opts.maxLength ?? 40);
|
||||
if (repoSuffix) {
|
||||
pwd = `${pwd}${repoSuffix}`;
|
||||
}
|
||||
|
||||
const showScratchIcon = scratch && opts.stripWorkPrefix !== false;
|
||||
const showScratchIcon = scratch && stripPrefix;
|
||||
const icon = showScratchIcon ? theme.icon.scratchFolder : theme.icon.folder;
|
||||
const content = withIcon(icon, pwd);
|
||||
return { content: theme.fg("statusLinePath", content), visible: true };
|
||||
|
||||
@@ -97,6 +97,13 @@ export interface SegmentContext {
|
||||
status: { staged: number; unstaged: number; untracked: number } | null;
|
||||
pr: { number: number; url: string } | null;
|
||||
};
|
||||
/**
|
||||
* Set when the path cwd is a *linked* git worktree, naming the shared
|
||||
* primary checkout (the project). Lets the path segment collapse the
|
||||
* base-prefixed `<base>/<project>/<worktree>` path to the project name —
|
||||
* the worktree/branch is already shown by the git segment.
|
||||
*/
|
||||
worktree: { projectName: string; worktreeName: string } | null;
|
||||
usage: {
|
||||
tier?: string;
|
||||
fiveHour?: { percent: number; resetMinutes?: number };
|
||||
|
||||
Reference in New Issue
Block a user