1298 Commits
Author SHA1 Message Date
l0ng-ai ffce219ffa Merge pull request #895 from doitian/fix/894-codex-newlines-conpty
fix(terminal): preserve Codex newline chords through ConPTY
nightly
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 7c0e339be4 docs(keymap): terminal shortcuts keep their chord; upgrading configs (#868)
A chord added in config wins a tie only against another app-wide action.
Copy, paste, find, clear scrollback and insert newline are bound in the
Terminal context, which is deeper, so they keep their chord while a
terminal is focused. Also tells people upgrading that a shortcut recorded
in Settings up to 26.9.2 was saved as a string. It now adds instead of
replacing, and a list brings back the old meaning.

Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
2026-09-15 23:01:51 +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 93546cfb83 Merge pull request #884 from l0ng-ai/fix/874-bell-flicker
fix(terminal): hold one steady bell flash through a burst of bells (#874)
2026-09-15 23:00:43 +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 faa992c78c Merge pull request #878 from netcatty/feat/pi-ui-prompt-events
Add Pi UI-prompt events so the pane reports waiting-for-user
2026-09-14 18:55:42 +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 85b4450756 fix(agents): keep Crush's PreToolUse from opening a turn it can never close
Crush fires only PreToolUse, and it mapped to prompt-submit. With no Stop
behind it the pane stayed on Working until Crush exited: every close asked
whether to cut Crush's work short, the tray and dot stayed on working, and
fork warned mid-turn. Map it to tool-complete, which still records
session_id/cwd for resume and bumps activity without moving the status.
2026-09-14 18:31:59 +08:00
l0ng-ai 2de26bbf35 fix(pi): restore the pre-prompt status when a UI prompt closes
ui_prompt_end sent prompt-submit unconditionally, but Pi also opens
prompts while idle (/model, a command's select). Closing one left a
finished or fresh pane reading "working" with no agent_end to clear it.
Remember the last turn event and re-emit that instead.
2026-09-14 18:31:45 +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
l0ng-ai d9c08d2986 Merge pull request #879 from hhdebb/fix/reread-the-font-fallback-chain
fix(terminal): reread the font fallback chain instead of cloning it
2026-09-14 18:27:04 +08:00
Fabrice Aneche 3fefb647e1 test(agents): isolate Crush hook test from XDG_CONFIG_HOME 2026-09-13 20:59:42 -04:00
Silas SuandCommandCodeBot 0499890f76 fix(macos): answer the Dock's reopen so a retired tty7 can come back
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>
2026-09-14 00:18:07 +08:00
hhdebb fdda34ab1c fix(terminal): reread the font fallback chain instead of cloning it
`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.
2026-09-13 17:34:31 +08:00
netcatty a8da265087 Add Pi UI-prompt events so the pane reports waiting-for-user
The shared Pi / Oh My Pi bridge only subscribes to four lifecycle events, so
a pane running Pi never shows "waiting for you" while a dialog is open —
`ask_user_question`, permission gates and any other extension prompt all run
under the hood with the status dot still reading "working".

tty7 already has the vocabulary for this (`question-asked`,
`permission-request`), and `AgentEventKind::QuestionAsked` /
`PermissionRequest` already map to `AgentStatus::Waiting` in
`cli_agent.rs`. The Pi extension seam to feed them is
`pi.on("ui_prompt_start")`, which fires around every blocking user-facing
prompt with `event.kind` telling select / confirm / input / editor / custom
apart.

Two handlers, each guarded on its own so an Oh My Pi fork that does not
expose the hook loses only that event rather than the whole bridge:

- `ui_prompt_start` → `permission-request` for `kind === "confirm"` (a
  permission or destructive-action gate), `question-asked` otherwise.
- `ui_prompt_end` → `prompt-submit` so the status returns to working once the
  dialog closes. Without it the pane would stay on "waiting" until the next
  stop, which is wrong for the model's continued work after an answer.

Deliberately not included: `tool-complete`. The Pi bridge emits with
`spawnSync`, so one event per tool call would block the extension host for
the duration of a process spawn on every read/grep/edit.

The test that asserts the bridge's subscriptions now covers both new event
names.

Verified: the `format!` template still compiles and renders both new
handlers, and the bridge contains every string the test asserts.
2026-09-13 15:47:07 +08:00
Austin Spraggins 1dd0a0a083 fix(terminal): stop injecting Ctrl-U after session exit 2026-09-12 14:04:00 -07:00
l0ng-ai 52b2742977 Give the SSH host editor the credentials it was missing
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
2026-09-12 12:06:44 +08:00
Fabrice Aneche c2d2db2cfc added support for Crush agent 2026-09-11 21:56:22 -04:00
l0ng-ai 66c42e48b7 Merge pull request #873 from WhiteDG/feat/qodercli
feat(agents): add Qoder CLI integration
2026-09-11 15:00:07 +08:00
l0ng-ai 62cd72922a Merge pull request #865 from hhdebb/fix/strip-the-agent-status-mark
fix(tabs): take the agent's status mark off the title it writes (#847)
2026-09-11 14:43:40 +08:00
l0ng-ai d050b63151 Merge pull request #872 from hhdebb/fix/report-the-terminal-to-assistive-clients
fix(terminal): report the terminal as the focused element
2026-09-11 14:43:36 +08:00
l0ng-ai 63b3951ef3 fix(agents): detect Qoder through the binary its docs tell you to run
The npm package installs two binaries. `qoder` is a dispatcher that routes
to the CLI for a bare invocation, a flag, or a prompt, and only hands off to
the IDE for `ide`/`chat`/`serve-web`/`tunnel` or a path that exists — and it
is the one the documentation tells people to run. Both are
`#!/usr/bin/env node` scripts, so what the pty carries is node plus the path
to the shim; the dispatcher's child, where `qodercli` appears on the path,
is not the process group leader and is never read. Detecting `qodercli`
alone missed every session started the documented way.

An IDE launch now wears the CLI's avatar for as long as the launcher takes
to exit, which is the cost of covering the common case.

Also: `--session-id` restores a session rather than naming a new one, so say
that where the flag is stripped, and assert Qoder has no `Notification` seat
instead of putting a payload through a hook map that has none.

Claude-Session: https://claude.ai/code/session_01LAqfzqELnoDWU56LBXS1Nh
2026-09-11 14:38:40 +08:00
hhdebb 2f63a01f7b fix(terminal): report the terminal as the focused element
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.
2026-09-11 11:09:36 +08:00
White 9e338b1d60 feat(agents): add Qoder CLI integration
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.
2026-09-11 10:30:19 +08:00
hhdebb 56238bf3bb fix(tabs): take the agent's status mark off the title it writes (#847)
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.
2026-09-11 01:10:21 +08:00
hhdebb d1f224f634 fix(window): hide the title bar in fullscreen where it is the app's to draw
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.
2026-09-10 20:40:26 +08:00
l0ng-ai a60d992c99 Merge pull request #862 from l0ng-ai/feat/remote-latency-row
feat(ui): say how far away a remote pane's shell is
2026-09-10 18:31:45 +08:00