mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
c0366cd143fd316d8502f8964ce161e1db447ac7
87
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c0366cd143 |
fix(cli): name the limit of the empty-segment fix, and settle the captures the e2e compares (#841)
CI found both new end-to-end cases red on Linux and macOS while green on Windows. One was a bad assumption in the test; the other was the test telling me the fix is narrower than the first commit claimed. The real finding is the resize case. On Unix a resize raises SIGWINCH and the shell repaints its prompt, so the segment the resize opens is *not* empty — it holds the repaint. Dropping byte-less segments therefore leaves the newest non-empty segment being a bare prompt, with the pane's output still stranded in the segment sealed behind it: on CI the default form came back as 174 bytes of prompt escapes and no marker. On Windows nothing answers the resize, the segment stays empty, and the fix reaches past it to the output — which is why the assertion passed there. It was asserting an accident of the platform. So the fix stands but is smaller than "a resize no longer costs you the pane's output": it makes the zero-byte answer impossible, and that is all. It cannot do more. Nothing in the byte stream distinguishes a prompt repaint from output the pane meant, so no client-side rule can tell which side of the boundary the answer is on. The boundary is the flaw — the default form's unit is the last resize, an event in the window rather than in the pane — and moving it means redefining what the default returns (the last screenful of the ring, say), which would shrink what every caller with a never-resized pane gets today. Left alone, and said plainly instead: in `what_was_asked_for`'s doc comment, and in a warning in the CLI reference telling anyone reading a pane under the GUI to ask for `--scrollback`. The test now asserts what the fix actually guarantees on every platform — the default form answers with bytes rather than with the resize's placeholder, and it is the end of what `--scrollback` returns, which is what would catch a fix reaching for the wrong segment. The marker is pinned against `--scrollback`, the form that promises to hold it. The `--tail` failure was a race in the test, not in `--tail`: the whole and the tail were separate calls and the pane advanced between them, so the tail carried a prompt line the whole capture had not caught up to. Both cases now read the whole answer on either side of the others and require the two readings to match before comparing anything, which is what makes the comparison a statement about the code rather than about the moment. Both also read `--scrollback` now, so neither depends on where a segment boundary happens to fall, and neither pins exact pane content — the macOS runner prints a zsh banner into the pane. Claude-Session: https://claude.ai/code/session_01UUyWQXzcBAoBzaSX8pc7nU |
||
|
|
5ac26b5dd9 |
feat(cli): add capture --tail N (#841)
Every "how did the last command end?" query went through `| tail -n 5`, which ships the whole grid down a pipe to throw most of it away — and on Windows, where `capture` is just as useful, there is no `tail(1)` to pipe to. `--tail N` keeps the last N lines of the answer instead. It is a third independent choice beside `--scrollback` (how much of the ring) and `--plain` (in what form), and it composes with both. The trim runs last, after `--plain` has decided what a line is: a shell line the pane wrapped over three rows is one line to the grid and three to a byte counter, so `--plain --tail 1` hands back the whole of the last line rather than its final row. The raw form counts the same way `tail` does, splitting on the newline and leaving the CR of a CRLF attached to the line it ended, so the bytes stay the pane's own. A trailing newline terminates the last line rather than opening an empty one, which is the difference between `--tail 1` answering the last line and answering nothing. `N` must be at least 1: a tail of zero lines would print an empty result that reads exactly like the blank-pane ambiguity this issue is about, so it is a usage error (exit 2) instead. `--json` reports the tail in `text` but leaves `bytes` the size of the whole replay, so the pair still says "this was trimmed" rather than "this came back short". Left alone deliberately: the daemon still replays the entire ring on every observe, so the saving here is the pipe, not the wire. Bounding what crosses the wire means teaching `ClientMsg::Observe` a limit and versioning the protocol for it, which is a much larger change than the papercut warrants — and the default (newest-segment) form has always received the whole ring and discarded most of it, so this adds no new cost. The docs say so rather than implying otherwise. Claude-Session: https://claude.ai/code/session_01UUyWQXzcBAoBzaSX8pc7nU |
||
|
|
0752110b54 |
fix(cli): stop a resize from emptying capture, and say when a replay renders blank (#841)
`tty7 capture %n` answered a live pane with zero bytes and exit 0, which is byte-identical to a pane that had genuinely never printed. The report guessed at the replay grid. It is not the grid — it is the segment the grid was handed. The daemon's replay ring splits on resize: `ReplayRing::resize` seals the segment holding the output and pushes an empty one at the new geometry, and `ReplayRing::replay` sends every segment it holds, empty tail included. `RealBackend::capture` then kept "the newest segment" for the default (non-`--scrollback`) form, and for any pane resized since it last printed, the newest segment is that empty placeholder. A pane restored from disk lands in the same state, because seeding the ring ends in a resize too. Measured end to end against a real daemon before the fix: a pane holding 954 bytes of scrollback answered `capture` and `capture --plain` with 0 bytes and exit 0 while `capture --scrollback` returned all 954. The fix drops byte-less segments before choosing the newest one — they carry nothing in either form, so `--scrollback` drops them too and both forms describe the same bytes. The daemon still sends them: its trailing `Size` is how an attaching client learns the pane's current geometry, and that is not the CLI's to take away. What this does not explain is the reporter's other half — that `--scrollback` came back empty in the same episode. Filtering empty segments cannot cause that, and neither could I reproduce it: 60 rounds of `capture` against a pane spewing 4000 escape-laden lines produced no empty result, and an 8 MiB ring (the cap) replayed whole in ~400 ms, well inside the 300 ms per-frame settle window that was the other suspect. So the silent-empty class is left distinguishable rather than declared closed: `--json` now carries `bytes`, the size of the replay counted before anything renders or trims it, and the one case where an empty answer is not an empty replay — bytes in, no text out — says so in a line on stderr. Zero bytes is a pane that printed nothing; bytes with no text is a screen whose content did not survive the grid. Deliberately no retry loop and no warning on a condition that has not been established. Claude-Session: https://claude.ai/code/session_01UUyWQXzcBAoBzaSX8pc7nU |
||
|
|
03c3081a6e |
feat(ports): detect and forward what a remote pane is serving
A remote workspace's ports were never listed. The pane lives in the peer's registry and QueryProcs asks this machine's daemon, which has never heard of it, so the answer was an empty list — indistinguishable on screen from a pane serving nothing. Add a control request so the peer answers instead, gated on a feature so an older server says "I cannot tell you" rather than "nothing is listening". With the ports visible, the forward becomes something the user should not have to think about: a port opens on a click, and a new one is forwarded unasked, at the same number where that number is free here. The watch runs with the panel shut, which is when a port appearing is most worth saying something about. Ports and Forwards were two sections that never mentioned each other; a row is now a port, and the forward is where that row says it comes out. Adding one by hand asks for one number instead of five fields, with the rest of the ssh -L grammar one disclosure away. Claude-Session: https://claude.ai/code/session_01TPXrptp2rCGKjXaz4xE3Lq |
||
|
|
fc94022ed0 |
feat(agent): add TraeCode CLI support (#807)
* feat(agent): add TraeCode CLI support * fix(settings): index TraeCode agent hooks |
||
|
|
92c1ae9f26 |
feat(ui): make opening a new window a bindable action (#793)
Refs #710; does not close it (the request was a jump list opening a chosen existing workspace). Registers NewWindow globally as well as on the render root, since with the tray icon on (the default) closing the last window retires to the tray and leaves no window to dispatch it. |
||
|
|
8c1315b7ab |
feat(ssh): edit the host from the tab that is connected to it (#801)
Refs #438. |
||
|
|
42424c7737 |
feat(ui): add a bindable Close Window action (#773) (#778)
26.9.0's tray-retire model made closing the last window the "keep the daemon, drop the UI process weight" gesture, but that path was reachable only from the OS red close button: no action, no palette entry, nothing to bind. `⌘W` closes a pane or tab, `⌘H` hides the window but keeps it in memory, and `⌘Q` stops the server. There was nothing that closed the window and left the shells running. `CloseWindow` is that action, with no default key — the slot is left free. The logic that decided what a window close means (detach the workspace, and on the last window retire to the tray if an icon is actually up, otherwise quit) moves out of `on_window_should_close` into `prepare_window_close`, so the button and the action share one decision instead of two that can drift. Notably not routed through `close_window_for`: that one recycles the last window onto a fresh workspace, which is what deleting a workspace wants and not what closing a window wants. It reaches the command palette, the Keybindings UI, and the reference table of actions with no default key. In the palette it sits beside Quit, because that pair is the whole point of the action: both end the window in front of you and only one takes your shells with it. Their subtitles now say which — including Quit's, which had been promising "shells keep running" while calling `daemon::spawn::stop()`. Co-authored-by: bytehello <bytehello@users.noreply.github.com> https://claude.ai/code/session_01LKMZVh6mUBxXAn6v7P6JC6 |
||
|
|
d8a0245e75 | fix(terminal): name every ligature feature off, not just calt (#788) | ||
|
|
e231b16fb3 |
feat(ssh): allow remote image clipboard writes (#766)
* feat(ssh): allow remote image clipboard writes * fix(ssh): keep a profile's clipboard grant across a re-attach A native ssh pane's OSC 5522 permission is decided by the spec that dialled the host, and the daemon is the only side that holds it. A window reopening onto a pane that outlived it attaches by pane id, has no spec to read, and sends `allow_remote_clipboard_write: false` — which the daemon took as the new answer and the pane's own view took as a refusal. Both sides then said no, so the first restart after switching the permission on turned every copy into an `EPERM` with the switch still reading "on". Pin the spec's answer in the pane and route both attach and detach through one decision point, so a pane that carries a spec keeps that spec's answer whatever an attaching client claims, and a pane without one — everything on a remote `tty7-server` — is exactly as permitted as its controller says. On the client side, refuse only what the pane can see is forbidden and leave the verdict to the daemon otherwise. Also: release a failed transfer's buffered bytes instead of parking up to `MAX_CLIPBOARD_BYTES` per pane until the next request, and answer the capability probe with the permission actually in force rather than a constant that always reads as "off". --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
0b6c332618 |
feat(settings): add interface font family configuration in appearance typography (#761)
* feat(settings): add interface font family configuration in appearance typography * fix(settings): let the interface font go back to the system face Three things the new **Interface font family** row spelled once and needed twice. `apply_theme` only wrote `Theme.font_family` when the setting was `Some`, and `Theme::change` never puts it back — it rewrites the field only when a theme config names a face, and none of ours does. So picking a font worked, and picking **Default** back saved `None`, redrew every window in the font the user had just cleared, and only came true at the next launch: a setting that looked like it had applied instantly and had not. The face is now assigned in both directions, against the stock value read once before anything overrode it. The dropdown's first row borrowed the bold/italic label, "Default (match primary)" — which promises the *terminal's* primary family. The interface falls back to the system UI font instead, so the row said the chrome would come out in Hack while the description beside it said the opposite. It gets its own label in all three locales. `ui_font_family` was also the one key in `config.json` that disappeared when unset; every other optional key is written as `null`. Dropped the `skip_serializing_if` so the file still lists it, and documented the key in the two tables that enumerate the typography settings. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
024d368925 |
docs(skill): restructure the agent skill around a delegation playbook (#702)
* docs(skill): restructure around a delegation playbook SKILL.md becomes a slim routing layer: a what-are-you-here-to-do section up front, the pane/run/wait primitives, and four delegation rules that survive even when the reference is skipped. Everything specific to running another agent moves to references/delegation.md, which adds what the old text never had: per-worker git worktree isolation, a delivery contract collected through git instead of screen scraping, a launch-verification checklist, a babysit loop, and a fan-out harvest with short per-worker timeouts so one stuck worker cannot stall the round. Also replaces the last remaining 'claude -p' example (the fan-out one #699 missed) and keeps every snippet valid under both bash 3.2 and zsh. * docs(skill): un-deadlock the fan-out harvest loop Fresh read of SKILL.md and references/delegation.md. Every internal anchor resolves and the two files agree on the primitives; three things did not hold up: - The harvest loop passed `--changed`, which cannot work there. `wait` compares against the state standing when *that* wait began, so a worker that reached `done` while you were waiting on a different one is already in `done` when its own turn in the round comes up — refused, every round, forever. Each pane runs one turn, so a standing `done` is this turn's; drop the flag and note the one thing it was buying (a just-answered `waiting` worker needs to leave that state before it is requeued). - The same loop folded `wait`'s exit 1 into its 124 branch, so a pane that died got requeued instead of reported — and requeued at full speed, since a dead pane answers immediately. Split the three codes. - SKILL.md described `--plain` unwrapping "a line the shell wrapped at column 249" while two other passages state a pane is 120 columns. Say "at the pane's width", as references/commands.md already does. No typos or grammar slips found. Every bash block in both files parses under bash 3.2 and zsh. * docs(skill): two failure modes from the playbook's first live run Dogfooded the delegation playbook end to end (worker reviewing this very file). Two failures it hit that the text did not cover: - A turn aborted by an API error emits no turn boundary, so the status stands at 'working' forever and wait sleeps through it. Diagnose from the screen's error line; recover by telling the still-alive interactive session to continue. - A short capture tail cuts off the spinner line and shows only the TUI's always-present input box, which reads as idle. Tail 15+ lines and read for the spinner; 'bottom looks like a prompt' is only evidence on a shell pane. |
||
|
|
2cdc26f357 |
Wire hooks, resume and detection for Kimi Code CLI (#694)
* feat(agents): wire hooks, resume and detection for Kimi Code Kimi Code CLI takes its hooks as [[hooks]] entries in the same config.toml that holds the user's providers and models, so this adds a third install strategy — a format-preserving TOML merge on toml_edit — beside the JSON map merge and the owned files. Like Qwen it reports permission requests first-class, so it gets no Notification hook. Resume rides `kimi --session <id>`; fork stays unwired, Kimi documents none. Closes #693 Signed-off-by: Austin Spraggins <spragginsdesigns@gmail.com> * fix(agents): harden the Kimi Code TOML hook merge and its resume flags The TOML merge strategy the Kimi wiring introduces round-trips a shared config.toml cleanly, but three gaps sat behind it. `hooks_state` counted only the marked entries that still named an event, so a hand-edit that dropped the key off one of nine entries left the remaining eight matching the roster exactly and the file reported Installed with a broken entry in it. Every marked entry now counts, which is what the JSON merge already did and what `refresh_hooks` needs to see. A `hooks = []` spelled as an empty inline array made install fail outright -- toml_edit keeps an empty array and an array of tables apart, but the two say the same thing and neither carries any configuration. It is now promoted rather than refused. Every other wrong-shaped `hooks` key -- a string, a table, a non-empty inline array -- still refuses with the file left byte-for-byte alone. `Stop` is not the only way a Kimi turn ends: its own event reference says `Stop` does not fire on interrupts and `Interrupt` fires instead, and a turn that dies on an error reports `StopFailure`. Without those two an Esc or a failed turn left the pane on "working" for good and `tty7 wait` could only ever time out. Both are observation-only events and report the same end of turn `Stop` does. On resume, `--agent` and `--agent-file` join the stale flags: Kimi rejects either next to `--session` at startup, and resuming rebinds the session agent by itself, so replaying them turned a working resume into a launch error. Tests cover the wrong-shaped `hooks` keys, a config.toml that does not parse on both install and uninstall, a file that does not exist yet, a second install being byte-for-byte the first, mangled and surplus marked entries, an uninstall threading between the user's own entries and the tables after them, and the `--session=<id>`, bare `--session`, `--continue` and `--agent` spellings on the resume path. --------- Signed-off-by: Austin Spraggins <spragginsdesigns@gmail.com> Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
958d8b7442 |
feat(window): dock the code panel and the diff overlay beside the terminal (#625) (#685)
* feat(window): dock the code panel and the diff overlay beside the terminal (#625) Opening a file covered the workspace. The terminal underneath kept running and was neither visible nor typeable, so reading a file while an agent talked was a toggle loop: open it, close it to read the reply, open it again. The Files tree already docks; the two surfaces you go to *from* it did not. They dock now, as a flex sibling of the terminal column rather than a narrower overlay — that distinction is the feature. `set_grid_size` is driven by the terminal element's laid-out bounds, so a column takes width away from the grid and the PTY reflows into what is left; a card painted over half the workspace would have left the grid full width with half of it hidden. `overlay_top` stops ordering a pair and starts choosing between them: a column has one child, and two `flex_1` siblings would split it and fight. Fill mode keeps the old vector, the old opaque paint and the old platform hoist untouched, so nothing about today's overlay changes for anyone who picks it. - Half the terminal column by default; drag the divider, double-click it to cycle a third / half / two thirds, or use the palette commands. Two thirds deliberately runs past the half-window cap the side panels obey — only the terminal's floor binds it. - `DOCUMENT_MIN_W` joins the width budget: both side panels reserve it the way they already reserve each other, and the column is derived from the *live* sidebar and panel widths rather than their floors, so a panel someone dragged wider is width the terminal keeps. - A window too narrow to seat both fills for that frame. The fallback is derived at render time and never stored, so widening re-docks on the next frame with nothing to undo. - Fill or dock is per tab, on the header's context menu. Reading a long file over the whole window in one tab while an agent keeps half of another is the normal case, and one global switch made each of those flip the other. A tab that has not been told reads `document_layout` from the config, which is what a fresh tab starts as — and which the menu therefore does not write, since every untold tab is reading it. - Everywhere but macOS the title bar spans the workspace, which left a bar's height of nothing above the column. The header is drawn into it, and behaves like the title bar it now sits in. With the detail panel closed the column reaches the window's right edge, so the header stops short of the trailing chrome through a width the tab strip's own reservation shares. - The docked headers drop the traffic-light inset they never had to clear, and the diff header's branch name becomes the thing that yields so the view toggle and the close tile survive a column's width. New in `config.json`: `document_ratio`, and `document_layout` for what a fresh tab starts as. Four new actions, bindable and unbound by default. * fix(window): hold the docked column to widths the strip and the file agree on Three defects in the document column, each with a guard test that fails without its fix. The tab strip did not know a column had taken width off it. On macOS the strip lives inside the terminal column and sizes itself to the window less the detail panel, so a docked document left it 340 points wider than the column it sits in and the chips ran on under the column — the same overrun the panel's own reservation was added for. Everywhere else the strip spans the workspace and the column's hoisted header is drawn over its trailing end with no fill of its own, so a chip left under it showed through the file name and stayed clickable through it. The column's width now comes off `strip_w` on macOS and off `corner_w` elsewhere, which is where the panel's already goes. The divider wrote widths the file would not keep. `Config::sanitize` holds `document_ratio` to 0.2..=0.8; the drag clamped in pixels only, so a column pushed against either edge of a wide window was saved outside that band and reopened somewhere else — on a 2560-point body, 232 points from where it was dropped. The band is a pair of shared constants now and the drag clamps to it, the way the font size and its stepper were made to agree in #550. The palette named the config's layout rather than the tab's. Fill is per tab, so a tab told to fill was still offered "Document: Fill Window" — a row that named the state it was already in and did the opposite. It reads the active tab through `ChromeState` now. Also: `document_layout`'s doc comment still described the global switch an earlier draft had, three lines after the field became a per-tab default. |
||
|
|
7bcb91d8af |
fix(input): give the PTY back the Ctrl chords tty7 was eating (#684)
* fix(input): give the PTY back the Ctrl chords tty7 was eating Follow-up to #682, which handed Ctrl+V to a full-screen program but left three neighbouring holes of the same shape: a key the terminal answers without the keymap ever seeing it. The C0 table was half a table. `input.rs` mapped the alphabet, `[ \ ]` and Ctrl+2, and nothing else — so `Ctrl-^` (Ctrl+6, vim's alternate file), `Ctrl-_` (readline's undo, typed as Ctrl+/ or Ctrl+Shift+-) and Ctrl+3..8 produced no bytes at all. They were not mis-encoded, they were silent: gpui filters control characters out of `key_char` on all three backends, so the text fallback had nothing to offer either. The table is now the VT-220 one, each digit beside the punctuation that shares its key, because every platform hands Ctrl+Shift+6 over as `^` with the Shift already spent. Ctrl+/ is xterm's addition rather than VT-220's and is spelled out with the reason. The twenty-six letters fold to `& 0x1f`. `on_key_down` swallowed plain Ctrl+1..9 off macOS with a bare `return`, left over from when tabs lived on ctrl-digits — they have been on Alt+1..9 for a long time, so nothing claimed those chords and the block only deleted keys. It also sat before `keystroke_to_bytes`, so not even the kitty protocol got through it. Gone. Ctrl+V is now a binding. `AlternatePaste` carries `ctrl-v` off macOS in a `Terminal && !alt_screen` context, and the pane declares `alt_screen` whenever a full-screen program owns the grid, so the behaviour #682 settled on is unchanged — paste at a prompt, SYN inside vim — while the keymap can finally express it, the Keybindings page lists it, and the user gets a say: `"AlternatePaste": ""` hands Ctrl+V to the shell everywhere, including readline's `quoted-insert`, and `"PasteText": "ctrl-v"` pastes on every screen the way Windows Terminal does. That cohort is real — Warp keeps Ctrl+V pasting on Windows on purpose, as a removable binding, for exactly this reason. The hardcoded arm in `handle_cmd_shortcut` now answers Cmd+V alone, which is macOS's only paste chord and carries no control code to lose. Last, the rule about control codes is one function instead of an assertion buried in a test. `steals_a_control_code` plus a commented `control_code_binding_allowed` back both the defaults test and a new runtime warning, so a hand-edited config.json that takes EOF away from every shell says so in the log. It warns rather than refuses: a chord the user asked for by name is theirs to spend, the way the tmux preset spends Ctrl+B. The invariant that still fails a build is that no *default* spends one silently. Tests: `cargo test --bin tty7-app` 1360 passed, 1 known flake (`a_routed_auth_prompt_carries_the_machine_that_raised_it`, green on a rerun and on a clean tree). New: the whole VT-220 table asserted byte by byte, with Ctrl+- held out; `ctrl_6_reaches_the_pty_as_rs`, `ctrl_v_pastes_at_a_prompt` and `ctrl_v_reaches_a_full_screen_program_as_syn` drive the real keymap through `simulate_keystrokes` rather than calling into the view; the keymap tests cover both escape hatches and the context that withholds the binding. #682's two `handle_cmd_shortcut` tests are replaced by those three, which assert the same behaviour at the layer that now decides it; its end-to-end SYN test stands unchanged. The gpui tests are unix-only, so CI is what runs them. * fix(input): ask the grid, not the last frame, before Ctrl+V pastes `AlternatePaste` carries `Terminal && !alt_screen`, but gpui matches a keystroke against the frame it last painted, so the context outlives the switch: a full-screen program that took the screen after that paint is still "at a prompt" as far as the keymap is concerned, and the clipboard lands in it. In vim's normal mode that runs as commands. The action now re-reads the terminal mode and propagates instead, which hands the chord to `on_key_down` and encodes it as the SYN the program is waiting for. Also: - the two escape-hatch assertions in `paste_ships_both_terminal_chords_off_macos_and_retires_together` built a one-entry binding table instead of the default one, so both passed without the hatch working — an emptied `AlternatePaste` cannot dispatch anything when it is the only entry in the table. They now apply the config line on top of the whole default table, and the `PasteText: ctrl-v` case checks both screens; - the keyboard-shortcuts page claimed every other Ctrl chord reaches the program, which Ctrl+Tab and the Windows/Linux font-size chords do not; - `steals_a_control_code` documents `@` and the backtick, which are in the set it walks but were not in the list beside it. |
||
|
|
3c95995e82 |
fix(input): hand Ctrl+V to a full-screen program on the alternate screen (#677) (#682)
In vim or neovim on Windows and Linux, Ctrl+V pasted the clipboard where the editor expected blockwise Visual mode. Windows Terminal (with its ctrl+v binding removed), WezTerm and Alacritty all send the key; macOS was never affected, since Cmd+V is the paste chord there. Ctrl+V was not a keybinding at all. `on_key_down` hands plain Ctrl+C, V and X to `handle_cmd_shortcut` off macOS, and of the three the "v" arm was the only unconditional one: Ctrl+C copies with a selection and otherwise falls through to SIGINT, Ctrl+X falls through outside the editor, but Ctrl+V always consumed, so SYN never reached the PTY -- `input.rs` had the byte, unreachably -- and an empty clipboard turned the key into nothing at all. #270 set the rule that off macOS ctrl-<letter> belongs to the terminal and anything sitting on one must fall through; Ctrl+V was the exception that had escaped it. The arm is now contextual like its neighbours. On the alternate screen it falls through, and `keystroke_to_bytes` sends 0x16, or the CSI u form when the program has the kitty protocol on; off it Ctrl+V pastes exactly as before, and Cmd+V on macOS is untouched. The alternate screen is the gate rather than `input_active` because the editor is inactive whenever shell integration is missing or the prompt editor is off, and gating on that would take paste away from every such user; a program that has switched screens is precisely the case reported. Inside such a program paste is Ctrl+Shift+V, Shift+Insert or the right-click menu, all of which still stage a clipboard image for an agent. The same block did not exclude Shift, so Ctrl+Shift+C/V/X reached the hardcoded path whenever the keymap had nothing on them -- exactly the state rebinding Paste leaves behind, which #271 promised would retire Ctrl+Shift+V, but it went on pasting behind the user's back. Only unshifted chords enter the block now; the shifted ones are the keymap's alone. The right-click menu advertised Ctrl+C, Ctrl+X and Ctrl+V off macOS as though they were the bindings, next to a Select All row that already showed its hint on macOS only. The three rows take the same treatment, which is also what the command palette does. Three view tests pin the split -- Ctrl+V falls through on the alternate screen while Cmd+V still pastes there, Ctrl+V pastes off it, and a key down on the alternate screen arrives at the PTY as SYN and nothing else -- and the keymap's paste test now asserts that no default claims ctrl-v in the Terminal context. The shortcuts reference notes where plain Ctrl+V pastes and where it is the program's. Fixes #677. |
||
|
|
ef333bf055 |
feat(terminal): make the wheel-zoom modifier configurable (#676)
Cmd-scroll zoomed the font with no way to move it or switch it off, so a thumb left on Cmd resized the terminal mid-scroll (#668). The modifier is now a setting: the platform modifier by default, or Ctrl, Alt, or none. Stored as the choice rather than the resolved key, so one config file still means the same thing on a Mac and on a Linux box. Settings -> Terminal -> Mouse carries the picker; off macOS Ctrl and the platform modifier are the same key, so it shows one cell for them. |
||
|
|
9c2869a25f |
Trim the app's long-winded copy, add four dark themes (#663)
* refactor(i18n): drop the About page shell primer and trim the long copy The About page carried a "How shells work" section explaining that shells live in a background server. Nothing linked to it and the Updates and Server sections below already say what happens to those shells, so it was a paragraph of prose the page did not need. Remove it, its search index entry, and its three L10nKeys. Then cut the padding out of 48 strings across settings rows, dialogs and notices. Two patterns accounted for most of it: the restart-server dialogs stated "your shells keep running" up to four times each in different words, and the config.json failure notices packed three subordinate clauses into every sentence. Nothing is dropped but repetition and clauses the reader can infer — every consequence a dialog asks the user to weigh is still spelled out. en, zh and ja stay in sync. * feat(themes): add Catppuccin Mocha, Gruvbox Dark, Nord and Tokyo Night Four more dark built-ins, taking the set from nine to thirteen. The docs table and description are updated to match. * fix(themes): give Catppuccin Mocha its rosewater caret, refresh a stale builtin count |
||
|
|
0295a98915 |
feat(sftp): open remote text files in the built-in editor
A click on a file in the SSH Files panel used to start a download; the only way to change a remote file was download, edit, re-upload. Now a click opens it in the built-in editor and Cmd-S saves straight back over the pane's own SFTP channel, matching what the Files panel already does locally and over a remote workspace. - protocol: SftpOp::ReadFile/WriteFile and SftpOpResult::File, bytes as base64; the reply carries the body plus the stat it was read under - daemon: ReadFile enforces the caller's size ceiling before and during the read; WriteFile rewrites in place (truncate, not temp-and-rename) so the file keeps its mode and ownership - SftpHost: a Host over the pane's SFTP route, so the editor's existing open/save path works unchanged; git/search/watch honestly Unsupported - editor: an open buffer holds the host it was read from, and save/reload/dedup/watch key on (host, path) instead of the active host - panel: single click opens (dirs navigate, text files edit), the same gesture as the local tree; binary or oversized files get the local tree's toast, and Download moves to the context menu Review follow-ups, in this PR: the SFTP host stays out of HostRegistry, which means "a machine this window has a link to" and is swept as such — filing the pane's channel there made Cmd-S return silently once a workspace deletion took it back out. The cursor-jump lookup, the status bar's path, and the SCM panel's repository all key on the buffer's own host now. Closes #656. |
||
|
|
9fc0f331e8 |
feat(tabs): drag a tab in as a pane, and a pane out as a tab (#651)
* feat(tabs): drag a tab in as a pane, and a pane out as a tab A tab dragged by its chip or its sidebar row can be dropped over the panes to become one of them, and a pane dragged by its grip can be dropped on the strip or the sidebar to become a tab of its own. Both carry the panes across as they are: nothing is spawned and nothing is killed, so a shell mid-command, an SSH session or an agent mid-turn keeps running. The landing is read the way a pane drag's already is, minus the middle: an arriving tab has nothing here to trade places with, so a pane's core means "split it the way it is longest". A tab that was itself split arrives with its own shape intact and takes one share of the row or column it joined. A pane on its way out is offered a caret between two tabs, and the last pane in a tab is offered nothing, being a tab of its own already. Picking a tab up no longer switches to it: the strip and the sidebar now activate on the click rather than on the press. Without that the merge cannot be expressed at all — pressing the tab to drag it would put it on screen, leaving no other tab to drop it into. Two things in the machine tree had to follow: * Panes that change tabs are told as PaneMove, one at a time, rather than as a tab closing and another being rebuilt around them. * The tabs the machine already has are reconciled before new ones are created, so a pane leaving for a tab of its own is given up by the old tab before the new one asks to register it. The machine refuses a pane that is in two tabs at once, and the refusal desynced the window. Closes #621 * test(tree-sync): a tab grafted above a whole layout still converges * fix(tabs): keep a click on the close button from switching tabs Switching on the release rather than the press means every click inside a chip or a sidebar row now reaches the row itself, and gpui-component's `Button` does not stop propagation on a click it handled. So one click on a tab's close button ran `close_tab(i)` and then `activate(i)` — with `i` by then naming whichever tab had slid into that slot, which moved the active tab somewhere nobody asked for. A click into the rename field did the same: it switched away from the tab whose name was being typed, and took the focus out of the field with it. Both now hold the click where they handled it, the way they already held the press. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
422808191d |
feat(sidebar): group a tab by its folder when its cwd is not a repo (#631)
`sidebar_grouping` gains a third, opt-in mode, `repo-or-directory`: group by repository home as before, and when the repo probe has landed and answered "not a repo", group under the cwd itself instead of filing every such tab under Scratch. A probe that has not run yet resolves to no decision, so a tab keeps the group it already has rather than bouncing through Scratch mid-probe. The decision lives in one `resolved_group` free function shared by the per-frame key derivation and spawn-time seeding. The default (`repo`) and flat modes behave exactly as before, and an unknown value in an existing config still degrades to `repo`. Knock-on: `machine_mirror::subject_path_of` names a window after its most common group, so in the new mode a window of plain shells takes its name from the most common directory rather than from the first pane's cwd. Closes #620. |
||
|
|
0346e35b40 |
fix(shell): stop injecting into a zsh or fish the user gave arguments to (#629)
The zsh and fish arms of `shell_integration::setup` never checked `has_custom_args`, so a shell the user launched with their own arguments was injected anyway — fish had `-C <script>` appended to its argv, zsh had its ZDOTDIR swapped. Both arms now sit behind the same gate bash, PowerShell and WSL already used, hoisted to a single early return ahead of the dispatch so a new ShellKind cannot silently reintroduce the bug.
Docs now describe what the code does: the `shell` row's own `{"program": "fish", "args": ["-l"]}` example loses integration under this rule, and the shell-integration note distinguishes user-written arguments from the ones detection supplies (Git Bash, WSL).
Part of #624; the native-input-mode half is separate.
|
||
|
|
72db26d15a |
feat(prompt): let the shell's own line editor own the prompt (#633)
Closes #624 tty7's inline editor takes the prompt the moment OSC 133 reports one, and until now the only way to keep it off was to hide the shell's own name from tty7 so integration never armed — which costs the prompt boundaries, cwd and exit codes as well. Someone who binds `history-beginning-search- backward-end` to Up in their zshrc had no way to reach it, and the local history the editor walks instead is per-view: a command run in one pane is not in another's list, so the shell's shared history looked broken too. The new `prompt_editor` switch (Settings -> Input -> Prompt, on by default) hands the line back. Off, every key at the prompt goes to the PTY, so ZLE / readline / fish do the editing and what the user bound behaves as written. Shell integration is untouched by it. The gate is one line in `input_inactive_reason`, which every path that could take the prompt from the shell already asks: keys, IME commits, paste, Tab, the completion and reverse-search menus, the input bar. That is what makes this a mode rather than a special case per key. `shell_owns_prompt` learns the flag too, and that half matters more than it looks: the gap hold and the typeahead record both exist to feed the local editor, and `flush_typeahead` sends ^U to erase the line before moving it there — on a line only ZLE is editing, that erases the user's work. Ctrl-R landing on the PTY also stops raising the missing-integration notice: the shell owning it is what was asked for. Turning it off mid-line hands what is typed to the shell the way an unknown chord does, so the text is still on the prompt to finish. Live panes follow the switch, including a hand edit of config.json in another window. Tab completion and history search are menus tty7 opens inside that editor, so the page greys them out and says why while it is off. Only their text dims — a switch already draws its thumb at 35% when disabled, and dimming the row on top of that leaves a pill with nothing visible in it. Their stored values are left alone and come back with the editor. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
1424da0891 |
fix: nine UX fixes across the diff overlay, layout, settings and pane spawn (#623)
Found by driving a dev instance and measuring what an idle window costs. Two of them were frame loops that never stopped. A `Head` diff overlay calls itself stale when the cached git status disagrees with the snapshot on screen, and every landed probe wakes that check by touching the cache — but the read published its counts and left the branch behind, so a branch switched outside tty7 made the disagreement permanent: two `git` processes a lap, forever, with `refreshing…` pinned to the header and an idle window at 7% of a core. And the home page asked for a frame sixty times a second to change one glyph's opacity twice, which made a window with nothing open in it eight times more expensive than one running a shell. Both now settle: the diff read publishes the branch it found, and the home cursor flips a bool on a timer the way the terminal's own does. The rest: - The tab sidebar and the right panel each capped themselves at half the window and knew nothing about the other, so together they could take all of it — 260 points of terminal on a 720-point window. Both now cap at whichever binds harder: half the window, or what is left after the terminal's floor and the other panel's floor. The same cap bounds the drag, so a panel dragged to its limit stays where it was dropped. - Only a HEAD diff may correct the sidebar's counts. Those numbers mean `git diff --numstat HEAD`; an unstaged or staged patch answers a smaller question, so opening an untracked file from the Source Control panel took the staged lines off the total on the click. - An untracked row in the diff overlay had no click target, and once focused could not be left — the breadcrumb looks the path up in `files`, where an untracked file has no entry. Both ends fixed. - A new pane keeps the name its directory was reached by. `cwd()` alone loses it: the shell falls back to `getcwd()`, so `/tmp/x` became `/private/tmp/x` in every tab opened from the first. `PWD` carries it, and POSIX has the shell discard a `PWD` that names the wrong directory, so this can correct the name and cannot invent one. - The settings search now sees into the Keybindings page, which is generated from the binding table rather than the static index — so searching for a feature finds its shortcut, and the page filters to the matches. Closes #444. - The settings reading column is centred rather than pinned to the nav: on a window as wide as the display it was made for, 640 points of settings sat beside 1600 points of nothing. - `New Workspace…` takes the ellipsis its three sibling actions already carry — it opens a form asking for a name and a host. Every fix has a test. The re-probe loop is pinned end-to-end with `render_probe::draws() == 0` against a real repository, confirmed to fail on the old behaviour before it was kept. |
||
|
|
a2d53a9597 |
fix: 19 项低危 UX 问题(#584–#602) (#615)
* fix(scm): say what "discard all" actually discards (#594) The group-level Discard prompt asked to "discard every change in this repository", but discard_all_ops has only ever swept unstaged edits and untracked files — staged changes survive, as the function's own comment notes. Users confirmed under one belief and the code kept another. Narrow the prompt to the operation's real footprint, in all three languages. * fix(scm): keep the amend toggle when its confirmation is cancelled (#595) scm_commit cleared scm.amend when Commit was pressed, before the "rewrite the last commit?" prompt. Answering Cancel returned to a panel whose amend mode had silently been dropped, so the next Commit created a brand-new commit — exactly what the user had just declined to risk. The toggle now clears where scm.committing arms, at dispatch in run_git_op, extending the rule the armed flag already followed: a cancelled confirmation leaves nothing behind. * fix(cli): answer a wait timeout in the success path's JSON shape (#589) The 124 branch returned {pane,status,timed_out} while a finished wait returns {pane,status,matched,stale,activity,message,session_id} — so the one branch a consumer writes error handling for was the one missing its fields. The timeout now carries the full shape plus timed_out, and reference.mdx documents the schema and the flag. * fix(cli): report a failed wait on stderr, even under -q (#590) wait's failures are structured exits (124, or 1 when the pane died first), so they never passed through the anyhow path whose eprintln is the only thing quiet mode cannot silence — contradicting the documented "errors still go to stderr". Both exits now print their headline to stderr, the discipline pane close already established. * docs(cli): describe owner as the workspace that may attach (#591) commands.md still claimed the CLI stamps a literal "tty7-cli" owner on the panes it spawns — the behaviour the orphan-workspaces work removed, because an owner names the workspace allowed to attach and a stranger's stamp got the panes respawned. Every spawn path now writes the workspace id, or nothing while the pane is still unfiled. Bring commands.md in line with reference.mdx, and note the absent case in both. * docs(cli): close five contract drifts between the tables and the code (#592) - The key tables listed pgup/pgdn as aliases but not pgdown, which the parser has always taken; both references name it now. - "Case-insensitive" was flat wrong for Alt: M-x keeps its case because Alt is a prefixed ESC, unlike Ctrl. Both references note the exception. - procs' ports JSON has carried addr since the field exists; both schemas show it. - TTY7_WS is tab ls's default too; both environment tables say so. - split --ratio's clamp to [0.05, 0.95] was discoverable only in code; both split sections document it. * fix(cli): doctor exits 1 when the server is unreachable (#592) doctor is the verb people run when something is not working, so an unreachable server is *the* finding — not a row to exit 0 over while `tty7 doctor || alert` never fires. The table and JSON still go out (the context rows are the other half of what doctor is for), and stderr carries the headline under -q. MockBackend grows an `unreachable` flag so the branch is testable; no Status/Routes round-trips happen once hello has failed. * fix(settings): refuse a Start-in path that names no directory (#601) The custom path was stored unchecked, and the daemon's picker then skipped it — not a directory — so every new pane silently started in the fallback directory and the typo read as a tty7 bug. Settings now marks the field red and refuses to save, the proxy row's pattern (#551), with the red line and the commit gated on one shared predicate so they can never disagree; a hand-edited config.json holding such a path gets a log::warn! naming it at the moment the fallback engages. * fix(terminal): rescan search highlights when the pane's width changes (#586) A match point is an absolute (line, column) against the width it was scanned at, so a column change reflows the text out from under every highlight. Output rescans them (Wakeup → refresh), but a quiet local pane has no output coming and the drift outlasted the resize indefinitely. set_grid_size now rescans on a column change with the output path's discipline — selection and scroll untouched — and takes the Context it needs to do so; a rows-only change reflows nothing and stays cheap. * fix(terminal): keep the grid selection when the search bar opens and closes (#584) The selection that seeds the query is the thing being searched for, yet opening the bar ran recompute_matches' unconditional clear — right for its other callers, where the user *changed* the query and the old selection names nothing — and closing cleared it again, so select → Ctrl+F → Esc lost the selection every time. The seeded selection is now restored after the opening scan, and close_search no longer clears; a query the user actually changed still retires the stale selection, the discipline refresh_matches_after_output already stated. * fix(tabs): a zoomed pane stays zoomed across a tab switch (#599) Zoom was a window-level value that activate() cleared unconditionally, so looking at another tab and coming back restored the split layout — while a zoom is a tab's temporary view state, like its focused pane. It now rides with the Tab: activate stashes the outgoing tab's zoom and brings the incoming tab's back. The clears that genuinely reshape the layout (drag, split, close) still stand, and a stashed zoom whose pane exited while the tab was away is validated away rather than restored. * fix(tabs): track an open rename box by tree id, not index (#598) The rename box held only an index, which drifts the moment any other tab closes or the strip reorders — so close_tab_inner and apply_tab_order threw the half-typed name away on any unrelated tab event, and a reorder mid-rename still left a window where the commit landed on whichever tab had taken the index over. The box now names its tab by tree id end to end (start, render match, commit): only closing the renaming tab itself ends the rename, and the name lands on the tab the box was opened on wherever it has since moved. * fix(i18n): move seven hard-coded user-facing strings into the language tables (#602) Seven spots rendered English no matter which UI language was set: the shell-integration notice that explains why a wrapper was blocked or never engaged, the titles a pane wears once its process exits or the server loses it, the loopback forward's failure line, the tray tooltip that lists running agents (whose separator also wanted a CJK enumeration comma), the cursor-shape choices in settings, the command palette's empty-result hint, and the updater's install hint. Each is a L10nKey now with en/zh/ja entries, so the parity guard keeps them translated from here on. The palette's empty state was also wrong in content, not just language: every menu suggested connecting over SSH when nothing matched, including menus that have no hosts in them. The hint now only appears in the quick-connect menu; everywhere else the palette suggests a different search instead. Verified on Linux: the title/palette/tray suites (48 tests) and the i18n parity guard all pass. * fix(terminal): show remote path completion is listing, and say when it fails (#585) Tab-completing a path on a remote workspace had two silences. The whole network round-trip painted nothing, so a slow link read as a broken Tab key; and a listing that failed was unwrapped into an empty candidate list, so "the directory is empty" and "the listing never happened" ended in the same nothing. A pill over the pane's bottom-right corner — the style the integration notice already uses, factored out — now says the listing is running from the moment it starts, and a failed listing sets a notice with its error instead of the empty vector. The failure pill stays until the next keystroke dismisses it, and the trailing notify after an empty listing closes the menu brings the "listing…" pill down with it. Verified on Linux: the new gpui test covers the idle/listing/failed states, and the neighbouring completion tests still pass. * fix(files): quote cd Here / Insert Path for the shell the pane runs (#593) Both file-tree actions wrapped a path with spaces in POSIX single quotes whatever the focused pane's shell was. In cmd.exe a single quote is an ordinary character, so `cd 'C:\Users\me\My Documents'` split at the first space and cmd complained about 'C:\Users\me\My' — while the same action was fine in PowerShell and bash, which is why only cmd users ever saw it. shell_quote_for takes the pane's shell program (the pane already knows it — the settings page lists it) and picks double quotes for cmd.exe, single quotes for everything else; an unknown shell keeps the POSIX form, and a path that needs no quoting stays bare either way. Windows paths cannot contain a double quote, so the cmd form has nothing to escape. * fix(cli): pane close fails for a pane the registry does not hold (#588) `tty7 pane close %99` printed {"closed":[99]} and exited 0 for a pane that never existed. The workspace path cannot drift this way — PaneClose answers — but an orphan has no workspace to route through, so close hangs it up directly, and that kill is fire-and-forget: the daemon never says whether it knew the pane, so Ok(()) only ever meant the bytes reached the socket. A reaper script chasing the orphans `pane ls --all` points at would read the ghost success as cleanup done. The direct path now reads the running-pane registry once per batch and refuses ids it does not hold: the miss lands in `failed` with exit 1, next to the failures kill itself can report. A pane that exits between the listing and the kill is gone either way, which is what closing it wanted, so that race still reports closed. * fix(session): a launch that leaves workspaces running says so (#597) Quitting with several windows open and starting again restored only the most recent one; every other open window was marked detached — panes alive, nothing on screen, the only trace a "left N detached" log line. The workspaces were reachable from the sidebar, but nothing said they existed, so they were easy to forget entirely. restore_one now returns how many windows it detached, and both launch paths (normal startup and the CLI-driven open) push an in-app notification into the restored window naming the count and where to reopen them. The count rides the return value rather than firing the notification inside the store, because the store has no window to notify in — and a launch that detaches nothing, like the reattach-the-last- closed case, stays silent. * fix(switcher): list the local machine's orphan panes, with a way to close them (#596) A pane whose workspace went away — an interrupted `tty7 run`, a forgotten workspace that kept its shells — was invisible everywhere in the GUI: not in the sidebar, not in the switcher, not in the tray. It kept its process and its memory, and the only way to even learn it existed was the CLI's `tty7 pane ls --all`, which a GUI-only user never runs. The switcher's local machine group now carries a "Background panes" block under its workspace rows: one line per live pane the daemon's registry holds and no workspace does — id, owner, cwd — each with a Close button. The listing is the same PaneClient::list the CLI's reaper reads, fetched off the UI thread when the panel opens; closing kills and then re-lists, so a pane that survived simply stays on the list instead of pretending to be gone. The block steps out of the way while the search field holds a query, which narrows the panel to workspaces. Local on purpose: a remote machine's orphans belong to its own daemon, and routing a listing per host is what the CLI reaper is already for. The block joins no keyboard navigation — the panes are not workspaces and the arrows have no business landing on them. * fix(updater): keep Inno's progress window on screen during the install (#600) The Windows installer ran /VERYSILENT, so from the app quitting for the update to the watcher bringing the new build up — tens of seconds, longer under an antivirus scan — the screen held nothing at all: no window, no progress, no tray note. "Clicked update, the app vanished" reads as a crash, and double-clicking the icon does nothing while the files are being replaced. The installer now runs /SILENT instead. Nothing about the flow becomes interactive — /SP-, /SUPPRESSMSGBOXES, /NORESTART and /CLOSEAPPLICATIONS are untouched — but Inno's own progress window stays on screen for the gap, which is exactly the span the user had no word about. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
3c0a700907 |
feat(switcher): flat workspace list, create form, connect-time workspace sync (#616)
* feat(switcher): flatten the workspace list, add a create form, sync remote listings on connect The switcher's left column is now one flat most-recently-used list across all machines, each row carrying its machine and link state; the per-machine tree, headers, and the Other Machines band are gone. Machine trouble (install progress, connect errors, parked routes) moves to contextual banners under the list, and machine verbs move into each row's menu. Cmd+Shift+N now opens a create form instead of silently swapping the workspace: a name prefilled with the usual generated codename, and a host combobox (searchable dropdown) defaulting to this computer. Creating on a machine with no live link connects first and completes when the link is up. Connecting to a machine also mirrors its workspace listing into the local store, so its workspaces survive a restart without a connection. Mirrored references are marked synced: launch restore skips them, and their clock follows the machine only until this client opens them. * fix(switcher): drop a parked create when its machine's connect is called off Disconnect clears the in-flight connect, so finish_connect never runs and the PendingCreate outlived the intent behind it: the next successful connect to that machine would have silently created a workspace nobody was waiting for anymore. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
6c26b35acc |
fix(control): bump the dialect to v6, and publish a tty7-server for macOS (#605)
* fix(control): bump the dialect to v6 so an out-of-date server says so The control dialect has been renamed, extended and cut since it was last numbered, all of it against CONTROL_VERSION 5: the machine tree replaced WorkspaceList/Get/Put/Delete with WorkspaceTree, MachineGet and the tab/pane verbs, GitStream arrived with its chunk and end events, and ReplyOk::Attached and FileMeta went away. A peer left behind by any of that still answers the hello, because the number it answers with still matches. It is also still sitting at the path the installer looks for, tty7-server-c5p5, so a client decides it already has the server it needs. Then the first call reaches a variant the peer has never heard of, the frame fails to decode, and the read loop takes the whole link down with it. What the user sees is a remote workspace that opens with no tabs and a git detail pane that never fills, with nothing anywhere saying why. Moving the number puts all three guards back: the hello is refused with the message that names the old build, the remote binary is looked for at c6p5 and installed rather than trusted, and a stale local daemon gets the restart prompt it should have been getting all along. Document the rule next to the constant while it is fresh: move it when a variant is added or removed. The feature strings only cover what a peer can safely ignore, and a request it cannot decode is not that. * feat(remote): publish a tty7-server for macOS hosts A remote workspace has been Linux-only for no reason anyone chose: the installer derives the asset name from `uname -sm`, and the only names it knew were the two musl builds. A Mac on the other end of an SSH profile got "a remote tty7 workspace needs a Linux host" and stopped there. Publish the two Apple slices alongside them and teach the installer to ask for them. `Darwin arm64` and `Darwin x86_64` now map to tty7-server-macos-aarch64 and tty7-server-macos-x86_64; everything past that point already worked, because nothing under it was ever Linux- specific — the install path is POSIX, the upload is SFTP, and the dialect probe runs the binary before trusting it. The machine names are matched per system rather than by architecture alone. Linux says aarch64 on one distribution and arm64 on the next, while a Mac only ever says arm64, so honouring Linux's spellings under Darwin would be guessing at output no Mac produces. Static linking is not the instrument on macOS — Apple ships no static libSystem — so assert-macho.sh stands in for assert-static.sh with the guarantee that actually matters: every dependency resolves under /usr/lib or /System/Library, so nothing the destination Mac lacks can be picked up from a build runner, and the binary carries the signature arm64 refuses to run without. Not signed or notarized beyond that, deliberately. The binary is never downloaded by the Mac that runs it: the client fetches it, verifies it against checksums.txt and writes it over SFTP, which sets no quarantine attribute, so Gatekeeper is not in the path. ASSET_X86_64 and ASSET_AARCH64 become ASSET_LINUX_*, which is what they always meant and could not keep meaning next to a macOS pair. * fix(ci): sign the x86_64 macOS server, and stop the guard flaking on it Two faults the first green run hid from each other. The linker ad-hoc signs the arm64 slice because Apple Silicon will not execute anything unsigned, and leaves x86_64 bare. That is fine on an Intel Mac, but the x86_64 server is also what an Apple Silicon box gets when it asks through a Rosetta shell, and handing that machine an unsigned binary is a guess about Rosetta nobody needs to make. Sign both slices ad-hoc in the workflow — no identity, no secrets, nothing to do with the notarized signing the GUI bundles get. The guard that caught it was itself unreliable: `codesign -dv | grep -q` under `pipefail` reports failure whenever grep wins the race, because -q exits on the first match and the writer takes SIGPIPE. Small output means the writer usually finishes first, which is why the arm64 job passed and x86_64 failed on the same signed-or-not question. Capture into a variable and match afterwards, the way the release workflow already does it. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
49901d7f8a |
fix(cli): let --enter press the key it is shorthand for (#581) (#606)
`--enter` is documented as sugar for `--key enter`, but the send dispatch counted only `args.keys`, so `tty7 send %42 --enter` answered "needs TEXT ... or a --key to press" and pressed nothing. The key list is now built before the dispatch and the dispatch counts it, so a marked address with `--enter` and nothing else runs what the pane already has typed, and a bare `send --enter` presses Enter where the caller sits. An unmarked id is deliberately left out of that promotion. #567 made the address slot take bare ids, and `send 83 --key C-c` addressing pane 83 is fine because `--key` says "press this" and nothing else. `--enter` does not: `send 2 --enter` reads at least as much like typing 2 into your own pane and running it, and turning it into a keystroke at pane 2 would be the silent retarget #567 spent its diff closing. It stays a loud error, now naming both spellings (`send %83 --enter`, `send %PANE 83 --enter`) rather than only the typing one. The reference, the bundled skill reference, `send --help` and the `--enter` help all said the old thing in slightly different words; they now say the same thing as each other and as the code. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
664b766698 |
fix(settings): split Shell Arguments like a shell, quote them on the way back (#551) (#573)
* fix(settings): split Shell Arguments like a shell, quote them on the way back (#551) The field split on raw whitespace, so `-c "echo hi"` became four argv fragments with the quotes still attached, and it silently rewrote config too: `build_shell_inputs` refilled the field with `args.join(" ")`, which cannot spell an argument containing a space, so a legal `"args": ["-c", "echo hi"]` in config.json re-committed as three argv on the next blur without the user typing anything. Parse with shell-words rules instead and quote each argument on the refill, so field text and the argv array round-trip losslessly. An unbalanced quote cannot become argv at all, so commit refuses it and the row explains why under the input — the proxy field's pattern. The field description in en/zh/ja now says quoting works. Program gets the milder half of the same treatment: a bare command that detection (a PATH probe) never saw is almost always a typo like `pwsh7` that today only surfaces when a pane fails to open, so the row warns under the field. It never refuses — the field stays free-text so a shell detection missed remains reachable — and anything spelled as a path is taken at its word. The comparison reuses core's `same_shell_program`, so "known" here means exactly what the new-tab menu dedup means. shell-words was already in the tree via portable-pty, so the direct pin adds no new code. * fix(settings): split Shell Arguments as argv, not as POSIX source Review pass over the #551 fix. Splitting with `shell-words` bought the quoting contract at the price of two silent rewrites of its own, both the same shape as the bug being fixed: a backslash outside quotes is a POSIX escape, so `--dir C:\Users\me` committed as `C:Usersme` on the platform where that is how a path is spelled, and `#` opens a comment, so `--tag #1 --verbose` committed as one argument. The refill was noisier than claimed too — `shell_words::join` quotes on `=`, `~`, `*`, `?` and `[`, so an existing `--color=auto` came back as `'--color=auto'`. Nothing here is a shell: the field is a text spelling of an argv array that goes to `CommandBuilder` directly. So split and join are now a local pair sized to exactly that job — quotes group, `\"` and `\\` inside double quotes escape, everything else is a character — and the direct `shell-words` pin goes away again. They are exact inverses, which is what `config.json` needs, and the test walks the round trip over the cases a space-join cannot spell plus the two above. Drops the Program nudge. `shells::inventory()` inserts the *configured* shell into the inventory it returns, so `pwsh7` is in `self.shells` from the next refresh onward: the warning could only ever flash between the commit and the refresh landing, and never appeared at all on a later visit to Settings. Its test passed because it hand-built an inventory that version of the value could not be in. Making it true needs core to say which rows were detected rather than configured, and that is a serialized protocol struct — too much for a nudge the issue itself called the milder half. Refusing the arguments no longer drops the Program typed or picked beside them: the stored argv stays as it was, which is what "this value was not saved" already told the user, and the shell picker works again while the arguments field is mid-edit. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
cb473daf27 |
fix(cli): refuse a broken send address instead of typing it into your pane (#538) (#567)
* fix(cli): refuse a broken send address instead of typing it into your pane (#538) A lone positional that starts with `%` but fails parse_pane (`%3x`) used to fall through to the text branch: the typo was typed into the caller's own pane and any --key followed it there, so one wrong character redirected an interrupt to whatever the caller was looking at. The guard now propagates the parse error when the `%` is followed by a digit — "clearly tried to write an address" — and leaves `%`-led text whose second character is not a digit (`%s/foo/bar/`, `%!sort`) on the text path it always was, per the review's narrowing. The explicit address slot also accepts bare ids now: `pane ls --json` prints `83`, not `%83`, and refusing the bare form made the workaround for the typo hole (`"%${TTY7_PANE#%}"`) uglier than the hole. This matches what pane_from_env already accepted and closes the missing-`%` variant of the same mistake. Tests cover the branch with a `Context { pane: Some("5") }` — every existing send test used `Context::default()`, where the fallback errors OUTSIDE_SHELL before the guard is reachable, which is why the hole had no test. Also correct the `ws rm` docs (#539): the reference claimed its panes become orphans found via `pane ls --all`, but the code has hung them up since #319; only a hang-up failure (reported by pane id) leaves orphans. The site reference, the skill reference, and `ws rm --help` now say so. * fix(cli): keep the send guard to what actually looks like an address The narrowing was described more widely than it works: a digit-led token that fails to parse (`3x`) still types, only `%` then a digit refuses, so the reference and the skill both promised an error that never comes. Say what the code does and point at the two-argument form as the way to type an address-shaped string anyway. Now that the `%` is optional, `parse_pane` also has to be stricter than `u64::from_str`, which accepts a leading `+`: a bare `+5` meant as text would otherwise address pane 5. An address is digits and nothing else, and `pane_from_env` delegates rather than repeating the read. The broken-address arm parsed twice and ended in an `unreachable!` that a future edit could walk into; one match on the parse result carries the error out directly. A lone bare id is the one behaviour this takes away, so it says how to type the number instead, and a test pins that it never quietly presses a key at the pane the id names. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
d343fd8a13 |
feat(links): open file links in tty7, resolved on the pane's own host (#568)
* feat(links): open file links in tty7, resolved on the pane's own host A clicked file path now opens in the built-in editor at the line and column the link named, and the Files panel reveals it; a directory link opens the panel on that directory. Settings -> Terminal -> Links -> Open files with picks between the built-in editor, the OS file association and a command, migrating anyone who had already set link_file_command. Detection is split into a filesystem-free candidate parser and a probe callback, so a pane whose paths live on another machine resolves them there instead of against the local filesystem -- an absolute path used to open this machine's copy silently. A pane running ssh typed into a local shell can answer for neither side and no longer offers file links at all. Relative paths are measured from the directory the work is happening in (the agent's, not the shell's kernel cwd) and then from the repository around it, and a path that matches nothing under either now says so instead of the click doing nothing. * fix(links): keep a remote path off the local openers, and off a dead end Review follow-ups on the file-link work. - A file resolved on another machine now opens in the built-in editor whatever `link_file_open` says. Under `system` or `command` the path was handed to a local `open` / `code --goto`, which threw away the resolution just done on the pane's host and silently showed this machine's copy — the same bug this branch set out to fix, left live for two of the three modes. A directory outside every tree root says so instead of opening a local file manager on a path that belongs to the far side. - `flush_link_probes` takes the host before it takes the wanted paths. `take_wanted` moves them into the in-flight set on the promise that a call is carrying them; a host that had gone away broke that promise for good and left those paths permanently unanswered — no underline, and a click that says nothing. - `~` no longer borrows this machine's `$HOME` for a pane whose paths are elsewhere. A cwd outside `/home` and `/Users` used to fall back to it, so `~/.zshrc` on a Linux box became `/Users/me/.zshrc` and was asked about — and possibly answered — over there. - An unresolved absolute or `~`-rooted path no longer claims it was looked for under the pane's directory. It never was: roots are only for relative paths. - A pending tree reveal counts down whether or not its row was found. A row that never reported bounds kept the request alive for good, re-issuing a scroll on every render and holding the column against a hand scroll. - The repo root comes from `GitStatusCache` when the git-status probe has already asked about that directory, rather than a second round trip. Tests: the migration `link_file_open` exists for (an old config with a command lands on Command, one without on the editor), a probe with no host staying wanted, and `~` refusing this machine's home for another one. * test(links): only claim a leading slash is absolute where it is `is_rooted` asks `Path::is_absolute`, the same question `FileCandidate::paths` asks before it decides the roots do not apply — and on Windows `/etc/hosts` answers no to both. The predicate is consistent; the assertion was not, so it now lives in a unix-gated test of its own next to the untouched one. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
473c94ecba |
docs(skill): show how to update an installed tty7 skill
`skills add` does not refresh a skill that is already installed, so the one install line left existing users with no documented way forward. Add the `skills update tty7` counterpart to both READMEs and give the agent-skill page a short Updating section covering update and remove. |
||
|
|
4da3868797 |
feat(shells): let the new-tab menu carry entries the user wrote (#534)
Closes #443 |
||
|
|
b7196ae49a |
Give the side panel's Info tab rows that do what they show (#531)
* feat(right-panel): give the Info tab rows that do what they show The panel's Session table rendered every fact the same inert way, and its two actions sat in a strip of their own under the whole list — unlabelled, four rows below the path they acted on. Rows now carry their own shape: `changes` is the sidebar's green-and-red `+N −M` and opens the same diff overlay under the same setting, an agent wears the same status dot its tab does, and what a row can do appears at the end of it on hover, in the strip Source Control rows already use. A port row hands over its address instead of leaving it to be retyped, and the lit panel tile closes the panel the way every other activity bar does. * fix(right-panel): answer for the row the pointer is actually on Review of #531 found the new rows promising more than they could keep. Port rows keyed their element id on the port alone, but a port is only unique with its pid — a pre-forking server puts one row per worker on screen, and gpui handed them a single interactive state, so a click on one lit the tooltip and the pressed fill on all of them. The `changes` counts were read off `Tab::git_status`, which resolves a split tab to its *first* leaf, while the click target came from `detail_pane`, which resolves it to the *last focused* one. Inert text could disagree harmlessly; a button could not, and clicking `+2 −0` opened another pane's repository. Both now come from the pane the rest of the rows describe. A port is only `localhost` if localhost reaches it. `lsof`'s bind address was parsed and dropped, so a server on `172.17.0.1:8080` was offered as `localhost:8080` — a refused connection, or somebody else's service. `PortEntry` carries the address (`serde(default)`, so an older daemon still answers), and the wildcard and loopback binds keep the `localhost` spelling anyone would type. The browser tile hung off `remote_context()` — where the *shell* is — though the ports come from the pane's own process tree either way. It hid the tile on the one pane where it works, a `ssh -L` forward listening on this machine. It is about the host now. The action strip is opaque and pinned to the row's right edge, so on the working-directory row it covered the leaf that the head-first elision exists to preserve. The value holds that width back for good rather than on hover: taking it on hover would re-elide the path under the pointer, which is the pixel-shifting the strip is absolutely positioned to avoid. Also: the counts were `flex_1`, so the whole rest of the line was the button and empty space underlined numbers it was nowhere near; the agent pip was pinned in pixels inside rem-sized text and slid off its line at any interface scale but 100%, and drew Waiting as a thin ring where the tab strip punches a hole in a filled dot — one rule, two dialects; the panel-toggle chrome tile, which on macOS lives inside the panel it closes, still dropped focus into the destroyed element and left ⌘J dead; and `scm/detail.rs` kept a third copy of `ROW_INSET`. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
c2950fc434 |
feat(ui): forget orphaned remote workspaces when a profile is deleted (#508)
Deleting an SSH profile used to leave every remote workspace entry that had connected through it behind, labelled with a bare internal id and retrying a route that could never work again. `RemoteRef` now carries a `RouteSnapshot` of the profile it was made from — name, user, host, port — written at creation and refreshed on every reopen, `serde(default)` so older session files load. The snapshot serves labels only: `PartialEq`/`Hash` ignore it, or a refresh would split one entry into two. Deleting a profile cascade-forgets the entries routing through it. Forgets, not deletes: `WorkspaceRemove` is never sent, so the sessions on the remote machine keep running and connecting again under a new profile brings them back from the machine's own workspace list. An entry holding a live or in-flight link is left alone, as is one whose window is still on screen — a window whose workspace the store has forgotten reads as local, and its next tab would open a local shell on what the user still sees as a remote box. Whatever survives parks instead: no retries, no error, and an inline action to drop it deliberately. A live or preempted link outranks a lost route. Labels fall back from the live profile to the snapshot to a placeholder, so no branch renders a bare UUID. Resolving the live name reads memory rather than reparsing `~/.ssh/config`, because that path runs on every frame of a window with a remote workspace open. Closes #485. |
||
|
|
657bb4e0b7 | chore(assets): crop the hero screenshot tighter around the window | ||
|
|
27880c0f14 |
feat(cli): wait on commands, press keys, and reap orphan panes (#505)
* feat(cli): wait on commands, press keys, and reap orphan panes
`tty7 wait` was the orchestration primitive for agents only. A pane with
nothing reporting agent status read as `idle`, so `--until idle` returned
success instantly about a shell that was midway through a build, and there
was no state that meant "the command finished" at all.
Panes with no agent now report `no-agent`, and `free` ends the wait when the
foreground command has exited — the process-tree question `procs` could
already answer but nothing could block on. `send --key` covers the keystrokes
text cannot express, which is what a worker stopped at `waiting` is usually
asking for. `pane close` takes several panes and `--orphans` clears what an
interrupted `run` leaves behind. `doctor` finally performs the hooks check
its own help has advertised.
The skill shipped in this repo predated `wait` entirely and taught a
hand-rolled `procs` polling loop with no notion of delegation; it now covers
the loop, and its agent statuses, `ws rm` orphan claim and not-implemented
list are corrected against the code.
* fix(cli): close the gaps review found in wait, --key and pane close
Five things the first pass got wrong, in the order they bite.
`--until free --changed` waited on a command it had already missed: the
"something ran" edge is only set by a poll that catches the pane busy, and a
command that starts and finishes inside one 500ms interval never is. That is
indistinguishable from a command that never ran, so the timeout now names both
doors instead of letting a finished build read as a hang.
`free` also outranked the agent ladder, which is backwards. A pane whose depth-0
process *is* the agent — the tree cannot tell that apart from a shell at its
prompt — reads free for its whole turn, so a `waiting` the caller explicitly
asked for could be overwritten by a process-tree fact and then withheld by the
`--changed` rule that comes with it. `free` is now consulted only when none of
the requested agent states answered, which is both cheaper and what the docs
already claimed. An empty process tree is "we could not see in" rather than
"free" for the same reason `no-agent` exists.
`--key M-X` sent `ESC x`: the whole spelling was folded to lowercase, which is
free for Ctrl (the C0 rule clears the case anyway) and wrong for Alt, where the
character rides through as itself.
`send --help` listed the key vocabulary by hand next to the table it is a list
of; it had already drifted by one alias. It is generated now.
And a `pane close` batch that could not close everything raised an error, which
left `--json` holding prose exactly when a cleanup script needs to know which
panes are still its problem. It exits 1 with `{"closed":[…],"failed":[…]}`, with
the complaint still on stderr so `-q` reports it.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
|
||
|
|
00e1aa8218 |
docs: correct claims that no longer match the code
Audited every page under docs/ against the source. Fixes for what the code actually does: - agents: the status vocabulary is idle/working/waiting/done, not running/waiting/idle; hook rows grow a separate Uninstall button; the Settings table labels read "Copilot CLI" and "Grok Build"; Copy Session ID lives in the tab's context menu, not the pane's - cli: `pane ls --all` reports the owning workspace id, not "tty7-cli"; document bare `tty7 [PATH]` as the GUI launcher it is instead of listing it as unimplemented; note `active_tab` and the `diagnostics` array; wait also defaults to $TTY7_PANE - git: the branch dropdown is a plain list with no search box and no stash-and-switch, and checkout is not a palette command; quote the diff overlay's own overflow notice rather than the sidebar's - window: the unread marker tracks a finished agent turn, not any output; rows cannot be dragged across groups; the sidebar and `tty7 tab ls` resolve labels differently; drop Toggle Commit History and Checkout to from the palette's Git group; ~/.ssh/config aliases are not palette entries - terminal: Ctrl+R dedups by command text and shows no directory; Esc does not dismiss a ghost suggestion; document Cmd+Enter - remote: GSSAPI is an ordinary Auth choice, not a managed-connection-only mechanism - fonts: Maple Mono NF CN leads the chain on Windows and Linux only; list the real per-platform defaults - settings paths: the three Links settings and per-pane history were filed under the wrong sections |
||
|
|
c4645aeea8 |
feat(terminal): draw a scrollback scrollbar down the right edge of a pane (#480)
A pane's scroll position lives in alacritty's `display_offset` — rows of scrollback, not pixels of laid-out content — so it has no `ScrollHandle` to hand a scrollbar. `TerminalScrollHandle` implements gpui-component's `ScrollbarHandle` over the grid instead, which lets the pane draw the same `Scrollbar` the sidebar and every list already use: same theme, same `Scrolling` show mode, same fade-out. The bar never touches the terminal. `set_offset` only records the row it wants; `sync_scrollbar` applies that on the next render — clearing the sub-line remainder and cancelling an in-flight smooth scroll on the way — and reports back where the grid actually ended up. Scrollback piling up at the live edge is deliberately not reported: the bar shows itself whenever the offset it reads changed, so a pane printing a build log would otherwise hold a thumb on screen for as long as the output ran. Every other change passes through, including the history shrinking, which is a cleared scrollback rather than growth. Closes #432 Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
49bfe59410 |
docs: drop the orchestration skill tty7 no longer installs
The in-app switch that wrote `~/.claude/skills/tty7-orchestration` was
removed in
|
||
|
|
707fd1867b |
docs: add a Mintlify documentation site (#478)
38 pages under docs/, written against the source rather than the README: config keys and their clamps from core::config, default keybindings from ui::keymap, every CLI verb and flag from tty7-cli, agent aliases and hook/fork/resume support from core::cli_agent, and Settings paths taken from the actual en-US strings. docs/features.md and its zh-CN translation are retired — everything in them now lives in a page of its own, plus the two things they carried that nothing else did (IME input, the performance notes). README and README.zh-CN point at docs/ instead. Screenshots and videos are placeholders for now: docs/images/placeholder.svg with a caption naming what each shot should be. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
35bbad5155 |
docs(features): drop the removed orchestration skill entry (#457)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
86799220ca |
feat(splits): rearrange a tab's panes by dragging one onto the layout (#445)
* feat(splits): rearrange a tab's panes by dragging one onto the layout Hovering a pane floats a small grip along its top edge; dragging it picks the pane up and puts it somewhere else in the same tab. Three landings, resolved from where the pointer is: * a pane's edge — split that pane and take the side dropped on * a pane's middle — trade the two panes' places * the band along the outside of the tab — sit beside everything else as a full-width or full-height band, which is the only way to say "make this a full-height column" in one gesture from the middle of a 2x2 The landing is highlighted while the drag is in flight, and is offered only once the tree agrees the drop changes something, so the highlight is never a promise the drop does not keep. * pane: move_leaf / move_leaf_to_edge / swap_leaves, each built on a clone and installed only when the layout really differs * pane_drag: the pointer-to-landing geometry, the drag state, and the grip * tree_sync: reconcile a tab that kept its panes but changed shape with a single PaneMove instead of closing and rebuilding the tab * feat(splits): drop a pane beside its neighbours, not on top of one Trying the drag out on real layouts turned up three ways the drop model asked for more precision than it should have. A drop on a pane's side always halved that pane, so putting a new column into a row of columns was only reachable at the very edge of the window, where the band rule took over. A side facing a neighbour in the same row or column now joins that run: the newcomer takes an equal share and the others give it up in proportion, keeping whatever relative sizes they were dragged to. A side facing across the run has no run to join and still halves the pane it landed on. The band along the tab's edge was a flat 26px, which on any real window is a hair's breadth. It is now measured against the pane it is read in — a sixth of it, floored at 32px and capped at 120 — and only counts on a side that faces the window rather than another pane. Landing there takes an even share of the columns that side already has instead of half the tab, so a third column is a third and not a half. The highlight is no longer drawn from the rule. The drop is carried out on a deep copy and the dragged pane's new rectangle is measured off it, so the preview and the result cannot disagree; the copy is deep because sharing a run out writes ratios the live tree's splits hold in common. Also: the grip is a quiet 22x3 bar that grows to 40x5 under a fixed 56x10 target (it needs an id of its own, or gpui settles its size before the group-hover is known), and every rearrangeable pane keeps an 8px strip clear above its grid so the grip never sits on the first row. * fix(splits): pin a drop to the pane it was offered against Review follow-ups on the pane drag. A drop zone named its target by position in the tab's leaf order, but it is read on one frame and carried out on the next: a pane closing in between shifts every index after it, and the drop lands beside a pane the user never aimed at. The zone now carries the target itself once the frame that drew it has resolved it, so a target that has gone refuses the drop instead of sliding it sideways. Alongside it: * `Pane` is no longer `Clone`. The two copies it can be asked for differ in whether they share their splits' sizes, which is not a difference to leave to whichever one `.clone()` happens to mean; `shallow_clone` is now named and private, next to `deep_clone`. * `edge_landing` no longer hands back a share that only a test read. The test reads it off the split the landing produced instead, which is the number the drop actually lands. * A test pins the invariant the drop zones rest on: `leaf_rects` comes back in the order `leaves` does. * Drop a doc comment that had landed on `close_focused` describing a different method, and an `Option` in `drop_pane` that was wrapped only to be unwrapped two lines later. * The changelog claimed every rearranged tab now syncs as one `PaneMove`. Only a drop beside a single pane does; a drop beside a whole group is not something `PaneMove` can name, and still takes the rebuild. Both entries move under `Unreleased` — v26.8.2 was tagged before either landed. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
68d6b4b062 |
feat(terminal): zoom the font with the platform modifier and the wheel
Holding Cmd (Ctrl off macOS) and scrolling over a terminal now resizes the font instead of the scrollback, which is what you reach for when showing a pane to someone else. A wheel detent is one step whatever the platform bills it as, and a trackpad accumulates until the fingers have travelled three lines, so a flick does not run the font end to end. Steps go out as the existing IncreaseFontSize/DecreaseFontSize actions, so the clamp and the saved setting stay in one place. |
||
|
|
fee48a4c99 |
Merge origin/main into integration/polish
main shipped v26.8.2 and 15 fixes while this branch was open. Resolved: - zh: main's #417 decided the background process is called "server" in Chinese, and that decision is newer than this branch's "服务器" — took it, kept this branch's typographic quotes around {machine}, and whitelisted SettingsServer in the new every-key-is-translated test, since the zh heading is now that English word on its own. - presets.rs: this branch factored main's inline `clear` closure into Theme::clear_ink / ansi_seed; same arithmetic, so kept the methods. #400's border and caret floors and #413's legible-palette flag both survive untouched. - app.rs: took main's Option-typed `alive` argument, kept this branch's note on why a dropped tab is worth a sentence. - README / docs: agent count is now exactly 18 with Oh My Pi, so the precise number replaces both "17" and "~18"; the zh feature doc keeps its translated menu names and gains Oh My Pi in the fork list. Six keys main added are gone because the surfaces that used them were rewritten here: the home screen's relative time now runs to years, hook failures name install vs remove, Full Screen left the View menu on purpose (AppKit adds its own), the SFTP filter says "search files", and the settings index titles its CLI row by its own label. |
||
|
|
a6beb6d8b2 |
docs: give the agent count the exact number the code has
CLIAgent::ALL is [CLIAgent; 17], so "~17 CLIs" and "and ~10 more" were hedging about a constant. Six agents are named in the features list, so the remainder is exactly 11. |
||
|
|
4e4057278c |
docs(features): point the settings paths at sections that exist
Five of the paths named a page and section pair that was never there: tab completion and history search moved to Input → Prompt, copy on select and smart selection to Input → Selection & clipboard, and opacity/blur sit under Appearance → Transparency, not a "Window" section — that name belongs to a heading on the Window & Tabs page, so following the old path landed you somewhere real and wrong. The Chinese page also quoted labels in English that its reader sees in Chinese — Appearance, Window, Follow theme, Scratch, Copy Working Directory, Settings → Keybindings — and shortened 窗口与标签页 to 窗口与标签. All checked against the strings the app ships. |
||
|
|
cf61fc900c |
docs(features): quote the tray item the app actually shows
|
||
|
|
817447bd48 |
feat(agents): recognize Oh My Pi and install its status hooks (#405)
Issue #376 asked for `omp`. Oh My Pi is a fork of Pi (can1357/oh-my-pi, descended from badlogic/pi-mono), but the fork is where the similarity stops for our purposes: it ships one binary of its own — `omp`, the only `bin` in `@oh-my-pi/pi-coding-agent`, and it never installs a `pi` — and it keeps its config under `~/.omp`. A pane running it was therefore not detected at all, and aliasing `omp` onto `CLIAgent::Pi` would have been worse than nothing: the status bridge would land in `~/.pi`, and Resume Session would offer `pi --session <id>` to a binary that spells that flag `--resume`. So it gets its own variant, wired the whole way through: | | | |---|---| | Detection | argv stem `omp`, distinct from `pi` in both directions | | Avatar | its own mark, normalized from the project's `assets/icon.svg` | | Resume | `omp --resume <id>`, opting out on `--no-session` | | Fork | `omp --fork <id>` — a verified fork command, so the menu item appears | | Hooks | Settings → Agents, at `~/.omp/agent/extensions/tty7/index.ts` | The status bridge is the one piece the fork did not change. Oh My Pi inherited Pi's extension contract intact — same default-exported factory, same `session_start` / `agent_start` / `agent_end` / `session_shutdown`, same `ctx.sessionManager.getSessionId()` — so `pi_extension_ts` now takes the agent and substitutes two things, the package it imports the type from and the slug it calls the emitter with. Pi's generated file is byte-identical to before, so no installed bridge goes stale. `--resume`, `-r` and `--session` are three spellings of one flag in Oh My Pi; all three shed when a session command is rebuilt, while `--session-dir` is a different flag and rides along. `fork_command` now honors the same `--no-session` opt-out `resume_command` already did — Oh My Pi rejects `--fork` outright under it, and no existing agent declares an opt-out. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
6fa353f8ca |
fix(ui): one confirmation idiom, and copy that names what it is looking at
- The SSH close confirmation was the app's only bespoke in-app dialog: no scrim, no Escape, no click-outside, no focus — the two buttons were the only way out, and an on_key_down there could never have fired because nothing inside the card is focusable. It now asks through the same platform prompt as the other eleven confirmations, which gets Escape and dismissal from the OS. - The switcher's tab column reported "No workspace or machine matches." That is the other column's noun. - ja left "bind" and "to" in English inside a form whose other labels are Japanese; zh had already translated both. - The bind and target host fields in an SSH forward rule were pinned at 104px, which does not hold an EC2 internal hostname. Same floor, but they now take a share of the row's slack instead of handing all of it to the free-text description beside them. - docs/features.md still described ⌃⇥ as "next tab"; since the two-column switcher landed it holds a panel open and commits on release. Also drops two more names for actions the app calls Zoom Pane and Clear Scrollback. |