mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-23 00:02:27 +00:00
0d58eef4bc0d26b665be2aa8545d524cd55bc0b4
486
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0d58eef4bc |
feat(control): attachment and takeover ride the machine tree too
WorkspaceAttach / WorkspaceDetach (and the hello-names-a-workspace shorthand) now record their data half on whichever workspace stores the server carries: the retired record store, the machine tree, or — on a full daemon while clients migrate — both, since they describe the same workspace. The behavioural contract is untouched and now survives the record store's retirement: newcomer always wins, the displaced session is pushed Preempted (and closed only when its link was dedicated), and a preempted session's tidy-up detach cannot evict the usurper — the token check lives in the tree's runtime-only attachment exactly as it did in the store's. A server carrying neither store answers the same refusal a store-less server always has. WorkspaceId gains FromStr (the inverse of its Display) because the attach verbs predate the typed tree and carry the id as a string. The end-to-end test drives a takeover on a server serving the tree and no record store at all, asserting the tree's own attachment record moves with it. |
||
|
|
f89aae957a |
feat(ui): hold a supervised control link to the local daemon
The GUI now dials this machine's own daemon over the control dialect, exactly as it does a remote one: one machine, one daemon, one control link. The link lives in its own global rather than RemoteConnections — inserting it there would register a wire-backed Host for this machine (local files and git must keep going through the in-process LocalHost) and would break the HostId::LOCAL-never-holds-a-control-connection invariant. No routing either: the daemon's control socket is right here, so connecting is a Unix connect plus a ControlHello. Supervised on its own forever loop at the remote pump's cadence, because that pump deliberately parks when the last remote workspace closes and a purely local session is the common case. Each turn also drains the shared control-event queue, so local pushes (Layout deltas, Preempted) are delivered under HostId::LOCAL even with the remote pump stopped; the observer install is shared with the remote supervisor so whichever comes up first, reader threads never find nobody listening. Reconnects ride the same 1/2/4/…/30s backoff a remote machine gets, with ensure_running first — the daemon is the GUI's own child, and a cold start legitimately races its listener. Unix-only like the control listener it dials; on Windows the loop compiles to a supervision no-op and the pane path is untouched. |
||
|
|
b48bb0ada1 |
feat(daemon): pane facts flow from the pane server into the machine tree
The tree's pane records are only worth reviving from if they hold what the machine itself observed, so the pane server now publishes into the MachineStore the daemon serves: the reader thread reports OSC 7 / probed cwd changes and the sniffer's agent facts (identity, native session id, launch argv, coarse status) after each chunk that changed them, and DeathReporter::report flips the record to live == false however the death was noticed — that flag is the client-visible 'awaiting revival' state, and it now comes from the process that owns the PTYs on the very event, not only from the next restart. The store rides a process-wide slot (installed by control_services, same shape as the control event observer) so the three pane-spawn paths need not thread it through; without one installed, observing is a no-op, which keeps unit tests and tree-less servers quiet. Facts are published outside the pane state lock and only on a real change, so the reader's hot path pays two clones and a compare. AgentFacts.status tightens from a free string to the existing AgentStatus enum while no wire client depends on it. |
||
|
|
57d72c91ad |
feat(control): machine-tree verbs and incremental Layout deltas
Teach the control dialect the semantic operations the machine tree serves: MachineGet / WorkspaceTree pulls, WorkspaceCreate / Rename / Remove / Touch / SetActiveTab, TabCreate / Close / Rename / Move / SetGroup, and PaneSplit / Close / SetRatio / Move / Replace. Replies carry the daemon's own tree types (a created workspace or tab comes back whole; close operations answer the pane ids that left the tree so the caller can kill their PTYs), and every operation broadcasts a ControlEvent::Layout delta to every connection but the writer's — the same origin-exclusion mechanism the record store uses, one delta at a time instead of whole-record last-writer-wins. The server advertises a new 'machine-tree' capability bit only when it actually carries a MachineStore; both daemons now do, alongside the retired opaque record store, which keeps serving unchanged while clients migrate. Delta fan-out rides its own bounded queue and forwarder thread per connection, so a peer that stopped reading stalls nobody's edit; the drop-on-overflow tradeoff is documented against the keepalive that reaps such a peer and the full pull every reconnect starts with. The request/reply/event enums lose their Eq derive: split ratios are f32. End-to-end tests drive the shipped tty7-server binary over real pipes: capability advertisement, tree ops landing in the server's own file, dead-pane revival across a real process restart, and delta delivery between two live clients. |
||
|
|
15c18356f0 |
feat(core): daemon-owned machine tree with semantic operations
Add core::machine: the workspace/tab/pane tree a machine's daemon owns outright, replacing the client-owned-schema model of the opaque record store. Leaves hold a pane id and nothing else; every fact about a pane (cwd from OSC 7, title, ssh spec, agent identity) lives once in the pane registry, which is what makes revival sound: a reopened store force-clears every live flag, so after a daemon restart the tree itself says every leaf is awaiting revival — no client-side instance stamps or id-reuse heuristics required. Operations (workspace create/rename/delete/touch/set-active-tab, tab create/close/rename/move/regroup, pane split/close/set-ratio/move/ replace) validate against the held tree, persist atomically, roll back on a failed write, and broadcast incremental LayoutDelta events with origin exclusion so a writer never hears its own echo. Persisted to machine.json beside the old store's file, serde with #[serde(default)] throughout so the daemon can keep evolving the schema, corrupt files quarantined instead of overwritten. |
||
|
|
bbeeec51d7 |
refactor(daemon): share one run_daemon between tty7 and tty7-server
Extract the control-listener-plus-pane-server startup from tty7-server into tty7_core::daemon::server::run_daemon, and point both binaries at it. The local daemon now serves the control dialect exactly like a remote one: one machine = one daemon, whichever binary happens to be running it. The bound control socket (and a bind failure) is still reported on stderr with the historical 'tty7-server:' prefix — a headless server's log file is off by default, and the remote_router test reads that exact line back to prove the client derivation and the server bind agree. |
||
|
|
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> |
||
|
|
7194236985 |
fix(terminal): prevent fullwidth punctuation overlap and speed OSC mark scanning (#250)
* fix(terminal): stop wide glyphs overlapping after fullwidth punctuation gpui's apply_force_width_to_layout tells a base glyph from a zero-advance combining mark by whether the shaped x advanced past half the forced width, and CJK fullwidth punctuation fails that test (U+FF08 advances ~0.47 em against a 0.6 em half-slot). In a batched wide run the glyph after such a character was classified as a mark and painted on top of it. Shape each wide glyph on its own line instead: the first glyph of a line is unconditionally a base, so the heuristic never misfires. * perf(terminal): intern wide-segment strings via char_string Each wide glyph now shapes alone, so its text is a single-char string — reuse the char_string memo instead of allocating a fresh String per cell per frame. The interned SharedString is also what keys gpui's line layout cache, so a CJK-dense repaint allocates nothing. * perf(terminal): skip MarkScanner's Text state ahead with SIMD memchr The scanner runs over every batch the client receives, and ordinary output — where the only byte that matters is ESC — dominates each one. Skip to the next ESC with memchr instead of stepping per byte, exactly as tty7-core's OscTokenizer already does: measured on an 8 MB batch of plausible output, 1.6 GB/s became 8.3 GB/s. Declare memchr for the root crate — it left with the OSC tokenizer's move down to tty7-core, and this is the first use since. * fix(terminal): advance segment_row past each wide glyph The unbatching change dropped the `col += 2` along with the batching loop it lived in, so the wide-glyph arm pushed its segment and looped on the same column forever, growing `segs` until allocation failed — the 6 GiB abort on the Windows CI runner, and a machine-freezing memory climb under a local `cargo test`. --------- Co-authored-by: lizhi <lizhi20@xiaomi.com> Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Co-authored-by: l0ng-ai <ysdpk123@gmail.com> |
||
|
|
148cb2f82a |
fix(linux): set window icon and application ID (#254)
Set app_id "tty7" on every platform (WM_CLASS on X11, desktop-entry match on Wayland) and attach a 256px _NET_WM_ICON to X11 windows. macOS and Windows already get their icons from the bundle / exe resource. |
||
|
|
f660ddb1cc |
fix(ui): safe delete prompt default, platform-aware reveal labels, and searchable agent settings (#255)
* fix(ui): make Cancel the default button on the file-tree delete confirmation
The file tree's delete prompt was the only destructive prompt in tty7 with
the destructive action first. On macOS (NSAlert) and Windows (TaskDialog)
the first button is the Return-key default, so pressing Return deleted -
including recursive folder deletion. Linux uses gpui's fallback renderer,
which is click-only, so the swap only reorders the buttons there.
Safe option first, matching every other destructive prompt; the literal
"Cancel" is what gpui maps to PromptButton::Cancel and the Escape key.
* fix(ui): stop saying "Finder" on Linux and Windows
The file-tree context menu and the SFTP job tooltip hardcoded
Finder-flavoured labels; only the right panel's Info row was
platform-conditional. Extract that conditional into
right_panel::reveal_label() and use it at all three sites, so the action
reads "Reveal in Finder" on macOS and "Open Folder" elsewhere.
Side effect of sharing the helper: the SFTP tooltip's "Show in Finder"
becomes "Reveal in Finder" on macOS, converging a third name for the
same action.
* fix(settings): index the Grok Build agent row and re-align drifted index titles
The Grok Build agent renders a settings row but had no search-index
entry, so the row was unreachable by search. The other five agent
entries carried mechanism suffixes ("Claude Code hooks", "Pi
extension") that no longer match the rendered rows, which are titled by
HookAgent::display_name(); the index said "Option acts as Meta" where
the row says "Option (⌥) acts as Meta".
Align the titles with the rows, keep the mechanism words as search
keywords, and extend the index tests: the pinned-title list gains the
Option row, and a new test derives the Agents entries from
HookAgent::ALL so adding or renaming an agent without updating the index
fails the suite.
* no-mistakes(review): add plain search keywords for Option-acts-as-Meta entry
* no-mistakes(document): document copy fixes in changelog and apply rustfmt
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
|
||
|
|
6f842c3007 |
fix(file-tree): stop a watcher event repainting a window with nothing to draw (#249)
* fix(file-tree): stop a watcher event repainting a window with nothing to draw
Issue #243 made two claims. The flicker was fixed independently on main by
|
||
|
|
9c00648875 |
fix(ui): keep the window-drag arm alive across a repaint, and make every header draggable (#252)
* fix(ui): make every header draggable, and keep the arm alive across a repaint (#221)
Two changes to the same code, which is why they land together.
Five rows that stand in for the title bar — the tab rail's top zone, the
settings page's top strip, the detail panel's top zone, and the code and
diff overlays' headers — armed their drag with an `Rc<Cell<bool>>`
allocated inside the render function. A redraw between the press and the
first drag event handed the next frame's listeners a fresh, zeroed cell
while the press had written to the old one, so the whole hold was dead
until you released and tried again.
The press itself schedules that redraw: these rows carry `on_double_click`,
and gpui calls `window.refresh()` on mouse-down for any element with a
click listener. So a drag only survived if the first move beat the next
vsync — 16ms at 60Hz, 8ms on ProMotion. A mouse press physically nudges the
pointer and often won that race; a trackpad press is a finger pushing down
without translating, and almost never did. That is the trackpad-vs-mouse
split the issue reports. The terminal's cursor blink (a 530ms `cx.notify()`
loop) disarms it on its own even with no press at all.
`window_move_gesture` now holds the flag in `window.use_keyed_state`, which
survives frames — where gpui-component's own `TitleBar` has always kept it,
and why the ordinary caption strip was never affected. Keyed rather than
`use_state` because one builder serves several call sites and `use_state`'s
`CodeLocation` id would collide when two of these rows are on screen at
once (the rail's top zone plus an overlay header is a real combination).
A longer-lived flag has to be cleared explicitly, so releasing outside the
row disarms too; with a per-frame cell the frame boundary did that for free.
Nothing else about these rows changes — same hit boxes, same geometry, same
`WindowControlArea::Drag`, same double-click.
Grabbing the window by a header is a property of the whole app, not a
per-surface feature, so a user never has to learn which rows are draggable.
Written down beside `window_move_gesture`, along with the two things it
takes beyond arming the gesture: non-controls inside a header take no hit
box (the rule #202 set for the "duo" mark, so the drag falls through them),
and a header whose contents *do* take hit boxes by design needs a floor on
its flexible spacer.
- `panel_title` — the detail panel's section header, shared by Info,
Outline, Changes, Files and the remote Files browser — is draggable now.
Its one un-`occlude()`d control (SFTP's refresh tile) gains the wrapper
every control on a drag row needs, or Windows' HTCAPTION eats its clicks.
- The horizontal tab strip keeps a bare 80px slice of caption. Its spacer
was a `flex_1` with no minimum, so it collapsed to exactly 0px once the
chips saturated the row (~7-8 tabs on a 1440px window), leaving only three
6px gaps and a hairline above and below the chips to grab — the "the
region that works seems very small" half of the report. The chip row's
fixed-chrome reserve is corrected to match: a stale flat 100px, sized when
the corner held a 30px "+" and a 30px "⋯", becomes the ~137px the corner
actually occupies plus the handle. Chips reach their minimum width and
truncate a tab or two sooner, and the window is always grabbable.
- The rail's top-zone spacer gains the same floor.
`ui::app::window_drag_tests` drives the real `title_bar_drag` row through
gpui's test platform, where `start_window_move` is `unimplemented!()` and a
panic is therefore a reliable "the window would have moved" detector. It
pins the invariant (press → repaint → move still drags), that a press alone
does not, that a release disarms, and that two rows on screen keep separate
arms. A control test keeps the old per-frame-cell pattern alongside and
asserts it still loses the drag to the identical event sequence — without
it, the invariant test could pass for the wrong reason.
* no-mistakes(review): occlude resize handles; correct chip-reserve arithmetic
* no-mistakes(document): reorder changelog sections; record non-draggable header exclusions
* no-mistakes(document): make panel grab-handle docs version-neutral and platform-accurate
* no-mistakes(document): make workspace_head panel-width doc version-neutral
* docs(changelog): re-file Unreleased entries after the rebase onto main
The rebase onto
|
||
|
|
12d8cf549b |
fix(ci): read the AppImage version from [workspace.package]
The crate split left the root manifest leading with `version.workspace = true`, so `grep -m1 '^version'` returned that line verbatim and the sed fell through unchanged. Every AppImage since was named `tty7-version.workspace = true-linux-x86_64.AppImage`, which broke the nightly publish job at `sha256sum -c` — the space-split name resolved to three missing files. bundle-linux.sh, bundle-macos.sh and bundle-windows.ps1 were already anchored on `= "`; this was the one that got missed, and the only one without a guard to catch the miss. |
||
|
|
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
|
||
|
|
1f09a62568 |
fix(ui): give corner-filling children their own radius (#236) (#244)
* fix(ui): stop filled children squaring off rounded corners The cursor-shape toggles (Block / Bar / Underline) reported in #236 look rough because the selected segment's fill covers the whole corner of the track it caps, and its outer edge is a hard, unantialiased vertical cut. The track's own border arc is drawn correctly and antialiased — it just floats *inside* that square, so the corner reads as a stair-step. The controls were relying on `overflow_hidden` to shape their end segments' fills to the track's rounding. It cannot do that. `gpui::ContentMask` is a bare axis-aligned `Bounds`; `Style::overflow_mask` builds it from the element's bounds shrunk by the border widths and drops `corner_radii` entirely, and every shader applies it as a hard `clip_distances < 0` discard. So the mask only ever cuts a square, and it never antialiases the cut. A container's own corners come from somewhere else — the quad shader's SDF, `saturate(0.5 - distance)` — which is why a plain rounded card renders smooth while anything with a filled child in its corner does not. That divergence is the whole bug, and the reporter's screenshot shows both halves of it: the corner with the selected fill is square, the corner without one is a clean arc. The fill has to carry the radius itself, so it goes down the SDF path too. It sits one border-width inside the track, so the concentric radius is `outer - border`. `ui::rounding` states that rule once, with the constants and the corner-assignment helpers, and unit-tests the invariants (inset is strictly tighter than the outer radius, clamps at zero, only the end segments cap the track). Applied to every place a child paints a fill into a rounded corner: * the segmented controls (the reported one, plus the others `segmented` serves), * the −/value/+ steppers' hover fills — those glyph boxes also had to be pinned to the track's content height, because a padded auto-height box measures 31px against a 22px content box and its rounded corner would land 4½px outside the visible strip, * the theme picker's flush-mounted previews, * the diff overlay's card headers and the row that closes a card. Not reproducible locally: this is a rendering-geometry defect, not a platform one, but it is most visible at a device pixel ratio of 1, where the clip's hard edge is a whole physical pixel. Verified by reading the gpui mask/shader source and the reporter's screenshot pixel by pixel, and by the geometry tests; the on-screen result is left for visual acceptance. Refs #236 * no-mistakes(review): round diff card header when body is empty * no-mistakes(document): point Unreleased changelog link at v26.7.6 * no-mistakes(document): untrack AGENTS.md per gitignore dev-tool convention --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
9ca3319239 |
feat(agents): fork an agent session and copy its session id (#241)
* feat(agents): fork an agent session, and copy its session id A coding-agent conversation is a single thread: to try a risky direction you either lose the one that got you there, or you don't try it. Every agent tty7 resumes already knows how to branch — `codex fork <id>`, `claude --resume <id> --fork-session`, `opencode --session <id> --fork`, `grok --resume <id> --fork-session` — but nothing in tty7 reached them, so the capability was invisible from the terminal that already knows every pane's session id. Fork is a per-agent capability beside the existing resume table (`CLIAgent::fork_command`), not a Codex special case: it is the same `match self` shape, it reuses the same id validation and the same launch-flag replay, and four installed agents qualify today. Every command was checked against that CLI's own `--help`; agents with no fork tty7 could verify return `None` and are never offered the action, since a guessed flag shape would only ever produce a usage error in the pane. Flag replay needed one correctness fix to survive this. A forked pane's own argv *is* a fork command, so relaunching it would replay the stale subcommand and id (`codex fork <old>` → an old id as a positional prompt) or double the modifier (`--fork-session --fork-session`). `codex fork` now sheds its subcommand exactly as `codex resume` did, and `--fork-session` / `--fork` join their agents' stale session-targeting lists. That also settles restore: a forked pane restores through `resume_command`, which now drops the fork flag — a restart continues the fork rather than branching it again. Placement follows where the user asked from. A pane-level ask is spatial, so the pane right-click menu offers Split Right / Left / Down / Up (pane splits gained a `before` slot for the Left/Up half, which the tree had no way to express). A tab-level ask isn't, so the tab context menu — inherited verbatim by the sidebar rows, which is where the request came from — opens the fork in a new tab with no placement question. The bare action behind the palette, the File menu and Settings → Keybindings takes the tab-level meaning. The three ways a fork can't run all surface rather than no-op: no session id yet (hooks not installed) and a remote pane (the command would shell the *local* agent) render the row disabled instead of hiding it, so the capability stays discoverable, and the action paths that have no row to grey out say so in a notification. Forking mid-turn is allowed but announced — agents fork from the persisted transcript, so the turn in flight is absent from the copy — and the parent is untouched either way. Copy Session ID sits beside Copy Working Directory. Codex has no copy-or-duplicate subcommand, so "copy the session" is the id: paste it into `codex resume`, a bug report, or another tool. Deliberately not built: any reading or writing of an agent's own session files. tty7's exposure stays the public CLI contract plus the hook payload's session id, so a change to Codex's rollout format or its version-numbered SQLite index costs at most a visible shell error. Forked tabs also look exactly like their parent, by decision — "Rename Tab" is the answer. Closes #211 * no-mistakes(review): perf(terminal): compute fork menu enablement at menu-open time * no-mistakes(document): docs: correct fork action surfaces, label, and remote limits * fix(agents): label forking the same for every agent The fork row said "Branch Session" on Claude Code and "Fork Session" everywhere else, on the strength of a source comment claiming "Claude Code calls it branching". It does not. `claude --help` documents the flag as `--fork-session`, described as "When resuming, create a new session ID instead of reusing the original"; the only occurrences of "branch" in its help are an unrelated git-branch review option. The claim came from otty's own UI wording, which I mistook for Claude's vocabulary and then wrote into the source as fact — so the comment goes with the special case rather than being left behind as a false statement about someone else's tool. The split was also inconsistent with itself: Grok takes the identical `--fork-session` flag and was already labelled "Fork Session". Every agent that has the capability calls it forking — `codex fork`, `--fork-session` on Claude Code and Grok, `--fork` on OpenCode — so one wording covers all four. `fork_label` keeps returning `Option<&'static str>`: it is still the UI's single capability gate (`None` = no verified fork command, no row offered), and per-agent wording stays expressible should one ever genuinely diverge. Generated commands are untouched — the existing table test still pins `claude --resume <id> --fork-session` and the other three verbatim. Also drops the two doc sentences that promised the per-agent label, and the stale "Branch Session" mentions left in comments; no occurrence survives anywhere in the tree. * no-mistakes(review): fix(agents): fork the pane the tab menu row named * no-mistakes(document): rewrap fork menu comment after label unification * fix(agents): repoint Pi's token-gate comment after the rebase Rebasing #211 onto #240 moved the session-id token gate out of resume_command and into the shared session_command_flags helper, so Pi's comment pointing at "the token gate above" no longer names anything. Comment only; the gate itself is unchanged. * no-mistakes(document): correct fork placement rationale in menus and changelog * chore: untrack AGENTS.md per gitignore dev-tool convention tty7 keeps agent-memory files out of the repo: `/CLAUDE.md` is already ignored, and on disk it is a symlink to `AGENTS.md`, so tracking the target defeated the convention. Ignore `/AGENTS.md` alongside it and drop the tracked copy; the file stays on disk, where the notes belong. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
a88a8b6515 |
test(host): bound watch coalescing by elapsed windows, not a constant
The burst case asserted at most 4 batches for 50 writes, which really asserts the machine finishes 50 writes inside 400ms. A loaded Linux runner spread the burst over ~1.4s, so the coalescer correctly emitted one batch per 100ms window and the test went red on main. Measure how long the burst actually took and allow ceil(burst / WATCH_COALESCE_WINDOW) + 2 batches. A host that stops coalescing still fails: dropping the window to 0ms turns the case red. |
||
|
|
9246340c43 |
Merge pull request #242 from l0ng-ai/feat/ssh-settings-redesign
refactor(ssh): rebuild Settings → SSH as a host library |
||
|
|
c3cfeb5daa | Merge remote-tracking branch 'origin/main' into feat/ssh-settings-redesign | ||
|
|
ed4dbee8e3 |
Merge pull request #235 from l0ng-ai/feat/remote-workspace
feat(remote): remote workspaces |
||
|
|
816a45cc17 |
fix(ci): build the unrepresentable-path case on Windows too
The `to_frame` guard reached for a Latin-1 filename through `os::unix::ffi`, in a test module that is not Unix-only. It now asks each platform for a path it accepts and `str` cannot hold — Latin-1 bytes on Unix, an unpaired surrogate on Windows — which compiles there and, more to the point, actually asserts the refusal on both. |
||
|
|
6f9324727c |
Merge remote-tracking branch 'origin/main' into feat/remote-workspace
The Pi extension conflicted: main gave it the session-id forwarding doc, this branch had already moved it from `current_exe` to the target machine's binary. Both kept. `a_detached_result_lands_after_its_view_is_dropped` now pumps to a deadline instead of parking once — a `Host` call runs on `HostOps`' own pool rather than gpui's executor, so there is nothing for `run_until_parked` to wait on until the answer has already crossed back. |
||
|
|
54cf9f2a8f |
fix(ui): keep blocking host work off the UI thread and off gpui's pool
Five findings from review, all about where blocking work runs and what a stale handle is still pointing at. - `live_pane_count` ran a routed `List` — an SSH handshake, and on a WSL route as far as installing the server — straight from the Stop/Delete action handler. That is `guard_off_ui`'s debug abort in a dev build and a frozen window in a release one. It is now split into a UI-thread read and a background count, with the prompt raised through the window handle afterwards. - `teardown_workspace_forwards` blocked the UI thread on a daemon reply that waits for the SSH server to acknowledge `cancel_tcpip_forward`. On a machine that has gone unreachable — exactly when someone reaches for Stop Workspace — it never came. Backgrounded, and `on_workspace` now sets a read timeout so the thread is not parked forever either. - The file tree's and editor's watch subscriptions had no record of which host opened them. A reconnect inserts a fresh `RemoteHost` under the same `HostId`, so `set_dirs` failed on a dead `ControlClient`, was warned and dropped, and nothing opened a new one: after the first reconnect the tree stopped seeing remote changes for the life of the window, and the editor's external-change detection — what stops a save clobbering someone else's edit — was silently off. Both now compare the host by pointer and reopen when it differs. - Closing a remote window that was empty *because its machine could not be reached* deleted the workspace: its `RemoteRef`, cached layout and geometry, while its panes were still running over there. Only a machine that answered licenses dropping the entry. - `HostOps` ran blocking calls on gpui's background executor, which on Linux is a fixed pool with no blocking tier. Four stalled host calls on a four-core client took every worker, including the one the reconnect needed to clear the stall. They now run on their own elastic pool. |
||
|
|
4486ee6849 |
fix(control): spawn on backlog, and hold a watch until its id is out
Two scheduling bugs on the server side of a connection. The pool asked `idle == 0` before spawning, but `idle` counts a worker from before it parks until after it has re-acquired the lock on its way out — so through the whole wake-up window a worker already handed a job still looked free, and the second `notify_one` in that window went to a thread that had left the wait set. A client pipelining k+1 frames onto k parked workers left the last one queued behind a `git status`. The rule is now "more queued than parked", which counts both sides of the window and cancels it out. `WatchOpen` started its forwarder before the reply carrying the watch id was written, and both go through the same sink. A directory that changed in that instant could push a batch the client dropped — it files the id only once `call` returns — and since the tree relists only on a watch event, that change stayed invisible. The forwarder is now parked and started by `finish`, and dropped outright when no reply went out, so a cancelled `WatchOpen` no longer leaves an OS watch behind either. |
||
|
|
0ec8e050ad |
fix(workspaces): make a handover atomic and stop two stores clobbering one file
The takeover moves two things — the `WorkspaceStore`'s record and the server's `AttachRegistry` handle — and each was internally locked, which is not the same as the pair moving together. Two clients attaching one workspace at the same instant could each win a different table, after which the store named a session the registry had already evicted and no `detach` could clear it: the workspace reported a takeover against a client that had disconnected hours ago. Both moves now happen under one handover lock, dropped before the displaced client is written to so a peer that has stopped reading still cannot hold up the next attach. `WorkspaceDelete` had the same split with no race needed at all: the store dropped its attachment and the registry kept its handle, so the next client to attach that id evicted a session nobody displaced — and, that entry being dedicated, closed its whole link. Two `tty7-server --stdio` sessions arriving while no daemon was up each served in-process, each with its own store over the one file. `persist` writes the whole document, so the second to save silently dropped the first's changes, and their separate registries made takeover a no-op between them. The probe path now starts the daemon and bridges to it — the rule `bridge_panes` already follows one dialect over — and the store re-reads when the file has moved underneath it, which covers the cases where two writers are deliberate. `MAX_RECORD_BYTES` and `MAX_WORKSPACES` did not bound their product: seventeen maximal records put the array past `MAX_FRAME`, after which every `WorkspaceList` was unencodable and every client showed an empty list. The document is now bounded at the save, and only when growing, so an over-large file can still be deleted back under the limit. |
||
|
|
ba5b29818d |
fix(control): stop a reply that cannot be encoded from reading as a hang
Six findings from review, all in the same seam — what happens when a message cannot go on the wire, and who is allowed to conclude the link is dead from that. - `Sink`/`ControlClient::send` encode before writing, so 'this message cannot be serialized' and 'this link failed' are distinguishable. Only the second can have put bytes out, and only the second is grounds for marking the connection dead — the client used to go `Reconnecting` over an oversize `WriteFile` the server never saw. - `Conn::finish` answers an unencodable reply with the error instead of dropping it. Dropping left the client waiting out the request's whole deadline for a reply that was never coming. - `Search` drops the hits whose paths are not UTF-8 rather than letting one Latin-1 filename make the whole reply unencodable. `SearchHit` is the only `PathBuf` on this wire; serde refuses such a path outright. - The control socket is bound under a tightened umask. `bind` creates the node at `0777 & ~umask` and the `chmod` was a window — under `umask 002` a group-connectable one, onto unauthenticated `ReadFile`. - Binding no longer re-permissions a directory it did not create. With `$TTY7_CONTROL_SOCK` or the hashed fallback the parent can be `/tmp`, and 0700 there locks every other user out of it. - A blob is filed under the `pending` lock, so a caller timing out in the gap cannot leave a whole file's contents in the side table for the life of the connection. |
||
|
|
6d70939824 |
feat(agents): make Pi a first-class agent with icon and resume (#240)
* feat(agents): make Pi a first-class agent, not a fallback one
Pi panes drew the generic robot glyph every unbranded agent shares, so a
Pi tab was indistinguishable from an Aider or Qwen one in the sidebar,
the tab chip and the tray menu (#225). Auditing the rest of the registry
turned up two more places Pi was on a default rather than handled.
The avatar. Repo practice, from the most recent addition (
|
||
|
|
ff76c01ac7 |
fix(ci): pick mtime nanoseconds a Windows SystemTime can hold
A Windows `SystemTime` is a FILETIME, whose tick is 100ns, so `UNIX_EPOCH + Duration::new(_, 123_456_789)` came back as `123_456_700` and the assertion failed on a rounding this conversion never saw. Every nanosecond figure in the case is now a multiple of 100, which still exercises the full nanos field. |
||
|
|
26f3a73f58 |
fix(ci): gate the tty7-server test suites that need --stdio on Unix
`--stdio` is refused on Windows by design, and the control socket it probes for is Unix-domain, so `stdio_conformance` and `workspace_store` join `remote_router`/`routed_pane` in carrying a file-level `cfg(unix)`. `cli.rs` keeps its argument-handling cases everywhere — `--version`, `--help`, `agent-hook` and the usage error say nothing about transports — and gates only the bridge and probe cases, which spawn a `--stdio` child or stand up a listener. |
||
|
|
0fba41a0e5 |
fix(ci): keep the control-server tests off Windows
Every test in `host::server`'s test module talks over a `UnixStream` pair, so the module needs the same `unix` gate the `tty7-server` integration tests already carry — without it the Windows leg of `build & test` fails to compile `tty7-core`'s lib test with 19 errors. The three pool tests are plain threads and channels, so they move to their own module rather than being gated away from a platform they work on. |
||
|
|
c951318f93 |
fix(tests): stop the keybinding tests from wiping the real user config
The gpui keybinding tests in `ui::app` never pinned a config dir, so every
`update_config` they run ends in `Config::save()` — a *full* overwrite — against
the developer's real `~/.config/tty7/config.json`. A plain `cargo test` reset the
whole live config to `Config::default()` plus the shortcut the test recorded;
the user-visible symptom was a rebind reappearing days after it was reset.
Guard it at the type level rather than trusting each test to remember: a
`#[cfg(test)]` `Config::save`/`load` on the GUI newtype panics when the resolved
config dir is the platform's real one. An inherent method wins over the `Deref`
to `CoreConfig`, so under `cargo test` every call site routes through the guard
and otherwise straight to core — no call site opts in. `default_config_dir` is
pub now so the guard compares against it instead of re-deriving the layout.
`set_config_dir` is first-call-wins and process-wide, which means every test has
to pin the *same* path: a test pinning a name of its own silently redirects
whichever test loses the race. Nine hand-copied `tty7-covtest-<pid>` literals now
go through one `pin_test_config_dir()`.
|
||
|
|
f0cfb47b06 |
refactor(ssh): rebuild Settings → SSH as a host library
The section was a field editor wearing a list's clothes: the widest column on the page said "Select a profile to edit" and nothing else, the global security toggles hung underneath whichever host happened to be open, and port forwards had to be typed as `L bind:port target:port` into a text area that taught the syntax nowhere and silently dropped any line it couldn't parse. The master column now leads with a title and a filter — past a dozen hosts, finding one is the job — with Add and the `~/.ssh/config` import demoted to icon affordances beside it. Hosts bucket by group into collapsible sections; a collapsed header keeps showing how many of its hosts are connected, and collapsing the group holding the selection hands the detail pane back to Defaults rather than stranding a selected row nobody can see. A live query force-expands every group. `Defaults` is now a pinned row with its own page, so "every host starts from these and can override one" is something the list's shape says. Each per-host override names the value it currently inherits instead of claiming a mechanism exists. A selected host leads with its own name, address, jump chain and live status, then Save (disabled with nothing to write) and Connect. Save leaves the form open — with the list permanently beside it, closing back to an empty pane read as the selection being thrown away. Port forwards are one row of inputs per rule, and a rule that can't connect is flagged in place rather than dropped on save. With nothing selected the pane offers quick connect and, when `~/.ssh/config` holds aliases tty7 hasn't linked, an offer to link them. |
||
|
|
c469e10312 |
Merge remote-tracking branch 'origin/main' into feat/remote-workspace
# Conflicts: # Cargo.lock # Cargo.toml # src/core/config.rs # src/ui/pane.rs |
||
|
|
5f77c40f96 |
feat(agents): install agent hooks onto the machine that runs the agent
Hook installation was written for one machine — this one — and a remote workspace runs its agents on the far side of the connection, where none of that is true: a different `$HOME`, a different filesystem separator, and a `tty7` binary at a path this client published rather than the one it is running from. `HookTarget` is that machine, borrowed for the length of one background task: `local` resolves our own home and binary, `remote` takes the home a handshake reported and the `tty7-server-<version>` this client installed there. Every path the installer builds now goes through it, via `Host::join` rather than `PathBuf::join` — a Windows client installing onto a Linux box was writing `/home/me\.claude`. The three things that are only true locally (our own environment variables, atomic writes, running the `codex` CLI) are gated on `is_local` instead of assumed. Settings grows a machine picker so the page states which one it is acting on. Also in this commit, three unrelated UI fixes: - The settings sidebar's search placeholder sat 6px right of every nav label under it — a `small` (14px) magnifier where the rows use 16, a 4px gap where they use 8, and an `Input` that adds `input_px` (12px at the default size) whether or not it draws a box. All three corrected, so the placeholder starts on the rows' 32px text column. - The "'X' is still running — reopen it from the workspace menu" toast is gone, along with the `workspace_detach_hint_seen` flag that existed only to show it once. Detaching a workspace is what ⌘W has always done here and the Window menu already lists what came off screen; a one-time lecture on top of that is noise. Old configs carrying the key still load — `Config` doesn't deny unknown fields. |
||
|
|
8239b298a9 |
revert(panel): put the detail panel's tab tiles back at chrome scale
|
||
|
|
9a34576877 |
fix(theme): split the selection ladder into a resting and a cursor rung
Folding three signed-off values into one `SELECTED` target is what made the chrome shout. The old ladder had `hover` 0.09, `sidebar_sel` 0.12 and `list_active` 0.17; the ratio rewrite kept only the loudest of them and pointed every selected fill in the app at it. On the Light theme the rail's selected row went from #E2E2E2 to #C0C0C0 — a silver slab at twice the perceived step it had ever had — and it took the switch tracks, the input grounds, the plain buttons' active state and every chrome tile's *hover* (which paints the sidebar's selected grey by design) with it. The two jobs were never the same: - A resting selection — a rail row, a lit toggle, a switch track — sits there all session next to unselected siblings. It stays quiet and leans on the text channel, which is what `Surface` carries both for. - A cursor — the one menu row under the pointer, the palette's keyboard row — is transient and alone on its surface, and the eye is already tracking it. It gets the loud rung. So `SELECTED` drops to 1.30:1 and a new `CURSOR` takes 1.70:1, each anchored to the Dracula value it was signed off at (`mix(bg, fg, 0.12)` and `0.17` respectively). Dracula lands within 1.02:1 of both old greys, and every other theme is pulled onto the same two perceived steps instead of the same one. `PRESSED` follows `SELECTED` down to 1.55:1 — it is "one step past selected", not a fixed distance from the surface. Only `accent` (gpui-component's name for a menu row highlight) and `list_active` move to the new rung; everything else was already asking for a resting selection and now gets one. The per-surface ladder test drops its floors to match and gains a cursor-vs-selected assertion, so the two rungs can't quietly re-merge. |
||
|
|
68683647ee |
feat(remote): build remote panes off the UI thread, and redraw the switcher
Opening, restoring, closing or listing a remote pane each ran a routed, blocking connection straight from a gpui input callback, freezing the whole window for the length of an SSH setup. Pane tree leaves become a PaneSlot that can be Ready or Connecting, so the slot lands in the layout immediately and the wait happens in it. connect_routed now carries the same debug UI-thread guard the Host trait uses, so the class of bug fails loudly instead of shipping. The workspace switcher is redrawn on one alignment axis with one icon size, a guide rail tying a remote machine's rows to it, and the command palette's card geometry. Launch restores only the workspace the user was last in. |
||
|
|
1996f2ae12 | chore(release): v26.7.6 v26.7.6 | ||
|
|
540255e6a1 |
refactor(panel): give the detail panel's top zone back to the panel
The panel's title-bar-height top zone carried seven controls belonging to three different layers: four panel tabs, the panel toggle, the "..." menu, and the workspace chip. All seven at chrome scale, in a column the user can drag down to MIN_WIDTH. The row wants 268px there — it overflows by 68px before anything else goes wrong. The layers are the real defect; the crowding is a symptom. Switching to Files is "what does this panel show", the workspace chip is "which machine am I on", and they sat side by side, same size, same weight. Three changes, none of which touch the window layout — both columns stay full-height and the corner controls stay where they are: - The workspace chip moves to the head of the rail, as a full row with the workspace's name rather than a monogram in a corner. The rail below it enumerates that workspace's tabs, so this is where the name belongs; it also stops a window-scoped control from competing for the panel's width. Note it is *not* folded into the repo group headers under it — those are repositories, and one workspace holds several. - The panel's tab tiles drop to TILE_SIZE_SM. That constant exists for tiles inside a panel, which is exactly what these are, and the one size step separates them from the window chrome sharing their row without spending a divider on it. - On macOS panel_title draws nothing. The tile row above it already says which tab you are on, and so does the content — a file tree is Files, a diff is Changes. It cost a whole row: tiles, then a title, then a search box, before one line of content. Changes' file count moves into that tab's tooltip, where it stays readable without switching tabs. Outline's count does not survive: it needs the active leaf, which needs a &Window right_panel_tabs has no reason to take, and its list is one click away. A tab passing `trailing` still gets the row; none currently do. Off macOS nothing changes: the panel hangs below a spanning title bar there, and panel_title is still that panel's tab switcher. The top zone now needs ~164px against a 200px minimum. |
||
|
|
6888fd2bc8 |
Merge pull request #238 from l0ng-ai/chore/dedupe-resvg-component
chore: finish the resvg dedupe — bump gpui-component to 0.47 |
||
|
|
c2da286a7f |
docs(changelog): reference the dedupe PR number
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5cf767c9d9 |
chore: finish the resvg dedupe — bump gpui-component to 0.47
resvg 0.47 landed in tty7 (#227) and the gpui fork (#237), but gpui-component still declared its own resvg = 0.45.1, keeping a legacy resvg/usvg/tiny-skia 0.45/0.11 stack in the tree. The fork now pins 0.47 (l0ng-ai/gpui-component@2264ff99 — no source changes needed; its only resvg user, the Windows native-menu rasterizer, uses APIs unchanged across the bump), so this moves the pin and drops the last duplicate: the lockfile now carries a single resvg/usvg/tiny-skia stack at 0.47/0.12, and `cargo tree -i resvg@0.45.1` matches nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
191eda4460 |
Merge pull request #237 from l0ng-ai/chore/dedupe-resvg
chore: dedupe the resvg stack with the gpui fork |
||
|
|
d2808f8225 |
docs(changelog): reference the dedupe PR number
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c04f0ab8f6 |
chore: dedupe the resvg stack with the gpui fork
PR #227 bumped tty7's direct resvg to 0.47 while the gpui fork still pinned 0.45, so the tree compiled two resvg/usvg/tiny-skia stacks. The fork's tty7 branch now carries resvg 0.47 (l0ng-ai/zed@3aac3ef); move the gpui pin there so gpui's SVG renderer and tty7's tray-icon rasterizer share one 0.47 stack again. gpui-component still declares its own resvg 0.45.1 (semver-incompatible with 0.47), so one legacy 0.45 stack remains until that fork catches up - noted in the manifest comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8cb3e2cf2a |
Merge pull request #228 from l0ng-ai/dependabot/cargo/sha2-0.11.0
deps: bump sha2 from 0.10.9 to 0.11.0 |
||
|
|
d534bdbb75 |
fix(remote): keep a remote workspace's layout across reconnects
A remote workspace came back empty every time. The layout was thrown away on both sides of design §10's storage split: `record_session` blanked a remote entry's `session` before anything could push it, and `claimable_session` blanked it again on the way out, so the record pulled from the machine on connect was discarded the moment the window opened from it. Nothing ever called `push_remote_layout` outside workspace creation either, which left the machine's own `workspaces.json` holding a record with no tabs in it. The entry's `session` is now what it was always meant to be — this client's cache of a record the machine owns — and the split is enforced by reachability instead of by erasure: - `claim` / `record` ask `machine_is_connected` first. A connected remote window stores and reopens its layout like any other; an unreachable one opens empty and, the half that matters, leaves the cached layout alone so there is still something to rebuild from. - `save_session` pushes to the machine that owns the record, so the remote's copy tracks every structural change rather than only the workspace's creation. - `finish_attempt` hydrates a window that came up before its machine did. Only an empty one: a window with tabs is one the user is working in. The invariant the erasure was protecting still holds, and holds where it belongs — every leaf routes through `pane_workspace_for`, so a remote workspace's panes attach or spawn over there, and an unreachable machine fails the spawn rather than falling back to a local shell. Also stop matching a native-SSH leaf's `pane_id` against a remote workspace's alive set. That pane lives in this client's daemon however the window is bound, and pane ids are unique only within one daemon, so a collision would have swapped the user's SSH tab for whatever the machine happened to be running under the same number. |
||
|
|
4814d94b81 |
fix(remote): keep file-tree listings on screen while they refresh
A watcher event dropped the cached listing for the directory it touched, so on a remote host every row under that directory left the screen for a whole round trip and came back — a file rewritten a few times a second made the tree strobe. Mark the listing stale instead: it keeps painting while the replacement flies, and is swapped out when that lands. Also stop relisting a directory for its own watcher event. Whatever changed inside it arrives as an event on that child, while the directory's own event fires for everything inside it including the dot-files the row filter skips on purpose — so relisting for it handed back exactly the round trip that filter exists to save. A remote $HOME with an agent rewriting `~/.claude.json` went from two relists a second to none. |
||
|
|
2c18d6b7f4 | Merge origin/main into dependabot/cargo/sha2-0.11.0 | ||
|
|
d23b509091 |
Merge pull request #227 from l0ng-ai/dependabot/cargo/resvg-0.47.0
deps: bump resvg from 0.45.1 to 0.47.0 |