Commit Graph
960 Commits
Author SHA1 Message Date
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 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
l0ng-ai 119baa270c Merge pull request #888 from l0ng-ai/fix/870-hydrate-unread-badge
fix(agents): don't re-badge a turn the reader already saw when a pane is rebuilt
2026-09-15 23:02:25 +08:00
l0ng-ai b8459836c8 Merge pull request #886 from l0ng-ai/fix/858-light-theme-dim-contrast
Keep SGR 2 (dim) text legible on light themes
2026-09-15 23:00:59 +08:00
l0ng-ai eb5aae172d fix(agents): stop a reattach from badging every restored agent tab
Restarting the app leaves the daemon and every agent in it running, so
each restored tab attaches to a pane whose turn ended long ago. The
daemon replays that pane's stored agent status as an ordinary
`AgentStatus` frame, and nothing on the wire said it was stored state —
so the view read `None -> Done` as a turn finishing right then and put an
unread badge on every agent tab that was not the focused one, for results
the reader had already read. A relink did the same on every dropped link.

Keep the distinction where the value lives: the shared slot now carries
`replayed` alongside the status, under the same lock, so a reader cannot
observe one without the other. The reader thread marks the first status
frame on an attached link as the replay; the first frame of live output
closes that window, which is what a pane with no session to replay needs
(its next agent's first report is live, and must stay an edge). The view
takes that mark once and adopts the status as its baseline instead of an
edge.

The notification path already required the previous status to be
Working/Waiting, so only the badge was wrong.

Claude-Session: https://claude.ai/code/session_01FG2s9mbZu6LbjjmU54X7kt
2026-09-15 09:01:04 +08:00
l0ng-ai 6370fd4cdd test(terminal): pin that a DECTCEM-hidden cursor paints no caret (#844)
The issue reads `CursorShape::Hidden` as reachable only through DECSCUSR,
but alacritty derives it from `TermMode::SHOW_CURSOR`, so `?25l` already
suppresses the painted caret: focused bar/underline, the focused block's
reverse-video cell, and the unfocused outline. Claude Code itself ends each
frame with `?25h` at its input point, which is the caret the reporter saw.

Route the paint decision through `GridSnapshot::painted_cursor` and pin it
with a test that paints real frames for the reporter's stream shape (alt
screen, 69 hides / 75 shows ending hidden): focused, unfocused, `?25h`
restoring it, and a re-attach replay. With the hidden filter disabled the
test fails with `Some((4, 12, Block))`.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-14 21:51:20 +08:00
l0ng-ai 50957f3124 Keep SGR 2 text legible on light themes
Faint text was painted as its ink at 66% alpha over the cell. On a light
background that fixed fade collapses the WCAG ratio: Catppuccin Latte's
foreground fell from 7.06:1 to 3.18:1, Rose Pine Dawn's to 3.08:1, and
every bright-black the palette rescue had lifted to 4.5:1 fell back to
~2.5:1 on all four light builtins (#858).

On a light cell the fade is now walked back toward the ink until it clears
the 4.5:1 text floor, capped at the ink's own ratio. Dark cells, and the
legible-palette switch turned off, keep the plain fade byte-for-byte.

Fixes #858

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-14 21:49:08 +08:00
l0ng-ai 436ea04f33 fix(keymap): a keybinding in config adds a chord instead of replacing the default (#868)
effective_bindings kept one chord per action and set_binding overwrote that
slot, so "NextTab": "cmd-shift-]" silently took Ctrl+Tab away.

A string in keybindings now adds a chord beside the action's default (or
preset) chord; "" still unbinds, as configs and the docs already rely on; a
list is the exact chord set, [] unbinds. Configured chords are installed after
every shipped one, so a chord the user names wins a tie with another action's
default. The Settings page lists every chord of an action, and recording a
shortcut writes the list shape (it sets the binding) and takes only the stolen
chord from the action that had it.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-14 21:48:19 +08:00
l0ng-ai ef55505a6a fix(agents): don't re-badge a turn the reader already saw when a pane is rebuilt
Switching workspaces or reopening a window from the tray throws a pane's
TerminalView away and builds a new one over the same daemon pane. The new
view starts with no last status, so an agent that was already Done arrives
as None -> Done, which poll_agent_status cannot tell from a turn finishing
live, and every unfocused rebuilt pane got its unread badge back.

The daemon now counts finished turns per agent session (turns, bumped on
entering Done), and views leave an app-lifetime mark per (host, pane) of the
session, turn count and badge the reader was last shown. A rebuilt view's
first sight of Done takes the badge back from a matching mark instead of
raising a new one; a turn that finished while the view was gone has no mark
or a lower count, and still badges.

Fixes #870

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-14 21:45:15 +08:00
l0ng-ai 37c2ef2176 test(keymap): pin additive keybinding semantics (#868)
Regression tests for issue #868: a chord added in config.json must join the
action's default chord rather than replace it, an empty string or list must
still unbind, a list replaces the chord set, the tmux preset composes, a user
chord wins a tie with another action's default, and Settings recordings write
the exact-set shape.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-14 21:40:42 +08:00
l0ng-ai 04878af279 fix(terminal): hold one steady bell flash through a burst of bells
Every visual bell armed its own 150 ms clear timer, so when bells arrived
faster than that - holding Backspace on an empty bash prompt, or Tab with
nothing to complete, rings at key-repeat rate - an older bell's timer blanked
the flash a newer bell had just lit, and the pane strobed for as long as the
key was held. Only the timer armed by the latest bell clears the flash now.

Fixes #874

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-14 21:40:04 +08:00
l0ng-ai bf76baeba1 Merge pull request #864 from hhdebb/fix/hide-the-title-bar-in-fullscreen
fix(window): drop the dead window buttons from the title bar in fullscreen
2026-09-14 19:03:39 +08:00
l0ng-ai 89279c4769 Merge pull request #875 from l0ng-ai/feat/ssh-host-credentials
Give the SSH host editor the credentials it was missing
2026-09-14 18:58:31 +08:00
l0ng-ai 611c6030a4 Merge pull request #881 from akhenakh/crush
added support for Crush agent
2026-09-14 18:56:50 +08:00
l0ng-ai 71f2312f3b Merge pull request #877 from spragginsdesigns/fix/exit-typeahead-ctrl-u
fix(terminal): stop injecting Ctrl-U after session exit
2026-09-14 18:55:15 +08:00
l0ng-ai 9b0ff8c626 Merge pull request #880 from fish2lab/fix/macos-reopen-window
fix(macos): answer the Dock's reopen so a retired tty7 can come back
2026-09-14 18:53:52 +08:00
l0ng-ai 1f189e2ee0 fix(window): keep the title bar row in fullscreen, drop only its window buttons
Hiding the whole title bar took the tab strip with it: with tabs on top
every chip, the New Tab tile and the panel/menu tiles vanished in
fullscreen, the docked document header (drawn only over the spanning bar)
disappeared, and the strip's drop band kept claiming a row that was now
terminal.

What is actually dead in fullscreen is minimize/maximize/close. The row now
stays; in fullscreen off macOS the strip goes into a plain row of the same
geometry instead of `TitleBar`, which always draws those buttons, and the
room reserved for them (strip width, chrome band over the panel, document
header padding) comes back. The notice text says the window buttons are
hidden rather than the title bar, and the keymap test whose premise was the
bar disappearing is replaced by one pinning the controls width.
2026-09-14 18:35:15 +08:00
l0ng-ai 609e1b04a6 fix(terminal): let Ctrl-D close the typeahead record only on an empty line
Readline, PSReadLine, a cooked tty and tty7's own editor all treat Ctrl-D
as end of input only when the line is empty; with text on it Ctrl-D is an
edit. Discarding a record that still holds unsubmitted text dropped the
owed wipe, so text typed during a gap followed by Ctrl-D stayed on the
shell's line and was glued to the front of the next command. Ctrl-D now
discards only a record with no unsubmitted text, and taints otherwise.
2026-09-14 18:33:22 +08:00
l0ng-ai 2e8a43a35e fix(settings): pass the host-boundary guard and follow the default keys
The passphrase box checks which key file is on this machine with
std::fs::metadata, which the host-boundary guard rejected; allowlist it
beside the existing std::fs::read entry for the same client-side key.

An empty key field now resolves to the ~/.ssh defaults build_spec_inner
offers, so a default encrypted key can be given a passphrase from the
form. The key is also re-resolved when host or user change, since they
fill %h/%r in the path. Drop the unused SettingsForget string.
2026-09-14 18:32:55 +08:00
l0ng-ai a433ef0a31 test(windows): pin the config dir before the reopen restore saves views.json
Run on its own, the no-window reopen test reached WindowViews::save with no config-dir override and could overwrite the developer's real views.json.
2026-09-14 18:31:02 +08:00
l0ng-ai 237cea849c Merge remote-tracking branch 'origin/main' into fix/hide-the-title-bar-in-fullscreen 2026-09-14 18:27:20 +08:00