Skip to content

Commit 39cfd80

Browse files
max-sixtyclaude
andauthored
fix(statusline): reserve a fixed 5 columns instead of 20% of width (#2871)
The Claude Code statusline runs `wt list statusline` as a subprocess with all three standard streams piped, so `terminal_width()` can't detect the terminal directly and `detect_parent_tty_width()` walks the process tree to find a TTY. That fallback reserved 20% of the detected width for Claude Code's own UI messages, which scales badly: on a 200-column terminal it gave up 40 columns, far more than the chrome needs. This reserves a fixed 5 columns instead, so wide terminals keep nearly all their width and narrow ones still get a sensible margin (`saturating_sub` guards against terminals under 5 columns). Also adds a weekly tend maintenance task that scans the agent CLIs Worktrunk integrates with (Claude Code, Codex, Gemini CLI, OpenCode) for changes to the surfaces Worktrunk depends on — the statusline JSON schema, worktree-lifecycle hooks, and plugin install mechanisms — and files an issue when something relevant changes. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d1f65ec commit 39cfd80

2 files changed

Lines changed: 51 additions & 14 deletions

File tree

.claude/skills/running-tend/SKILL.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,27 @@ git grep -niE "claude|codex"
288288

289289
Check the latest IDs at <https://docs.anthropic.com/en/docs/about-claude/models> and <https://developers.openai.com/codex/models>. The recommended commit-message commands should use the most recent fastest model from each vendor (Haiku for Anthropic, the smallest current Codex variant for OpenAI).
290290

291+
## Weekly Maintenance: Agent App Integration Surfaces
292+
293+
Worktrunk ships a plugin for each agent CLI it integrates with, and those CLIs
294+
change their integration surfaces without notice. Each week, scan the upstream
295+
changelogs and flag changes that affect what Worktrunk consumes or produces.
296+
297+
| App | Source to check | Integration surface |
298+
|-----|-----------------|---------------------|
299+
| Claude Code | `gh api repos/anthropics/claude-code/contents/CHANGELOG.md -H 'Accept: application/vnd.github.raw'`, plus `curl -sL https://code.claude.com/docs/en/statusline.md` for the statusline JSON schema | statusline stdin JSON, `WorktreeCreate`/`WorktreeRemove` hooks, plugin marketplace, `/wt-switch-create` |
300+
| Codex | `gh release list -R openai/codex -L 10` | plugin marketplace |
301+
| Gemini CLI | `gh release list -R google-gemini/gemini-cli -L 10` | native extension loading |
302+
| OpenCode | `gh release list -R sst/opencode -L 10` | plugins API in `~/.config/opencode/plugins/` |
303+
304+
What to flag:
305+
306+
- **New statusline JSON fields**`src/commands/statusline.rs` parses `workspace.current_dir`, `model.display_name`, and `context_window.used_percentage`. A newly added field (rate limits, session cost, PR review state) may be worth surfacing in `wt list statusline`.
307+
- **Renamed or removed hook events**`WorktreeCreate`/`WorktreeRemove` route agent worktree creation through `wt`; a renamed event silently disables isolation rather than erroring.
308+
- **Changed plugin install mechanisms**`wt config plugins {claude,codex,opencode} install` and the Gemini extension manifest break if the marketplace or plugins-directory contract changes.
309+
310+
Don't open a PR speculatively. File one issue per relevant change, linking the upstream entry and noting what Worktrunk would need to do. If nothing changed, say so and move on.
311+
291312
## README Date Check
292313

293314
The README blockquote opens with a month+year (e.g., "**April 2026**"). During daily

src/styling/mod.rs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ pub fn terminal_width() -> usize {
104104
/// stdout, and stderr — so [`terminal_width`] always falls through to its
105105
/// `usize::MAX` sentinel, and the statusline output would overflow the bar.
106106
/// As a last resort, this walks up to 10 parent processes looking for a TTY
107-
/// and asks `stty size` for its dimensions, reserving 20% for Claude Code's
108-
/// own UI messages.
107+
/// and asks `stty size` for its dimensions, reserving 5 columns for Claude
108+
/// Code's own UI messages.
109109
///
110110
/// Every other caller should use [`terminal_width`] — the parent-TTY walk is
111111
/// a statusline-specific workaround, not a general fallback.
@@ -131,10 +131,7 @@ fn statusline_width_fallback(base: usize) -> usize {
131131
///
132132
/// This is a fallback for subprocesses (like Claude Code hooks) that don't have
133133
/// direct TTY access. Walks up to 10 parent processes looking for one with a TTY,
134-
/// then queries that TTY's size.
135-
///
136-
/// Returns 80% of the detected width to reserve space for Claude Code's UI messages
137-
/// (like "Approaching context limit").
134+
/// then queries that TTY's size via `stty` and [`statusline_width_from_stty_size`].
138135
#[cfg(unix)]
139136
fn detect_parent_tty_width() -> Option<usize> {
140137
use crate::shell_exec::Cmd;
@@ -160,14 +157,7 @@ fn detect_parent_tty_width() -> Option<usize> {
160157
.run()
161158
.ok()?;
162159

163-
let cols = String::from_utf8_lossy(&size.stdout)
164-
.split_whitespace()
165-
.nth(1)?
166-
.parse::<usize>()
167-
.ok()?;
168-
169-
// Reserve 20% for Claude Code UI messages
170-
return Some(cols * 80 / 100);
160+
return statusline_width_from_stty_size(&String::from_utf8_lossy(&size.stdout));
171161
}
172162

173163
if ppid == "1" || ppid == "0" {
@@ -179,6 +169,17 @@ fn detect_parent_tty_width() -> Option<usize> {
179169
None
180170
}
181171

172+
/// Convert `stty size` output (`"<rows> <cols>"`) into a statusline width.
173+
///
174+
/// Reserves 5 columns for Claude Code's UI messages (like "Approaching
175+
/// context limit"). Returns `None` when the output has no parseable column
176+
/// count.
177+
#[cfg(unix)]
178+
fn statusline_width_from_stty_size(stty_size: &str) -> Option<usize> {
179+
let cols = stty_size.split_whitespace().nth(1)?.parse::<usize>().ok()?;
180+
Some(cols.saturating_sub(5))
181+
}
182+
182183
/// Calculate visual width of a string, ignoring ANSI escape codes
183184
///
184185
/// Uses unicode-width for proper handling of wide characters (CJK, emoji).
@@ -232,6 +233,21 @@ mod tests {
232233
assert!(width > 0);
233234
}
234235

236+
#[cfg(unix)]
237+
#[test]
238+
fn statusline_width_from_stty_size_reserves_five_columns() {
239+
// `stty size` prints "<rows> <cols>"; the column count loses 5 to
240+
// Claude Code's UI chrome.
241+
assert_eq!(statusline_width_from_stty_size("24 200"), Some(195));
242+
assert_eq!(statusline_width_from_stty_size("24 80"), Some(75));
243+
// Saturates rather than underflowing on a terminal narrower than 5.
244+
assert_eq!(statusline_width_from_stty_size("24 3"), Some(0));
245+
// No parseable column count.
246+
assert_eq!(statusline_width_from_stty_size(""), None);
247+
assert_eq!(statusline_width_from_stty_size("24"), None);
248+
assert_eq!(statusline_width_from_stty_size("24 wide"), None);
249+
}
250+
235251
#[test]
236252
fn test_toml_formatting() {
237253
let toml_content = r#"worktree-path = "../{{ repo }}.{{ branch }}"

0 commit comments

Comments
 (0)