A colon suffix holding a line range wasn't read as a location, so the
whole token failed to resolve and the link disappeared. Read the range's
first line instead, the way the #L10-L20 anchor already does.
Opening a file, link, URL or config.json, revealing a path, and the custom
link_file_command all spawned `open` / `xdg-open` / `explorer` and dropped
the Child. Dropping a Child never waits on it, so every one of these clicks
left a defunct process under tty7-app for as long as the GUI ran.
Add `core::proc::spawn_reaped`, which spawns the command and waits on it
from a short-lived thread, and route all six fire-and-forget spawns through
it.
Show all and search bound the host list to 360px inside the scrolling settings page, and GPUI scrolls every scroller under the pointer, so the wheel moved the page along with the list. Give the list the same wheel handler the Agents page got in #1214, now shared as nested_list_wheel: an event the list can absorb stays in the list, and only one that starts at its edge reaches the page.
* fix(daemon): hand the daemon over to the new build when the GUI relaunches after an update
An update replaces the bundle and relaunches the GUI; the daemon keeps
running from the replaced binary so the panes survive. On macOS that
also loses the daemon's Local Network attribution (#909, #917): once
the executable it started from is gone from disk, every pane shell gets
EHOSTUNREACH on the LAN until Settings → Restart Server.
The handoff already does what the restart does without the cost — same
pid, same ptys, far side disclaims responsibility afresh — so the GUI
now asks for it at launch whenever the daemon is on another build and
advertises the feature. A native-SSH pane is the one thing a handoff
closes, so when the daemon holds one the first window asks instead,
with the Restart Server dialog. Everything that cannot hand over is
left as it was, and Settings still offers the restart.
* fix(daemon): never hand the daemon over to a debug build at launch
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
The pane-mode tracker now records the queries it spots as one ordered
list of `Query` values instead of a counter per kind, and the daemon
answers a detached pane's from one `detached_reply` table. That table
also answers `CSI ? 996 n` from the appearance the window last drew:
a program in a hidden pane waits on it the same way it waits on DA1.
Folds of old output (the ring's evicted head, a handoff's refold) drop
their queries, so they neither pile up nor get answered twice.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix: shell history, config ZDOTDIR, keymap hot-reload and window close
- A ZDOTDIR set in config `env` no longer replaces the zsh integration
redirector; it is handed to it as TTY7_USER_ZDOTDIR instead.
- Shell integration reports the shell's HISTFILE to
`<config>/shell-histfiles`, so ⌃R and GUI history find history kept
outside $HOME/.zsh_history.
- The config watcher watches the canonical config dir: FSEvents reports
resolved paths, so a dir reached through a symlink (/tmp) never
matched and `keybindings` edits were not picked up.
- ⌘↩ in the ⌃R menu runs the selected line instead of toggling full
screen.
- Close Window (shells keep running) gets ⇧⌘W on macOS and a File menu
entry.
* fix: keep the newest reported histfiles; bind the menu's run to cmd-enter
- shell-histfiles is append-only and panes starting together append the
same path; reading the first 32 lines let stale or duplicate entries
hide every path reported later. Keep the newest 32 distinct ones.
- secondary-enter is ctrl-enter off macOS, where the Ctrl+R menu only
accepts on that chord. Bind cmd-enter, matching the menu's own key
handler on every platform.
The new cut follows one story: quit the app and the agents keep
working, reopen where you left off, every agent at a glance, one agent
handing work to another through the CLI, and sessions restored after a
reboot. Every product frame is the real app. The alt text now matches.
A docked document was drawn in a fraction of the space its column had. The
column itself was sized correctly — `document_body_px` and
`document_column_px` agreed with what the column measured on screen — but
the box holding the surface was `w_full()`, a share of whatever the flex
ancestor chain resolved to, and on a fractional display scale that share
resolved against the wrong ancestor entirely.
On a 1.75x Wayland surface (GNOME's `scale-monitor-framebuffer`), a
404-point column handed its file a 123-point content box, so the editor
laid out at a fifth of the width it was given, soft-wrapped every few
characters, and left the rest of the column empty. The same build under
X11, where the scale is 1, put the file across the column.
surface scale 1 scale 1.75 fixed
column (measured) 404.0 403.4 403.4
content box 404.0 123.4 403.4
editor 380.0 99.4 380.6
wrap width 302.9 22.3 303.4
The column is sized in points where it is drawn, so its content is sized
the same way: a point count has no ancestor to be resolved against, and
reads the same at every scale. `DOCUMENT_RULE` names the rule on the
column's left edge so the border and the width that leaves room for it
cannot drift apart.
Fill is untouched — it never went through this box — and the diff view
docked beside the terminal gets the same fix for free.
* feat(github): row links to GitHub; stacked pull requests drawn as one run
A hovered list row shows Open on GitHub. Pull requests based on another
listed pull request's head branch (in the same repository, the owner
compared case-insensitively) are gathered into one run, each above its
base, joined by a connector through their glyphs and ending in the
stack's base branch. Two pull requests on the same base branch off it in
lanes side by side; a cycle draws no trunk. The branches come from /pulls,
already fetched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(github): stack rows as per-row cells
A stacked row is now one Cell per lane (Empty, Pipe, Node, Join) and the
trunk its own StackRow, so the UI and the tests both just map cells. The
row constants move to github::rows, which no longer imports panel_github,
and stack_order takes the list's &[Item].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With several agents running across tabs and windows, finding the one
blocked on a permission prompt meant scanning tab dots or opening the tray
menu. Go to Next Agent Needing Attention (cmd-shift-U, and ctrl-shift-U off
macOS; FocusNextAgentNeedingAttention to rebind, also in Search Everywhere)
reveals it directly: waiting agents first, the longest wait at the head,
then finished turns nobody has read. Pressing again from one of them moves
to the next and wraps; from anywhere else it starts at the head. It searches
the tabs of every open window, the same agents the tray menu lists, and
reveals a pane in another window the way a tray pick does; a workspace no
window is showing is not searched. With nothing to attend to it says so
("No agent in your open windows needs you") instead of doing nothing
silently. The ordering is a pure function over the tray's agent rows, with
tests.
Ordering by age needs to know when a status began, so AgentSessionState
carries since_ms: the daemon stamps it whenever a hook event, an assumed
interrupt or a stale-turn sweep moves the status. Hook events are stamped
one at a time, so a status that leaves and comes back within one batch
(Done, a new prompt, Done again) starts a fresh age. It is wall-clock time,
not an Instant, because it crosses the socket and survives a handoff; ages
across machines therefore follow each machine's clock. An older daemon
simply leaves it out and those rows queue behind stamped ones.
The tray menu now reads "needs input · waiting 3m", the workspace switcher
shows the same age beside the tab (computed the same way, on the same
clock), and `tty7 agents` gains a FOR column. Ages are in whole minutes in
the GUI so the tray snapshot, and with it the menu, changes at most once a
minute.
Claude-Session: https://claude.ai/code/session_017RJFBHCC4dbUua21a2TedQ
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(tabs): add sidebar new tab options menu
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(tabs): split every New Tab button, not just the sidebar's
The sidebar header was the only place the + opened a tab outright; the
strip and the collapsed rail still dropped the menu on every press, so the
same glyph did two different things depending on where the tabs sat and
whether the rail was open.
Route all three through new_tab_button as one split control, count its
chevron in the title bar width budgets, render the trigger's own box so
its interactivity is not dropped, and drop tests that only re-checked
segment_corners and a bool setter.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
After a link drops, the attach sent over the new one asked for the
workspace the same way opening it does, so a client that slept through
another machine taking the workspace (the Preempted push never reached
it) took it straight back on waking, and the two traded it for as long
as the first kept sleeping.
An attach for a workspace this client already held is now made only if
nobody else is holding it, the rule windows on the daemon's own machine
already follow; otherwise the window opens taken over, with Take Back.
Opening a workspace and Take Back still take it.
Fixes#1193
What SSH `exec` sends is parsed by the user's login shell first. The
daemon launch, the running-server probe and the stop went out as bare
POSIX scripts, so on a host whose shell is fish the launch died on its
first word (`out=…`), no daemon started, and the client waited out its
15s timeout before reporting that the server "logged nothing" to a
startup log the script never got to create.
- Send those three scripts through `sh -c`, as the quick probe already
did, via a shared `under_sh`.
- Make `shell_quote` read back identically under fish: fish treats a
backslash inside single quotes as an escape, which broke any script
quoted a second time for `sh -c`. Backslashes now sit outside the
quotes.
- Treat a non-zero status from the launch as a failed launch and carry
the shell's stderr into the error, instead of reporting a timeout.
Windows only reports a click on a toast that has left the screen through
COM: the shell activates the ToastActivatorCLSID named on the shortcut
that carries the AUMID. tty7 had no such class, so clicks from Action
Center did nothing.
- Stamp a fixed ToastActivatorCLSID on the Start Menu shortcut (runtime
and installer); a shortcut lacking it is rewritten.
- Register the class per-user as a LocalServer32 (never from a cargo
build directory) and as a running class object at GUI startup.
- Implement INotificationActivationCallback; the toast's launch string
carries the pid-checked pane identifier, so a click on a toast from an
earlier process only brings the window forward.
- Queue tray actions that arrive before the dispatch loop exists, which
is the cold-start case when COM launches tty7 to deliver the click.
- Deduplicate a popup click that arrives through both COM and the
in-process Activated event.
- The uninstaller removes the CLSID registration.
Fixes#1179
* fix(gateway): draw a pane's replay only once it is all in
The gateway drew the phone's first picture of a pane as soon as the replay
paused for 30ms or ran past 200ms, and forwarded any replay segment that
came after that drawing raw. The ring replays oldest first, so over a slow
link the phone first saw the front of the ring (a restored session's old
screen), or a blank drawing followed by the whole ring streamed past it.
Replay segments now only ever reach the phone through a drawing, and no
drawing is made until the replay is over: at the first message it does not
consist of, or after 500ms of quiet when nothing follows it. The quiet only
counts once the feed has said something, so a link slow to deliver the
first frame is not taken for a replay that has ended.
Fixes#1180
* fix(gateway): count a segment still arriving as the replay going on
A replay segment can be megabytes over a slow link. PaneOutput buffers a
frame until it is whole and returns WouldBlock on any read gap longer than
the 30ms draw settle, so the serve loop saw silence mid-segment and, once
half a second had passed since the last whole message, drew the mirror
before the replay was in.
Bytes of the next message now count as the feed having said something.
The drag and release handlers listen on the whole window and clamp the
pointer to the nearest cell, so with mouse reporting on (e.g. tmux
`mouse on`) a drag over the title bar, the sidebar or a neighbouring pane
was sent to every mouse-mode pane as a drag along its edge, dropping tmux
into copy-mode. A click anywhere likewise sent a stray release.
Track the button whose press a pane reported and only report drags and
the release for that grab. A grabbed drag still follows the pointer out
of the pane, clamped to its edge. The grab is dropped on any buttonless
move so a lost release cannot leave it stale.
Fixes#1182
A program killed while it held kitty keyboard flags left its push on a
stack. The prompt-time scrub (`CSI = 0 ; 1 u`) only rewrote the flags in
effect, so the next pop, or the next program to enter the alternate
screen, brought the dead program's flags back. Escape was then encoded as
`CSI 27 u` and Ctrl+C as `CSI 99 ; 5 u`; behind ConPTY those arrive as
key records with no virtual key, so Escape and Ctrl+C were lost (#1155).
No escape sequence clears the stacks, so the alacritty fork gains
`Term::reset_keyboard_modes`, which empties both screens' stacks and
clears the flags in effect. The reader calls it when the shell's prompt
returns, and at every OSC 133 A/B/D mark while replaying a pane on
reattach. The daemon's `TerminalModes` forgets the stacks at the same
marks, so the replay prelude does not push them back.
Flags a program pushes after the prompt are untouched, and the key
encoder is unchanged, so #895 keeps working.
Bumps the alacritty pin to fe249ca8.
Native SSH host panes run tty7's prompt editor over the remote shell, so
the remote ZLE never sees up-arrow, Ctrl+R or autosuggestion keys —
tty7's own list is all the user gets. That list was never seeded from
the remote ~/.zsh_history / ~/.bash_history / fish history: the seeding
path only handled workspace hosts and bailed for any pane with a remote
context.
Read those files over the pane's own SFTP connection instead, deferring
the read until the connection reports Connected. Nested `ssh` from a
workspace host still starts from tty7's own record. A load that lands
late keeps commands run meanwhile, and only the newest load is installed.
Fixes#1142
zsh escapes every byte in 0x83..=0xA2 (and NUL) as Meta (0x83) followed
by the byte xor 0x20 when writing $HISTFILE. tty7 decoded those files
with from_utf8_lossy directly, so CJK commands showed up as mojibake or
U+FFFD in history search, next to tty7's own clean copy of the same
command.
Undo the escaping at the byte level, but only for zsh files: by name
(.zsh_history, .zhistory) or, for a custom local HISTFILE, by the
presence of extended-history lines. bash and fish files are left alone,
since valid UTF-8 like `ー` (E3 83 BC) contains 0x83.
Also read tty7's own history file as bytes with a lossy decode, so one
invalid byte no longer drops the whole file.
Fixes#1175
Under Linux client-side decorations the viewport includes the
transparent shadow inset, but the title bar row sits inside the frame
padding. Sizing the strip off the raw viewport overflowed the row by
both paddings and clipped the window's close button. Subtract
`window_paddings` before reserving the lead and window controls; the
paddings are zero under server-side decorations and on maximized or
tiled windows, so other platforms are unchanged.
Fixes#1169
The Unix stop and running-build probes took the first process whose
executable matched */tty7-server-*. Every SSH pane link keeps a
`--stdio --pane` bridge of the same binary alive on the remote, and
/proc/[0-9]* expands lexically, so a 7-digit or wrapped pid sorts ahead
of the daemon: the restart sent SIGTERM to the bridge, the daemon kept
serving, and the restart timed out after 10s.
Both probes now require the `--daemon` argument (an exact cmdline entry
under /proc; `ps -p <pid> -o args=` equal to `<comm> --daemon` in the ps
fallback), and the stop signals every matching daemon instead of the
first, so an older dialect's daemon is still stopped on upgrade.
Fixes#1181
* fix(ui): align title-bar rows and share document header switches
Keep #1146's leading space derived from the macOS traffic lights, rather
than the previous fixed 80pt. Add a shared content centre for the title-bar
rows, so headers with different bottom borders align with those buttons.
Keep the editor's full leading space: a control's inner padding should not
reduce the gap before its clickable area or shift docked file tabs.
Share Dock / Fill controls between the editor and diff headers, and use
matching icons with localized tooltips for Split / Unified view.
Validation: formatting and Host boundary checks passed; GUI tests passed
(2506 passed, 2 ignored); local visual acceptance passed.
* fix(ui): give icon switches accessible button names
Expose each shared switch cell as a button with its localized tooltip name
and pressed state. This lets assistive technology identify the icon-only
choices and use GPUI's existing accessible click handling.
Formatting, Host boundary, and diff checks passed. GUI tests: 2506 passed,
2 ignored.
* fix(ui): share the terminal button in filled document headers
Use the editor's terminal button in both filled headers. Clicking it docks
the current file or diff beside the terminal without closing its content.
The shared builder also provides an accessible button name.
The click test verifies that the diff, focused file, expanded state, view
mode, and other tabs' layouts are preserved. GUI tests: 2507 passed, 2
ignored. Build, formatting, Host boundary, and visual acceptance passed.
* fix(ui): draw Split / Unified as two panes so they don't read as Dock
The diff header now holds both switches side by side, and a frame with
one vertical stroke was the Dock glyph shifted a point and a half. Split
is two frames next to each other, Unified two frames stacked.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(daemon): a pane no window shows still answers DA1
The client's emulator answers a pane's queries, so a pane with no client
attached answered nothing, and the replay on attach drops replies. Claude
Code ends its OSC 11 theme query with DA1 and waits for it; started in such
a pane it hung on that query and skipped every later theme check. The
daemon now answers DA1 itself while no client is attached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(daemon): write the DA1 answer after the state lock is released
A pty whose program is not reading its input blocks the write, and
holding the pane's state lock across it would stall everything else
that needs that pane.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(titlebar): the title bar's lead moves with the traffic lights
#1139 moved the lights from 9pt to 16.5pt in, but the 80pt `TITLE_BAR_LEAD` the tab strip, editor header and search band start at stayed put, so the gap after the zoom button shrank from about 17pt to 9.5pt. On macOS the lead is now derived from the lights: their inset, their 54pt span (three 14pt buttons on a 20pt pitch), and the inset again, 87pt. The strip-width calculation that hard-coded 80 uses the constant, the search-band tests use it instead of 80, and a test checks the lights end, with their inset, before the lead. Off macOS nothing changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(titlebar): TitleBar pads the strip by the lead, not its own 80pt
gpui-component's TitleBar hard-codes an 80pt left padding on macOS, so
raising TITLE_BAR_LEAD to the traffic lights' 87pt left the tab strip at 80
while the strip width, search band and document headers assumed 87. Set the
padding from TITLE_BAR_LEAD so they all start at the same x.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
send_input_to_a_dead_pane_answers_an_error raced the reaper: the session
ending after Exited lets the server drop the dead pane, so the input can
find no pane at all and answer "no such pane". Both are refusals.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The host menu's Disconnect ran RemoteLinks::disconnect inside an update
of the window's own Tty7App. The disconnect detaches the panes of every
window on that machine by reading each window's app, this one included,
which panicked on the double lease. The panic happened inside the
mouse-up platform callback, where it cannot unwind, so the process
aborted and every window went down with it.
Defer the disconnect until the update is over, and add a regression test
that reproduces the original panic without the fix.
With the inline prompt editor live, every click on the input row went to
the editor, including clicks on the prompt the shell drew ahead of it.
A click left of the input start was mapped to the editor's first
character, so double-clicking the branch in ` tty7 git:(main)` ran a
word select over the editor's own (often empty) text and selected
nothing, and its first click had already thrown the caret to the start
of the line.
A click on the prompt now bypasses the editor and goes to the grid's
selection like a click on any other output: double click selects the
word, triple click the line, and the caret stays where it was. Clicks
on the typed text are unchanged.
Co-authored-by: wenlingang <lingang.wen@longbridge.sg>
Copy Raw is a keymap action (default cmd-alt-c) instead of a chord
hard-coded in the key handler, and `raw` threads through
copy_contextual into the one copy path, which now owns the
Claude-pane policy. copy_unwrap.rs depends only on alacritty_terminal:
the boundary rule splits into a Block that tracks fences and tool
output and a pure `sep` with early returns. The fixture tests run
through a real Term at the fixture's width instead of a hand-built
row model.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(fonts): turn stroke thickening off in gpui, not through AppleFontSmoothing
font_thicken: false pinned AppleFontSmoothing to 0 in the process's
argument domain. That never left the process, but AppKit reads the same
preference, so tty7's native menus and alerts lost their smoothing too,
most visibly dark text in light mode. The terminal's own dark text looked
unchanged, since gpui never thickens it in the first place.
The gpui fork now exposes set_glyph_dilation, which reaches its
rasterizer alone; apply_font_thicken calls that and writes no
preference. A test checks that turning thickening off leaves
AppleFontSmoothing alone (red on the old implementation).
Fixes#1119
* test(fonts): keep the font_thicken test out of config_reload_tests' doc comment
* fix(sftp): say why SFTP would not start instead of "Timeout"
Every way an SFTP channel can fail before the protocol starts ended the
same way: the library's 10s request timeout, shown in the Files panel as
"sftp init failed: Timeout". Each has its own cause and fix on the server:
- sshd without a `Subsystem sftp` line answers the request with
CHANNEL_FAILURE. russh's request_subsystem only sends the request, so
the refusal went unread and the INIT was sent into a channel nobody
was listening on.
- A `Subsystem` line naming an sftp-server that is not installed: the
channel is accepted and then closes without a byte.
- A shell startup file that prints on non-interactive logins puts its
text where the version reply belongs; read as a packet length it asks
for gigabytes that never arrive.
The subsystem reply is now read (bounded at 20s), and the first bytes
back are checked to be the header of an SSH_FXP_VERSION packet before
the stream is handed to the library. Each case is reported at once with
what to change on the server; text ahead of SFTP is quoted.
Fixes#1126
* fix(sftp): let the probe's reason win over the library's end-of-stream error
A long-running full-screen program paints its whole screen once, after
?1049h, and only differences from then on. Once the replay ring has
dropped that first paint, a reattaching client is put into the alternate
screen and handed nothing but differences from a screen it never saw,
painted onto an empty one.
On attach and observe, when the pane is in the alternate screen, the ring
no longer carries the switch into it, a program owns the pane and the
pane is alive, step the pty one row away and back 50 ms later. The
program hears a size change and repaints in full. Only the pty moves:
the ring keeps its size and no client or lease sees a Size, and the size
put back is read when it is put back. One nudge per pane per 2 s.
Fixes#1122
A wheel report carried the cell of the last button press: only a click,
or motion for programs that ask for every move, ever updated it. tmux asks
for button events only (1002), so after clicking its window list every
notch over the panes was reported as being over the window list. The
wheel now resolves the cell under the pointer from the grid geometry of
the last frame.
On Linux the IME candidate window only moves when the caret rectangle is
pushed to the platform, and gpui pushes it on focus and around a preedit,
never because the caret moved. A TUI moves the caret on its own, so the
candidate window stayed where the last composition or the focus-in left
it. The pane now invalidates the IME coordinates whenever its anchor
moves while focused.
Fixes#1131
* feat(terminal): copy rejoins the lines Claude Code wrapped itself
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(terminal): keep copy_unwrap fixtures LF on Windows checkouts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(settings): build the join-wrapped row with its Selection & Clipboard siblings
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
gpui only rasterizes subpixel (ClearType) glyphs when the window's
background appearance is Opaque. tty7 never asked for Opaque: with no
backdrop material the window was always Transparent (or the WCA blur),
even at the default opacity of 1.0 where nothing behind it can show
through. Every glyph on Windows therefore fell back to grayscale
antialiasing, regardless of the system ClearType setting.
On Windows, hand the window Opaque whenever the fill is fully opaque
and the resolved backdrop is plain translucency or the WCA blur. DWM
materials (Mica / Mica Alt / Acrylic) are left alone. Lowering the
opacity switches back to Transparent at runtime as before.
Fixes#1120
On a light theme Settings drew its dropdowns, buttons, menus, chips and switch knobs pure white, which stands out on a tinted page such as a cream theme. They are now the page colour lifted a step and made opaque, so they keep the theme's hue; a white theme still gets white controls, and dark themes are unchanged. The change is the `raised` colour in `settings/kit.rs`. `cargo test --bin tty7-app settings` gives 58 passed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A new or deleted file has one side only, so in the split view half its card was empty. Such a file now draws unified whatever the view, and the others still split. A drag takes its mode from the row it starts on, so selecting and copying in one of these files works in either view. `file_mode` in `diff_list.rs` picks the mode, with tests; `cargo test --bin tty7-app diff` gives 90 passed, 1 ignored.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The lights sat 9pt in from the left edge, the inset of a plain 28pt title bar, which crowds them into the corner of the 48pt bar. AppKit's own toolbar windows inset them as far from the left as from the top: measured on macOS 26, the close button sits 19pt in and 19pt down in a 52pt unified toolbar, and 12pt in and 13pt down in a 40pt compact one. `traffic_light_position` now uses the top gap it already derives (16.5pt) for `x` as well, and its test asserts the two match. The lights still end well inside the tab strip's 80pt lead. `cargo test --bin tty7-app traffic` passes.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ssh): a link whose peer stops answering is noticed and redialled
A connection whose peer went silent with the TCP socket still established
(a stalled path, a host that rebooted under it) was never noticed: nothing
is sent on an idle link, russh waits on a channel confirmation forever, and
the cache kept handing the connection out. The machine read "connected",
the GUI sat on "Reconnecting…" indefinitely, routed CLI calls hung, and
Retry Now dialled nothing new.
- Every connection now gets an SSH keepalive (15s, 3 misses) unless its
profile sets its own interval or turns it off with 0.
- Channel opens wait at most 20s. A connection that does not answer in time
is marked dead, so the next open dials a fresh one; what already runs on
it is left alone. The remote probes' exec requests are bounded too.
- When direct-streamlocal is refused on a connection whose server came from
the per-connection note, the note is dropped and the server proved again
(which starts it) before falling back to a per-link stdio server. The note
outlives a server that exited, or a host that rebooted, while the link
stayed up.
* fix(ssh): a slow forward target or a failed re-prove no longer costs the link
A direct-tcpip open is answered only once the server's own connect to the
target settles, which for a target that drops SYNs outlasts the 20s open
bound on a healthy link. Marking the connection dead there stopped every
forward on it and sent the next pane to dial and authenticate afresh.
direct-tcpip is unbounded again; the keepalive catches a dead peer.
A re-prove of a stale server note that fails now falls back to the
per-link --stdio server instead of failing the link.