Skip to content

feat(list): custom template columns and cached PR numbers in the picker - #3073

Merged
max-sixty merged 14 commits into
mainfrom
custom-list
Jun 19, 2026
Merged

max-sixty merged 14 commits into
mainfrom
custom-list

Conversation

@max-sixty

@max-sixty max-sixty commented Jun 14, 2026 •

Copy link
Copy Markdown
Owner

Two display features for wt list and 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 a wt list column: a minijinja template rendered per row over branch, worktree_path, worktree_name, and vars.*, with optional width and drop priority. Values expand before the skeleton renders, from in-memory data only — vars come 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 list with the available-variables hint; undefined values render as empty cells (the intended sparse-column shape). wt list --format json gains a columns map per item, and its vars field now reads from the snapshot too (the previous --get-regexp line-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 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: 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 --yes gate locally.

This was written by Claude Code on behalf of max

max-sixty and others added 6 commits June 11, 2026 14:42
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 worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

max-sixty and others added 2 commits June 14, 2026 10:54
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
worktrunk-bot previously approved these changes Jun 14, 2026

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_columns only stresses worktree-attached rows; a template that references {{ worktree_name }} (so needs_worktree is true) rendered against a branch with no worktree would hit this.
  • render.rs:321–329 — the ColumnKind::Custom(i) arm of render_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 of wt list. Probably needs a WORKTRUNK_SKELETON_ONLY / pre-skeleton dump test (or extending the picker dry-run shape used in test_picker_*).
  • collect/mod.rs:1069–1070 — the emit_warning(...); Vec::new() arm in match resolve_custom_columns(...) when a progressive handler is present. A picker dry-run with a deliberately invalid [list.custom-columns.X].template would exercise it, mirroring test_list_custom_column_invalid_template on the picker side.

Fine to leave

  • custom_columns.rs:115–117 and :172 — parse-after-validation fallback (unreachable in practice, per the inline comment) and its matching None-template render arm.
  • columns.rs:46 — ColumnKind::Custom(_) => "" in header(); layout substitutes the resolved name, so the bare arm is structural.
  • ci_status/mod.rs:628,634 — populate_from_cache early returns (repo_path error, branch missing).
  • git/repository/config.rs:211 — values.last() empty fallback in all_vars_from_snapshot.

Happy to push a commit adding the three tests if useful.

@worktrunk-bot
worktrunk-bot dismissed their stale review June 14, 2026 20:22

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>
max-sixty and others added 5 commits June 15, 2026 09:39
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>
@max-sixty
max-sixty merged commit a7e29f7 into main Jun 19, 2026
37 checks passed
@max-sixty
max-sixty deleted the custom-list branch June 19, 2026 18:37
@max-sixty max-sixty mentioned this pull request Jun 21, 2026
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants