mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
3c95995e828cbdf5a4ed5411fd76fcffbb3e7327
72
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
05bfa8a269 |
feat(ui): add ja-JP locale and split i18n into per-language modules (#372)
The single en/zh tuple table becomes one module per language behind a `SUPPORTED_LANGUAGES` table, and Japanese joins English and Simplified Chinese. - `gui_language` accepts `ja-JP`; anything unrecognized still falls back to `en`. - The language picker and `refresh_locale_state` both read `SUPPORTED_LANGUAGES` instead of keeping their own copy of the code list. - Language names in the picker stay endonyms (English / 简体中文 / 日本語) in every locale, as English and Chinese already were. - The zh and ja key tables are exhaustive, so a new `L10nKey` fails the build until it is translated rather than silently rendering English. Co-authored-by: Chihiro WATANABE <chihiro.watanabe@live.jp> |
||
|
|
e47b49dfdd |
fix(bundle): declare macOS TCC privacy keys for child processes (#323)
* fix(bundle): declare macOS TCC privacy keys for child processes tty7 currently ships no NS*UsageDescription keys and no data-access entitlements, so macOS falls back to a repeated "access other apps' data" prompt whenever a child process (shell, coding agent, mole, etc.) touches a protected folder such as ~/Library/Containers, Mail, Messages, or Calendar. kitty and Kaku both declare these privacy intents, which converts the prompt into a single, clear one-time grant. Add the folder/volume usage descriptions and the matching personal-information and device entitlements to the macOS bundle so the app behaves like its terminal peers. * fix(bundle): rework TCC usage strings per review - Correct problem statement: describe child-process-denied-without-prompt instead of the Full Disk Access framing (no NS*UsageDescription key exists for that class). - Add the full usage-string set (camera, microphone, contacts, calendars, reminders, photos, location, motion, local network, bluetooth, speech recognition, system administration, apple events), kitty-style wording. - Use macOS spellings: NSCalendarsFullAccessUsageDescription / NSRemindersFullAccessUsageDescription / NSLocationUsageDescription. - Drop every entitlement that has no matching usage string; keep only com.apple.security.automation.apple-events. - Restore trailing newline at EOF in bundle-macos.sh. - Document the Full Disk Access manual-grant requirement in docs/features.md. * docs: rewrite macOS privacy as feature notes (en + zh-CN) * fix(bundle): drop the apple-events entitlement, tidy the privacy docs The entitlement did not do what its comment claimed. Nothing in tty7 or in gpui's mac platform layer sends an Apple event, and it would not help the case this change is about either: the hardened-runtime automation check runs against the process actually sending the event, which is the pane's child carrying its own signature. What TCC reads off tty7.app is the usage string in Info.plist, which stays. Entitlements are per-executable and never inherited, so granting this one only widened what injected code could reach under an identity that already holds disable-library-validation. Docs: spell out the four Full Disk Access paths instead of running them together as one nested path, drop motion from the user-facing list (Core Motion has no macOS implementation, though the key stays for kitty parity), and place the section identically in the English and Chinese files. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
e1531cdea6 |
revert(windows): drop the taskbar status dot (#377)
The per-window taskbar overlay badge (#355, for #199) is removed, and with it the in-flight follow-up that was making its green "finished a turn" state reachable: the feature is not wanted. Nothing shipped — the badge only ever existed in Unreleased — so this is a plain removal rather than a deprecation, and its CHANGELOG entry goes with it instead of gaining a "Removed" counterpart. What goes: `ui::taskbar` and its `ITaskbarList3::SetOverlayIcon` poll, the `taskbar_status_icon` config flag and its Settings → Window & Tabs row and strings, `Tty7App::taskbar_signals`, `TerminalView::shell_busy` / `RemoteTerminal::shell_busy` (the overlay was their only caller), the `raw-window-handle` dependency and the `Win32_UI_WindowsAndMessaging` feature it needed, and the feature docs in both languages. A stale `taskbar_status_icon` left in someone's `config.json` is ignored, as any unknown key is. The tray badge and the in-window status dots are untouched; they were always the ones the taskbar was mirroring. |
||
|
|
27bb1864df |
feat(windows): taskbar status overlay per window (#355)
* feat(windows): taskbar status overlay per window (#199) Stamp a colored status dot on each window's taskbar button using the same palette as the in-window agent dots: - blue while a shell command or agent is working, - amber when an agent is waiting on the user, - green when work finishes while the window is unfocused (cleared on activation). Adds a `taskbar_status_icon` setting (default on, Windows only) and a Settings -> Window & Tabs row. The overlay is updated by a foreground poll that aggregates agent status and shell busy state across each window's panes, diffing against the current taskbar badge and only calling ITaskbarList3::SetOverlayIcon when the badge changes. Includes unit tests for overlay priority and the done-while-unfocused edge tracking. * fix(taskbar): retry a failed overlay instead of caching it as drawn Four fixes on top of the overlay: - A failed SetOverlayIcon was still recorded in `shown`, so a badge the taskbar never took was remembered as drawn and never retried. Stamp now reports success, and a failure drops the interface so the next tick re-creates it — which is also what an Explorer restart needs. - `create_failed` was a permanent latch: one CoCreateInstance failure killed the badge for the whole process, though Explorer may simply not be up yet when the first window opens. Use the tray's attempts/cooldown backoff instead, which this module otherwise copies. - The overlay's accessibility description was hard-coded English in an app that localizes everything else. Reuse the panel and tray strings. - Render the dot at 32px, not 16. SetOverlayIcon wants 16x16 at 96 dpi, so at 150%/200% scaling the shell upscaled a 16px icon; `tray::icon` already renders at 32 off macOS for the same reason. Also drops the Win32_Graphics_Gdi feature: CreateIcon, DestroyIcon and HICON all live in Win32_UI_WindowsAndMessaging, and the build and the taskbar tests pass without it. Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Co-authored-by: l0ng-ai <ysdpk123@gmail.com> |
||
|
|
2fa518a767 |
refactor(settings): rescope the About page (#350)
About had grown three sections that change system state and that nobody looks for under "About": a PATH install, a registry write, and a daemon restart. Two of them move out. The `tty7` CLI goes to Agents. That page already describes tty7 <-> agent integration in one direction (hooks reporting session status); the CLI is the other direction, and its own description leads with "so scripts and coding agents can drive tty7". The Loading and Unavailable arms there no longer return early, since the CLI toggle is about this GUI's own host rather than whichever machine the hook rows describe. The Windows Explorer context menu goes to the installer, which is where VS Code and Git for Windows put theirs: writing shell verbs is an install-time decision, not a runtime preference. A task checkbox drives new `--register-explorer-menu` / `--unregister-explorer-menu` flags, so the key layout stays in core::explorer_context_menu instead of being copied into the .iss. `status()` existed only to paint the settings UI and goes with it. The uninstaller unregisters unconditionally: an install that registered once and was later upgraded without the box ticked still holds keys that would otherwise point at a deleted exe. Server restart stays — it is about the app itself. Also fixes localization the About section had skipped: eight hardcoded English strings in the update block now have keys, and the orphaned SettingsCheckUpdatesDesc key (which still claimed "tty7 never updates itself", contradicted by the macOS in-app updater) is reused for a one-line description in place of a 60-word account of the updater's internals. Finally, terminology in the Chinese UI. hook, agent, worktree, diff and fork are read and spoken in English by Chinese developers, so translating them lost more than it gained. Scrollback was worse than a style question: 回滚 means rollback, the opposite direction. 窗格 for pane is kept — that one is standard. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
8a342f2ca9 |
feat(ui): GUI localization for en and zh-Hans (#303)
* feat(ui): add GUI localization for en and zh-Hans
* feat(ui): localize search placeholders and relative time
* feat(ui): localize palette, switcher, and sftp strings
* feat(ui): localize home shortcut labels
* feat(ui): localize tray, ssh prompt, and editor strings
* feat(ui): add plural/select i18n helpers and localize sftp/settings labels
* feat(ui): localize settings search, forwards panel, and file tree
* feat(ui): localize code editor and right panel
* feat(ui): localize stop/delete workspace confirmations with plural support
* feat(ui): localize diff overlay with plural-aware summary
* feat(ui): localize pending pane, worktree prompt, and home time strings
* feat(ui): localize app menus, tray, tab strip/sidebar, and remote status strings
* feat(ui): localize switcher, file_tree, machine_mirror fallback strings
* feat(ui): localize ssh prompts, theme presets, host error wrapper, and finish remote strings
* feat(ui): localize command palette strings
* feat(ui): localize app.rs notifications, prompts, placeholders, and parse errors
* feat(ui): localize remaining theme, switcher, settings, and sftp strings
* style: cargo fmt
* feat(ui): add language selector to settings
* fix(ui): refresh locales across windows
* refactor(ui): make GUI language selection explicit
* fix(ui): localize Explorer settings after merge
* fix(ui): keep persisted theme names out of the GUI locale
A theme's name is data, not chrome: it is written into the theme YAML and
matched back with `trim_end_matches(" (custom)")`. Translating it meant a
Chinese GUI forked "Nord" into "Nord(自定义)", the next fork stacked a second
suffix on it, and the name stayed Chinese after switching back to English. The
derived-name fallback had the same problem. Both are English again.
Also in this pass:
- Give each test thread its own locale override. The locale is process-wide and
tests run in parallel, so the two tests that switched to zh-CN could flip the
language out from under another thread's English assertions.
- Rebuild the menu bar when gui_language changes in config.json, the way the
in-app picker already does — otherwise the menus kept the old language.
- Document the values the setting actually accepts. The docs still described
`auto` and `zh-Hans`, which sanitize() resets to `en`.
- Put the English words back into the Chinese search keywords for the language
setting; the other 58 keyword sets keep them.
- Drop the unused is_zh_hans helper.
---------
Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
|
||
|
|
c396e85e0c |
docs(skill): launch workers interactively and wait on --changed
The taught loop started its worker with `claude -p`, but headless print mode never stops to ask, so the `waiting` state steps 3-4 are built on could not arrive. Step 2 now launches interactively. Every wait after a send passes `--changed`, with the reason spelled out: without it the loop re-reads the state it just walked in on. The exit-1 "worker died" branch is documented too, and both guards are asserted in the skill-content test. Also gives the install round-trip test a Drop guard, so a panic cannot leave `CLAUDE_CONFIG_DIR` set for whatever runs next in the process. |
||
|
|
e6bdf44f3c |
feat(cli): session CLI for scripting and agent orchestration (#248)
* feat(cli): `tty7 wait` + the agent-coordination note The two pieces of the original session-CLI PR that main's own CLI doesn't cover, rebuilt as a minimal delta against it. `tty7 wait %N --until waiting,done --timeout 600` blocks until a pane's agent reaches a requested state — the orchestration primitive that lets one agent sleep until its peer blocks on a permission prompt or finishes a turn, instead of screen-scraping. A poll of `AgentStates` rather than an `events` subscription on purpose: a one-shot stateless question composes into scripts, survives a server restart mid-wait, and needs no cursor management. Agentless-but-live panes read as idle via the machine tree; a dead or vanished pane reads as exit, which ends every wait (matched only when asked for). Timeout exits 124, the `timeout(1)` convention. The coordination note is discovery for the whole CLI: a marked, idempotent block describing the verbs, installed into ~/.claude/CLAUDE.md (always; CLAUDE_CONFIG_DIR honored) and ~/.codex/AGENTS.md (only when ~/.codex exists). A one-time "Let your agents coordinate?" prompt fires the first time a pane detects a coding agent; a Settings → Agents switch drives the same install/remove, with state read from the files themselves. Uninstall strips exactly the marked block; an unterminated block is left alone rather than truncated at a guess. * feat(agents): replace the global note with an orchestration skill Per review: global instructions tax every session's context and hand every agent — workers included — the ambient authority to orchestrate its neighbours. The common shape is primary → workers: one agent owns decomposition, dispatch, waiting and aggregation; workers just do bounded tasks. A Claude Code skill fits that exactly. `core::orchestration_skill` installs ~/.claude/skills/tty7-orchestration/SKILL.md — only its one-line description rides in context until the user or the primary agent explicitly invokes it, and workers never see it. The body can therefore afford the full delegation loop (tab new → send → wait → answer-or-capture → pane close) instead of a token-starved cheat sheet. The file is wholly tty7-owned: install is a plain overwrite (also the version-refresh path), and uninstall keys on an ownership marker so a user's hand-written skill under the same name is refused, not deleted. Gone with the global note: the first-agent-detected prompt, its config flag, and the CLAUDE.md/AGENTS.md writers — the Settings → Agents switch now drives the skill install instead. --------- Co-authored-by: l0ng-ai <ysdpk123@gmail.com> |
||
|
|
0c9f4baa3a |
fix(cli): make the PATH install reversible, honest, and safe to migrate
Follow-up on the review of #277. Seven fixes, no change to what the feature is for. An AppImage copy is now claimed with a marker file instead of being inferred from "am I an AppImage right now". Keying off the runtime meant that a user who moved from the AppImage to the tarball hit their own copy, read it as somebody else's binary, and never got another install for as long as that file sat there. The Windows uninstaller takes {app} back out of HKCU\Environment. Nothing did before: the entry is written by the app at runtime, so Inno never knew it existed and every uninstall grew the user's PATH by one dead entry. Unix has no equivalent hook and still leaves its symlink behind; that is now stated in the module docs rather than left to be discovered. An occupied candidate directory no longer ends the scan, and every platform now reports whether the install actually wins the lookup. `Occupied` on /opt/homebrew/bin used to mean giving up while ~/.local/bin sat free, and Windows — which appends to PATH and so never collides — reported `Installed` even when an existing tty7 earlier on PATH kept beating it. A new `InstalledShadowed` names the winner. `cargo run --release` no longer repoints the developer's real tty7 at a build tree. `cfg!(debug_assertions)` only covered the debug half of that. The Windows registry PATH is read, matched, and written as UTF-16 throughout. It went through `to_string_lossy` before, so a value the registry holds but Rust cannot represent as a String would have been written back with U+FFFD in place of its characters — the exact PATH corruption the surrounding code is careful to avoid. Two tests mutated $HOME and $PATH while the rest of the binary's tests ran beside them, and src/ui/home.rs mutates $HOME too. `candidate_dirs` takes home as a parameter, `place` takes its mode, and the PATH-joining and registry- joining rules are pure functions — so no test in this module touches the environment any more. 5 tests become 11, and the Windows joining logic is covered on every platform. Also: the config flag reaches Settings → About and both features docs instead of being config.json-only, startup reads config.json once instead of twice, and the CLI's strip failure warns like its sibling instead of being swallowed. |
||
|
|
794ae89d24 |
feat(cli)!: drop the attach verb and the design doc
The CLI's user is the coding agent; run/send/capture/events cover it. Workspace-level ws attach/detach stays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv |
||
|
|
b10a40a581 |
docs: attach is deferred — the CLI's primary user is the coding agent
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv |
||
|
|
9f88bf223b |
docs: tty7 CLI end-state design
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv |
||
|
|
f6fa34a016 |
feat(ui): make the sidebar diff preview optional and bound its cost on large working trees (#247)
* feat(diff): make the sidebar diff preview optional and bound its cost Clicking a sidebar row's `+N −N` opens the working-tree diff overlay. On a big tree that could stall the window, and not everyone wants an in-app diff viewer in the first place. Two halves, matching the report. The setting: `sidebar_diff_preview` (Settings → Window & Tabs, on by default, persisted in `config.json`). Off, the branch and the counts stay exactly where they are and read exactly the same; they lose only the pointer cursor and the `toggle_diff_overlay` handler, so the press falls through to ordinary tab activation. Both come off one value — `diff_click_cwd` — so they cannot get out of step. The performance work. All five of the reporter's hypotheses held up against v26.7.6, and each fix is measured on a 300-file / 90 000-line / 4.5 MB diff (release, macOS arm64): 1. The full diff was buffered before parsing — `git_status::git` uses `Command::output()`. Now streamed line by line through the new `git_status::git_lines` into an incremental `DiffParser`: peak transient buffer 4 552 060 bytes → 50 bytes, at ~1.7× the parse CPU (3.97 ms → 6.76 ms) on the background thread, where it never touches a frame. 2. The snapshot was deep-cloned per holder inside `this.update`, i.e. on the UI thread. Now shared behind `Arc`: 2.41 ms → 11 ns per holder. 3. The element tree is not virtualized — confirmed, not cured. Rendering is not being redesigned here; instead the element count is bounded (see 4) and `MAX_RENDERED_FILES` caps the cards built at all, with a "… and N more" line for the tail. 4. Auto-collapse was per file, and counted only +/− while the rendered body also has context lines. Added `AUTO_COLLAPSE_TOTAL_LINES` over *retained* lines: sixty forty-line files, none individually large, went from 2400 side-by-side rows to zero, under a summary saying the diff is too large to render efficiently and pointing at expanding individual files or `git diff`. 5. The Changes panel probed independently and kept its own snapshot. Both now go through `spawn_shared_diff_probe`, which dedupes by cwd and installs one `Arc` into every watcher; opening the overlay while the panel already shows that repo now paints from the panel's snapshot instead of re-probing. Plus a repo-wide retention budget (`MAX_TOTAL_LINES`, `MAX_FILES_WITH_HUNKS`): 90 000 lines / 6.2 MiB of line text → 20 000 / 1.2 MiB. The `+N −N` totals deliberately escape every cap — they are compared against `--numstat` to detect staleness, so a capped total would disagree forever and re-probe in a loop. Small diffs are untouched: a forty-file, twelve-lines-each tree is not oversized and still opens expanded, asserted directly. Not verified: anything requiring the GUI. No frame timings, no visual check of the oversized banner or the settings row, and `AUTO_COLLAPSE_TOTAL_LINES` is a judgement call anchored on row count rather than a measured frame budget. Refs #239. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(diff): bound the untracked list and stop a moving default flipping toggles Five findings from the review of the previous commit, all confirmed against the source before acting. The untracked list escaped every bound the previous commit added. `git ls-files --others` reports the whole tree of anything not yet ignored, so a fresh clone before `node_modules` / `target` / `.venv` reach `.gitignore` answers with tens of thousands of paths — read through the buffering helper, retained without a cap, ignored by `oversized()`, and rendered one non-virtualized row each. That reaches the overlay without going through the diff at all, which is why the diff budget never saw it. It is now streamed through `git_lines`, capped at `MAX_UNTRACKED`, counted toward the oversized threshold, and rendered at most `MAX_RENDERED_FILES` rows with a "… and N more" tail. The reported count stays the true total via `untracked_total` — same split the diff side already makes between what is retained and what is counted, because a count that shrank with the budget would read as files having disappeared. The overlay's expand state was an inversion set — "files flipped away from their default" — which was correct while the default was per-file and stable. The repo-wide `collapse_all` moves the default for every file at once, so any refresh crossing the oversized threshold inverted every explicit choice simultaneously: the two files the user had opened snapped shut and the rest sprang open. It now stores absolute intent (`HashMap<String, bool>`), answered before the default is even computed, so nothing about the snapshot can reach it. Covered by a test that asserts an explicitly opened and an explicitly closed file both survive a transition in either direction while an untouched file follows the default. The Changes panel dropped freshly landed snapshots. `install_diff_snapshot` only wrote `right_panel.diff` when the panel was the one waiting, so a probe the overlay started was discarded even when the panel sat on that exact repo — the overlay rendered the new snapshot while the panel kept the old one, same window, same repo. The wait (`diff_pending`) and the data (`diff_cwd`) are now claimed separately: with probes deduped per cwd there is at most one in flight, so there is no out-of-order overwrite to guard against. The oversized banner reported `retained_lines()` as "diff lines", which after the budget fires is what was kept, not what changed — it read "20000 diff lines" directly under a header showing the exact +90000/-0. It now states loaded-of-total and names which cap ate the difference, and lists each axis that tripped the threshold so a big untracked list never reads as a claim that the diff is big. And the Changes panel deep-copied every untracked path String on every frame, on the UI thread, for two `len()`/`is_empty()` reads — the same cost class the `Arc` switch removed from the probe path. 995 tests pass, fmt clean, no new clippy warnings. The app was not built, launched, or driven; visual acceptance is the owner's. Refs #239. * no-mistakes(review): cap Changes panel rows and fix oversized banner truncation notice * no-mistakes(review): drive banner per-file truncation off parser flags * no-mistakes(document): correct changelog cost count and overlay-trigger docs * fix(diff): reconcile the diff-overlay work with the host-aware git refactor The rebase onto main lands this change on top of the remote-workspace work (#235, #242), which moved the git helpers into `tty7-core` and made every read take the pane's `Host`. Adapting rather than papering over: - `sidebar_diff_preview` moves to the core `Config`, where the struct now lives. - The diff and untracked reads go through `git_status::git(host, cwd, ..)`. - The shared probe, its in-flight set, and `install_diff_snapshot` key on (`HostId`, `PathBuf`) — the same path on two machines is two work trees — as does the Changes panel's `diff_pending`. - `diff_click_cwd` became generic over what identifies a repo, so the setting gate did not need to learn about hosts. - Main's newer card rounding reads `truncated`, which is an `Option` here now. The streaming diff read is deliberately absent at this commit: `Host::git` is buffered, so it is restored on top of a streaming host API in the next one. Refs #239. * feat(host): stream git reads whose size scales with the work tree `Host::git` returns a fully-buffered `Output`, and the remote implementation round-trips one over the wire. That is the right shape for the reads tty7 does constantly — `rev-parse`, `symbolic-ref`, `--numstat` — all of which answer in bytes. It is the wrong shape for `git diff HEAD`, whose output scales with the work tree rather than with anything the UI can show: this repository's own `git log -p -n 400` is 8.3 MB, and the diff overlay keeps a small fraction of it. So `Host` grows a second entry point rather than changing the first. `git_lines` delivers the same invocation a line at a time, and its **default implementation buffers** — every host gets it for free, nothing that works today changes shape, and it stays inside the "every git read funnels through the host" invariant instead of becoming a way around it. Overriding it is an optimisation, never a behaviour change: a test asserts the streamed and buffered reads yield identical lines. The local host reads straight off the pipe. The remote host adds `ControlRequest::GitStream`, answered with `ControlEvent::GitChunk` pushes and a terminating `GitEnd` carrying the exit status — the shape `WatchOpen` already proved out. Two protocol details worth the reader's attention: The **client** picks the stream id, which is why the reply carries none. Ids only need to be unique within a connection and a connection has one client, so choosing it client-side lets the receiver be registered *before* the request goes out. A server-assigned id arrives in the reply, leaving a window where a chunk that overtook it reaches a client with no entry for that id and is dropped under the unknown-id rule — silently losing the front of the diff. `WatchOpen` needs a whole deferred-start mechanism to close that window; this does not. The feature is **advertised and checked**, not assumed. A server predating `GitStream` cannot decode the variant, and an undecodable frame ends the connection — so sending it blind would not degrade, it would disconnect. Servers advertise `git-stream`; a client that does not see it uses the buffered `Git`, which is the path every remote pane used before this existed. Covered by a test against a peer advertising only `control` and `host-rpc`. Chunks carry newline-terminated line data batched to ~64 KiB, not verbatim slices of stdout: the server reads through `git_lines` itself, so line content survives exactly while `\r\n` and a missing final terminator are normalised away. The only consumer is line-oriented. Framing per batch rather than per line is what keeps a 90 000-line diff from becoming 90 000 frames. Re-measured on the rebased code, against real git output, release build (the previous figure was taken before the host refactor and no longer holds): buffered: 8 269 409 bytes resident, read 817 ms + parse 12 ms streamed: peak transient chunk 64 KiB, read+parse 557 ms Lower peak memory *and* faster end to end — parsing now overlaps with git producing output instead of waiting for all of it. The earlier synthetic measurement showed streaming costing ~1.7x CPU; that was an artifact of reading a warm page-cached file, where there was nothing to overlap with. The app was not built, launched, or driven; visual acceptance is the owner's. Refs #239. * fix(host): make unsubscribing a watch take effect at the drop, not after it CI's Windows job failed `watch_drop_unsubscribes`: an event for a file created *after* the subscription was dropped still reached a consumer holding a clone of the receiver. Tearing the watcher down is not instantaneous. The OS backend runs its own thread, and on Windows a `ReadDirectoryChangesW` completion can fire during teardown, reach the event closure while `raw_tx` is still alive, and be forwarded by a coalescer that has not yet noticed the disconnect. So "dropped" meant "stops delivering shortly", which is not what the subscription promises — and for a remote host it is the difference between releasing a server-side watch and leaking one. The handle now closes the delivery channel in its own `Drop`, before any of that unwinds. Batches already queued stay readable — `close` stops sends, not receives — which is the one thing a consumer racing its own drop may legitimately still see, and exactly what the conformance test allows for. Not this branch's bug: the change here is to git reads, not watches. But main is flaky in the same family — it failed the sibling `watch_coalesces_within_window` eleven hours ago and was hardened for that one — so this fixes the cause rather than loosening the test. Refs #239. * refactor(host): make the streaming git read part of the protocol Remote workspaces have never shipped a release, so there is no deployed server to negotiate with. The `git-stream` feature flag, the `has_feature` check and the buffered fallback behind it were all guarding against a peer that cannot exist — dead code that would have to be maintained, and read by the next person as evidence that older servers are out there. `ControlRequest::GitStream` is simply part of the control protocol now. Buffered `Host::git` stays exactly as it was, for the many reads that answer in bytes and have no reason to stream. The remote test that proved the fallback becomes one that proves the stream: the peer serves `GitStream`, splits a line across two chunks, and the client reassembles it — the case the reassembly exists for. Refs #239. * no-mistakes(review): fix remote git-stream deadlock, chunk encoding and stray docs * no-mistakes(review): stop git-stream batch growing after a send failure * no-mistakes(document): note git-stream protocol delta in remote-workspace contract doc * fix(host): bound a git stream's wait, its lines, and its concurrency Three ways the streaming git read could still hold or hang more than it should, all found reviewing #239's implementation. A stream is answered by pushes, so neither of the failure paths the rest of the client relies on covers it: the request deadline was satisfied by the immediate `Unit` reply, and keepalive watches the link, which stays up while a server-side git wedges on a network filesystem. The reader parked forever, on one of a small pool of blocking threads, and the diff probe it belonged to never released its per-repo claim — so that repository's overlay and Changes panel were stuck on "Loading…" for the life of the process. `git_lines` now waits `GIT_STREAM_IDLE_TIMEOUT` between chunks. Between, not across: a slow-but-alive read must be allowed to take as long as it takes, which is why a total deadline would be the wrong instrument. Draining moved to `drain_git_stream` so all three exits are reachable from a test without waiting out two minutes. "Incremental" bounded the number of allocations but not the size of any one of them: a line is only complete at its newline, so a work tree with a minified bundle rebuilt the whole-output peak inside `LineSplitter`, on both ends of a remote link and in the server's outgoing batch. Lines are now capped at `MAX_LINE`, and what is cut says so in the line itself rather than silently shortening a rendered diff. That also bounds the server batch, which makes `GIT_STREAM_CHUNK_MAX` a frame backstop rather than the only thing standing between a bundle and a 32 MiB payload. Finally, `GitStream` is the one request that spawns a thread outside the bounded worker pool, so nothing counted them. `MAX_CONCURRENT_GIT_STREAMS` per connection now does, with the slot returned by a guard so a refusal, a failed spawn and a panicking read all give it back — a leaked slot would be a permanent refusal, not a transient one. * fix(diff): stop the overlay re-walking the tree per frame, and re-probe a folded-in refresh Two things the shared-probe work left on the render path. The overlay asks six whole-snapshot questions while building its element tree — oversized, totals, retained lines, budget fired, per-file cap fired, untracked count — and each accessor walked `files` on its own, `oversized` walking the hunks too. `files` is deliberately uncapped (only hunks are), so that was six walks over a list whose length is the size of the working tree, on the UI thread, on exactly the tree this module exists to keep responsive. `DiffSnapshot::stats` answers all six in one pass and the per-question accessors are gone, so nothing can drift from it. Computed rather than stored, because the snapshot is built by hand with `..Default::default()` throughout the tests and a cached count would read as zero for every one of them. Deduping probes per repository is what makes one `git diff` answer every watcher, but a probe describes the tree as it was when it *started*. A refresh triggered after that — a command finished, an agent turn ended — folded into the running probe and was answered with a snapshot already known to be stale, with nothing left to trigger another look: the overlay's own re-check is gated on `loading`, which the landing clears, and the `GitStatusCache` change that would have re-armed it has been spent. A folded-in request is now remembered and re-issued when that probe lands. It converges rather than loops, because a quiet tree never sets the flag. * fix(diff): bound the stream queue, and stop two thresholds answering the wrong question Review follow-ups on the sidebar-diff branch. Five findings, four of them about a bound that was claimed but not held. The remote streaming read bounded both ends and not the middle. The reader thread serves the whole connection, so it cannot wait on a slow consumer — parking it there stalls every other reply and the keepalive with it — and an unbounded queue was the price. That reassembles the whole diff in a channel, which is the peak the buffered read was replaced to avoid, one container further along. The queue is now bounded instead of back-pressured: each chunk is charged to the stream's arrears, the drainer credits them back, and a stream 32 MiB behind is cut loose with an error rather than served. Real back-pressure would need credit-based flow control in the dialect; this is not that, and says so. `oversized` counted untracked paths on its file axis, and collapsing every file body removes no untracked rows — that section has no bodies to fold. A tree with an un-ignored node_modules and three edited files hid the three cheap things, kept the expensive one, and told the reader their working tree was too large to render. The untracked list is bounded where it is built: MAX_UNTRACKED on retention, MAX_RENDERED_FILES on rows. AUTO_COLLAPSE_TOTAL_LINES counted the context lines git prints around every hunk — four to six retained per line actually changed — against a threshold set as if it were reading `+N -N`. It fired on trees whose header said 400. 8000, compared against the 20000 the parser stops retaining at, since collapsing everything is the heavier of the two interventions and should not arrive first by much. A probe that could not run produced an empty file list, which renders as "Working tree clean" — a claim about the repository, made because a read timed out. Newly reachable, too: a stream can be refused or go silent where a buffered read could only arrive or error. DiffSnapshot::read_failed keeps the two apart. Also: StreamStop's doc comment had been glued onto StreamSlot, leaving the enum undocumented and the guard described twice; the overlay header asked totals() beside stats() rather than through it; and the watch-teardown fix riding along on this branch was in neither the PR body nor the CHANGELOG. Tests: the queue budget both ways (a stream that outruns it is cut loose, a larger one that is drained is not), the untracked axis, an ordinary context-heavy afternoon sized to fail against the old threshold and pass against the new, and empty-because-broken against empty-because-clean. Each was checked to fail against the behaviour it replaces. 1603 pass, 0 fail; fmt clean; no new clippy warnings in the touched files. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bed22d899e |
Keep workspaces whole: remote reopen/restart recovery, and cross-workspace restore guards (#257)
* feat(remote): keep a remote workspace whole across reopens and restarts Reopening a remote workspace — or coming back to one whose `tty7-server` had been replaced — landed on a screen of `tty7 — disconnected` panes with their coding-agent conversations gone. Several independent holes added up to that; this closes them together, and picks up the surrounding work the same session produced. **Telling a restarted server from a blinked link.** `ControlHelloOk` now carries an `instance` minted once per server *process*. Nothing else in the handshake changes across a restart — `build` and both dialect numbers survive it — so a reconnect had no way to know its `pane_id`s were dead. It does now: a different instance rebuilds the window from its layout (same tabs and splits, fresh shells in the saved cwds) instead of re-attaching to a process that is gone. An absent instance means *unknown* and is never read as a restart. **An attach can now fail.** `Attach` has no synchronous reply, so the client returned `Ok` unconditionally and the daemon's `Error` frame was read much later by the reader thread, which has no arm for it — the pane then landed in the *link is down* state instead of falling back to a fresh shell. The client now reads far enough into the reply to classify it on the kind byte (the snapshot behind it can be megabytes) and hands those bytes to the reader thread, so a successful attach loses none of its replay. Local and remote attaches get different waits: the local one is on the UI thread. **The agent session survives to be resumed.** `TerminalView` raises `AgentSessionChanged` when the pane's agent reports a new native session id, so the layout on file catches up instead of waiting for the user to happen to open a tab. A pane that is still connecting now carries its agent through `PendingSpawn` — a save landing in that window used to write `agent: null` over the record — and `land_pane` sends `--resume` when the attach turned out to need a fresh shell. **Ending sessions says so on file.** "End Sessions" kills the panes and then drops their ids from the record, pushing the cleared layout to the machine that owns it (design §10: the remote's copy wins, so a local-only clear would be undone by the next open — the open this exists for). **The new-tab dropdown lists the window's machine.** `Host::shells` and a `Shells` control request (dialect v2) make the "+" menu a property of the machine the window is bound to. A remote window filled from this computer's `/etc/shells` offered `/bin/zsh` on a box whose zsh is elsewhere, and every pick failed to spawn. **An install reports its bytes.** The download and the SFTP upload each report progress, relayed to the client over the routed connection as a `RoutePrompt::InstallProgress`, and painted as a bar under the machine's row in the switcher. ~8 MB across two hops behind the word "connecting…" was indistinguishable from a hang. **The installer compares dialects, not version strings.** `tty7-server --protocol` prints what a binary speaks without starting it, so a connect adopts an already-running server it can talk to rather than prompting about a build difference and uploading 8 MB the machine did not need. **Switcher.** A machine's `⋯` menu holds "New Workspace" (it was a row under every machine, pushing the list a quarter of a card down) and a new "Disconnect", which drops the connection and leaves the windows open and read-only. The suspension lasts exactly as long as that machine has a window on it. Also drops three design/contract docs for the now-shipped remote-workspace work. * fix(session): stop one workspace's panes from being restored into another A restart put a copy of one workspace's seven tabs — cwds, layout and recorded agent sessions — in front of another workspace's own tabs, and auto-resumed every one of those agents a second time: six `claude --resume <id>` pairs running in parallel against the same conversations, one set per window. The record-level corruption that seeded it is still unattributed, but every mechanism that let it propagate, amplify, or go unnoticed is closable, and this closes them. **Panes now know their owner.** `Spawn` can carry the workspace the pane is created for; the daemon stores it immutably and reports it in `List`'s `PaneInfo.owner`. Restore refuses to re-attach a pane another workspace owns (`pane_attachable`) — before this, a saved id landing on somebody else's live pane attached silently, which is how one window could pick up another's shells. The field rides a new `SPAWN_OWNED` frame with a struct payload (the legacy spawn payloads are positional tuples an old daemon cannot grow), gated on a new `pane-owner` feature string: a client only sends it to a daemon that advertises it, so the legacy kinds stay byte-for-byte what old daemons expect. A pane with no recorded owner stays attachable by anyone — that is the pre-field behavior, not a new risk. **Saved pane ids are bound to the daemon process that issued them.** `DaemonVersion` now carries an `instance` minted once per process (the local twin of the control hello's), the GUI caches it at the `ensure_running` handshake, and each local workspace records it as `daemon_instance` beside its layout. Claiming a workspace whose ids came from a different instance blanks them first: daemon pane ids restart from 1, so after a reboot every saved id points at whatever unrelated shell holds the number now, and the aliveness check cannot tell a survivor from a squatter. A blank on either side means "cannot tell" and never trips it. Unlike the duplicate-claim case below, this path keeps the agent resume — the pane is genuinely gone with its daemon, and the fresh shell resuming the conversation is the feature. **A duplicate claim loses its agent resume along with its pane id.** `dedupe_pane_ids` kept the loser's layout *and* its `agent_session_id`, so the blanked leaves took restore's spawn-fresh path and auto-typed `claude --resume` for conversations the winning workspace's panes were still running — the doubling above. The winner keeps the panes and the resume; the loser keeps only cwds. **Cross-workspace saves are caught at the write.** Every terminal view remembers the workspace whose window created it, and `save_session` logs an error naming both ids if a window ever records a pane created for a different workspace — the tripwire for the still-unattributed seed corruption, so a recurrence is caught in the act instead of reconstructed from `session.json` archaeology days later. Wire compatibility both ways: `PaneInfo.owner`, `DaemonVersion.instance` and `Workspace.daemon_instance` are `#[serde(default)]` struct fields (old peers' JSON decodes, new fields are ignored by old readers), and `SPAWN_OWNED` is feature-gated as above. `daemon_instance` is client-owned in the design-§10 storage split — it names the local daemon, and the field-census test pins the classification. * fix(session): resume the agent when a local pane dies mid-restore `session_to_pane` decided whether to send a coding agent's `--resume` from `restore.is_none()` — i.e. from whether the pane looked alive when the restore started. But `alive_panes_on` runs one `List` at the top of the restore, while the attaches happen per leaf afterwards. A pane that exited in between failed its attach, fell back to a fresh shell inside `spawn_shell_terminal_in`, and then landed in the `restore.is_some()` arm: an empty shell with its conversation dropped. `ShellParts.restored` already answers this exactly, and the remote path already reads it in `land_pane`. Carry it onto `TerminalView` so the synchronous local path can read it too, and branch on that instead of re-deriving the answer from a set that may be stale by the time it is used. No behaviour change on the paths that were already correct: a view that was never restoring anything reports `restored: false`, which is the same answer `restore.is_none()` gave them. * fix(remote): check the server instance against the record, not just memory A remote workspace's pane ids were only guarded against server restarts by `RemoteLinks::instances`, an in-memory map. On the first connect after the client starts, every machine is a first sighting, so `server_restarted` answers false — and a `tty7-server` that was replaced while the client was closed sails straight through. Its pane ids restart from 1, so the saved ones now name unrelated shells, and the reconnect attaches to them: the exact id-reuse failure the local side already guards against. `Workspace::daemon_instance` was local-only for the stated reason that a remote server's identity is tracked live per connection. That tracking is correct but not sufficient — it cannot survive the client restart that makes the question worth asking. So the field now means the same thing on both sides: which process minted the pane ids in this record. `WorkspaceStore::serving_instance` picks the local daemon or the far machine's server depending on the workspace, and `finish_attempt` compares it per workspace before deciding to re-attach or rebuild. It stays client-owned: it records what *this* client last saw, so two clients on one remote workspace each keep their own and neither may overwrite the other's. An unreachable machine still records nothing, which is what keeps a good stamp from being erased with `None` — that would disarm the next check. Also in these three files: the §N references to the deleted design docs, cleaned up as part of the sweep in the following commit. * docs: drop the references to the deleted design documents The three documents this branch removed were cited ~280 times: `design §10`, `contract §8`, `§17` and friends in comments, five references by file path in code and manifests, five in CI workflows and one in the release skill. Every one of them now points at nothing. Rewritten rather than merely stripped, because most were not decoration: "design §10 makes the remote's `workspaces.json` the authority" becomes a statement in its own right, and the several that carried a Chinese phrase from the document as their justification say the same thing in English instead. Where the reference was purely parenthetical it is simply gone. Not touched: `PRD §7.1`, `brief §8` and the like, which name documents this branch did not remove and were already external before it, and the `RFC 4648 §10` test-vector citation, which is a real specification. The `host boundary` CI job loses `(§10.6)` from its name. It is not one of the required checks, so branch protection is unaffected. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
64403cf860 |
feat(terminal): expose the prompt editor's soft newline as a rebindable InsertNewline action (#246)
* feat(keymap): make the prompt editor's soft newline a bindable action
Shift+Enter and Opt/Alt+Enter have inserted a literal newline into the
command editor since the multi-line prompt editor landed in
|