Migrates the `wt switch` picker from skim 0.20.5 (tuikit backend) to
skim 4.8.0 (ratatui/crossterm), removes the `vendor/skim-tuikit/` patch
tree we carried against the old line, and makes the full test suite
green under the new backend.
## Why the vendor tree goes away
We vendored skim-tuikit for two patches: `alt-<digit>` key parsing and
an `Output::flush` `write_all` fix for dropped bytes under PTY pressure.
Both are moot in 4.8. Key parsing is native — `binds.rs` routes every
single character (digits included) through `KeyCode::Char` rather than a
hardcoded `alt-a..alt-z` arm list. The flush fix is subsumed by stdlib
`BufWriter::flush` in the ratatui backend, which loops over partial
writes correctly. So the migration carries zero vendor patches against
skim, and the build machinery that kept the vendored source alive
(Taskfile `vendor-diff`, flake `vendorSrc`/`extraDummyScript`) is
deleted too.
## The picker rewrite
skim 4.x is a different API. `WorktreeSkimItem::display()` now returns
`ratatui::text::Line` instead of an `AnsiString`. alt-r removal rides
skim's `reload(remove {})` token (the selected row's `output()`,
expanded into the reload command) instead of the old
`as_any().downcast_ref` path (which never worked across compilation
units) plus a signal file. Preview-tab switching and the progressive
list redraw are driven by `Skim::event_sender()` +
`Event::Render`/`Event::RunPreview`.
## The regression that motivated the bulk of this
skim 4.x renders on demand. There is no 100ms tuikit heartbeat
re-rendering the frame, so the progressive `wt switch` list rendered
blank while collection ran in the background — the original report. The
fix pokes `Event::Render` from the list-collect callbacks (throttled to
16ms). The same on-demand model surfaced four more regressions, all
fixed here and verified head-to-head against the 0.20 picker in a
terminal: preview-tab lag, current-row highlight, Shift-Tab back-cycle
(crossterm reports Shift-Tab under three distinct `KeyEvent` shapes, so
all three are bound), and alt-r removal (4.x `execute-silent` is
fire-and-forget and raced its reader).
## Help text and the test harness
Two things the integration suite caught that the unit tests did not (the
prior pass ran `--lib --bins` only):
- **Styled help.** skim 0.20 transitively pulled in
`clap/unstable-markdown`, which renders worktrunk's `///` doc-comments
as styled help. skim 4 with `default-features = false` drops it, so help
reverted to raw text (`[experimental]` → `\[experimental\]`). Restored
by depending on `unstable-markdown` explicitly, keeping help output
identical and the `test_help` / `test_step_alias` /
`test_docs_are_in_sync` snapshots passing unchanged.
- **PTY harness.** skim 4.x queries cursor position (`ESC[6n`) at
startup in partial-height mode and blocks in `select()` for the reply;
`portable_pty` is a bare PTY and never answered, so every
`switch_picker` test failed init with "Cursor position detection timed
out." The Unix harness now answers the query, mirroring the existing
ConPTY responder. skim 4.x also draws the list/preview separator one
column left, so the snapshot panel-split columns shifted to match. The
regenerated picker snapshots show two cosmetic changes: the match
counter no longer overlaps the preview tab header, and the HEAD column
shows the full short-SHA instead of a truncated one.
## Minimal-versions CI
The skim 4.8 dep tree pins tighter floors than our manifests declared,
so the nightly `minimal-versions` job needed updating. It now runs `-Z
direct-minimal-versions` — minimize only our own direct deps and let
transitive crates resolve normally — and the manifests raise each
under-specified floor to the minimum the workspace builds against
(largely mirroring skim 4.8's own requirements). Full `-Z
minimal-versions` would instead drag in skim's transitive TUI/image
stack (ansi-to-tui, ratatui's `instability` macro, color-eyre,
ratatui-image → image/avif), whose crates under-declare their floors and
don't compile at the picked versions; direct minimization confines the
check to floors we own, so no transitive pins are needed. Normal
resolution (the committed `Cargo.lock`) is unchanged. Full floor list
and the `signal-hook` libc-pin detail are in the `ci(min-versions)`
commit message.
## Reviewing
Start at `src/commands/picker/mod.rs` (the `run_skim` entry point, the
action keybinds, and `parse_reload_remove_token`), then
`progressive_handler.rs` (the render pokes) and `items.rs` (the
`Line`-based `display()`). Test-harness changes are in
`tests/common/pty.rs` (the `ESC[6n` responder) and
`tests/integration_tests/switch_picker.rs` (the panel-split columns).
The rest is the dependency swap, the vendor and build-machinery
deletions, and regenerated snapshots.
All 3980 tests pass (`cargo run -- hook pre-merge --yes`), including the
`shell-integration-tests` PTY suite that exercises the picker end-to-end
across the list, previews, scroll, create/remove, and accept flows. No
automated test drives a real terminal; the interactive surface was also
checked by hand against the 0.20 picker.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
worktrunk stripped `CLAUDECODE` before spawning the
`[commit.generation]` LLM command, and the recommended Claude Code
command carried a leading `CLAUDECODE=`, both to get past Claude Code's
nested-session check that rejected `claude -p` launched from inside
another Claude Code session.
That check is gone. It's absent from the `claude` binary across every
current build (2.1.157 through 2.1.162), and `CLAUDECODE=1 claude -p …`
runs cleanly (exit 0, empty stderr). So the workaround is no longer
needed.
This drops it: `src/llm.rs` no longer calls `.env_remove("CLAUDECODE")`,
and the recommended command loses the `CLAUDECODE=` prefix in the source
of truth (`dev/config.example.toml`), the `wt config --help` text, and
the Taskfile bench command. Docs, the skill reference, and the config
help snapshots are regenerated to match.
Caveat: there's no published guarantee the nested-session check stays
gone, and a Claude Code old enough to still have it (older than roughly
late May 2026) would again block nested commit generation. Since the
check is absent from the implementation across all current builds, that
risk is low.
Tests: 1236 lib + 693 integration pass; fmt and clippy clean.
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Fixes intermittent partial-first-render in `wt switch`'s picker — under
tmux PTY pressure, rows past the first ~1024 bytes silently vanished.
Root cause is in `skim-tuikit`: `Output::flush` calls
`stdout.write(&self.buffer)`, which `io::Write::write` is allowed to
satisfy with a short write. The unwritten remainder is then discarded by
the immediate `self.buffer.clear()`. Instrumenting the flush made it
obvious:
```
flush: buf=8464 wrote=1024 dropped=7440
flush: buf=3382 wrote=1024 dropped=2358
flush: buf=6096 wrote=1024 dropped=5072
```
Fix is a one-line `write` → `write_all`. Carried as `[patch.crates-io]`
pointing at `vendor/skim-tuikit/`, which is byte-identical to
`skim-tuikit` 0.6.6 from crates.io except that single line and an added
`LICENSE` file (the crates.io tarball omits it; MIT requires the
notice).
Upstream PR:
[skim-rs/skim#1056](https://github.com/skim-rs/skim/pull/1056). Drop the
vendor once released.
## Notes
- `task vendor-diff` prints the exact patch we carry — makes it easy to
audit.
- `.pre-commit-config.yaml` and `.typos.toml` extended to skip `vendor/`
so upstream source isn't linted against our style.
- `skim-rs/tuikit` (the older pre-rename repo) has the same bug but is
archived; upstream fix targets `skim-rs/skim @ feat/dynamic-header`
where the current `skim-tuikit` 0.6.x crate actually lives.
## Test plan
- [x] `pre-commit run --all-files` passes
- [x] Reproduced original partial render in tmux (intermittent, ~50% of
runs with 14 items at cold start)
- [x] Post-fix: 15/15 reliable full renders in the same repro
- [ ] CI
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This PR adds OpenCode integration: activity tracking markers in `wt
list`, plugin installation via CLI, `wt config show` diagnostics, and
LLM commit generation detection.
Continues the work started in #1295 and #1533 which added OpenCode to
the docs and example config.
## What's included
- **Activity tracking plugin** (`dev/opencode-plugin.ts`): maps
`session.status`/`session.idle`/`session.deleted` to branch markers,
same pattern as Claude Code's plugin
- **`wt config plugins opencode install/uninstall`**: installs the
plugin to `~/.config/opencode/plugins/worktrunk.ts` — source embedded
via `include_str!()`, no npm needed. Sits under `wt config plugins`
alongside Claude Code's `wt config plugins claude`.
- **`wt config show` OPENCODE section**: shows plugin install status
with actionable hints (only when `opencode` is on PATH)
- **`LlmTool::OpenCode` variant**: detected via PATH for commit
generation auto-config
## Docs approach
Kept deliberately low-profile — no dedicated docs page, no README
mention. OpenCode is discoverable via `wt config show` and a mention in
tips-patterns. If it becomes popular, docs prominence can increase.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Maximilian Roos <m@maxroos.com>
Co-authored-by: Claude <noreply@anthropic.com>
The Claude/Codex recommended commands were duplicated across 4 locations
(Rust code, llm-commits.md, config.md, Taskfile) with no automated sync
— they were already drifting (missing `CLAUDECODE=` prefix, missing `-c
system_prompt=''`).
Now the double-commented entries in `config.example.toml` are the single
source of truth. `recommended_config()` parses them at runtime via
`include_str!` + `LazyLock`, and a new sync test verifies
`llm-commits.md` matches. Also fixes the existing drift in all copies.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Without this flag, `claude -p` saves the commit generation conversation
to session history. When the user later runs `claude --continue`, it
resumes that ephemeral commit session instead of their previous working
session.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add LLM setup prompt for first-time commit configuration
Implement one-time interactive prompt when users attempt commit/merge/squash without LLM configuration. Detects available tools (claude, codex) and offers auto-configuration with preview on `?`. Adds `skip-commit-generation-prompt` flag to suppress re-prompting, `set_commit_generation_command()` config method, and reusable `prompt_yes_no_preview()` utility. Updates commit message templates to clarify format requirements, fixes Claude Code command syntax (spaces to equals in flags), and adds visual styling to section headers in documentation.
* Update snapshots for template format changes
Co-Authored-By: Claude <noreply@anthropic.com>
* Simplify commit template format guidance
Remove redundant trivial-changes line from template format section.
Fix duplicate "# Other" header from merge. Document skip-commit-generation-prompt
in first-run prompts section alongside skip-shell-integration-prompt.
Co-Authored-By: Claude <noreply@anthropic.com>
* Add unit tests for command detection functions
Cover command_exists() and detect_llm_tool() with basic unit tests
to improve test coverage on the commit generation module.
Co-Authored-By: Claude <noreply@anthropic.com>
* Add PTY tests for commit generation prompt
Test the interactive prompt flow for LLM commit configuration:
- No tool found → sets skip flag
- User declines → sets skip flag
- User accepts → saves config
- User requests preview → shows preview
Uses fake claude script to test the prompt path that requires
a detected LLM tool.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* added verbose flag
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
* docs: add Codex config and optimize Claude Code for LLM commits
Add OpenAI Codex CLI as an option for LLM commit message generation.
Configure with gpt-5.1-codex-mini model, low reasoning effort, and
read-only sandbox. Uses jq to parse JSON output.
Update Claude Code config with optimization flags that disable tools,
skills, settings, and system prompt for faster text-only output.
Add bench-llm-commits task to Taskfile for comparing tool performance.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
* perf(tests): add pre-built fixture to speed up Windows CI
Instead of creating repos from scratch for each test, copy a pre-built
fixture with worktrees and remote already configured. This should
significantly reduce test time on Windows where process spawning is slow.
- Add tests/fixtures/standard/ with repo, 3 worktrees, and bare remote
- Update TestRepo to copy fixture instead of running git init/commit
- Add fixture cleanup to doc-generating tests for clean output
- Remove git sample hooks and boilerplate from fixture
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tests): correctly clean fixture state for isolated tests
- Change `remove_fixture_worktrees()` to take `&mut self` and clear the
worktrees map so `add_worktree()` can recreate branches
- Add remote removal to select and approval tests to prevent
`origin/main` from appearing in snapshots
- Update select snapshots for fixture-based commit hashes
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tests): use relative paths in fixture for portability
The fixture gitdir files contained absolute paths to the local
development machine, causing CI failures. Updated to use relative
paths which work after the _git → .git rename during fixture copy.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tests): remove invalid remote from bare repo fixture
The bare repository in the fixture had a remote configured with an
absolute path to the local machine, which wouldn't exist on CI.
Bare repositories don't need remotes configured.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tests): remove origin in approval tests for consistent project_id
Tests manually create approvals using a computed project_id based on
repo path. With the fixture's origin remote, worktrunk computes a
different project_id from the remote URL. Removing origin makes both
use the same fallback (directory path).
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tests): improve fixture copy robustness for CI
- Add platform-specific copy: `cp -r` on Unix, `robocopy` on Windows
- Add verification that essential paths exist after copy
- Add verification that origin.git is a valid git repository after rename
- Include stderr in error messages for better debugging
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tests): add .gitkeep to preserve empty git directories in fixture
Git doesn't track empty directories, so objects/info, objects/pack, and
refs/tags were missing on CI after checkout. These directories are
required for a valid git repository.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tests): ensure LF line endings in fixture git files
Add .gitattributes to force LF line endings for git internal files
(packed-refs, HEAD, config, etc.) to prevent CRLF corruption on Windows.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tests): handle file copies separately from directories on Windows
robocopy only works with directories, not files. Use Rust's std::fs::copy
for individual files like .gitattributes, and robocopy only for directories.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tests): enable rerere in test git config for consistent output
Git's rerere feature produces "Recorded preimage" messages during
conflicts. Enable this in test config so output is consistent between
local machines and CI.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(tests): restore accidentally deleted hook_clear tests
The tests test_hook_clear_no_approvals and test_hook_clear_with_approvals
were accidentally removed during the fixture refactoring. This restores them
with the necessary remote removal so project_identifier matches "repo"
(directory name) instead of the fixture's remote URL.
Also adds remote removal to test_hook_show_approval_status for consistency.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: remove unreferenced ping_pong snapshots
These snapshots used feature_a but the tests now use feature, leaving
these orphaned.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Require manual publish when asset files are deleted to prevent
accidental removals. Refactor demo build to only copy starship config
for docs target and simplify setup_demo_output usage.