Repository navigation
feat(list): custom template columns and cached PR numbers in the picker - #3073
Conversation
Each [list.columns.<Header>] entry in user config adds a wt list column: a minijinja template rendered per row (branch, worktree_path, worktree_name, vars.*), with optional max width and drop priority. Values expand before the skeleton renders, from in-memory data only — vars come from the bulk git-config snapshot via the new Repository::all_vars_from_snapshot(), so no subprocess runs per cell. Widths are measured from content like Branch/Path; a column that is empty on every row is dropped entirely. Details: - Validation at resolution: unknown top-level vars and misspelled filters abort wt list with the available-variables hint; undefined values are tolerated (they render as empty cells by design, the sparse-column shape). The picker shares the row renderer and shows the columns; a broken definition there degrades to no columns plus a stashed warning since collect runs while skim owns the terminal. - wt list --format json gains a `columns` map per item, and the `vars` field now reads from the snapshot too (the previous --get-regexp line-parse truncated multiline values). - Worktree identity is only computed when a template references it (to_posix_path shells out to cygpath per call on Windows). - expand_template's vars injection is extracted as vars_map_to_value; template_environment is exposed for the column renderer, which holds one environment and one parse per column across all rows. Ref: #1982 (the custom-columns proposal from its discussion thread) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # src/commands/list/collect/mod.rs # src/commands/list/json_output.rs # src/commands/list/layout.rs # tests/snapshots/integration__integration_tests__help__help_list_long.snap # tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap
The interactive picker skips the networked CiStatus task, so until now it had no CI column at all. Cached statuses are local data, though: collect now fills rows from .git/wt/cache/ci-status/ when the task is skipped under a progressive handler, so PR/MR numbers fetched by earlier wt list --full or statusline runs render in the picker — aligned with the same MaxPrNumber ratchet width wt list uses, and with zero network access. A valid cache entry renders as-is. An entry whose TTL passed or whose branch head moved keeps its PR/MR number dimmed (is_stale): the number still identifies the PR when the pipeline color may be outdated. Expired entries without a number are dropped. The CI column is allocated only when some row had a usable entry — a layout-only copy of the skip set diverges from the spawn set exactly then — and rows the cache can't fill resolve to blank rather than a pending placeholder, since no task repaints them. The skeleton renderer paints pre-resolved CI cells immediately, like custom columns. The dry-run path (WORKTRUNK_PICKER_DRY_RUN) now dumps the rendered rows (ANSI-stripped) alongside the preview-cache entries, which is what makes picker row content assertable in integration tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
The two features are cleanly separated, the docstrings are excellent, and the test coverage is thorough — the cache-population state machine, the layout/width measurement, the vars snapshot parity, and the picker dry-run dump are all well exercised. Two things to flag before this lands.
Required test (linux) is red — two of the new snapshots are stale
test_list_custom_columns and test_list_custom_column_empty_everywhere_dropped fail in CI (and in affected tests (linux/macos, advisory) and code-coverage — same root cause; the sibling …_json and …_invalid_template tests pass). The diff is a trailing ␛[0m reset at the end of every row: the actual output emits one, the committed .snap files don't.
4 │-…␛[1mMessage ← committed snapshot: no trailing reset
4 │+…␛[1mMessage␛[0m ← actual CI output
Every existing wide list snapshot already ends each row with that reset (e.g. list_config_malformed_system_config_non_section_field.snap has …␛[2mInitial commit␛[0m), so the rendering is consistent — the two new files were just captured without it. Regenerating with cargo insta test --accept (limited to those two) should fix all four checks. Worth a glance at why the local pre-merge gate passed with these committed: it suggests the .snap files diverged from a local insta accept after a later change. Want me to push the regenerated snapshots?
[list.columns] collides with open PR #3065
#3065 (open, by @Faria22) also introduces a [list.columns] user-config section, but as a flat map of built-in-column visibility toggles (ci = false, path = false, …). This PR makes [list.columns.<Header>] a table of custom template columns. The two are mutually exclusive serde shapes for the same documented key — a user can't express both, and whichever merges second breaks the other's config examples. Since the config file format is a protected interface, it's worth deciding which design owns [list.columns] (or namespacing one, e.g. [list.custom-columns]) before either lands, rather than after.
Everything else read clean: the MaxPrNumber ratchet heals the picker's CI-column width on every render (worst case a one-render # degrade, per its docstring), the Custom(u8) 256-cap guards the index aliasing, and the column_display_index tuple change has both call sites updated.
The two wide colored-table snapshots were captured before merging main and missed two cosmetic changes the merge introduced: the per-row trailing ANSI reset (#3056/#3061) and dropped empty-valued env-header entries (#3037/#3063). Table content is unchanged; only the line-final `\x1b[0m` and the snapshot env block differ, matching every other wide list snapshot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The custom template columns feature claimed the `[list.columns]` user-config key, which collides with the column-visibility toggles in open PR #3065 (also `[list.columns]`, but a flat map of built-in-column bools). The two are mutually exclusive serde shapes for the same protected key; namespacing custom columns under `[list.custom-columns]` lets both land independently, and "custom-columns" is the precise name for what this defines. The key never shipped, so no migration is needed: a clean rename of the serde key (field `columns` -> `custom_columns`, `rename = "custom-columns"`) plus every user-facing string -- error messages, the --help source in cli/mod.rs, config.example.toml, the docs mirrors, and snapshots. The JSON output field stays `columns` (a separate surface: the item's rendered values, not the column definitions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
codecov/patch failed at 96.26% (project 97.44%). Per CLAUDE.md this is a merge gate. Dismissing the approval until the gap is addressed.
Most of the misses are in src/commands/list/custom_columns.rs and src/commands/list/render.rs. Three of the eight uncovered spans are testable and worth picking up; the rest are "unreachable after validation" fallbacks that read fine as-is.
Testable
custom_columns.rs:141–151—item.worktree_data().…unwrap_or_default(), the branch-only-item arm of the worktree-identity build.test_list_custom_columnsonly stresses worktree-attached rows; a template that references{{ worktree_name }}(soneeds_worktreeis true) rendered against a branch with no worktree would hit this.render.rs:321–329— theColumnKind::Custom(i)arm ofrender_skeleton_row. None of the snapshot tests render a skeleton, so this path never runs in CI even though it's the path real users see on the first ~50ms ofwt list. Probably needs aWORKTRUNK_SKELETON_ONLY/ pre-skeleton dump test (or extending the picker dry-run shape used intest_picker_*).collect/mod.rs:1069–1070— theemit_warning(...); Vec::new()arm inmatch resolve_custom_columns(...)when a progressive handler is present. A picker dry-run with a deliberately invalid[list.custom-columns.X].templatewould exercise it, mirroringtest_list_custom_column_invalid_templateon the picker side.
Fine to leave
custom_columns.rs:115–117and:172— parse-after-validation fallback (unreachable in practice, per the inline comment) and its matchingNone-template render arm.columns.rs:46—ColumnKind::Custom(_) => ""inheader(); layout substitutes the resolved name, so the bare arm is structural.ci_status/mod.rs:628,634—populate_from_cacheearly returns (repo_patherror, branch missing).git/repository/config.rs:211—values.last()empty fallback inall_vars_from_snapshot.
Happy to push a commit adding the three tests if useful.
codecov/patch failed — see follow-up review for testable gaps
…er-degrade paths
`codecov/patch` flagged three uncovered spans in the custom-columns diff that
the existing snapshot tests miss:
- `custom_columns.rs` worktree-identity lookup — only runs when a template
references `worktree_path`/`worktree_name`; the prior tests used `vars.*`
templates, so `needs_worktree` was always false. A `{{ worktree_name }}`
column over `wt list --branches` exercises both arms: a worktree row resolves
its directory name, a branch with no worktree falls back to an empty cell.
- `render.rs` skeleton `Custom` arm — the piped `wt list` snapshot tests skip
the progressive skeleton, so this path (which real users see in the first
~50ms) never ran in CI. A picker dry-run renders the skeleton rows.
- `collect.rs` picker degrade arm — a broken template aborts `wt list` but the
picker stashes a warning and renders without the column. A picker dry-run
with an invalid template covers it.
The remaining uncovered diff lines are the unreachable-after-validation
fallbacks (parse failure after a validated template, the structural
`Custom(_)` header arm).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconcile the v0.60.0 release with the custom-columns feature. Both sides changed `JsonItem::from_list_item`: main replaced the `repo_url` arg with a `repo: Option<&GitRepoInfo>` + `ci_provider_override` pair (#3021, #3120), while this branch added a trailing `custom_columns` arg. The merged signature carries all three (main's two, then `custom_columns`), and every call site (`to_json_items`, the statusline path, and the JSON unit tests) passes the combined argument list. `ItemKind::Branch` also gained a `BranchScope` payload on main (gutter sigils, #3115); the `new_branch_with_scope` constructor now sets both `custom_values` and `kind: ItemKind::Branch(scope)`. `to_json_items` keeps this branch's snapshot-based `all_vars` read alongside main's `repo_info`/forge-platform lookups. The `wt list --help` snapshot was regenerated from the merged `cli/mod.rs`, combining main's width-chopped example tables with the custom-columns column row and config section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
worktrunk 0.61.0 Created-by: HarmonybrewBot Commit-by: HarmonybrewBot Merged-by: HarmonybrewBot Description: Created by `brew bump` --- Created with `brew bump-formula-pr`.<details> <summary>release notes</summary> <pre>## Release Notes ### Improved - **`wt list` custom columns**: Each `[list.custom-columns.<Header>]` entry in user config adds a column to `wt list` (and the `wt switch` picker), rendered per row as a minijinja template over `branch`, `worktree_path`, `worktree_name`, and `vars.*`, with optional `width` and drop priority. Values expand from in-memory data only — no subprocess runs per cell — and a column that is empty on every row is dropped. `wt list --format=json` gains a `columns` map per item. The feature is experimental, so the config shape may still change. ([#3073](max-sixty/worktrunk#3073)) - **`--config-set` for inline config overrides**: A global, repeatable `--config-set <toml>` flag overrides any user-config key for a single invocation, layered above config files and `WORKTRUNK_*` env vars. The value is a real TOML fragment, so arrays and tables work natively (`wt --config-set list.full=true list`); nested tables deep-merge, and a malformed or invalid override drops the whole `--config-set` layer with an attributed warning rather than failing the command. ([#3138](max-sixty/worktrunk#3138)) - **Picker shows cached PR/MR numbers**: The `wt switch` picker skips the networked CI-status fetch, so it previously had no CI column. It now fills PR/MR numbers from the local `.git/wt/cache/ci-status/` cache populated by earlier `wt list --full` or statusline runs, with zero network access. A stale entry (TTL passed or branch head moved) keeps its number dimmed; expired entries without a number are dropped. ([#3073](max-sixty/worktrunk#3073)) - **Faster file copies on macOS**: After a reflink (`clonefile` on APFS, which already preserves mode bits), worktrunk now skips the redundant follow-up `chmod` on macOS, saving one syscall per file in `wt step copy-ignored` and every other copy path. Linux (btrfs/XFS) still sets permissions, since `FICLONE` clones data extents only and drops the execute bit. ([#3149](max-sixty/worktrunk#3149)) ### Internal - **Picker migrated to skim 4.8 (ratatui/crossterm)**: The `wt switch` picker moved off skim 0.20.5 (tuikit) to skim 4.8.0, dropping the vendored `vendor/skim-tuikit/` patch tree (both patches it carried are now native or upstream). Two cosmetic picker changes come with it: the match counter no longer overlaps the preview-tab header, and the HEAD column shows the full short-SHA. ([#3137](max-sixty/worktrunk#3137)) - **All command spawns route through one trace chokepoint**: `CommandTrace` is now the sole emitter of `[wt-trace]` command records, so a spawn path can't silently skip tracing — `git worktree add`, previously an unattributed gap, now shows up as a labeled slice in `wt-perf timeline`. ([#3134](max-sixty/worktrunk#3134)) ### Documentation - **Hook-approval skill guidance**: The bundled worktrunk skill now frames hook approvals as user consent and no longer advocates `--yes` to bypass prompts. ([#3146](max-sixty/worktrunk#3146)) ## Install worktrunk 0.61.0 ### Install prebuilt binaries via shell script ```sh curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-installer.sh | sh && wt config shell install ``` ### Install prebuilt binaries via powershell script ```sh powershell -ExecutionPolicy Bypass -c "irm https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-installer.ps1 | iex"; git-wt config shell install ``` ### Install prebuilt binaries via Homebrew ```sh brew install worktrunk && wt config shell install ``` ## Download worktrunk 0.61.0 | File | Platform | Checksum | |--------|----------|----------| | [worktrunk-aarch64-apple-darwin.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-aarch64-apple-darwin.tar.xz) | Apple Silicon macOS | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-aarch64-apple-darwin.tar.xz.sha256) | | [worktrunk-x86_64-apple-darwin.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-x86_64-apple-darwin.tar.xz) | Intel macOS | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-x86_64-apple-darwin.tar.xz.sha256) | | [worktrunk-x86_64-pc-windows-msvc.zip](https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-x86_64-pc-windows-msvc.zip) | x64 Windows | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-x86_64-pc-windows-msvc.zip.sha256) | | [worktrunk-aarch64-unknown-linux-musl.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-aarch64-unknown-linux-musl.tar.xz) | ARM64 MUSL Linux | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-aarch64-unknown-linux-musl.tar.xz.sha256) | | [worktrunk-x86_64-unknown-linux-musl.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-x86_64-unknown-linux-musl.tar.xz) | x64 MUSL Linux | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.61.0/worktrunk-x86_64-unknown-linux-musl.tar.xz.sha256) | ### Install via Cargo ```sh cargo install worktrunk && wt config shell install ``` ### Install via Winget (Windows) ```sh winget install max-sixty.worktrunk && git-wt config shell install ``` ### Install via AUR (Arch Linux) ```sh paru worktrunk-bin && wt config shell install ``` </pre> <p>View the full release notes at <a href="https://github.com/max-sixty/worktrunk/releases/tag/v0.61.0">https://github.com/max-sixty/worktrunk/releases/tag/v0.61.0</a>.</p> </details> <hr> See merge request: Harmonybrew/homebrew-core!12090
Two display features for
wt listand the interactive picker, developed together because they share the column-layout and progressive-rendering machinery.Custom columns (
[list.custom-columns])Each
[list.custom-columns.<Header>]entry in user config adds awt listcolumn: a minijinja template rendered per row overbranch,worktree_path,worktree_name, andvars.*, with optionalwidthand drop priority. Values expand before the skeleton renders, from in-memory data only —varscome from the bulk git-config snapshot, so no subprocess runs per cell. Widths are measured from content like the Branch and Path columns; a column that is empty on every row is dropped.Unknown variables and misspelled filters abort
wt listwith the available-variables hint; undefined values render as empty cells (the intended sparse-column shape).wt list --format jsongains acolumnsmap per item, and itsvarsfield now reads from the snapshot too (the previous--get-regexpline-parse truncated multiline values). The picker shares the row renderer, so the columns appear there as well; a broken definition degrades to no columns plus a stashed warning, since collect runs while skim owns the terminal.The key is
[list.custom-columns], not[list.columns], to avoid colliding with the column-visibility toggles in #3065 (which claims[list.columns]as a flat map of built-in-column bools — a mutually exclusive serde shape for the same protected key). Namespacing here lets both land independently.Ref #1982 — the custom-columns proposal lives in that thread. The issue's own title is a separate directory-naming request, so this doesn't close it.
Cached PR/MR numbers in the picker
The picker skips the networked CiStatus task, so until now it had no CI column at all. Cached statuses are local data, though: collect now fills rows from
.git/wt/cache/ci-status/when the task is skipped under a progressive handler, so PR/MR numbers fetched by earlierwt list --fullor statusline runs render in the picker — aligned with the sameMaxPrNumberratchet widthwt listuses, and with zero network access.A valid cache entry renders as-is. An entry whose TTL passed or whose branch head moved keeps its PR/MR number dimmed: the number still identifies the PR when the pipeline color may be outdated. Expired entries without a number are dropped. The CI column is allocated only when some row had a usable entry, and rows the cache can't fill resolve to blank rather than a pending placeholder, since no task repaints them.
Key files
src/config/expansion.rs,src/config/user/sections.rs,src/git/repository/config.rs— column resolution, the template environment, and the bulk git-config snapshot.src/commands/list/layout.rs,src/commands/list/render.rs— column width allocation and cell rendering.src/commands/list/ci_status/mod.rs—populate_from_cache, the cache-only fill.src/commands/picker/mod.rs— the dry-run dump (WORKTRUNK_PICKER_DRY_RUN) that makes picker row content assertable in tests.Testing
Integration tests cover both features: custom columns (table render, JSON output, empty-column drop, invalid-template error) and the picker (cached PR numbers appear in the dry-run dump, uncached branches stay blank). Unit tests cover the cache-population logic (valid, expired-with-number, head-moved, dropped). Verified against the full
cargo run -- hook pre-merge --yesgate locally.