From 6740e26261c84a3cf613198d4d77d4c382ec692d Mon Sep 17 00:00:00 2001 From: Justin Parker Date: Fri, 31 Jul 2026 22:20:38 -0700 Subject: [PATCH 1/2] Add git worktree awareness to status line Sessions in a linked worktree previously showed only the worktree's directory name, so the underlying repository was unidentifiable and the session looked like an unrelated project. The project segment now renders as "repo | worktree". Repo identity comes from git rev-parse --git-common-dir, so it is correct regardless of how the worktree directory was named. A leading repo prefix is stripped from the worktree name, and the name collapses to a bare marker when it merely restates the branch already on screen -- matched across / _ . - separators and type prefixes like feature/, and only when the branch is displayed. Gated behind the new items.worktree config flag (default on). Costs one extra timeout-bounded git call, skipped entirely outside git repos. --- src/statusline-claude/README.md | 31 +++++++ src/statusline-claude/statusline-config.json | 1 + src/statusline-claude/statusline.js | 90 ++++++++++++++++++-- 3 files changed, 117 insertions(+), 5 deletions(-) diff --git a/src/statusline-claude/README.md b/src/statusline-claude/README.md index 57b41d7..7a6dfd4 100644 --- a/src/statusline-claude/README.md +++ b/src/statusline-claude/README.md @@ -20,6 +20,14 @@ A persistent three-line status bar for Claude Code that displays model info, pro ╰─╯ 2m ($0.18) │ ▰▰▱▱▱▱▱▱▱▱▱▱ 18% (36k) │ 88k in · 3k out ``` +**Git worktree** — session running in a linked worktree at `C:\git\plan2code-user-auth`: + +``` +╭─╮ Opus 4.6 | High │ plan2code ⑂ │ feature/user-auth │ +12 -3 +│★│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ +╰─╯ 3h 5m ($4.62) │ ▰▰▰▰▰▱▱▱▱▱▱▱ 42% (84k) │ 5h: 28% · 7d: 61% +``` + **Line 1:** Planny icon | Model name + reasoning effort | Project name | Git branch | Uncommitted changes **Line 2:** Planny icon | Gray separator **Line 3:** Planny icon | Session duration + cost | Context window bar | Usage (rate limits or token counts) @@ -65,6 +73,7 @@ Edit `~/.claude/statusline-config.json`: "model": true, "effort": true, "project": true, + "worktree": true, "branch": true, "contextBar": true, "planUsage": true, @@ -83,6 +92,7 @@ Edit `~/.claude/statusline-config.json`: | `items.model` | `true` | Show model name (Opus, Sonnet, Haiku) | | `items.effort` | `true` | Append the current reasoning effort level (Low/Medium/High/XHigh/Max) to the model segment, e.g. `Sonnet 5 \| High`. Reads `effort.level` from stdin; hidden when the current model doesn't support an effort parameter (field absent from stdin). | | `items.project` | `true` | Show project directory name | +| `items.worktree` | `true` | Detect [git worktrees](https://git-scm.com/docs/git-worktree) and render the project segment as `repo ⑂ worktree` instead of just the worktree's directory name. See [Worktree Display](#worktree-display). Requires `items.project`. | | `items.branch` | `true` | Show current git branch (falls back to short SHA when detached HEAD) | | `items.contextBar` | `true` | Show context window usage bar with percentage | | `items.contextTokens` | `true` | Append raw tokens used in the context window next to the percentage, e.g. `42% (84k)`. Reads `context_window.total_input_tokens` (the same input-token count `used_percentage` is derived from — excludes output tokens). Requires `items.contextBar` to also be enabled. | @@ -101,6 +111,27 @@ Malformed config falls back to defaults silently. Type errors on individual keys | Copilot CLI | Planned | Deferred — CLI lacks status line script support as of v0.0.421 | | Others | Not supported | Codex CLI, Gemini CLI have built-in status, not scriptable | +## Worktree Display + +With `items.worktree` disabled, a session in a linked worktree shows only that worktree's directory name (`plan2code-user-auth`) — the underlying repository identity is lost, and the session looks like an unrelated project. + +When enabled, the project segment becomes `repo ⑂ worktree`: + +| Worktree directory | Branch | Rendered | Why | +|--------------------|--------|----------|-----| +| `plan2code` (primary checkout) | `main` | `plan2code` | Not a linked worktree — unchanged | +| `plan2code-spike` | `spike-thing` | `plan2code ⑂ spike` | Repo prefix stripped from the worktree name | +| `plan2code-user-auth` | `feature/user-auth` | `plan2code ⑂` | Name is redundant with the visible branch — collapses to the bare marker | +| `plan2code-user-auth` | `feature/user-auth`, `items.branch: false` | `plan2code ⑂ user-auth` | No branch shown, so the name is kept | + +Details: + +- **Detection** — a single `git rev-parse --git-dir --git-common-dir`. The two paths differ only inside a linked worktree. The true repo name comes from `--git-common-dir` (its parent directory, or the `.git` basename for a bare main repo), so it is correct regardless of how the worktree directory was named. +- **Prefix stripping** — a leading repo name followed by `-`, `_`, or `.` is removed, so `plan2code-user-auth` reads as `user-auth`. +- **Redundancy collapse** — worktree directories usually mirror their branch. The name is dropped (leaving `⑂`) when it matches the branch after normalizing case and `/ _ . -` separators, compared against both the full branch and its trailing segment so type prefixes like `feature/` don't defeat the match. This only applies when the branch is actually displayed. +- **Color** — the repo keeps its bold silver; the `⑂` marker and worktree name render in amber so a non-primary checkout is obvious at a glance. Monochrome output is `repo ⑂ worktree`. +- **Cost** — one extra `git` invocation, subject to the same 1.5s timeout, and skipped entirely outside git repos. + ## Usage Display The usage segment reads data directly from Claude Code's stdin JSON — no API calls, no auth needed, no background processes. diff --git a/src/statusline-claude/statusline-config.json b/src/statusline-claude/statusline-config.json index 14231af..ada0853 100644 --- a/src/statusline-claude/statusline-config.json +++ b/src/statusline-claude/statusline-config.json @@ -6,6 +6,7 @@ "model": true, "effort": true, "project": true, + "worktree": true, "branch": true, "contextBar": true, "contextTokens": true, diff --git a/src/statusline-claude/statusline.js b/src/statusline-claude/statusline.js index abffff4..6556332 100644 --- a/src/statusline-claude/statusline.js +++ b/src/statusline-claude/statusline.js @@ -28,6 +28,7 @@ const DEFAULTS = { model: true, effort: true, project: true, + worktree: true, branch: true, contextBar: true, contextTokens: true, @@ -90,6 +91,7 @@ const C = { // Plan2Code brand teal: rgb(190, 192, 200), // Silver — repo sky: rgb(120, 195, 255), // Light sky blue — branch + amber: rgb(235, 170, 95), // Amber — linked worktree marker muted: rgb(40, 120, 200), // Mid blue — model label: rgb(130, 135, 150), // Mid gray — field labels (5h, 7d) // Bar & quota thresholds @@ -150,6 +152,54 @@ function getGitBranch(stdinData) { return sha || ''; } +// Linked worktrees have their own .git dir under the main repo's +// .git/worktrees/, while --git-common-dir still points at the main repo's +// .git. Equal paths mean we're in the primary checkout. +function getWorktreeInfo(cwd) { + const raw = gitExec(['rev-parse', '--git-dir', '--git-common-dir'], cwd); + if (!raw) return null; + + const [gitDir, commonDir] = raw.split('\n').map((p) => path.resolve(cwd, p.trim())); + if (!gitDir || !commonDir) return null; + + const samePath = process.platform === 'win32' + ? gitDir.toLowerCase() === commonDir.toLowerCase() + : gitDir === commonDir; + if (samePath) return null; + + // commonDir is normally /.git; bare repos use .git directly + const commonBase = path.basename(commonDir); + const repoName = commonBase === '.git' + ? path.basename(path.dirname(commonDir)) + : commonBase.replace(/\.git$/i, ''); + + const toplevel = gitExec(['rev-parse', '--show-toplevel'], cwd); + const worktreeName = path.basename(toplevel || cwd); + if (!repoName || !worktreeName) return null; + + return { repoName, worktreeName }; +} + +// "plan2code-user-auth" in repo "plan2code" reads as just "user-auth" +function stripRepoPrefix(worktreeName, repoName) { + const lowerWt = worktreeName.toLowerCase(); + const lowerRepo = repoName.toLowerCase(); + if (!lowerWt.startsWith(lowerRepo) || lowerWt === lowerRepo) return worktreeName; + const rest = worktreeName.slice(repoName.length); + return /^[-_.]/.test(rest) ? rest.slice(1) : worktreeName; +} + +const normalizeName = (s) => s.toLowerCase().replace(/[/_.\s-]+/g, '-').replace(/^-|-$/g, ''); + +// Worktree dirs usually mirror their branch (plan2code-user-auth / feature/user-auth). +// Compare against both the full branch and its trailing segment so the common +// type prefixes (feature/, bugfix/, ...) don't defeat the match. +function isRedundantWithBranch(worktreeName, branch) { + if (!branch) return false; + const candidates = new Set([normalizeName(branch), normalizeName(branch.split('/').pop())]); + return candidates.has(normalizeName(worktreeName)); +} + // ============================================================================ // Formatters // ============================================================================ @@ -192,6 +242,34 @@ function getProjectName(stdinData) { return path.basename(projectDir); } +// Returns the project segment: "plan2code" normally, "plan2code ⑂ spike" in a +// linked worktree — the name collapsing to a bare ⑂ when the branch says it already. +function formatProject(stdinData, config, branch) { + const project = getProjectName(stdinData); + if (!project) return null; + const plain = (text) => (config.color ? `${C.bold}${C.teal}${text}${C.reset}` : text); + + if (!config.items.worktree) return plain(project); + + const cwd = stdinData?.workspace?.project_dir || process.cwd(); + if (!isGitRepo(cwd)) return plain(project); + + const info = getWorktreeInfo(cwd); + if (!info) return plain(project); + + const shortName = stripRepoPrefix(info.worktreeName, info.repoName); + // Only dedupe against a branch the user can actually see — otherwise the + // worktree identity would vanish entirely. + const visibleBranch = config.items.branch ? branch : ''; + const redundant = isRedundantWithBranch(shortName, visibleBranch) + || isRedundantWithBranch(info.worktreeName, visibleBranch) + || normalizeName(shortName) === normalizeName(info.repoName); + const marker = redundant ? '⑂' : `⑂ ${shortName}`; + + if (!config.color) return `${info.repoName} ${marker}`; + return `${C.bold}${C.teal}${info.repoName}${C.reset} ${C.amber}${marker}${C.reset}`; +} + function calculateContextPercent(stdinData, config) { const cw = stdinData?.context_window; if (!cw) return null; @@ -339,14 +417,16 @@ function formatLine1(stdinData, config) { } } + // Branch is resolved first — the project segment dedupes the worktree name against it + const branch = config.items.branch || config.items.worktree ? getGitBranch(stdinData) : ''; + if (config.items.project) { - const project = getProjectName(stdinData); - if (project) segments.push(config.color ? `${C.bold}${C.teal}${project}${C.reset}` : project); + const project = formatProject(stdinData, config, branch); + if (project) segments.push(project); } - if (config.items.branch) { - const branch = getGitBranch(stdinData); - if (branch) segments.push(config.color ? `${C.sky}${branch}${C.reset}` : branch); + if (config.items.branch && branch) { + segments.push(config.color ? `${C.sky}${branch}${C.reset}` : branch); } if (config.items.linesChanged) { From 8e457fa6e446d2829bc69ff5a381d0d90c73c4ab Mon Sep 17 00:00:00 2001 From: Justin Parker Date: Fri, 31 Jul 2026 22:20:38 -0700 Subject: [PATCH 2/2] Bump to v1.16.1 for status line git worktree awareness --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- version.json | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 815fbbb..b1cbc57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to Plan2Code will be documented in this file. +## v1.16.1 + +### ✨ Added + +- **Status line: git worktree awareness** — a session running in a linked git worktree now renders the project segment as `repo ⑂ worktree` (e.g. `plan2code ⑂ spike`) instead of only the worktree's directory name, which previously made the session look like an unrelated project + - Repo identity resolved from `git rev-parse --git-common-dir`, so it is correct regardless of how the worktree directory was named (bare `.git` main repos included) + - A leading repo prefix is stripped from the worktree name (`plan2code-user-auth` → `user-auth`), and the name collapses to a bare `⑂` when it merely restates the branch already on screen — matched across `/ _ . -` separators and type prefixes like `feature/`, and only when the branch is actually displayed + - Toggleable via the new `items.worktree` config flag (default on); one extra timeout-bounded `git` call, skipped outside git repos + ## v1.16.0 ### ✨ Added diff --git a/package.json b/package.json index 2158f71..1939f11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plan2code", - "version": "1.16.0", + "version": "1.16.1", "private": true, "bin": { "plan2code": "./install.js" diff --git a/version.json b/version.json index f8e2995..badd64a 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "name": "Plan2Code", - "version": "1.16.0", + "version": "1.16.1", "description": "A structured 4-step workflow methodology for AI-assisted software development", "keywords": [ "ai", @@ -17,7 +17,7 @@ "url": "https://github.com/jparkerweb/plan2code" }, "homepage": "https://plan2code.jparkerweb.com", - "releaseDate": "2026-07-20", + "releaseDate": "2026-07-31", "mode": "utility" }