Commit Graph
138 Commits
Author SHA1 Message Date
l0ng-ai f4e63d076e fix(daemon): detect shell vi mode via durable signals
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.
2026-07-16 14:56:51 +08:00
l0ng-ai 418433395b fix(terminal): release gap hold at vi prompts without a stale typeahead record
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.
2026-07-16 14:56:51 +08:00
ayamir 8180d22166 fix(terminal): support shell vi mode 2026-07-16 13:41:43 +08:00
l0ng-ai b079f91caf chore(release): v0.17.0 v0.17.0 2026-07-16 11:54:10 +08:00
l0ng-ai 501fd4a7de docs(readme): rewrite in minimal style, reposition as terminal workbench
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.
2026-07-16 11:45:33 +08:00
l0ng-ai e5dab7ee44 Merge pull request #100 from l0ng-ai/feat/per-tab-split-diff-overlay
feat(diff-overlay): per-tab lifecycle and side-by-side view
2026-07-16 11:33:49 +08:00
l0ng-ai 4cdd413f5d feat(diff-overlay): per-tab lifecycle and side-by-side view
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.
2026-07-16 11:27:03 +08:00
l0ng-ai 29a51edc0e Merge pull request #99 from l0ng-ai/fix/claude-dot-stuck-waiting
fix(agent-status): recover from Waiting once an approved tool completes
2026-07-16 11:10:26 +08:00
l0ng-ai 1509e92ce8 fix(agent-status): recover from Waiting once an approved tool completes
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.
2026-07-16 11:05:57 +08:00
l0ng-ai c6d57be076 Merge pull request #98 from l0ng-ai/feat/unread-count-dot
feat(tabs): show unread pane count in the avatar status dot
2026-07-16 10:45:55 +08:00
l0ng-ai ee04928ea4 Merge pull request #96 from l0ng-ai/feat/tab-context-menu-worktree
feat(tabs): per-tab context menu with git worktree tabs
2026-07-16 10:40:28 +08:00
l0ng-ai 4b9fdcde43 Merge pull request #97 from l0ng-ai/style/macos-popup-restyle
style(ui): macOS Sonoma-style popup panels
2026-07-16 10:35:22 +08:00
l0ng-ai 4cf91e778b feat(tabs): show unread pane count in the avatar status dot
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.
2026-07-16 10:35:20 +08:00
l0ng-ai 0033e957b5 fix(worktree): compare canonicalized paths so Windows CI passes
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.
2026-07-16 10:32:09 +08:00
l0ng-ai f9dba021fc style(ui): macOS Sonoma-style popup panels — pill highlight, taller palette search, whole-row viewport
- 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.
2026-07-16 10:25:23 +08:00
l0ng-ai 27669acf42 fix(worktree): skip the cleanup offer while another pane still lives in the checkout
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.
2026-07-16 10:24:04 +08:00
l0ng-ai 3ec1577a17 feat(tabs): per-tab context menu with worktree tabs
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.
2026-07-16 10:03:13 +08:00
l0ng-ai 8d4b067a82 chore(release): v0.16.1 v0.16.1 2026-07-15 23:00:52 +08:00
l0ng-ai ac9755e7cf style(sidebar): rustfmt the diff-count click handler 2026-07-15 23:00:05 +08:00
l0ng-ai 0536841687 Merge pull request #95 from l0ng-ai/fix/sidebar-diff-click-target
fix(sidebar): limit the diff overlay toggle to the +N/-N counts
2026-07-15 22:44:49 +08:00
l0ng-ai 919e9a0b2e fix(sidebar): limit the diff overlay toggle to the +N/-N counts
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.
2026-07-15 22:44:11 +08:00
l0ng-ai f678469aef chore(release): v0.16.0 v0.16.0 2026-07-15 22:06:23 +08:00
l0ng-ai f26045fbb5 style(diff-overlay): rustfmt the diff overlay module 2026-07-15 22:05:20 +08:00
l0ng-ai 6438d21886 Merge pull request #92 from l0ng-ai/feat/sidebar-diff-overlay
feat(git): click a sidebar git line to open a working-tree diff overlay
2026-07-15 22:01:03 +08:00
l0ng-ai 68cc4680d4 feat(git): click a sidebar git line to open a working-tree diff overlay
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.
2026-07-15 22:00:19 +08:00
l0ng-ai 8e47d4c231 Merge pull request #94 from l0ng-ai/feat/remember-window-size
feat(window): remember window size and position across launches
2026-07-15 21:59:48 +08:00
l0ng-ai d4450c4199 Merge pull request #93 from l0ng-ai/fix/agent-hook-tty-stdin
fix(agent-hooks): skip stdin read when it is a tty
2026-07-15 21:59:44 +08:00
l0ng-ai 19430424f5 Merge pull request #91 from l0ng-ai/fix/ring-replay-geometry
fix(daemon): segment the replay ring by geometry so attach replays wrap correctly
2026-07-15 21:59:40 +08:00
l0ng-ai 25c8ea6b79 feat(window): remember window size and position across launches
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
2026-07-15 21:51:31 +08:00
l0ng-ai 0334c97a65 fix(daemon): cap the replay ring's segment count
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.
2026-07-15 21:50:03 +08:00
l0ng-ai ffcb990d33 fix(agent-hooks): skip stdin read when it is a tty
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
2026-07-15 21:48:45 +08:00
l0ng-ai 846032183d chore: remove repomix skill and config 2026-07-15 21:23:31 +08:00
l0ng-ai b33fcc7035 chore(docs): drop the docs directory and stale plan files 2026-07-15 21:21:48 +08:00
l0ng-ai 363d2592f6 fix(daemon): segment the replay ring by geometry so attach replays wrap correctly
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.
2026-07-15 21:15:24 +08:00
l0ng-ai 1756bcc3a5 chore(release): v0.15.0 v0.15.0 2026-07-15 19:48:01 +08:00
l0ng-ai 5a1f4c8181 style: cargo fmt 2026-07-15 19:43:08 +08:00
l0ng-ai 30894a7946 refactor(palette): drop the Claude-only hook install command
Settings > Agents now owns hook installs with per-agent state (#87);
the palette entry covered only Claude and duplicated that path, so
remove it rather than grow one palette command per agent. Also drop a
leftover unused HookAgent import in the settings render.
2026-07-15 19:31:17 +08:00
l0ng-ai cd90a1afbe Merge pull request #90 from l0ng-ai/feat/daemon-handshake-ui-polish
Daemon protocol handshake with user-confirmed restart, SSH status dots, palette QuickConnect fix
2026-07-15 19:26:10 +08:00
l0ng-ai dc89af952d style(tabs): hide the close affordance until hover on active tabs too
The active tab row/chip used to keep its × always visible while the
others faded in on hover. Treat every tab the same — opacity 0 until
hover, space still reserved — so the sidebar and strip read clean.
2026-07-15 18:53:47 +08:00
l0ng-ai 0182a072cc fix(git-status): share one per-repo snapshot across panes
Each pane used to compute and hold its own git branch/diff snapshot,
refreshed only by its own events (cwd change, command end, agent turn
end). Tabs sitting idle in the same repo kept whatever they last saw, so
rows for one directory showed different +/− counts — or none at all when
a tab's last probe landed on a clean tree.

Snapshots now live in a process-wide GitStatusCache keyed by work-tree
root: every pane whose cwd resolves into the same repo reads the same
entry, refreshed by whichever pane probed last, and the sidebar observes
the cache so all rows repaint together. In-flight probes are deduped per
cwd (concurrent triggers fold into one git shell-out, with a rerun if
re-triggered mid-flight), and a failed git diff keeps the previous
counts instead of rendering the tree as suddenly clean.
2026-07-15 18:45:25 +08:00
l0ng-ai 35a16638f1 feat(daemon): ask before restarting a version-mismatched daemon
Startup used to silently stop a daemon speaking a different protocol,
killing every persisted session without warning. The old daemon is still
serving its panes fine — the mismatch may be benign for the messages
actually exercised — so the call is now the user's: keep it and reuse
the sessions, record the mismatch, and have the first window raise a
Keep Sessions / Restart Daemon prompt (restart reuses the confirmed
half of the existing Restart Daemon flow). Only a daemon that cannot
answer the handshake at all (wedged, timeout) is still replaced
outright, since it cannot serve its sessions either way.
2026-07-15 18:09:22 +08:00
l0ng-ai 2029f2ccf7 fix(ui): show SSH state as a corner status dot in semantic colors
The SSH avatar drew a 2px border ring in theme tokens, which read as a
second avatar shape next to the flat shell badge — and "connected" used
theme.accent, the list-selection grey in this app, so the ring showed no
state at all. Reuse the agent status_dot on the badge corner instead,
colored from the same hardcoded palette as agent dots (amber connecting,
green connected, red failed/disconnected, neutral for a foreground ssh);
the tab strip's inline 6px dot picks up the same RGB values.
2026-07-15 17:45:59 +08:00
l0ng-ai 611b671a5d fix(palette): stop offering QuickConnect rows for bare words
A bare word like "java" parses as a valid hostname, so every command
search got Connect/Save rows pinned above the real matches. Require the
query to look like a connect target (contain '@', ':' or '.') before
injecting QuickConnect rows.
2026-07-15 17:45:59 +08:00
l0ng-ai 96360c544e feat(daemon): version handshake so an upgraded GUI restarts a stale daemon
The daemon outlives the GUI binary, so after an app upgrade the running
daemon can speak an older wire dialect. ensure_running now asks a live
daemon for its protocol version (new Version request/reply, kind 40)
before reusing it and restarts it on a mismatch; a pre-versioning daemon
drops the unknown kind, which reads as "replace it" too.
2026-07-15 17:45:59 +08:00
l0ng-ai ed244680bc chore(release): v0.15.0-beta.1 v0.15.0-beta.1 2026-07-15 16:48:16 +08:00
l0ng-aiandl0ng-ai 2bf246f789 feat(agents): per-agent hook integrations with install state in Settings (#87)
* feat(agents): hook integrations for Codex, Copilot, OpenCode, and Pi with install state in Settings

Generalize the Claude Code hook install into a per-agent integration
layer, all feeding the same `tty7 agent-hook <agent> <event>` emitter
(still gated on the TTY7 env var):

- Claude Code / Codex: tty7-marked entries merged into their hooks map
  (settings.json / hooks.json); user hooks and settings are never
  touched. Codex install also runs `codex features enable hooks`,
  best-effort with manual-run advice when the CLI is missing.
- Copilot CLI / OpenCode / Pi: a tty7-owned hook file / JS plugin / TS
  extension, byte-compared for drift detection and ownership-guarded on
  install and uninstall.
- Copilot's catch-all notification hook is filtered in the emitter:
  permission/elicitation prompts escalate to permission-request,
  everything else stays silent.
- New Settings -> Agents section: one row per agent with install state
  (not installed / installed / outdated) and Install / Reinstall /
  Update / Uninstall actions.
- On launch (release builds only), integrations pointing at a moved or
  stale tty7 binary are rewritten in place.

* fix(agents): rustfmt + make the owned-file exe assertion Windows-safe

The generated integrations embed the exe path inside JSON/JS string
literals, so the test must look for its string-escaped form — on Windows
the raw path's backslashes appear as \\ in the content and the raw
substring never matches.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-15 16:17:32 +08:00
l0ng-aiandl0ng-ai 74e3383128 fix(terminal): shift the grid up when wrapped input overflows the bottom (#86)
* fix(terminal): shift the grid up when wrapped input overflows the bottom

While the local command editor is active, typed input never reaches the
grid, so the emulator's scroll-on-wrap never fires: a multi-line command
at a bottom-of-screen prompt grew past the window edge and its tail (and
caret) were clipped by the surface's overflow_hidden.

Emulate the scroll an echoing shell would perform: compute how many rows
the wrapped overlay spills past the last grid row (input_scroll_rows) and
raise the whole paint origin by that many lines — the top rows clip, the
overlay re-anchors to the shifted prompt row, and the wrapped tail lands
in the vacated strip. The shift is capped so the caret row never scrolls
off the top when the input is taller than the screen, and disabled while
scrolled into history or in reverse-search mode. The completion menu
follows the shifted anchor, and editor clicks/drags map through an
unclamped row so the rows past the old bottom stay clickable.

* style: rustfmt

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-15 16:17:09 +08:00
l0ng-aiandl0ng-ai cd9577c590 feat(agents): recognize CLI coding agents + git branch in the sidebar (#85)
* feat(agents): recognize CLI coding agents + show git branch in the sidebar

Observe (never wrap) third-party coding agents running in a pane — Claude
Code, Codex, Gemini CLI, Aider, Amp, OpenCode and ~10 more — and enrich the
UI around them, plus front each sidebar row with its git branch and diff.

Detection & identity
- Command-based detection over the foreground argv (launcher basename, and
  interpreter-wrapped `node …/cli.js` / `npx …` forms), with user rules via
  `agent_commands` in config. Brand avatars on the tab chip and sidebar row.

Rich status channel
- A per-pane state machine (idle / working / waiting-for-you / done) driven
  by agent-reported events over an OSC 777 sentinel channel
  (`tty7://cli-agent`, versioned JSON), sniffed daemon-side and streamed to
  the client (DaemonMsg::AgentStatus).
- `tty7 agent-hook claude <event>` + a palette installer wire Claude Code's
  lifecycle hooks up; the hook writes the sentinel to the controlling tty
  (with an ancestor-tty fallback for detached hook processes).
- Avatar status dot: working (blue) / waiting (amber) / done (green); an
  unread finished turn gets a crisp outer ring that clears on focus.

Notifications, resume, context feed
- "Needs your permission…" the moment an agent blocks; "finished after Ns"
  per turn, honoring the notify policy (rich turns suppress the coarse exit).
- Session resume: restored panes re-launch their conversation
  (`claude --resume …`), gated by `restore_agent_sessions` (default on).
- Palette commands send the current selection or the repo `git diff` to the
  running agent as a ready-made prompt.

Sidebar git line
- New `terminal::git_status`: off-thread `git` probe (branch, or short sha
  when detached; `git diff --numstat HEAD` line counts) with
  GIT_OPTIONAL_LOCKS=0, refreshed on cwd change or command finish, dropped
  on a stale cwd via a generation tag.
- Each row is avatar + title + `⎇ branch  +N −M` (green/red), sized to
  content; the redundant cwd/"Working…" lines and the aggregate rollup are
  gone — the status dot and branch line carry it.

672 tests pass.

* fix(agents): repair CI and refresh the git line when an agent turn ends

- The live PTY detection test used `sh -c 'exec -a codex cat'`, but
  `exec -a` is a bashism dash (Ubuntu's /bin/sh) rejects — spawn bash.
- cargo fmt over cli_agent.rs / view.rs / app.rs.
- An agent session is one long foreground command, so the back-to-prompt
  edge never refreshed the sidebar's branch/diff line while the agent
  worked — exactly when the working tree changes. poll_agent_status now
  reports a turn ending (transition into Done) and the poll reprobes git
  on that edge too.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-15 14:35:24 +08:00
ayamirandl0ng-ai e44855c3b6 feat(ssh): support Unix GSSAPI auth (#81)
* feat(ssh): support gssapi auth

* fix(ssh): pin the russh patch to an exact rev + fail on a stalled gssapi context

- [patch.crates-io] now pins rev 0d1d073 instead of tracking the fork's
  branch: russh is the credential-handling SSH protocol layer, and a
  moving branch would let `cargo update` silently pull unreviewed code.
  Documented the removal condition (upstream russh PR #737 releasing).
- gssapi_step: an incomplete context with no output token used to claim
  GssapiStep::Complete without a MIC, which servers reject with an opaque
  failure; return an error naming the stall instead.
- auth.rs module doc: include gssapi-with-mic in the Auto ordering.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-15 14:32:42 +08:00
l0ng-aiandl0ng-ai a2c1763de9 fix(terminal): keep each tab's active pane across tab switches (#84)
* fix(terminal): keep each tab's active pane across tab switches

Switching tabs reset the target tab to its first pane: focus_active
always focused first_leaf, and a Tab stored no notion of which pane was
active. Tab labels had the same root cause — an inactive tab holds no
window focus, so leaf_title always fell back to the first pane.

Record the focused pane per tab (Tab::last_focused) when leaving it or
opening a focus-stealing overlay (Settings), and restore it on return
via Tab::focus_target, degrading to first_leaf when the remembered pane
has closed. leaf_title now uses the live focus for the active tab and
the remembered pane for inactive tabs, so background labels track the
pane each tab is working in.

Add Pane::leaf_matching_or_first as the pure selection rule, unit-tested
against the u32 leaf model.

* fix(terminal): remember the active pane on every tab-leaving path

Cmd+T (new_tab_with_shell), native-SSH new tab (open_native_ssh_tab), and
reopen-closed-tab (Cmd+Shift+T) all move the active tab away without going
through activate(), so the departed tab never recorded its focused pane and
switching back jumped to the first leaf — the exact bug this branch fixes,
reachable from its most common entry point. Snapshot via remember_active_pane
before the switch in all three.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-15 14:21:06 +08:00