Commit Graph
979 Commits
Author SHA1 Message Date
l0ng-ai 962fd87ea5 feat(tabs): step to the next/previous tab without the switcher (#867)
Add SelectNextTab / SelectPrevTab, which move to the neighbouring tab in
the order the strip or sidebar shows them, wrapping, with no popup.
Defaults: Cmd+Shift+] / Cmd+Shift+[ on macOS, Ctrl+PgDn / Ctrl+PgUp
elsewhere. The tmux preset's prefix n / p now bind these (tmux
next-window semantics) instead of the MRU switcher, which never
auto-committed there.

NextTab / PrevTab keep their config names and Ctrl+Tab, but are labelled
Recent Tab Switcher on the Keybindings page; the palette's Next/Previous
Tab entries now show the SelectNextTab chord they actually run.
2026-09-23 15:20:34 +08:00
l0ng-ai 611c87af0c Merge pull request #931 from l0ng-ai/feat/palette-update-server
feat(palette): add "Update tty7 server" for this computer and the remote host
2026-09-23 12:34:29 +08:00
l0ng-ai 2450e8d320 Merge pull request #929 from l0ng-ai/fix/654-resize-drag-artifacts
fix(terminal): hand the prompt back to the shell before a reflow (#654)
2026-09-23 12:34:26 +08:00
l0ng-ai ecdbf16655 Merge pull request #928 from l0ng-ai/fix/919-settings-text-layout
fix(settings): keep keybinding action names on one line (#919)
2026-09-23 12:34:24 +08:00
l0ng-ai 4ee61bb09d Merge origin/main into fix/857-stream-escape-split
Claude-Session: https://claude.ai/code/session_01YX786Hyr4ivijWxv66zVkf
2026-09-23 12:34:17 +08:00
l0ng-ai 2571ee50db Merge pull request #926 from l0ng-ai/fix/891-remote-directory-group
fix(sidebar): group native SSH tabs by their remote folder (#891)
2026-09-23 12:32:22 +08:00
l0ng-ai b9f77a4b63 Merge pull request #925 from l0ng-ai/fix/896-wsl-files-panel
fix(files): list a WSL pane's cwd through its distro's \\wsl$ share (#896)
2026-09-23 12:32:19 +08:00
l0ng-ai 2506078d77 Merge pull request #924 from l0ng-ai/fix/820-ssh-password-prompt-loop
fix(ssh): retire a routed password prompt its attempt gave up on (#820)
2026-09-23 12:32:17 +08:00
l0ng-ai 2d3b4331e6 Merge pull request #923 from l0ng-ai/fix/893-missed-resize-sync
fix(terminal): re-send a resize whose echo never reached the grid (#893)
2026-09-23 12:32:14 +08:00
l0ng-ai 5fcace2350 Merge pull request #922 from l0ng-ai/fix/837-cursor-reset-default
fix(terminal): hand the prompt its cursor back when a command exits (#837)
2026-09-23 12:32:12 +08:00
l0ng-ai 35891e1b64 feat(palette): add "Update tty7 server" for this computer and the remote host
Local: restarts the local daemon onto the app's build through the existing
restart flow and confirmation, or says it is already current when the probed
daemon reports this build and no mismatch is pending.

Remote: a new RouteAction::UpdateServer makes the local daemon force-install
this build's server over one already speaking our dialect
(Installer::replace_forced: upload to a temp name, probe, rename, then
cycle_daemon), for SSH and WSL. Offered only when the window's workspace is
on a host whose server we install; gated on the local daemon advertising
FEATURE_UPDATE_SERVER, since an older one cannot decode the action.

Claude-Session: https://claude.ai/code/session_01YX786Hyr4ivijWxv66zVkf
2026-09-23 12:31:17 +08:00
l0ng-ai 7d6e92966c fix(terminal): hand the prompt back to the shell before a reflow (#654)
Narrowing a pane under a shell prompt whose right side sits near the edge
(an RPROMPT clock, a right-aligned segment) left fragments of the old prompt
behind, and a drag that narrowed and widened the window a few columns at a
time filled the pane with them. No size disagreement is needed: the reflow
wraps the prompt line onto a second row and keeps the cursor on the wrapped
tail, and the shell's SIGWINCH redraw - which moves up by the rows it last
drew, returns to column 0 and clears below - starts from there. The head of
the old prompt stays behind, still wrapping into the new one, and widening
joins every such head into one long line.

A column change at a prompt (shell integration active and at_prompt, primary
screen, not a ConPTY, not a replayed Size) now clears the cursor's line from
its first row down before the reflow and puts the cursor back as many rows
below that line's start as it was, which is the count the shell moves up by.
Applied where the grid reflows: the daemon's live Size echo and the
request-time path for routes without one.

Tests replay zsh 5.9's captured redraw sequence against the grid: a stepped
drag (checked to strand fragments without the guard), a wrapped command
line, back-to-back reflows before the redraw, a line at the bottom of the
screen, and the cases left alone (away from a prompt, rows only, alternate
screen); plus one end to end over the reader's Size echo.

Claude-Session: https://claude.ai/code/session_01YX786Hyr4ivijWxv66zVkf
2026-09-23 12:18:22 +08:00
l0ng-ai 67d6f3a839 fix(settings): keep keybinding action names on one line (#919)
On Linux the Keybindings page wrapped action names one to three CJK
glyphs per line and spilled them over the rows below, even though the
keycaps beside them left plenty of room. The label was a shrinkable
block sized to its own measured text, so any layout pass that measured
it narrower than it finally ended up left its painted lines wrapped at
that narrower width.

An action name is a single line: make it nowrap, and beside the keycaps
let it take the space they leave (flex_1) instead of sizing to its text,
so neither its box nor its lines depend on that measurement. Stacked
rows keep the whole row for the name as before.

The Agents half of the report (a long Codex install-failure note
squeezing the agent name to a glyph per line) is #897, already fixed on
main by #898 and not yet in a stable release.
2026-09-23 12:02:13 +08:00
l0ng-ai f1244015c5 fix(daemon): never cut the replay ring or the clipboard sniffer mid-sequence (#857)
Two places could turn the tail of an escape sequence into visible text.

The replay ring evicts from the front at an arbitrary byte once it reaches
its cap, and a replay starts the client's emulator in its ground state. A
cut through Pi's `ESC ] 133;C BEL` line mark was painted as `33;C` on every
re-attach; a cut through a CJK character as a replacement glyph. The ring
now folds evicted bytes through a minimal VT state machine and keeps
evicting until the front is outside every sequence and not on a UTF-8
continuation byte.

The OSC 5522 tokenizer dropped a held `ESC ]` (plus any undecided
identifier bytes) and the `ESC` that interrupted it, so `ESC ]` split from
`ESC [31m` by a read boundary reached the client as literal `[31m`. It now
forwards those bytes and lets the new sequence be judged normally; only an
interrupted clipboard write's own payload is still dropped.

The daemon's out-of-band stripping moves into `lift_out_of_band` so tests
can drive it: a Pi-style streamed frame passes through byte for byte at
every split point, and the client's emulator parses it to the same grid at
every split point.
2026-09-23 11:49:09 +08:00
l0ng-ai 45fdd53d1a fix(sidebar): group native SSH tabs by their remote folder (#891)
A native SSH pane is owned by this machine's daemon, so its paths are
kept away from every Host call and it never gets a git_status_cwd. The
sidebar only grouped from git_status_cwd, which left every native SSH
tab in Scratch under repo-or-directory grouping, even though the remote
shell reports its cwd over OSC 7.

Fall back to that reported cwd for native SSH panes (absolute POSIX
paths only) and resolve it as a settled 'no repo': repo-or-directory
files the tab under the folder, repo grouping keeps it in Scratch.
Typed 'ssh' and WSL panes are unchanged.

Fixes #891
2026-09-23 11:48:51 +08:00
l0ng-ai 14fc84ae98 fix(files): list a WSL pane's cwd through its distro's \\wsl$ share (#896)
A pane running wsl.exe reports its cwd as a POSIX path from OSC 7, but its
host is this machine. The Files panel rooted the tree at that path verbatim
and handed it to the local read_dir, so on Windows /home/me was read as
C:\home\me and the panel showed "Could not be read"; drops into the tree
failed the same way since they target the rooted directory.

Root the tree at TerminalView::files_cwd instead: a cwd the pane's host
resolves is used as-is, a WSL pane's POSIX cwd goes through the distro's
\\wsl$ share (the same mapping Tab completion already uses), and a cwd no
host here can read (a shell ssh'd onward) roots nothing instead of an
unreadable directory.
2026-09-23 11:39:11 +08:00
l0ng-ai b635fa57fb fix(ssh): retire a routed password prompt its attempt gave up on (#820)
#827 stopped a declined prompt from being asked again, but left the
prompts themselves with no notion of whether anyone was still waiting on
them. A routed auth prompt sat in the mailbox, in the parked queue behind
the sheet on screen, or on screen itself, until somebody answered it —
even after the connection attempt that raised it had timed out and moved
on. Answering it sent the secret into a dropped channel.

That is the report's sequence. A link drops while nobody is at the
keyboard; each reconnect attempt raises a password prompt and times out
unanswered, and before #827 the supervisor dialled again and again, so one
dead prompt per attempt piled up behind the first sheet. The user comes
back, types the password into a sheet nobody is listening to, the next one
comes up, one of them happens to be the live attempt and connects — and
the dead ones keep coming up however they are closed. #827 ends the
attempt loop on a timed-out password, but a key passphrase declined by
timeout still falls through to other methods and a transient failure, and
a single stale sheet is still left on screen either way.

A PendingAuth now carries a weak handle whose only strong count lives in
the responder for as long as it waits, so it can say when it has been
abandoned. The pump drops abandoned prompts instead of raising or parking
them, and takes down an on-screen routed sheet whose asker has gone,
moving on to whatever else is asking. The GUI also waits no longer than
the daemon's handshake does (the broker's 120s rather than 180s), so the
sheet comes down when the attempt behind it actually fails, not a minute
later.
2026-09-23 11:31:00 +08:00
l0ng-ai 6f76e53464 fix(terminal): re-send a resize whose echo never reached the grid (#893)
On a resize-echoing route (the local daemon and current remote servers)
the grid only reflows when the daemon's Size echo comes back. The
per-frame early-out trusted the request instead: once a size had been
asked for it was never sent again, so a Resize or echo that went missing
left the pane painting a grid shorter than its bounds, with the child
still drawing for the old size, until a divider drag asked for a
different size.

The early-out now checks that the grid actually holds the requested
geometry. While an echo can still be in flight (RESIZE_ECHO_GRACE) it
keeps waiting as before; past that, a grid that disagrees gets the size
sent again. The check try-locks the grid like the painter does, so the
UI thread never queues behind the reader. A size comparison that honours
alacritty's minimum grid also stops a one-column pane from resending on
the non-echo path.

Tests: a lost echo is retried after the grace and not before, a late
Size frame is corrected on echoing routes too, and a relink delivers a
resize made while the old link was down.
2026-09-23 11:29:56 +08:00
l0ng-ai d27dfa2ede fix(terminal): hand the prompt its cursor back when a command exits (#837)
`\e[0 q` / `\e[ q` already fall back to `Config::cursor_style`: alacritty
resolves an unset style to `default_cursor_style`, which we seed from the
config and re-apply on every live config change. What nvim actually sends
on exit is terminfo `Se`, and for `xterm-256color` that is `\e[2 q` -- an
explicit steady block, since that is xterm's default -- so the emulator
honours it and every prompt after nvim/vim is stuck on Block.

Track the shell-integration command marks in the pty reader, cut at their
exact byte offsets: note the cursor style at OSC 133;C and, if the command
left a different one behind by 133;D, restore it (back to "unset" when the
prompt was on the configured default, so it keeps following live config
changes). D is the first thing our precmd writes, so shell hooks that style
their own cursor (vi-mode plugins, precmd echoes) still have the last word.
Replayed snapshots go through the same cuts.

Tests: reset via `\e[0 q` and `\e[ q` lands on a configured bar/underline;
C, `\e[2 q`, D restores bar/underline, in one frame and split across frames;
unit tests for the mark parser and restore rules.
2026-09-23 11:25:16 +08:00
l0ng-ai 3c8b573a78 fix(ui): un-zooming a pane keeps focus in that pane (#869)
Un-zoom routed focus through the tab's `last_focused`, which only
focus-in writes, so any gap between that record and the pane actually
zoomed put the cursor in a different pane when the split came back.
Since #843 the record is kept current on focus-in, which is why the
reported sequence no longer reproduces on main, but the zoomed pane is
the answer outright: hand it to the tab before focusing, so un-zoom no
longer depends on the record having caught up.

Zooming with focus off the panes (a palette just closed, the tab strip)
also zoomed the tab's first leaf rather than the pane the tab
remembers; it now falls back to `focus_target`, the same pane a switch
back to the tab would focus.
2026-09-23 11:17:39 +08:00
l0ng-ai 5cf4ac5efa revert(sidebar): paint the agent disc solid again, mark in white
The last two passes at the avatar — a flat theme disc, then a bare mark
painted in the brand colour — each gave something up the solid disc had:
the flat disc lost the hue that tells one agent from another down a
column of rows, and the bare mark read lighter and less settled than the
disc beside the status dot. Neither was better than where it started.

Back to a solid fill of the agent's brand with the mark in its own ink,
and the hairline `needs_edge` adds for Codex and Grok's pure black on a
dark window. The shell avatar goes back to its muted disc. `mark_ink`
goes with the style it served; the toolbar changes from the same PR stay.
2026-09-23 11:02:43 +08:00
l0ng-ai 9b4dd53547 Merge pull request #916 from l0ng-ai/style/sidebar-marks-and-chrome
style(ui): give agent marks their brand colour back, quiet the toolbar
2026-09-23 09:33:20 +08:00
l0ng-ai fea497433d style(ui): give agent marks their brand colour back, quiet the toolbar
The sidebar avatar dropped its brand-coloured disc for a flat theme one,
which left a column of identical grey marks: hue had been the fast way to
tell one agent from another down twenty rows, and the disc was the only
thing carrying it. Painting the mark itself restores the reading at a
third of the coloured area, so it no longer competes with the status dot
beside it — colour says who, the dot says what it wants. `mark_ink`
keeps the hue and only lifts a value that cannot be seen on the surface
under it, which is Codex and Grok's pure black on a dark theme.

The toolbar above it had the opposite problem: `+` and the panel toggle
were drawn at `sidebar_foreground`, the rung a tab title uses, so the two
controls were the darkest marks in the sidebar. They drop to
`muted_foreground`, level with the workspace chip, and the hover fill the
variant already carried answers the pointer.

The glyphs themselves: the panel icons fill their compartment so they
read as a sidebar rather than a split box, and the plus carries a sixth
more stroke than the closed shapes beside it — an open form at the family
weight reads both larger and fainter, which is what made it the odd one
out. Its cap now lands on a half pixel rather than a whole one; the
whole-pixel rungs are 10px and 12px in a 16px box, and neither is the
size.

Claude-Session: https://claude.ai/code/session_01FG2s9mbZu6LbjjmU54X7kt
2026-09-23 09:10:30 +08:00
l0ng-ai ae89ff0110 Merge main and preserve SSH credential saving in settings workflows 2026-09-23 08:54:49 +08:00
l0ng-ai a26dab532a Align settings terminology and documentation navigation 2026-09-23 08:47:15 +08:00
l0ng-ai acb8075c64 Unify tty7 server terminology across settings and restart flows 2026-09-23 08:35:45 +08:00
l0ng-ai ec08949625 Keep About focused on app information and update status 2026-09-23 08:30:53 +08:00
l0ng-ai dcf389fd18 Merge pull request #913 from wenlingang/fix/912-momentum-tail-zoom
fix(terminal): let a flick's momentum tail keep the gesture it started as (#912)
2026-09-23 00:55:49 +08:00
l0ng-ai fd9f589c80 Merge pull request #914 from l0ng-ai/feat/right-panel-refresh
style(ui): unify native colors, icons, and sidebar hierarchy
2026-09-23 00:39:03 +08:00
l0ng-ai f9e8c6eda1 Merge pull request #908 from l0ng-ai/fix/889-sticky-osc-tab-title
fix: retire an OSC 0/2 tab title when the command that set it exits (#889)
2026-09-23 00:38:59 +08:00
l0ng-ai 4ebc1423c1 Merge pull request #906 from l0ng-ai/fix/901-alt-digit-bindings
fix(keybindings): give Alt+1..9 a way out, and stop one bad line resetting the file (#901)
2026-09-23 00:34:23 +08:00
l0ng-ai a4dbdc7bf1 Redesign settings navigation, search, and editing workflows 2026-09-23 00:24:38 +08:00
l0ng-ai 96e5f23eae fix: retire a replayed title whose C mark rolled out of the ring (#889)
A window that reattaches to a pane mid-command (app restart during a long
agent session) replays the daemon's 8 MiB output ring. Once the session
outgrows it, the command's 133;C is gone while its OSC 0/2 titles remain,
so TitleLifetime treated them as prompt titles and the command's D never
retired them: the tab stayed stuck exactly as in #889, while the daemon's
own record had already cleared.

The replayed Prompt state already says a command owns the pane. When it
does and the replay carried no prompt mark at all, everything replayed was
written under that command, so seed the reader's TitleLifetime as running
with a command-owned title.
2026-09-23 00:13:34 +08:00
l0ng-ai 0a32a6c38e fix(terminal): let the zoom latch die with its gesture
The latch added for #912 was cleared only on an event that was not part
of a gesture — but a new gesture's first event is `Started`, which
`track_scroll_gesture` reports as live, so the previous gesture's answer
was still sitting there for `get_or_insert` to find.

That is the same bug wearing the other coat: one ⌘-zoom, and every later
two-finger flick kept zooming with nothing held at all, until some wheel
notch or a stray non-gesture event happened to clear it. Deterministic,
where the original was merely likely.

Reset the latch (and the leftover zoom debt) when fingers go back down,
so each gesture answers "scroll or zoom?" for itself.

Guard: `a_new_gesture_is_not_bound_by_what_the_last_one_answered`,
verified red against the commit it fixes.

Claude-Session: https://claude.ai/code/session_01FG2s9mbZu6LbjjmU54X7kt
2026-09-23 00:12:11 +08:00
l0ng-ai f3de9a3a0d Drop TEXT_MONO imports left unused by the SCM row restyle 2026-09-23 00:10:23 +08:00
l0ng-ai 8839d89d57 fix(i18n): keybindings intro no longer says Backspace resets to default (#901) 2026-09-23 00:07:08 +08:00
l0ng-ai 6846571142 Refine native UI hierarchy, icons, and sidebar chrome 2026-09-22 23:20:31 +08:00
wenlingangandClaude Opus 5 3c3c069650 fix(terminal): let a flick's momentum tail keep the gesture it started as (#912)
macOS stamps every event of a momentum tail with the modifiers held at
delivery, so grabbing ⌘ while a two-finger flick was still coasting turned
the rest of a plain scroll into a zoom — dozens of font steps in a few
frames, down to FONT_SIZE_MIN and saved to config.json, which is why it
outlived a relaunch and read as the font randomly shrinking.

A trackpad gesture now answers "scroll or zoom?" once, at its first event,
and holds that answer until the stream dies. A wheel has no gesture to
belong to and still decides notch by notch. The latch is symmetric: a zoom
gesture that outlives its modifier keeps zooming rather than dumping its
tail into the scrollback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 16:13:32 +08:00
l0ng-ai 829d0f02ea fix(menu): shorten the Close Pane / Tab menu label to Close
The right-click menu and the File menu sit next to Split / New Tab items,
so the surrounding context already says what closes; plain "Close" (⌘W)
matches the macOS convention. The palette and Keybindings page keep the
full name, where there is no surrounding menu to disambiguate.
2026-09-22 12:11:24 +08:00
l0ng-ai 404b6afd1d fix: retire an OSC 0/2 title when the command that set it exits (#889)
A tab went on reading "✳ fixing the switcher" long after Claude Code had
quit and the pane was back at its own prompt in a real directory. An OSC
0/2 had no owner and no end — only another OSC 0/2 ever replaced it — so
the last title any program wrote in a pane outlived it forever, and the
`Osc` rung of the label ladder kept outranking the `Cwd` below it with a
name for a session that no longer existed.

The shell integration already says when a command starts and stops. A new
`core::osc::TitleLifetime` reads OSC 133;C / 133;D alongside the titles:
a title set *between* them belongs to that command and is retired by its
`D`; a title set at a prompt — the shell's own, or one pinned by hand —
belongs to nobody in particular and is left alone; a pane with no shell
integration sees neither mark and keeps every title, exactly as before.

Both readers run it over the same bytes, in stream order, so the tab strip
and the switcher can never disagree about whether a title is still current:

- the daemon's `OscSniffer` turns a retirement into the reset it already
  understood, clearing `PaneRecord::osc_title` for the switcher and CLI;
- the window's pane reader sends `AlacEvent::ResetTitle` after the chunk
  the emulator just parsed, so its own terminal's title goes back to the
  pane's default and `stated_title` says nothing.

Stream order is what keeps a re-titling shell whole: tty7's zsh helper
prepends the `D` emitter to precmd and the PowerShell one titles inside
its prompt function, so a shell's own OSC 0/2 lands after the `D` and is
simply the last word rather than something to undo.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-21 22:47:13 +08:00
l0ng-ai a723e71c9a fix(keybindings): give Alt+1..9 a way out, and stop one bad line resetting the file (#901)
Alt+1..9 are vim's tab keys, and tty7 takes all nine for Go to Tab.
Two things stood between the reporter and getting them back.

**Nothing in the app could leave an action unbound.** Backspace on a
Keybindings row that has recorded nothing *reset* the row — dropped the
override so the action gets its shipped chord back. On a row nobody had
overridden, which is every row the first time it is looked at, that is a
no-op: pressing it over Alt+1 left Alt+1 sitting exactly where it was,
which reads as the default restoring itself. `config.json` has spelled
"no chord" as `[]` since #868, but no gesture wrote it.

Backspace now writes that empty list. The row falls to `—` and grows the
**Reset** button every overridden row has, which is the way back to the
default. The capture hint names the key, and the docs say what it is for.

**A keybinding line serde could not read failed the whole `Config`.**
`keybindings` is a hand-edited map and was strict, so `"ActivateTab1":
null` — or a number, or an object — quarantined `config.json` and started
the app on built-in defaults. Every rebinding in the file then read as
its shipped default, and the next settings write persisted those
defaults over what the user had written. It now reads one entry at a
time, like every other hand-edited nested key here: the lines that name
a shortcut bind, a line that does not is logged and skipped.

Tests, each failing on the unfixed code:

- `ui::app::keybinding_gpui_tests::backspace_on_a_row_unbinds_the_action_rather_than_restoring_its_default`
- `core::config::tests::a_keybinding_line_that_cannot_be_read_does_not_take_the_config_with_it`

and `ui::keymap::gpui_tests::alt_digits_can_be_moved_off_the_tab_actions_for_good`
pins the merge and a save/reload round trip: a list replaces the shipped
Alt+1, `[]` leaves nothing, and neither comes back after a restart.

Fixes #901

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-21 22:42:45 +08:00
l0ng-ai ffce219ffa Merge pull request #895 from doitian/fix/894-codex-newlines-conpty
fix(terminal): preserve Codex newline chords through ConPTY
2026-09-19 09:39:13 +08:00
wick 2e9c7d714c Merge pull request #898 from wenlingang/fix/897-agents-row-note-width
fix(settings): cap the Agents row note again so it stops crushing the label
2026-09-19 09:07:49 +08:00
ian dd9446efe4 fix(terminal): preserve newline chords through conpty
ConPTY translates bare LF into Ctrl+Enter, so native Codex drops the
legacy Shift+Enter fallback and Ctrl+J. Preserve Ctrl+J for local
console readers while retaining raw PTY and Kitty input behavior.

Refs l0ng-ai/tty7#894
2026-09-17 15:03:53 +08:00
l0ng-ai d07850a98e Merge pull request #887 from l0ng-ai/fix/868-additive-keybindings
fix(keymap): a keybinding in config adds a chord instead of replacing the default (#868)
2026-09-15 23:26:50 +08:00
l0ng-ai fbcb5f1cbe Merge pull request #890 from l0ng-ai/fix/unread-badge-on-reattach
Stop a reattach from badging every restored agent tab
2026-09-15 23:24:30 +08:00
l0ng-ai dbee51396f fix(agents): let #888's read marks decide a reattach this app already watched
With both fixes on main, the replayed status adopted as a baseline made
`status == last_agent_status` return before the read-mark lookup ran, so
every rebuild (workspace switch, tray reopen, both of which reattach)
skipped #888: an unread badge was dropped and a turn that finished while
away never badged.

A replay is now only a baseline when the pane has no mark, i.e. this app
never watched an agent in it (the restart case #890 is about). Marks are
kept for every status, not just Done, so a turn that was running when the
old view went still badges, and the restore path checks the mark was a
finished turn. Also adds `turns` to #890's unix-only remote.rs tests,
which no longer compiled against #888's field.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-15 23:06:53 +08:00
l0ng-ai bad028f161 Merge pull request #885 from l0ng-ai/fix/844-dectcem-hidden-caret
test(terminal): pin that a DECTCEM-hidden cursor paints no caret (#844)
2026-09-15 23:04:15 +08:00
l0ng-ai aea3b38448 Merge remote-tracking branch 'origin/main' into fix/unread-badge-on-reattach 2026-09-15 23:03:29 +08:00
l0ng-ai 0f046c95d8 fix(agents): a relink's replay is still a live report to the view
A relink keeps the view and the status it saw before the link dropped, so
that is already the baseline. Marking the relink's replay as stored state
replaced a `Working` baseline with the daemon's `Done` and swallowed the
turn that finished while the link was down: no badge, no notification.
Only a cold attach, where the view has nothing to go on, adopts the replay.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-15 23:03:17 +08:00