Skip to content

feat: per-thread default working directories - #33

Open
ytaskiran wants to merge 18 commits into
mainfrom
thread-working-directory
Open

feat: per-thread default working directories#33
ytaskiran wants to merge 18 commits into
mainfrom
thread-working-directory

Conversation

@ytaskiran

@ytaskiran ytaskiran commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Each thread can carry an optional default working directory. Sessions launched from that thread start there. Threads without one start in the home directory.

How it works

Press c on a thread to open the directory picker. The top row is the directory you are browsing, and the cursor starts there:

 › ~/projects/                    ← Enter here accepts this directory
──────────────────────────────
   agent-harnesses/
   archive/
  • Enter on the top row accepts the directory you are browsing
  • Enter on a subdirectory descends into it, so Enter never records a directory you did not navigate to
  • Tab completes into the first match, so typing a segment and tabbing walks downward like a shell
  • Backspace deletes a filter character, or goes up a level when the filter is empty
  • Esc cancels

Typing filters the list; the filter is dimmed because Enter takes the directory, not the typed text. To point a thread back at home, Backspace up to ~ and press Enter.

Creating a thread opens the picker right after you name it. Esc there leaves a valid thread with no directory. The tree shows each thread's directory as a dim suffix, brighter on the selected row.

Notes for review

  • Behavior change: threads with no directory set now open in ~ rather than inheriting the directory tws was launched from. Launch behavior no longer depends on where you started the app.
  • A stale directory never blocks a launch. If a thread's directory is deleted, the session still starts, in ~, and tmux shows a warning. Inside tmux this goes through display-message, because switch-client ends the app before another frame is drawn.
  • Paths are stored absolute; the ~ form is display-only.
  • Thread.working_dir uses #[serde(default)], so existing state.json files load unchanged, and an older binary ignores the field.
  • All picker logic lives in core/workdir.rs with no ratatui dependency, which is what makes it unit-testable. app.rs grows one Mode variant and a handler that delegates every keystroke.
  • centered_rect moved from finder_modal.rs to components/mod.rs so both modals share one copy.
  • New [keys.dir_picker] config section; every binding is rebindable.

Verification

  • 198 tests pass; cargo fmt --all --check and cargo clippy --all-targets -- -D warnings clean
  • 33 unit tests cover the picker: listing rules, hidden-directory handling, filtering, cursor model, navigation, selection, and the launch-directory resolution table
  • Driven manually in an isolated tmux sandbox: picker navigation, Enter/Tab semantics on each row, persistence across restart, a launched session's pwd, the home fallback, and the stale-directory path both falling back and surfacing its warning

Review round

Eight findings from a review pass were verified against the code and all seven actionable ones fixed in f5811e9: the shorten_home fake-~ bug when home is unresolvable, new_session's discarded failure, the picker's cwd fallback, the filter reading as a path segment, "no subdirectories" vs "no matches", the stale Enter hint and missing Backspace hint, and a vestigial Option on the setter. One was deliberately deferred and recorded: previewing which entry Tab will jump to under a substring filter.

🤖 Generated with Claude Code

Enter used to select the highlighted subdirectory and exit, so pressing it
right after the picker opened silently grabbed the alphabetically-first
child instead of the directory you were standing in.

The current directory is now row 0 of the list, where the cursor starts.
Enter there confirms it; Enter on a subdirectory row walks into that row
instead of exiting. Tab still completes into the first match from row 0,
so typing a segment and tabbing walks downward like a shell.

Row 0 always exists, which removes the old "empty filtered set falls back
to current" special case.

Drops Ctrl+x. Backspacing up to ~ and confirming does the same job
without a second key to learn.
The "use <path>" list row duplicated the path line directly above it,
showing the same directory twice.

The path line now carries the prefix column and highlight the entries
use, so it reads as row 0 of one navigable column rather than a separate
element. Selection state shows as the marker plus highlight, and the
block cursor appears only while the line is active.
The path suffix only rendered on the selected thread, so it appeared and
vanished as the cursor moved down the tree.

It now always renders. Selection changes brightness rather than
visibility: unselected rows use a new thread_path_dim, derived like
thread_dim but pushed further toward the border colour so the path stays
legible without competing with the thread name. On the selected row the
tree's highlight style covers the whole line, so the path reads at full
brightness there.
- shorten_home: with no resolvable home, home_dir yields / and stripping
  that prefix turned every absolute path into a bogus ~/... path.
- new_session: the success bool was discarded, so tmux refusing the
  request led to switch-client on a nonexistent session and a silent
  exit. Adding -c made that reachable for a directory that stats but
  cannot be entered.
- open_dir_picker: an unset thread launches sessions in home, so the
  picker now starts at home instead of tws's launch directory. Pressing
  c then Enter on such a thread no longer silently records the cwd.
- The picker's filter text is dimmed while the path row is selected, so
  it does not read as a path segment Enter would take.
- An empty listing now distinguishes "no subdirectories" from a filter
  that excluded everything.
- Status hints: Enter is "use / open" since it descends on every row but
  the first, and Backspace's "up" hint was missing entirely.
- set_thread_working_dir takes a PathBuf; no caller clears the field
  since Ctrl+x was dropped.
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.

1 participant