Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"crates/wisp-config",
"crates/wisp-core",
"crates/wisp-fuzzy",
"crates/wisp-kindra",
"crates/wisp-preview", "crates/wisp-status",
"crates/wisp-tmux",
"crates/wisp-ui",
Expand Down Expand Up @@ -37,13 +38,15 @@ crossterm = "0.28"
ratatui = "0.29"
serde = { version = "1", features = ["derive"] }
serde_ignored = "0.1"
serde_json = "1"
thiserror = "2"
toml = "0.8"
wisp-app = { version = "0.2.0", path = "crates/wisp-app" }
wisp-config = { version = "0.2.0", path = "crates/wisp-config" }
wisp-core = { version = "0.2.0", path = "crates/wisp-core" }
wisp-embers = { version = "0.2.0", path = "crates/wisp-embers" }
wisp-fuzzy = { version = "0.2.0", path = "crates/wisp-fuzzy" }
wisp-kindra = { version = "0.2.0", path = "crates/wisp-kindra" }
wisp-preview = { version = "0.2.0", path = "crates/wisp-preview" }
wisp-status = { version = "0.2.0", path = "crates/wisp-status" }
wisp-tmux = { version = "0.2.0", path = "crates/wisp-tmux" }
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Wisp is a native Rust multiplexer navigation tool inspired by `tmux-sessionx`. I
- tmux session discovery, switching, and attachment, with optional Embers backend support
- sidebar pane and sidebar popup surfaces in addition to the main picker
- git worktree-aware picker: see only sessions for the current repo, or browse all worktrees
- [Kindra](https://github.com/Pajn/kindra) integration: spin up a fresh temporary worktree (branched off trunk) straight from the worktree picker
- zoxide-backed directory discovery
- fuzzy filtering and session previews
- configurable behavior through TOML config plus environment overrides
Expand All @@ -19,6 +20,7 @@ Wisp is a native Rust multiplexer navigation tool inspired by `tmux-sessionx`. I
- `wisp-tmux`: tmux snapshot/actions backend plus polling fallback
- `wisp-embers`: optional Embers snapshot/actions adapter and subscription bridge
- `wisp-zoxide`: zoxide provider and normalization
- `wisp-kindra`: Kindra (`kin`) temp-worktree detection and creation
- `wisp-preview`: preview generation and cache
- `wisp-fuzzy`: matcher abstraction
- `wisp-ui`: shared ratatui renderers and key translation
Expand All @@ -33,6 +35,7 @@ Requirements:
- `tmux` for tmux-backed flows
- an Embers checkout at `../embers` only when building with `--features embers`
- `zoxide` for directory candidates
- [Kindra](https://github.com/Pajn/kindra) (the `kin` binary) only for the temporary-worktree option in the worktree picker
- Rust toolchain new enough for edition 2024

Install the CLI:
Expand Down Expand Up @@ -84,6 +87,10 @@ Current Embers support covers the main picker, session actions, previews, live r

Use `--worktree` (or `-w`) to start the picker in worktree mode, which shows only sessions belonging to worktrees of the current repo alongside worktrees that don't yet have sessions.

When the current repo has [Kindra](https://github.com/Pajn/kindra) temporary worktrees configured (a `[worktrees]` section in `kindra.toml` with the `temp` role enabled), worktree mode appends a `+` row as you type. Your filter text is normalized into a branch slug — whitespace runs become dashes, and text that can't form a valid git branch name hides the row. Selecting it runs `kin wt temp -b <slug> <trunk>` to create a new temporary worktree branched off the repo's trunk, then creates and switches to a session in it. The trunk is resolved from the remote default branch (`origin/HEAD`), falling back to a local `main`/`master`.

To remove a temporary worktree, first close its session (the close-session key), which leaves a session-less worktree row. Pressing close again on that row — when it is a Kindra temp worktree — prompts for confirmation and then runs `kin wt remove` to delete the worktree. Deletion never forces, so a worktree with uncommitted changes is left intact and Kindra reports the error.

Example tmux binding:

Add this to `~/.tmux.conf` to open Wisp with `prefix + o`:
Expand Down
1 change: 1 addition & 0 deletions crates/wisp-bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ wisp-config.workspace = true
wisp-core.workspace = true
wisp-embers = { workspace = true, optional = true }
wisp-fuzzy.workspace = true
wisp-kindra.workspace = true
wisp-preview.workspace = true
wisp-status.workspace = true
wisp-tmux.workspace = true
Expand Down
56 changes: 56 additions & 0 deletions crates/wisp-bin/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,62 @@ pub fn branch_status_for_directory(path: &Path) -> Option<(GitBranchSync, bool)>
Some((sync, dirty))
}

/// Resolves the trunk branch name (e.g. `main` or `master`) for a repository.
///
/// Prefers the remote's default branch (`origin/HEAD`), then falls back to a
/// local `main`/`master`, and finally defaults to `main` so callers always have
/// a usable start point.
pub fn trunk_branch(repo_root: &Path) -> String {
if let Some(branch) = remote_default_branch(repo_root) {
return branch;
}

for candidate in ["main", "master"] {
if local_branch_exists(repo_root, candidate) {
return candidate.to_string();
}
}

"main".to_string()
}

fn remote_default_branch(repo_root: &Path) -> Option<String> {
let output = Command::new("git")
.current_dir(repo_root)
.args(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])
.output()
.ok()?;
if !output.status.success() {
return None;
}

let raw = String::from_utf8_lossy(&output.stdout);
let trimmed = raw.trim();
// `origin/main` -> `main`
let branch = trimmed.strip_prefix("origin/").unwrap_or(trimmed);
if branch.is_empty() {
None
} else {
Some(branch.to_string())
}
}

fn local_branch_exists(repo_root: &Path, branch: &str) -> bool {
// Capture output rather than inheriting the terminal: `status()` would let any
// git warning/hint leak onto the raw-mode picker and corrupt the display.
Command::new("git")
.current_dir(repo_root)
.args([
"show-ref",
"--verify",
"--quiet",
&format!("refs/heads/{branch}"),
])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +199 to +234

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

No timeout on git subprocess calls used to resolve the trunk branch.

remote_default_branch and local_branch_exists (called up to 3x from trunk_branch) shell out via Command::output() with no deadline. Per the call sites in main.rs (git::trunk_branch(repo_root) during temp-worktree activation and when building KindraTempContext), a stalled git process here would block the picker UI indefinitely — the same risk already flagged for wisp-kindra's subprocess calls, but at a separate call site.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/wisp-bin/src/git.rs` around lines 199 - 234, The git subprocesses used
by remote_default_branch and local_branch_exists do not have any timeout, so a
stalled git command can block trunk_branch and the picker UI indefinitely.
Update these helpers to run git through the project’s existing timeout-aware
subprocess pattern (the same approach used elsewhere for wisp-kindra), and have
trunk_branch propagate or handle timeout failures cleanly when calling
remote_default_branch and local_branch_exists.


/// Gets the git repository root based on the current tmux state.
/// Finds the current session's focused window path and resolves it to a git repo root.
pub fn worktree_repo_root(state: &DomainState, client_id: Option<&str>) -> Option<PathBuf> {
Expand Down
Loading
Loading