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
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
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.
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.
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.
Closing the last window with the tray icon on retires tty7 to the tray:
process alive, Dock icon up, nothing on screen. That state had no way back
through the icon. macOS relaunching an already-running app arrives as
`applicationShouldHandleReopen:hasVisibleWindows:`, gpui's delegate forwards
it to a callback registered with `Application::on_reopen`, and tty7
registered none — so the click was a no-op, and the only ways back in were
`⌘N`, the tray's "Show tty7", or quitting and relaunching.
`windows::reopen` takes that callback, in the two shapes the state has: a
window still registered is activated rather than doubled, and no window at
all goes through the pathless-launch restore (`restore_target` + `open_at` +
`announce_detached_at_launch`) — the same path the tray's windowless branch
takes, so the workspace that retired is the one that returns and not a blank
one beside it. `reopen_with` is the seam the tests drive, so a reopen that
opens a second window beside the one on screen cannot pass.
`Application::on_reopen` is registered beside `on_open_urls` in `main`,
because it has to exist before `run` — `keymap::init` runs inside the loop —
and the callback defers to the loop with `cx.spawn` rather than opening
windows on AppKit's delegate stack, the shape `on_open_urls` already uses.
`activate_window` is `makeKeyAndOrderFront:` on macOS.
Reported from a macOS machine where a lid close and wake left the process
frontmost with no window: `launchservicesd SETFRONT` at 23:51:40 with the
process still reported `running-active-NotVisible`, and the layout only back
after a quit and relaunch. The window itself being lost across display
sleep → wake is not explained by this change and carries no guess-fix here:
nothing in tty7 or in the pinned gpui hangs off display sleep or wake.
cargo fmt --check; cargo check --locked -p tty7 --tests; cargo test --locked
-p tty7 --bin tty7-app -- 1888 passed, 0 failed.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
`with_terminal` builds the chain once, out of `font_family` and
`font_fallbacks`, and from then on it is only ever copied: `set_font_family`
took it off the font it was replacing, and `alt_font` takes it off the
regular face when it builds bold and italic. Nothing reread it.
So a `font_fallbacks` edit had no live path at all — only panes opened
afterwards saw it. Changing `font_family` and changing it back did not help
either, because that path cloned the chain too.
Carrying the chain across a family change is also wrong on its own terms.
`fallback_chain` decides the pins from the family it is handed: it skips
pinning a last-resort face that the family already is, and pins the bundled
Hack otherwise. The chain built for `Hack` therefore has no Hack in it, and
reusing it after a switch away from Hack leaves the anchor missing.
`set_font_family` now rebuilds from the config, `reload_from_config` watches
`font_fallbacks` and pushes a rebuild into every open pane, and the rebuild
writes all three faces rather than the regular one alone — bold and italic
carry no chain of their own, so skipping them would strand two thirds of the
text on the old one.
A host could be described in full in Settings and still not be
connectable from there: there was no password box anywhere on the form.
The only way to store a password was to connect, wait to be asked, and
tick "remember" — and the only way to correct a wrong one was to connect
again and fail first. The key file lived two disclosure triangles deep
under Advanced, as a textarea of paths with nothing to pick one.
The form now carries the credential half of a connection, in an
Authentication block between the address and the collapsed sections:
- A password box, masked with a reveal toggle, seeded from the system
keychain so a stored password can be read back, corrected or cleared
without dialling anything. Clearing it and saving is how a saved
password is let go of.
- Identity files, moved up out of Advanced, with a Browse button that
opens the system picker and writes the path back as `~/.ssh/...`
rather than the absolute path the dialog hands over.
- A key passphrase box beside it, stored against the contents of the key
it unlocks — the same account the connect-time prompt uses. It follows
whichever key the field names, and says so when there is no readable
key to store one against.
Which boxes appear follows the method, the way every other SSH client
does it. The split is `build_spec_inner`'s: a password for Auto and
Password, key passphrases for Auto and Key, and nothing for Agent,
GSSAPI or 2FA — a box outside that would collect a secret, store it in
the keychain, and never offer it to anybody.
Nothing secret reaches the config file. That is also why Save could not
see a typed password: the dirty check compares profiles, and no profile
holds one. It now folds the two secrets in, so Save lights up for a
password the way it does for a port.
Saving moves a password with the address it is filed under — the
keychain accounts by endpoint, not by profile — and leaves nothing
behind under the old one, unless another host still dials it. A
passphrase belongs to its key rather than to this profile, so pointing a
host at a different key never touches the first key's entry.
Test dials with what is on screen rather than only with what is stored,
so it stops reporting a failure the form could not explain.
The layout is the other half of the report. These rows were built out of
the settings rows the rest of the page uses, which push their control to
the far right edge: right for a list of independent switches, wrong for
a form, and it left a hand's width of nothing between the word "Host"
and the box a hostname goes in. Labels now sit right-aligned against
their fields, descriptions and errors moved under the field they are
about, and the three that only restated their label became hints inside
the box.
Two bugs the new shape turned up: a percentage-width control inside a
flex-grown wrapper has no definite parent to resolve against, so the
host and key fields collapsed to one character and the method dropdown
clipped its own menu to "GSSAP"; and "Needs a host" appeared in red on a
form nobody had typed in, because the untouched check counted a port
field that opens on 22 and is never empty.
Claude-Session: https://claude.ai/code/session_01LAqfzqELnoDWU56LBXS1Nh
A terminal draws its own glyphs, so nothing outside the window can read
what is on screen. That much is a terminal being a terminal. What is not
is that the window reports no focused element at all.
gpui sets accessibility focus in exactly one place: a `div` that tracks a
focus handle and has an a11y node of its own. The terminal surface tracks
the focus handle and never asks for a role, so it has no node, so
`set_focus` is never reached — and a client asking the window what has
focus is handed the window.
Measured on Windows 11 26200 with a UI Automation probe: the window
answers with `WindowPattern` and nothing else, publishes zero
descendants, and `FocusedElement` is the top-level window, supporting
neither `ValuePattern` nor `TextPattern`. A screen reader has nothing to
say about a tty7 window for the same reason.
It reaches past screen readers. A dictation tool pastes its transcript
and then asks the focused element what it now says, to check the text
arrived. Against tty7 it gets no element to ask, concludes the paste
failed, and hands the transcript back for the user to paste by hand —
while the bytes it sent are already in the pty and the text is on screen.
That is what led here.
The fix is the surface asking for a role: it gets a node, and focus lands
on it. `MultilineTextInput` rather than `Terminal` because `Terminal`
maps to a document that reports itself as not editable, and "is this
something text can be put into" is the question these clients are
actually asking. The node carries no text of its own yet — reading the
grid out is a separate change with a cost per frame, and this one has
none: gpui builds the a11y tree only once something attaches to it, so a
window nobody is inspecting still builds nothing.
The path this fixes is platform-independent; it was verified on Windows,
where the dictation tool that surfaced it runs.
Hook events map Qoder's lifecycle to tty7's state machine: session start,
prompt submit, permission requests, MCP tool elicitation (an authorized MCP
tool can still pause for user input mid-call), tool completion, stop, and
session end. Compaction events are filtered out—Qoder emits a session-start
after compacting the active turn, which would reset the status line to Idle
without this filter, even though the turn is still running.
Settings path resolution respects QODER_CONFIG_DIR for local installs,
falling back to ~/.qoder/settings.json. Remote targets ignore the override
(a local env var must not redirect remote hooks).
Session commands support --resume and --fork-session. The resume command
strips conflicting flags (--resume, -r, --continue, -c, --session-id,
--worktree, --fork-session) from the original launch argv before appending
the new session id. The -w/--cwd flags survive (Qoder's -w means --cwd,
not --worktree). Both commands require session persistence: when
--no-session-persistence is present, there is no saved conversation to
reopen, so the commands return None.
Tests cover compaction preservation, MCP elicitation state transitions,
QODER_CONFIG_DIR's effect on the hook lifecycle (multi-case isolation),
resume/fork command generation, worktree flag handling, and persistence
requirements.
Localization complete for en/ja/zh. Icon embedded, search keywords wired.
Agents animate in the terminal title while they work, and they do not
agree on an alphabet: Claude Code cycles the quadrant circles and rests
on an asterisk, others step through the braille frames, some write
nothing at all. Rendered as they arrive, a column of tabs carries a mark
in front of some rows and not others, in three vocabularies — while the
row already says what the agent is doing, in one, with its status dot.
So the mark comes off, for everyone, with no setting. A switch would not
settle this: nobody opens settings to decide how a spinner is drawn, and
a default-off toggle buys two render paths to maintain forever in order
to answer a question that has one right answer per person and no way for
the app to know which.
**A known alphabet, not a shape.** The obvious rule — a leading character
that is non-ASCII and above some code point, followed by a space — matches
by shape, and `🔥 build`, or `📁 ~/repo` written by somebody's shell
integration, fits it exactly and quietly loses its first character with no
way to ask for it back and no clue as to what took it. Matching marks we
have actually seen costs the same and cannot do that: the braille block,
the four quadrant circles, and Claude Code's resting asterisk. When an
agent invents a mark that is not on the list, the failure is today's
behaviour — the mark stays — which is the safe direction to fail in, and
adding it is a line in the table.
Two things the rule insists on, both to keep it from reaching past what
it is for. A mark only counts with whitespace behind it, so `✳fixing` is
a word that starts with a character rather than a mark in front of one.
And a title that is *only* a mark keeps it: taking it would leave an empty
string, and an empty title is not a tab called nothing, it is a tab that
falls back to its number — less than the mark was saying.
It happens in `TabView::label`, which is where a title becomes a label, so
the strip, the sidebar, the switcher and the rename box's prefill all
agree without being told separately — and, because `label` reaches a
given name before it reaches the title, a tab somebody deliberately
called `✳ release` keeps what they called it. That ordering is the only
thing standing between a user's name and a rename behind their back, so
there is a test on it rather than a comment. Three existing tests carried
`✳` in their fixtures and now expect it gone. The one in `switcher.rs` was
asserting that a tab in another window is named the way a local one would
be, which is still exactly what it asserts; the one in `tty7-cli` is the
table getting this for free, since `tab_label` reads `label` and so
`tty7 ls` says what the tab strip says without either being told about the
other. The daemon's fixtures keep their marks on purpose: a title is stored
as the terminal wrote it, and only what turns one into a label takes
anything off.
This leaves the row with nothing moving in it, which is a real loss and is
answered separately: `AgentStatus::dot_rgb` returns three flat colours,
and a `Working` dot that breathes says the same thing in the vocabulary
the row already speaks.
On Windows and Linux a fullscreen window has no caption. The platform asks
what is under the pointer through `WM_NCHITTEST`, gpui answers from the
window control hitboxes the frame registered, and fullscreen clears
`WS_CAPTION` — there is nothing left to answer with. Measured on a
fullscreen tty7: `GetWindowLong` reports `WS_CAPTION` clear, and every
point along the top of the window comes back `HTCLIENT`, where the same
window a moment earlier answered `HTCAPTION`, `HTMINBUTTON` and `HTCLOSE`.
The bar was drawn anyway. `WindowControls` renders minimize, maximize and
close whenever the target is not macOS, without asking whether the window
is fullscreen, so all three sat there taking hover styling — gpui's own
dispatch reaches them fine — and doing nothing at all when clicked.
Dragging the bar did nothing either.
So on those two the bar goes. It is the app's own chrome there: a caption
to move the window by and the controls at its end, none of which a
fullscreen window has. Drawing chrome that cannot work is worse than
drawing none.
Not on macOS, and the reason is not that the bug is milder there — it is
that the premise does not hold at all. `WindowControls` draws none of the
three on macOS; the ones that go dead elsewhere are the system's traffic
lights, and the system hides them itself. What that bar does in fullscreen
is hold the band the system reserves: the traffic lights land on it when
the menu bar is revealed, and so does the translucent strip drawn under
the menu bar. Take the bar away and that strip lands on the terminal and
covers its first row instead — measured, and the difference is exactly
`TITLE_BAR_HEIGHT`. Fullscreen belongs to the system on macOS, and the bar
is part of how the system dresses the window rather than something broken.
Nothing on the bar becomes unreachable where it goes. Its controls are
actions first, dispatched from the window's root rather than from the bar,
and each has a chord or a seat in the palette, which has one;
`what_the_title_bar_offers_is_reachable_without_it` is that in a test.
Worth noting for anyone reading it: `ToggleTabSidebar` ships with no
chord, so in fullscreen the palette is how it is reached.
Entering says how to leave, because entering is the instant the bar
disappears — so only where it does, and only through the action: a window
that starts fullscreen because the setting says so is not a surprise
anybody needs explaining, and `startup_mode` is untouched by the toggle
either way. The chord comes from the keymap rather than from a string, so
it reads `F11` or whatever it was rebound to.
The notice carries an id, which is what keeps a held-down `F11` to one
notice rather than a column of identical ones: pushing under an id already
on screen replaces that one. Leaving through the action takes it back as
well. Leaving some other way lets it time out instead — a second or two of
a stale notice, which is not worth a per-frame watch on a state that lies:
`toggle_fullscreen` is spawned onto the executor on every backend, so
`is_fullscreen` still reports the old value when the action returns, and a
render-time test for "not fullscreen now" can take the notice back before
it has been seen.
Verified on Windows 11 26200, and on macOS 26.5.2 by a second pair of
hands: the macOS half of this is the reason the change is not applied
there. Linux is reasoned about rather than measured — it draws its own
chrome the way Windows does, and the same `WM_NCHITTEST`-shaped question
is answered through gpui's window control hitboxes.
GitHub's REST API answers a spent quota with 403 or 429 depending on the
endpoint and the era, and both carry the same x-ratelimit headers. Only
the 403 spelling reached the retry advice, so a 429 told the reader the
quota was gone without saying when it comes back.
Claude-Session: https://claude.ai/code/session_01E4EPKzHg1fm9HMmHkUYpER
The new `link_rtt` landed between `pane_procs`'s doc comment and
`pane_procs` itself, so the comment about walking pane process trees
documented the latency probe instead.
`format_rtt` also compared the unrounded milliseconds against 1000, so a
999.6 ms round trip printed as "1000 ms" — a millisecond reading past
the range the millisecond branch exists to cover. Round first, then pick
the unit.
Claude-Session: https://claude.ai/code/session_01E4EPKzHg1fm9HMmHkUYpER
The Session table named the machine a pane's shell was on but never the
distance to it. A remote workspace gone slow looked exactly like one that
had not, and the only way to tell them apart was to leave tty7 and ping
the box by hand — with nothing to say whether tty7's own link was the one
that was slow.
Time the control link's `Ping` and put the last measurement on a `latency`
row, drawn only where there is a network between here and the shell. Every
ping that comes back feeds it, the keepalive's included, so a link being
kept alive already carries a number before anyone asks for one. Nothing
else is timed: every other request does work on the far side, so its round
trip measures that work rather than the link, and a `ReadFile` of a large
file would read as a network seconds slow.
The poll rides the Info panel's existing process-and-port round, and only
while the panel is open — that round also runs with the panel shut,
watching for ports to forward, and a round trip for a row nobody can see
is the far end's time spent on nothing. A link that drops keeps its last
measurement rather than blanking: a dropped link is exactly when someone
is reading the row to work out why a pane stopped answering.
Claude-Session: https://claude.ai/code/session_01FG2s9mbZu6LbjjmU54X7kt
Every list in the right panel lays its column out a ROW_INSET short of
CONTENT_INSET and has each row pad itself back out, so a row's text lands
on the 12px rail and its hover and selection fill bleeds past it to 8. The
file tree ran its own pair of numbers instead: a px_1() column and a 6px
row inset, which put a depth-0 name at 10 and let the fill reach 4.
Two of those disagreements are visible. The tree sits directly under the
panel's search field, so the root row's folder glyph and the search
magnifier are two adjacent left edges 2px out of line. And a selected row
runs nearly edge to edge where the same row under Info or Source Control
stops 8px short.
Claude-Session: https://claude.ai/code/session_01E4EPKzHg1fm9HMmHkUYpER
`main` dropped the agent-turn cuts from the reader's `flush_batch!` and
added `foreground_command` to `replay_state`; both are kept, with the
per-pty `repair_cursor` flag replacing `REPAIR_PARKED_CURSOR` in the
simplified cut loop and the mode restore ahead of the new signature.
Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
Watching connecting slots is what lets a pane focused while it is still
coming up be recorded, but the record names the *pending* slot, and that
id dies the moment `land_pane` swaps the slot for the pane that came up
in it. The memory then names an entity no tab holds, `focus_target`
falls through `leaf_matching_or_first`, and the tab comes back to its
first leaf — the bug this branch is about, one landing later.
Nothing else writes the answer down in that window. `land_pane`
re-focuses the pane it built only when the pending slot still held
focus, and with focus off the panes the switch-away sample has nothing
to read either. So: split a pane, let focus wander off the panes while
the new one is still connecting, switch away and back, and you land in
the pane you did not ask for.
`replace_leaf_in` does the swap and carries the memory with it, and
`respawn_native_ssh_in_place` — the other place a live pane is
substituted for a dead one — uses it instead of repeating the walk.
Also pins the wiring this branch changed. The tests here drove
`remember_leaf_in` directly and never the subscription that calls it, so
deleting the one line that records a focus arrival left all four green.
`test_window::harness` makes the round trip reachable — real panes, real
gpui focus, a real `activate` there and back — and both new tests fail
without the code they are about.
Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
The shared table sent F3 as `SS3 R` / `CSI 1;<mods>R` to a client that had
negotiated the kitty keyboard protocol. Kitty's first version allowed that
letter form and then removed it: `CSI 1;2R` is also a Cursor Position Report
for row 1, column 2, so a client cannot tell Shift+F3 from an answer to its
own DSR. The protocol's table gives F3 as `CSI 13~` alone -- the VT220 `kf3`
-- and alacritty special-cases exactly this. Only the kitty path moves;
everyone reading terminfo still gets the `kf3=\EOR` we advertise.
F13..F24 were left unencodable on both paths for a terminfo reason that holds
only on the legacy one: `kf13` onwards are already the modified F1..F8, so
there is nothing to send. Kitty has no such clash -- it names them in the
private use area, `CSI 57376 u` for F13 -- so a client that asked for the
protocol now gets them, and the legacy path still sends nothing.
Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
`rustfmt` is a required check and was red on three hunks of this branch:
`owner_of`'s signature, the `--cwd` argv in `tab_new`'s adopt test, and the
two `tab_create` calls in `the_document_being_replaced_is_kept_beside_it`.
`keep_a_generation` also read the whole document before asking whether it
was going to keep anything. It runs on every persist — pane facts alone
flush every couple of seconds — and answers "too soon" on almost all of
them, so that was a full read of `machine.json` per write to produce one
copy every five minutes. The spacing check moves ahead of the read; the
`NotFound` arm still covers the machine that has never written a tree.
And a line continuation was missing from an assertion message in
`a_typed_name_waits_for_the_create_rather_than_racing_it`, so the failure
would have printed eighteen spaces mid-sentence.
Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
The switcher's scrim covers the whole window, the tile that opens it
included. Its mouse-down closed the switcher and then carried on down to
whatever sat beneath — for that tile, straight into `toggle_switcher`,
which reopened what had just closed. Clicking it a second time looked
like it did nothing.
Stop the press at the scrim. A click that dismisses is spent on the
dismissal and reaches nothing underneath it, so the tile toggles and no
control fires by accident on the way out.
Claude-Session: https://claude.ai/code/session_01XLMiHJR7RXvAGsR8S7jkHa
The tile's only hover state was a fill the palette derives one step off
the surface, which on the rail is barely a change at all — and the name,
the monogram and the chevron each pinned `muted_foreground`, so the
button's own hover ink never reached them. The one control at the top of
the column said nothing when the pointer landed on it.
Move the ink and the hover onto the row holding the three children and
let them inherit, the way a group header does: the text steps up to full
strength. The fill stays, it is simply no longer the whole signal.
Claude-Session: https://claude.ai/code/session_01XLMiHJR7RXvAGsR8S7jkHa
A tab's `last_focused` was written by one sample taken on the way out of
`activate`, and that sample asks which leaf holds focus at that instant.
By then focus is routinely somewhere else: the switcher's own search
input, a palette that just closed, the tab strip, the file tree
`activate` focuses itself, or a pane restored and never clicked. With no
leaf focused the write was skipped, the field kept a stale pane or the
`None` it was born with, and `focus_target` fell through
`leaf_matching_or_first` to the tab's first leaf. Two panes, click the
right one, switch away and back, land in the left one.
`watch_pane_focus` already subscribed to every pane's focus-in — it
existed to repaint the chrome that marks the focused pane — so the
record now rides on that callback instead of a new one. Focus-in is the
only moment that knows the answer without having to guess when to look,
and the subscription's cost and lifetime were already paid for. Which
tab gets the write is asked of the layout rather than assumed to be the
active one, so a pane dragged into another tab is remembered by the tab
holding it now; a leaf no tab holds is recorded nowhere. Connecting
slots are watched too, since a pane can be focused while it is still
coming up.
`remember_active_pane` stays. It never writes a wrong answer, only
sometimes none, and its callers want the field settled at a named moment
— before a pane is detached, before a tab is torn down — while the
layout can still answer.
Deliberately left alone: a restored tab still opens on its first leaf
until someone touches a pane. That is a different hole — persistence,
not a focus race — and closing it means giving a leaf an identity that
survives a restart. `SessionTab` has none, and an ordinal into the tree
is not one either, because `session_to_pane` drops leaves that cannot be
respawned and shifts every index after them. The report's third
suggestion, preferring a most-recently-active leaf recorded elsewhere,
was checked and dropped: `last_used` exists on `Tab` for the switcher's
MRU column and there is no per-leaf equivalent anywhere. No timestamp
was invented to create one, and with focus-in authoritative there is
nothing left for it to break a tie about.
Claude-Session: https://claude.ai/code/session_01UUyWQXzcBAoBzaSX8pc7nU
The box opens holding the tab's label as rendered, so it is never empty,
and `commit_rename` fires on `Blur` as readily as on Enter. Opening the
box and clicking away therefore stored that label as the tab's name — one
the user never typed, and one that means something different from the
title it was copied from: a name stops following the pane, so the tab
froze on whatever it happened to say at that moment.
Read the box against what it was seeded with. `rename_outcome` answers in
three states rather than two, so the one that has to keep working keeps
working: an emptied box still clears the name, which is the only way to
give a tab back to its pane.
`status_dot` places itself with negative offsets so that it overhangs the
avatar's edge — that overhang is what makes it read as a badge sitting on
the disc rather than a notch bitten out of it. But it is added as a child
of the element carrying `rounded_full`, so everything past the circle is
clipped along the arc and the badge comes back as a crescent.
Give the wrapper the positioning and move the disc down a level. The
radius then only ever clips the disc's own fill and mark, and the dot
becomes a sibling drawn after it, which settles the paint order too.
No test: the change is structural — which element carries the radius —
and the tests here cover the pure decisions behind the avatar
(`needs_edge`, `agent_status_label`) rather than its element tree.
Outside a repo a sidebar row grew a second line for its working
directory, so a group of plain shells was a column of two-line rows
describing paths that mostly agree. The path now rides on the title's
own line: it takes what it needs up to half the line and the title
keeps the rest, the same split a group header makes with the branch
beside its heading. A row keeps its second line only for a branch.
Claude-Session: https://claude.ai/code/session_01VuYUPiDEhQX6aQ4WQZbEGn
A group whose rows share a branch already prints it once on the header
instead of once per row, but the rule asked for at least two rows. A
one-tab group was left with a bare heading over a two-line row, a shape
no other group in the column has, and the branch down there described
the same repo the heading above it already named.
Claude-Session: https://claude.ai/code/session_01E4EPKzHg1fm9HMmHkUYpER
F1 through F12 produced no bytes at all. `functional_key` in
`src/terminal/input.rs` is the one place a named key becomes an escape
sequence, and it knew the cursor and editing keys and nothing else; the
`key_char` fallback below it cannot help, because a function key arrives
with no character, and `text_key_code` gives up the moment a key name is
longer than one character. Both encoders funnel through that function,
so the kitty path was equally blank. This was never a Windows or a
PowerShell problem — it was every platform, every shell — but it shows
up first on Windows because PSReadLine keeps CharacterSearch on F3,
HistorySearch on F8 and ClearHistory on Alt+F7, so the missing keys are
part of the shell's ordinary editing surface rather than something
exotic.
The table is `xterm-256color`'s, since that is what we advertise in
`$TERM`: `kf1`..`kf4` are the SS3 letters, `kf5`..`kf12` the numbered
tilde forms whose numbering starts at 15 and skips both 16 and 22.
Modified, they take the same `CSI 1;<mods>` and `CSI <n>;<mods>` shapes
the cursor keys already use, which reproduces `kf13` onwards exactly —
`kf13` is Shift+F1, `kf25` is Ctrl+F1. DECCKM is deliberately not
consulted: unlike `kcuu1`, `kf1` is SS3 under both `smkx` and `rmkx`.
It stops at F12 on purpose. In the entry we advertise there is no
capability left for a thirteenth key — `kf13` and up are already the
modified forms of F1..F8 — so sending the VT220 `\E[25~` for a physical
F13 would hand ncurses a sequence its own table reads back as Shift+F1.
Silence is the more honest answer, and F13+ keys are close to
nonexistent on the keyboards this bug was filed from.
Encoding alone would not have reached the shell, because two things
upstream were eating the keys first. The inline prompt editor had no arm
for a named key it does not bind, so F8 fell out of the bottom of
`handle_editor_key` and died on a `cx.notify()`; it now takes the same
route an unknown Ctrl chord takes, handing the line to the shell before
sending the key, which is what PSReadLine's history and character
searches need in order to act on it. And Find Next / Previous sit on F3
and Shift+F3 off macOS, where gpui matches the binding before the pane's
key handler ever runs; with no find bar open there is no match to step
to, so those listeners now give the keystroke back the way `EditorSave`
gives back Ctrl+S.
F11 keeps fullscreen. It is the chord Windows Terminal, GNOME Terminal
and konsole all use, no shell binds it, the modified forms still reach
the PTY, and it is one line of config away from being retired — but it
is now a stated choice with a test standing on it rather than an
accident. The reporter's second clause is explained rather than fixed:
`prompt_editor` is a real gate, and turning it off could not help while
the bytes did not exist, which is exactly what they saw.
Claude-Session: https://claude.ai/code/session_01UUyWQXzcBAoBzaSX8pc7nU