Three follow-ups to the repo grouping:
- ⌘N now targets the N-th row as displayed (activate_visual over the
same sections the render uses), so the badge digits read 1…9 top to
bottom under grouping instead of scattering tab-vector indices.
- SessionTab persists the sticky sidebar group, so a restored session
renders grouped on the first frame instead of starting flat and
reshuffling as git probes land; reopen-closed-tab keeps its group too.
- Same-named repo roots disambiguate their headers by extending parent
components until distinct (work/app vs fork/app); unique names stay
short. Grouping/naming logic moved to pure functions with unit tests.
The group key now reads the first pane's cwd instead of the focused
pane's, so switching focus between splits in different repos never
relocates the row (the branch line still follows focus). Also documents
the repo-grouped sidebar and the sidebar_grouping setting in
features.md / features.zh-CN.md.
Group the left tab sidebar's rows under a per-repo header, keyed on the
git work-tree root only (not the branch), so a branch switch or an
in-repo cd never relocates a tab — only changing repos does. Non-repo
tabs collect in a trailing Scratch group; the list stays flat when no
tab is in a repo.
Each row is two lines: the title on its own (never crowded out) with the
branch and +N/-N diff on an indented second line. A sticky per-tab group
key holds across an in-flight probe so the list doesn't flicker on a cd.
New sidebar_grouping setting (repo default / none) toggles it.
zsh: plugins like zsh-vi-mode rebind ^[ to their own widgets, so
sniffing the Esc widget for vi-cmd-mode missed them. Key off the main
keymap link instead (bindkey -A viins main), which both plain
bindkey -v and zsh-vi-mode establish.
bash: [[ -o vi ]] misses vi mode configured only in ~/.inputrc
(set editing-mode vi flips readline without the shell option); read
readline's actual mode via bind -v instead.
Text typed during a command gap is held for the next prompt's local
editor, but a vi prompt never engages the editor: the hold sat until the
150ms timer dumped it and recorded it as typeahead, and that record
could never drain during vi prompts. It lingered and flushed at the
next emacs-mode prompt, firing a spurious ^U and resurrecting the
long-consumed gap text into the editor.
A vi prompt now releases held gap input raw (the shell's line editor
owns it) and drops any pending typeahead record without its wipe.
- sni: import gpui::AppContext (Linux build broke on background_spawn)
- sni: push updates through a background updater task instead of blocking
the foreground poll loop on a DBus round-trip; bursts coalesce to the
newest snapshot, and channel close (Backend drop) shuts the service down
- reveal: keep the target pane maximized if it already is; only a
maximized sibling gets cleared
- icon: center non-square SVGs when rasterizing (was top-left aligned)
- stale comments: 'gave_up' poll-loop reference, prompt button name
A cross-platform tray / menu bar status item: the icon flips to an
attention state when any coding agent blocks on input, and its menu
lists agent panes (brand avatar + status dot, click to reveal),
switches the notification policy, forces an update check, and offers
Quit and Stop Daemon alongside the session-keeping plain quit.
macOS/Windows use tray-icon (muda menus, main-thread NSStatusItem);
Linux deliberately uses ksni (pure-Rust SNI over zbus) instead of
tray-icon's GTK+libappindicator backend so the AppImage stays lean,
with a slow-backoff retry for SNI hosts that appear after login.
Bitmaps are rasterized at runtime with resvg (already in the tree).
Gated by show_tray_icon (default on) with a Settings toggle; the 1s
foreground poll re-reads it, so toggles and config.json hot-reloads
apply live.
Double-clicking a tab label in the title-bar strip now forwards to
window.titlebar_double_click(), matching the rest of the titlebar, instead
of entering inline rename. The sidebar drops double-click rename too for
consistency. Renaming remains available via the tab context menu's
"Rename Tab" item, which is now its only entry point.
Slim the READMEs to an index (why / install / what's inside / benchmarks);
move feature details, keybindings, and performance notes to docs/features.md
(en + zh-CN). New tagline: a terminal workbench — shells, sessions, SSH,
coding agents. Sync the Cargo.toml description.
The overlay was a single app-global slot rendered over whatever tab was
active: switching tabs left a stale diff occluding the new tab's body,
with focus on the terminal underneath so Esc couldn't even close it.
Move the overlay state onto Tab so it follows product intuition: switching
away hides it, switching back restores it (re-probing if the status cache
disagrees with the shown snapshot), and closing the tab drops it.
focus_active now hands focus to the returning tab's overlay so Esc keeps
working; probe landings update every tab showing the same cwd.
Also replace the unified diff body with a GitHub-PR-style side-by-side
view: removed lines fill the left column, added lines the right, aligned
positionally within each change block; context lines land on both sides
and leftovers pair with a muted empty cell.
Claude Code has no 'permission replied' hook, so after the user approved
a mid-turn permission prompt the dot stayed amber for the rest of the
turn. Install a PostToolUse hook mapped to a new tool-complete sentinel
event: the first tool that finishes after approval flips the state back
to Working (guarded on Waiting, so completions during normal work are a
no-op and a straggler can't overwrite Done). Same shape as Warp's
ToolComplete transition.
Old daemons drop the unknown event; existing installs surface as
Outdated in Settings -> Agents with an Update button, so the stale
'points at another tty7' copy is reworded.
A split tab can finish several agent turns while you're away, but the
unread marker was a tab-level bool: three unread results looked exactly
like one. Replace the same-hue target ring with a count badge — when a
tab has unread finished turns, the green Done dot swells into a disc
with the number of unread panes, and shrinks back to a plain dot once
every pane has been seen.
- Tab::agent_result_unread (bool) becomes Tab::agent_unread_count
(usize), counting Done-and-unread panes; still zero while a busier
pane (working/waiting) owns the corner.
- status_dot takes the count and renders the numbered badge, centred on
the read dot's corner point so the swell reads as the dot growing in
place; count clamped to 9 so an absurd split can't overflow the disc.
- Sidebar rows reuse the same avatar, so both layouts pick it up.
Windows' std::fs::canonicalize returns \\?\-prefixed verbatim paths while
git reports plain absolute paths, so occupied()'s starts_with never matched
(silently disabling the shared-checkout guard) and two path-equality test
asserts failed. Canonicalize both sides in occupied(); strip the verbatim
prefix in the tests.
- Bump gpui-component fork (562341a, ff8af95): menus get a 6px rounded pill
highlight inset by 5px panel padding, inset hairline separators, a 10px
panel radius with a floatier shadow; searchable lists get a taller
Spotlight-style search row with 15px query text.
- palette: card matches the menu panel (10px radius, shadow_xl); rows are
fixed-height inset pills; the list viewport holds a whole number of rows
so the card's bottom edge never cuts the last visible row mid-height.
New tabs inherit the current cwd, so two tabs sharing one managed worktree
is common; removing the checkout on the first close would yank the
directory out from under the surviving shell. Before offering, scan every
leaf of every surviving tab (splits included) and stay quiet if any cwd
still sits inside the worktree.
Right-clicking a tab chip (strip) or row (sidebar) opens a context menu:
rename, New Worktree Tab, splits, copy working directory, and a close
group (tab / others / to the right or below).
New Worktree Tab opens a sheet pre-filled with a generated two-word
name (used for both the directory and the new branch) and the current
branch as the start point — all three editable, with a live path
preview. The checkout lands in the repository's own
.tty7/worktrees/<name>, kept out of git status by an auto-written
self-ignoring .tty7/.gitignore. The entry only appears when the tab's
cwd is inside a git repository.
Closing a tab that sat in a managed worktree offers to remove the
checkout (and its branch when fully merged); dirty checkouts default
to keeping and make the discard explicit.
Clicking anywhere on a sidebar row's git line (branch icon, branch name,
counts) toggled the diff overlay, so ordinary clicks on the lower half of
the row popped the overlay instead of activating the tab. Only the +N/-N
readout is the click target now; the icon and branch name fall through to
the row's normal activate handler.
The sidebar row's third line (branch + counts) is now a click target: it
opens a read-only, GitHub-style unified diff of that pane's repo as an
overlay covering the terminal body. The sidebar stays visible, so other
tabs' git lines remain one click away — peek at another session's
changes without switching tabs.
- terminal/git_diff: parse `git diff HEAD` into files/hunks/lines
(renames, binary, quoted paths, per-file line cap), plus the
untracked list `diff` can't see; probed off-thread, read-only via
the shared GIT_OPTIONAL_LOCKS=0 helper.
- ui/diff_overlay: per-file cards with collapsible hunk bodies (big
files collapse by default), line-number gutters, green/red tints,
Esc / close / re-click toggles; auto-refreshes off the existing
GitStatusCache triggers when shown counts go stale.
Persist the window's final geometry to window.json on quit (written by
the app-quit hook, tracked live by a bounds observer that records the
restore bounds while fullscreen). On launch, a normal startup window
reopens at the remembered size and position instead of the hardcoded
centered 1440x900; a remembered window that no longer touches any
display keeps its size but re-centers, and degenerate or malformed
state falls back to the default.
Gated by a new remember_window_size config (default on) with a switch
in Settings -> Window & Tabs and a search entry. The geometry is
written unconditionally so toggling the setting back on restores the
most recent quit rather than stale pre-toggle state.
Closes#89
Drag-resizing a pane whose TUI redraws on every SIGWINCH cuts a tiny
segment per column change; such segments never fill RING_CAP, so over a
long-lived pane's life they accumulated without bound and attach replay
(one full client-side reflow per Size) degraded linearly. Past
MAX_RING_SEGMENTS (64) the two oldest segments now merge, replaying the
older bytes at the newer geometry — like the byte cap, precision
degrades from the oldest scrollback first.
Also correct the client reader's pending_size comment: the one-lock
guarantee is per Size/Snapshot pair, not across the whole replay.
The agent-hook emitter reads its JSON payload from stdin until EOF,
which assumes the spawner pipes a payload and closes the stream (as
Claude Code's hooks do). OpenCode's plugin runner spawns the emitter
with stdin inherited from the pane's terminal instead: a tty never
reaches EOF, so the read blocked forever, hanging the plugin's async
init and leaving OpenCode's TUI blank on startup — while also
swallowing the user's keystrokes.
Skip the stdin read when stdin is a terminal; a bare event without
session_id/message is already a supported payload.
Fixes#88
The replay ring stored raw PTY bytes with no geometry history and attach
replayed all of it at the final recorded size. Any resize during a session
(pane split, window drag) meant older bytes re-wrapped at the wrong width
on replay, so a TUI's cursor-up redraws (Claude Code's inline renderer is
the canonical case) landed mid-frame and every redraw leaked stale rows
into the reattached pane's scrollback -- duplication that never existed
live (10 markers live vs 45 replayed in the regression scenario).
The ring is now a sequence of geometry-tagged segments: resize seals the
current segment (retagging an empty tail in place), cap eviction drops
emptied segments, and attach replays a Size -> Snapshot pair per segment.
The client reader already applies each Size to its grid right before the
paired Snapshot advances (pending_size), so it reflows between segments
exactly where the live client did -- no client changes needed.