mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
9f34cd3501e17f5259ceb267189d9c42fba84c2e
43
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9f34cd3501 |
fix(editor): stop scrolled-out text painting over the line numbers
Bump the gpui-component fork to 070d1a2, which clips the editor's scrolling content to the right of the gutter. Text, selections, indent guides and the cursor all paint from a bounds origin that horizontal scrolling has already shifted left, so scrolled-out content kept painting under the line-number column; the only thing hiding it was the gutter quad painted afterwards, which works only while `editor.gutter.background` is opaque. `apply_theme` clears that key to transparent so the panel can sit on a gradient or image window background without a seam, which is exactly the case the upstream code does not cover. Note that dependency in the theme, so the next person to touch it knows the transparent gutter is not free. |
||
|
|
3bc8a764f7 |
fix(theme): stop the code editor painting its gutter and current line in the stock syntax theme's colours (#636)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
0f5e63701e |
fix(ui): let the overlay scrollbars fade out again (#471)
* fix(search): wash a match in the accent, at a strength the theme can afford A search hit was washed from the terminal palette's selection colour at a fixed 1.45:1 against the background, so it read as a weaker selection on a grid that is already grey on grey — and 1.45:1 is under what a hairline is worth, spread over a whole cell. Two changes. The tint is now the theme's accent (`ActiveAccent`, already floored at 3:1 by `legible_accent`), which is the one colour the terminal surface has nothing else in. And the strength is derived per theme instead of fixed: the wash is opaque with the glyph drawn on top, so what it may spend is the theme's own text-contrast budget. A palette with 21:1 between text and background can afford a wash you cannot miss; one with 6.6:1 cannot, and a single constant has to be safe for the second. The current match drops its caret-coloured outline. That existed because a fill 2.1:1 off the background could not say "this one" on its own; now that it sits at the top of the theme's budget, the outline is the same colour saying the same thing twice. * fix(ui): let the overlay scrollbars fade out again macOS reports should_auto_hide_scrollbars() = false for anyone with a mouse plugged in, and apply_theme turned that into ScrollbarShow::Always for every list in the app. That preference is about legacy scrollbars, which take a gutter out of the layout; ours are overlay bars painted on top of the content, so Always parked an opaque bar over the switcher's tab column for as long as the panel stayed open, with nothing to fade it. Pin scrollbar_show to Scrolling instead, so every list fades its bar out after it stops scrolling. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
2dc6a88af6 |
merge: main into the Source Control branch
Conflicts were the two streams touching the same seams, resolved by taking the newer decision on each side: - main's interface font scale (rems tokens) wins in right_panel.rs; the SCM panel keeps its local px steps until it moves onto that scale, and the now-unused PANEL_TEXT constants are gone. - main's l10n_keys! macro (idents only) means the key list carries no doc comments any more; our SCM keys fold into it, and PanelUntracked stays deleted — its only caller was the panel this branch replaced. - main's Command::localized palette style carries our Git group; ORDER keeps main's visibility and our width. - main's ansi_seed/clear_ink refactor in presets.rs carries the lane colours: lanes() now clears through the same helper semantics uses. - file_tree keeps both: main's drag-and-drop targets and this branch's git decorations per row. - diff_overlay keeps both: main's sidebar-count write-back on snapshot install and this branch's epoch read and untracked preview. - main's window.prompt SSH-close confirmation supersedes the bespoke modal our branch still carried; main's tile-glyph revert stands. - main's two new guards are satisfied: the fourteen SCM actions carry authored names on the Keybindings page (their palette wording, plus a new CmdGitToggleGraph), ja translates ScmDetached, and CmdGroupGit joins the kept-in-English list — Git is a name. 2571 tests, 0 failures. |
||
|
|
62b922f2c2 |
Merge origin/main into integration/polish
main dropped the client-side command-mark store (#404) while this branch had just started reading it: the close confirmation names the command it is about to end, and the mark was the only place that text existed on the client. Keep both. The OSC 133 tokenizer main left in place already sees every mark, so the command line now rides alongside `zle_reading` and `shell_vi_mode` as one shared string — set on `C`, cleared on `B` and on a `C` that carries no line — instead of a store with a list, a lock and a cap. `busy()` reads that. The rest: - settings.rs takes main's opaque overlay surface and background layers, keeping this branch's no-match note and scrolled body. The inner `.bg()` goes, per main's reason: the root already paints it, and a second fill hides the theme image. - i18n keeps this branch's `every_key_is_translated_in_every_locale`, which walks `L10nKey::ALL` in all three locales, over main's hand-listed zh coverage test it replaced. It immediately caught three of main's new backdrop keys reading English in ja — Mica, Mica Alt and Acrylic, which is what Japanese Windows calls them, so they join the allowlist with that reason. - app.rs keeps both sides' tests and drops both sides' now-dead imports: `window_background` (main deleted the function) and `humanize_action` (this branch's keybinding note uses `keymap::action_entry` instead). Verified: `sleep 300` then ⌘W asks about "sleep 300"; ⌘W after it ends closes without asking. |
||
|
|
0106430ecd |
merge: main into the Source Control branch
The one conflict is an import list in `diff_overlay.rs`: this branch added `SharedString` for the unified view's row labels, main added `Background` and `Hsla` for the window backdrop work. Both sides are still used, so the resolution is the union. Worth recording why this merge happened when it did. `main` moving is not normally urgent — branch protection dropped its strict check, so a branch behind main still merges — but a *conflicting* branch is different: GitHub cannot compute `refs/pull/N/merge`, and every workflow that triggers on `pull_request` silently stops running. Three pushes in a row registered no CI at all on #424 while other PRs kept going green, which reads as a GitHub incident and is really just an unresolved conflict. |
||
|
|
61efe27f2d |
feat(windows): add native backdrop material presets (Mica / Acrylic /… (#412)
* feat(windows): add native backdrop material presets (Mica / Acrylic / Blur) Adds a Background material dropdown (Auto / Blur / Mica / Mica Alt / Acrylic / Off) that maps onto the native Windows backdrop APIs already provided by the gpui fork — Mica and Mica Alt via DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE), Acrylic via the new DWMSBT_TRANSIENTWINDOW material, and Blur via the classic ACCENT_ENABLE_ACRYLICBLURBEHIND path — with no fork changes required. * config: introduce WindowBackdrop in tty7-core with lenient kebab-case deserialization, defaulting to Auto for existing configs * theme: resolve the backdrop through a build-number fallback chain (Mica/Mica Alt need Windows 11 22H2, Acrylic needs 22H2 natively and 1809 via classic acrylic, Blur needs 1809; older builds fall back to plain translucency) and default the background alpha to SYSTEM_MATERIAL_OPACITY (0.82) while a material is active * settings: replace the blur toggle with a localized backdrop dropdown that only lists the presets the current Windows build actually supports, and keep the settings panel fully opaque so workspace translucency never shows through it * theme: make the file sidebar and right detail panel follow the window opacity so the backdrop material shows through the whole workspace, keeping row-level accents opaque for readability * i18n: add backdrop keys for en, zh-CN and ja-JP, covered by the translation completeness test * feat(theme): let the sidebar and right panel follow the window opacity * update GPUI * fix(windows): gate the sidebar translucency to translucent windows and sync the opacity slider fix(windows): gate the sidebar translucency compensation to active materials * fix(windows): derive the material opacity default from the resolved appearance * fix(theme): keep WindowBackdrop semantics consistent on non-Windows f * fix(theme): stop Windows-only materials from pinning the blur on other platforms * docs(changelog): document the Windows backdrop material settings * refactor(theme): share the default window-opacity derivation * fix(ui): keep gradient presets behind the settings panel and scope its fallbacks * fix(ui): keep the settings theme picker legible and the backdrop label honest f * fix(theme): let every backdrop variant defer to the local blur toggle on non-Windows * fix(settings): restore the backdrop dropdown selection on locale refresh * fix(ui): keep the opened-file editor surface opaque under window translucency * fix(settings): rebuild backdrop options after selection * fix(settings): ignore synced windows backdrop overrides on other platforms * fix(settings): preserve synced windows backdrop on non-windows reset * fix(diff): keep the full-window overlay background opaque * fix(windows): keep Auto opaque and stop the backdrop from misreporting itself Ten findings from a review of the backdrop-material work, all in the Windows-only paths. The root one: `material_active` treated `Auto` as a material whenever the legacy blur toggle happened to be on. `Auto` is the default in every config written before this setting existed, and plenty of them carry `window_blur: true` from the switch that no longer renders on Windows, so an untouched install would drop from opaque to 0.82 alpha - with its file sidebar and right panel at 0.15 - on first launch after the update, with no visible control to undo it. Only an explicit pick in the dropdown now buys the translucent defaults. The switch comes back on Windows while the backdrop is `Auto`, since that is exactly when the legacy flag still decides something. The rest: - Mica and Mica Alt fell back to `Blurred` with no lower bound, asking for a blur that does not exist below 1809 - and build 0, which is what a failed `RtlGetVersion` reports. They now degrade to plain translucency like `Blur` and `Acrylic` already did. - Acrylic is no longer offered below 22H2, where it resolves to the very same classic WCA blur as `Blur`. A test now asserts that no two offered presets render identically on any build. - `reload_from_config` re-applied the theme and the opacity slider but not the backdrop dropdown, so an external config change switched the window's material while the control kept naming the old one. - The settings, opened-file and diff overlays were made opaque so the OS backdrop cannot show through their text; that also hid the theme background image, which used to show through them. They paint their own copy of it now, and the fill they share moved into `theme::overlay_background`. - The SFTP transfers tray painted `workspace_surface_color` inside the right panel, which already paints it, stacking the same translucent surface twice into a darker band with a hard seam. - `apply_theme` re-issued `set_background_appearance` on every `Config` mutation in every window. With a DWM material that now costs a `SetWindowPos(SWP_FRAMECHANGED)` frame recalc, so dragging the opacity slider recalculated the frame once per mouse sample; it is skipped when the appearance is unchanged. * fix(ui): dim the overlay background image, and stop telling Windows it is macOS Two defects found while driving the previous commit's changes in the app. The overlays repaint the theme background image over their own opaque fill, so it survives them being made opaque - but nothing dimmed it. Before those overlays were opaque the image reached the eye through their translucent fill; painting it at full strength put the settings text straight on top of the wallpaper and made the panel unreadable at any image opacity above about half. They now paint the image and then the workspace's own fill over it, which is exactly the strength the image had through these overlays before, and which needs no new constant to say so. Shared as `app::overlay_surface_layers`, empty when the theme has no image so a themeless window paints no second pass of anything. The Windows-only blur row reused `SettingsBlurDesc`, whose text ends in "(macOS)". It gets its own key in all three locales, describing the job the flag actually still has on Windows: feeding the `Auto` material. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
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. |
||
|
|
5418d66903 |
feat(scm): draw the commit graph in the panel's history section
The history section now renders a real `CommitPage`: lanes, nodes, merge rings, refs and ages, over rows that behave like every other row in the panel. It replaces the three-lane figure from the G7·0 spike, whose shape it keeps unchanged — one canvas over the whole list, `paint_quad` for everything, lane centres snapped to device pixels before the quad is built. What the section is for decided most of the rest. 260px leaves about 26 characters beside the gutter, and this repository's subjects run to a median of 64, so reading a message here was never going to work: what a reader gets is where the branches are, where they merged, which refs sit where, and how recently anything moved. Two things buy back what can be bought: - The conventional-commit prefix comes off into a chip. `feat(terminal): ` is 12.7 characters on average, and the type is exactly the part that reads better as three coloured characters than as prose. The split is strict, so `Merge pull request`, `fix:it` and a bare URL all keep their whole line. - The lane gutter folds to a single column on request, worth another six. Lane colours are derived, not tabled. `Theme::lanes()` seeds from the palette in the order blue, yellow, magenta, green, cyan, red — no two neighbours share a hue family, red and green are never adjacent, and red is last because a panel three or four lanes wide never reaches it — then walks each one to `ACCENT_FLOOR` on the window, the sidebar and a popover. Across the nine builtins the worst contrast is 3.00:1 (untreated, `catppuccin_latte` sits at 2.31 and `rose_pine_dawn` at 2.05) and the worst adjacent pair is ΔE 13.8, against a JND of about 2.3. A hard-coded palette would have been the one colour in this file that ignores the theme, and the contrast tests cannot see a literal. Some notes on the drawing: - Segments are deduplicated by column before anything is painted, which is what makes the overflow bundle work: five lanes folded into the last column produce one line, not five stacked at five alphas. `project` is a pure projection and never feeds back into the layout, so dragging the panel narrower re-columns for free and no branch changes colour. - Cross-lane turns are right angles, and at a 12px pitch they read unambiguously — the same call tig, lazygit and `git log --graph` make. The horizontal runs half a line width past both centres, which is exactly what closes the corners the vertical stubs leave open. - No `paint_layer` per line. Zed's graph does that; each one is a full-drawable render pass. `BoundsTree` already orders overlapping primitives, and edges arrive sorted by `paint_rank`, so the node's own line lands last. - Nodes are rounded quads rather than paths: the quad shader rounds with an exact SDF and analytic anti-aliasing, where `PathBuilder` fills every vertex's `st` with `(0, 1)` and gets 4x MSAA alone. - Paging grows `requested` and re-runs the query. The layout is deterministic, so a longer run reproduces the same prefix row for row and nothing on screen moves; `--skip` is O(skip) and slides under you when a ref moves. It is a row, not a scroll trigger — a remote `git log` is an RPC, and scroll-to-load turns one flick into a burst of them. - Filtering hides the gutter. Lanes drawn across a subset of history would connect commits that are not adjacent, so a search result is a flat list, which is what it actually is. Seventeen tests. Four in `presets` run with the existing contrast batch and assert the floor on all three surfaces, adjacent ΔE, determinism and the seed order. The rest cover projection, snapped lane centres, the width clamp, the prefix split (including a Chinese subject, which is where byte indexing goes wrong), band deduplication, the filter and the scope label. One runs a real repository through a real pane and asserts the settled section draws zero frames while idle — a canvas that repaints every frame reads as correct code. |
||
|
|
d9a6553651 |
fix(theme): lift illegible bright ANSI slots to the text floor (#413)
* fix(theme): keep the bright ANSI half of the palette legible on the theme background fmt * feat(theme): make the bright-color legibility rescue toggleable fmt |
||
|
|
90e5069466 |
fix(menus): stop the View menu teaching Zoom Pane the wrong key
Zoom Pane is ⇧⌘↵. The View menu drew it as ⇧⌘E, and ⇧⌘E is Code Panel — press what the menu says and the code panel opens instead. gpui's macOS menu builder maps a keystroke to an AppKit key equivalent through a table that has no entry for `enter`; the fallback hands AppKit the literal string "enter", and AppKit takes the "e". Every other chord in these menus is a single character or a key that table knows, so Zoom Pane was the only item it could happen to. The item leaves the View menu, the way Enter Full Screen did above it for its own reason. Zoom Pane is still in ⌘P, in a pane's own context menu — which we draw ourselves and which prints ⇧⌘↵ correctly — and on the Keybindings page. |
||
|
|
85bf4d91ab |
fix(theme): write button labels and panel headings in the preset's ink
button_foreground and secondary_foreground both resolve from gpui-component's stock foreground, and apply_theme never overrode them — so a button's label and the detail panel's section headings were the only text in the window not written in the theme's own colour. On Rose Pine Dawn that is near-black next to the preset's #575279 everywhere else. |
||
|
|
63a4f0da6e |
fix(theme): light the splitters up in the theme's accent
drag_border was never set, so the sidebar and panel splitters, the pane divider and the drop target for a dragged-in file all lit up in gpui-component's stock blue — the same blue under all nine presets. Measured on Rose Pine Dawn: the handle went from #656361 to #907aa9, the preset's own accent, which is already what the focus ring and an on switch use. |
||
|
|
f3f0c4710f |
fix(theme): highlight the command line in the theme's own colours
kind_color() reaches for theme.green, .cyan, .blue, .yellow and .magenta, and apply_theme never set any of them — so every dark preset shared one stock ramp and every light preset another, and the line you were typing kept a palette the output right above it had already left behind. Point them at the preset's ANSI ramp, cleared for legibility through the same floor the semantic inks already use. |
||
|
|
2cc3ddf4b6 |
fix(theme): hand the preset's ink to every tooltip and menu
apply_theme set tokens.popover_foreground but never the field beside it, and that field is the one gpui-component reads for tooltips, dropdown menus and date pickers — they all kept the stock near-white and came out brighter than the window they float over. It also inverted the terminal's own menus: the completion and Ctrl+R rows painted resting text with it and the selected row with the preset's foreground, so the selected row was the dimmest line in the list. Wire the field up, and let those rows say muted_foreground, which is what they meant and what their icons already used. |
||
|
|
3f4d378582 |
fix(settings): give the slider knob the switch's knob
slider_thumb was left at gpui-component's primary_foreground, which on a dark theme paints a black disc on a dark page while the switch beside it carries a light one. Both are the same handle; use the same colour. |
||
|
|
a4f6fdc655 |
fix(settings): colour a filled slider like an on switch
slider_bar falls back to tokens.primary, the near-black we give primary buttons, so on one settings page a set slider and an on switch disagreed about what a set value looks like. Both are the same statement; give them the same colour. |
||
|
|
6cab19ce01 |
fix(menu): put SSH in the menu bar, and stop offering Full Screen twice
SSH is one of the reasons to pick tty7 and it had no entry in the menu bar at all — the only routes were ⌘P and Settings, both of which you have to already know about. Manage Profiles and Reconnect now sit in File beside the other things that open a pane; Remote Files and Port Forwarding sit in View beside the other panel toggles. Same labels as the command palette, so there is still one name per thing. AppKit adds its own "Enter Full Screen" to the bottom of any menu named View, so ours sat directly above it: the same command listed twice under two different shortcuts. Ours is gone; ⌘↵ still works and is listed on the Keybindings page. Drops the AppMenuEnterFullscreen key with it. |
||
|
|
5d14603722 |
fix(windows): advertise terminal background to TUI apps (#332)
* fix(windows): advertise terminal background to TUI apps * refactor(windows): keep the background hint out of config.json The daemon needs to know whether the window is light or dark when it spawns a Windows pane, because ConPTY drops the child's OSC 11 query before tty7's emulator can answer it. It was reading that from `Config::theme` — a field nothing had written since it went dead — which meant the GUI had to rewrite the user's `config.json` every time the effective preset changed sides. Move the hint to `appearance.json`, beside `machine.json` in the data dir, and leave `Config::theme` exactly as it was. It is derived state: written by the process that paints the window, read by the process that has to describe it, and of no interest to the user. A file of its own rather than a field on `Machine`, because the machine tree is owned by the daemon and flushed on a timer, so a second writer would clobber the workspaces and panes it had not seen. Absent, unreadable, and unparsable all read as light — what the default preset is — so a daemon that starts before the GUI has ever applied a theme describes the default window instead of guessing. Also silence the `unused variable` warning the hint parameter raised on every non-Windows build, where the `COLORFGBG` block it feeds is compiled out. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.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>
|
||
|
|
fe3bc17f8c |
fix(ui): soften overlay scrollbars (#293)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
c8db432b63 |
fix(ui): one word per concept — shells, server, connection
The workspace switcher's row menu now uses the same verbs as every other entry point (Stop/Delete Workspace), running processes are shells everywhere, SSH links are connections, and the background process is the server in all user-facing text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv |
||
|
|
8c1946d763 |
chore: strip every comment from the Rust sources (#268)
Removed all Rust comments -- line, block, and doc -- from the 139 tracked .rs files with `uncomment` 3.5.1. It parses each file with tree-sitter instead of matching text, so comment-like content inside string literals is left alone: the JavaScript plugin source embedded in agent_hooks.rs raw strings keeps its own `//` lines. Left alone: Cargo.toml comments and the shell scripts under scripts/. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
a4972d32d8 |
feat(core): daemon-owned workspace tree — semantic ops, incremental deltas, thin clients (#260)
* 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.
* 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.
* 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.
* 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.
* 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.
* 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.
* fix(core): review hardening for the machine-tree foundation
Findings from a correctness review of the new daemon-owned tree,
applied together:
- A dead pane can no longer be resurrected in the tree by its own last
output. On Windows the exit monitor reports the death while the
reader is still draining ConPTY's buffered bytes, and the death
report is latched; the reader's 'output is proof of life' publish now
asserts liveness only while the pane state still says alive.
- Delta delivery is ordered. Mutations were serialized by the state
lock but delivered after releasing it, so one writer's deltas could
overtake another's and leave every mirroring client on the losing
state with no cue to re-pull. A notify-order mutex now spans each
mutation and its own fan-out; cheap, because subscriber callbacks are
enqueue-only by contract.
- Implicit active-tab changes broadcast. tab_create's activation and
the close paths' heal now emit ActiveTabChanged, so a client applying
deltas never re-implements the server's heal rule; the one
inexpressible case (no tabs) needs no delta because it is a fact,
not surgery.
- The coarse agent status no longer drives disk writes: it flips per
hook event and is display-only, so it is outside the changed-facts
gate and merely rides along when a load-bearing fact changes.
- control_services reports which stores it serves on stderr again —
tty7-server configures no log sink, and 'no machine tree' was
invisible exactly where it matters, on a headless box.
- The local link's first connect attempt is immediate instead of one
backoff step late; the observation-slot test withdraws its store so
it cannot swallow later tests' observations; and locked()'s poison
rationale now says what is actually guaranteed.
* feat(control): let clients mint workspace and tab identities on create
A window names its workspace — in the registry, the view file, and any
operation it queues — before its first round trip completes, and the same
holds for a tab the moment the user opens it. Making the daemon the only
minter would force every client to hold its edits until a reply carried
the real id back. Ids are uuids, so a client-minted one is as unique as a
daemon-minted one; WorkspaceCreate and TabCreate now carry an optional
client id, keep it when it is free, and refuse a duplicate rather than
adopt it. Absent (older callers, tests) the daemon mints as before.
* feat(ui): windows speak semantic tree operations for every structural change
The write path of the client migration: each window now keeps a mirror of
what the daemon's tree holds for its workspace, and save_session — the
funnel every structural change already passes through — diffs the window
against that mirror and sends the recovered operations (TabCreate,
PaneSplit, PaneClose, PaneReplace, TabMove, ratio and label ops) over the
workspace's control link: the LocalLink for this machine, the machine's
RemoteConnections entry otherwise. Consecutive saves differ by exactly one
user action, so the diff recovers that action rather than re-shipping the
layout; changes no single op expresses rebuild the affected tab whole,
matching the delta contract's own granularity.
The mirror advances by running the server's own tree surgery (PaneNode's
split/remove/replace are public now), and any disagreement — a refused op,
a dropped link — resolves by one shared recovery path: drop the queue,
re-pull WorkspaceTree, re-diff. Fresh spawns are invisible until their
pane id lands; land_pane's save is when their create goes out. GUI tabs
carry a client-minted TabId, and a primed mirror re-points tabs it
recognizes by their panes, so a rebuilt window adopts the daemon's tabs
instead of churning them.
Workspace-level facts ride along: focus touches, renames, and deletions
now reach the machine's tree too, and the divider drag finally persists
the ratio it lands on (it previously reached disk only as a passenger on
the next structural change).
session.json is still written in parallel; it retires with the read-path
migration.
* feat(ui): local windows restore by asking the daemon's tree
The read path: opening a known local workspace no longer rebuilds from
session.json synchronously. The window opens empty and a background pull
(MachineGet — the workspace's structure joined with the pane registry,
which is where the revival facts live) rebuilds it the moment the daemon
answers; against the local daemon that is milliseconds, so the empty
state is effectively one frame — the same shape a remote workspace's
connect-driven rebuild has always had.
The lowering from tree to window is the revival decision: a leaf whose
pane record says live re-attaches by id, a dead one lowers to an id-less
leaf carrying the record's cwd, SSH spec and agent resume — the exact
shape that makes the existing builder spawn a successor and type the
agent's --resume. The save that follows diffs the successor against the
mirror and sends PaneReplace, spending the old record; revival needed no
op code of its own.
Restored tabs keep their daemon tab ids (SessionTab grows a never-
persisted tree_id), so the first save addresses the daemon's tabs instead
of churning them. A tree with nothing for the workspace falls back once
to the client's cached layout, whose adoption re-populates the tree
through the ordinary diff — the whole of the best-effort import.
* feat(ui): live windows apply the machine's incremental layout deltas
The pump's event drain now lands ControlEvent::Layout instead of debug-
logging it: each delta advances this client's mirror (by the same
surgery the server ran) and then the live window showing the workspace —
renames, regrouping, moves, active-tab changes and ratio drags in place;
TabCreated by building the tab and attaching its (writer-spawned, so
live) panes; TabRestructured by rebuilding the one tab while reusing the
views of panes the window already shows, because re-attaching a pane
this window holds would steal its own stream. Origin exclusion means
every delta arriving is another client's edit, and applying it to window
and mirror in one step leaves the next local diff with nothing to echo.
A delta that will not apply cleanly — a tab the mirror never heard of, a
drifted window — falls back to re-pulling the workspace and rebuilding
the window from the authoritative tree, the same single recovery path
every other failure already uses.
* feat(daemon): report panes the machine tree no longer references
With the tree now populated by clients' semantic operations, the daemon
can finally see panes nothing references. A periodic sweep reports them —
log-only, deliberately: an unreferenced pane is not proof of a leak (a
native-SSH pane opened inside a remote workspace's window runs in this
daemon while belonging to the other machine's tree), and reclaiming one
wrongly kills a session the user is looking at. The sweep's interval
doubles as a grace period: a pane is reported only after being
unreferenced across two consecutive looks, so an adoption still in
flight is never flagged. Reclamation can be layered on once the log has
shown the false-positive rate is zero.
* feat(ui): remote workspaces read and write the machine tree like local ones
Local and remote are now the same shape end to end. A remote workspace
opens empty unconditionally (connected or not) and is filled by the same
tree hydration a local window uses; the connect supervisor's landing
replaces the opaque-record refresh with it — a blinked link relinks the
pane streams and hydrates whatever opened empty meanwhile, a replaced
server process resyncs the window from the tree, whose force-cleared
live flags are what make every leaf revive. The remote picker lists
workspaces from MachineGet, deriving names from the tree the way a
local workspace derives its own; creating one lets the hydration's
WorkspaceCreate mint it on the machine; the record push, pull, refresh
(WorkspaceChanged) and remote delete paths are gone client-side.
Windows that have not yet seen their machine's tree sync additively: a
window that opened empty ahead of its pull may add tabs but never prunes
ones it has not displayed, so its ignorance can no longer read as 'close
everything' — the diff takes an explicit scope, and only hydration (or a
deliberately authoritative open, like restore-off) grants the full one.
* refactor(core): retire the client-side pane-identity defenses
The machine tree made this whole family unnecessary, so it goes rather
than lingers: daemon_instance stamps (a restarted daemon's tree says
live=false about every pane — a fact, where the stamp was a heuristic),
forget_stale_pane_ids on both layers, dedupe_pane_ids (the daemon
refuses a pane appearing twice in its tree, so there is no duplicate to
mop up client-side), the claim/record instance plumbing, and the
whole-record halves of the storage split (to_remote_json,
apply_remote_json, REMOTE_OWNED_FIELDS, CLIENT_OWNED_FIELDS, and the
store's apply_remote / remote_payload), together with their tests.
forget_pane_ids stays for now: it clears the client's cached copy, which
still serves as the one-time import fallback until the view file slims
down to pure view state.
* refactor(ui): a local daemon restart rebuilds from the tree too
The tree file survives the restart and the fresh daemon force-clears
every pane's live flag, so the resync path already expresses exactly
what the hand-rolled saved-session rebuild did: every leaf revives as a
fresh shell in its recorded cwd with its agent resumed. The pull waits
out the local link reconnecting to the fresh daemon.
* docs(core): drop a stale reference to the retired record verbs
* fix(ui): close the review findings on the tree migration
Review fixes, worst first:
- Pane ids never alias across daemon restarts: the pane registry seeds
its counter past everything the persisted tree references. A fresh
process minting from 1 handed new shells ids that dead leaves still
claimed — the tree marked the wrong pane live, revival stalled forever
on 'already part of this machine's tree', and an attach by the stale
id stole another workspace's stream. Ids are names now, not slots.
- An empty window only licenses WorkspaceRemove once it is *informed*:
a window whose hydration has not answered is empty because it is
waiting, and closing or swapping it mid-pull was deleting populated
trees. Remote workspaces also hydrate regardless of the restore
setting — their panes are running sessions, not a saved layout, and
the restore-off swap used to open them empty-and-authoritative and
close every tab on the machine.
- Tabs whose panes are all still spawning are *held*, not pruned: they
are invisible in the desired tree without being absent, and the Full
diff was closing them (spending the records the landing spawns'
PaneReplace needed) on every remote revival.
- A preempted window stays passive under deltas: applying the usurper's
TabCreated/TabRestructured attached to their fresh panes and stole the
streams they were typing into. The mirror is dropped instead; taking
the workspace back re-pulls it whole.
- Delta TabClosed tracks the active tab by identity (closing a tab to
the left no longer shifts focus and pushes the wrong active tab back).
- The hydrate/resync path drops the op queue like desync does, so ops
computed against an abandoned mirror cannot drain after the snapshot.
- A rebuilt remote tab no longer matches a native-SSH leaf's *local*
pane id against remote ids; delta-applied ratios clamp to the GUI
band; async completions use get_mut so a forgotten window's sync state
is not resurrected.
* feat(ui): a per-machine mirror of each daemon's tree feeds the read surfaces
The switcher, the Window menu, the title bar, the rename seeds, the
stop/delete confirmation and the liveness sweep all answered their
questions (display name, subject path, pane ids, pane count) from the
client's cached copy of the layout. The machine's tree owns the layout
now, so a new per-host MachineMirrors global holds each machine's last
pulled tree — filled by a MachineGet whenever a control link comes up
(and for free off every hydration, which already pulls the whole
machine), advanced by the same Layout delta stream the windows consume,
plus explicit notes for this client's own operations, which origin
exclusion keeps out of that stream.
The readers move over wholesale. A machine not pulled yet reads as
not-knowing rather than a stale guess: pickers show the shared fallback
for a beat (against the local daemon the pull lands within a frame),
and the pane-count prompt says the machine could not be asked instead
of counting against a cache. tree_display_name moves out of the remote
picker into the mirror as display_name_of — it was always the tree
flavour of Workspace::display_name, and now everything shares it.
This is the read-model half of retiring the client's layout cache; the
persistence shrink to pure view state follows on top of it.
* refactor(ui): client persistence shrinks to pure window views
The client file stops carrying layout. session.json's Workspace — id,
name, a whole embedded Session, geometry, open, last_active, host —
becomes WindowView { id, window, open, last_active, host } in a fresh
views.json (no migration by design; an old session.json is simply
ignored, and its panes revive from the machine tree like any daemon
restart). Everything the embedded layout used to answer already moved
to the per-machine mirror, so this deletes the write half:
- WorkspaceStore::claim answers only the id; record shrinks to
record_geometry. claimable_session / record_session — the
reachability-gated layout cache — go entirely, and with them the
one-time empty-tree import in finish_hydration: with no cached copy
there is nothing to import, and the machine answering "no tabs" is
the layout.
- The user-set name is purely the machine's fact now. rename /
rename_locally leave the store; the chip and switcher renames fire
WorkspaceRename directly (tree_sync::rename_workspace), the
WorkspaceRenamed delta needs nothing from the window because the
mirror already applied it, and WorkspaceCreate seeds no name.
- forget_pane_ids / blank_pane_ids and the layout-derived getters
(display_name, dominant_repo, first_cwd, pane_count, pane_ids) are
deleted with their tests — each had grown a mirror-side twin.
- switch_workspace always hydrates: with the tree as the only layout
source, restore-off governs what launch comes back to, not what a
deliberate switcher pick shows.
The retired opaque record store loses its one test that asserted its
file parses as a client Workspaces document — that coupling is the
thing this migration ends, and the store itself is next to go.
* refactor(server): retire the opaque workspace record store
Clients stopped sending WorkspaceList/Get/Put/Delete when the tree
migration landed, so the coexistence scaffolding comes out:
- core::workspace_store is deleted. Attachment and the data-directory
resolution (TTY7_DATA_DIR, XDG fallback chain) move into
core::machine, which was already their only consumer; Attachment
loses its vestigial serde derives (it never crosses disk or wire).
- The control dialect drops the four record verbs, the ReplyOk::Json
payload they answered with, and the WorkspaceChanged event. Their
serde names (and the workspace-store capability bit) are recorded as
burned rather than reserved by any mechanism — the dialect has no
numbered slots to hold, so a comment at each site is the guard, plus
the handshake test asserting the bit never reappears.
- host::server loses Services.workspaces, the verb arms, the
per-connection store subscription and its WorkspaceChanged forwarder,
and the store half of attach/detach/teardown. Attachment data now
lives solely in the tree: a workspace the tree does not list records
no data half (the registry's live handles still move, so takeover
behaviour is unchanged), and it appears the moment the workspace
does. Services::with_workspaces/and_machine collapse into
with_machine; control_services becomes a single match.
- The attach/takeover tests move onto MachineStore wholesale, attaching
to workspaces created in a real tree; the record-store round-trip and
fan-out tests go (tests/machine_tree.rs has carried the tree
equivalents since the verbs landed), and tests/workspace_store.rs is
deleted with the serde_json dev-dependency that existed only for it.
machine.rs gains the two guarantees the old suite held uniquely: an
attachment dies with its workspace structurally, and the default path
resolution ends at the documented file.
- The GUI's dead WorkspaceChanged arm and every stale doc reference go.
* refactor(ui): rename RemoteConnections to HostLinks
Purely mechanical, plus the doc sentences that carry the model: the
table holds one control link per machine, and the local machine is a
machine like any other — its link just lives in its own global
(LocalLink) because it is in-process rather than wire-backed. The old
name framed the table as remote-only plumbing, which the tree
migration made false in spirit: local and remote windows speak the
same operations over whichever link their machine answers on.
* fix(ui): a tree-driven tab rebuild keeps the native-SSH split it cannot name
A native-SSH pane opened inside a remote workspace's window runs in
this client's own daemon and is deliberately absent from the remote
machine's tree (its local id would collide with an unrelated remote
pane). The TabRestructured rebuild therefore had no leaf for it and
dropped its view on the floor: the local session kept running,
invisible from every surface — a true orphan only the daemon's log-only
sweep would ever mention.
The rebuild now sets such leaves aside while harvesting reusable views
and appends each back as a fresh half-and-half split on the right once
the tree's own panes are built. The old split geometry is unknowable
from the delta (the tree never held it), so the appended shape is the
one a split created it in; the next save changes nothing, because the
diff already lowers a remote window without its ssh leaves.
The resync path (a delta that fails to apply, a replaced server) still
rebuilds the whole window from the tree and drops such views — that
path discards every view it has by design, and is left as a known
residual. TerminalView grows a test-only ssh-marked pane constructor so
the kept-split property is pinned by a gpui test.
* docs(core): finish pointing the last session.json references at views.json
* fix(ui): kick every local window's sync when the local link comes up
A window built while the local control link was still dialing parks as
Unprimed { dirty } — start_prime's unreachable arm leaves the retry to
"the reconnect-triggered save", but the local link supervisor never
triggered one. On a first launch (window built before the auto-spawned
daemon binds its socket) nothing else re-enters sync_window until the
next structural change, so quitting before one loses the window's
layout: the machine never heard of it.
Reproduced end-to-end on a scratch daemon: fresh launch, no user
action, quit — the relaunch came up empty. With the link supervisor
calling tree_sync::on_link_up on connect, the same launch syncs the
tree within one pump tick.
* fix(ui): read a deleted workspace's kill list before the removal blanks the mirror
delete_workspace fired WorkspaceRemove first, and fire_workspace_op folds
the removal into the machine mirror synchronously on its way out — so the
kill list stop_workspace_keeping then read off that mirror was always
empty, and 'Delete Workspace' ended zero of the sessions its confirm
prompt promised to end. The kill list is now read before the op fires,
and both destructive paths receive it explicitly so the ordering is a
signature rather than a convention.
* fix(control): bump both dialect versions and gate tree verbs on the machine-tree bit
The tree migration deleted four control verbs and added seventeen, but
CONTROL_VERSION stayed at 2 — two builds that cannot understand each
other's requests would have shaken hands as equals. It is now 3, with
the history entry the file's format asks for.
PROTOCOL_VERSION moves to 4 for the service change underneath: a
pre-tree 'tty7 --daemon' has no control listener at all, so a GUI from
this build silently adopting one connects its control link into the
void forever and every window hydrates from a tree that never answers.
The bump routes that meeting into ensure_running's existing
keep-or-restart prompt.
Clients now also consume the machine-tree capability bit before any
tree traffic: a connected peer without it (a server with no home
directory keeps serving files and panes) classifies as a distinct
'unserved' state that is logged once and skipped, instead of a refused
round trip per operation.
* fix(ui): preempted windows stay passive and take-back rebuilds from the tree
Two halves of the same takeover contract were broken.
A preempted window kept pushing: sync_window had no preemption check, so
a click on the read-only tab strip sent WorkspaceSetActiveTab against
the usurper's session, and the next save Full-diffed the stale layout —
rolling the usurper's edits back wholesale. sync_window now returns
early for a preempted workspace, and preemption itself drops the
window's queue, mirror and 'informed' licence (tree_sync::on_preempted,
shared with the delta path's existing reset).
Take Back never rebuilt: the recovery attach ran the ordinary IfEmpty
hydration, which skips any non-empty window — and a preempted window is
by definition non-empty with the pre-takeover layout. retry_now now
marks the workspace as reclaiming, and finish_attempt rebuilds marked
(or still-preempted) windows via Adopt::Replace, honouring the 'take
back re-pulls whole' promise the delta path documents.
* fix(ui): delta application survives pulls in flight
Three overlap bugs between the incremental delta stream and the full
pulls it has no ordering barrier with:
- A TabCreated straddling a pull was applied by both — the snapshot
already carried the tab, and the delta inserted a second copy into
the machine mirror and the window mirror, and rebuilt a second GUI
tab whose attach stole the pane's single stream from the window
itself. All three application sites now replace by id.
- A delta arriving while a window's prime/hydration was in flight was
applied to the window even though the mirror side skipped it — a
TabCreated landing in a still-empty window made finish_hydration
read 'the user got here first' and skip adopting the tree, leaving
the window with only the concurrently-created tab forever. Window
application is now gated on the mirror being primed; the pull's
snapshot carries the delta's effect.
- A prime answered after a newer cycle (hydration, desync, preemption)
replaced it would install its stale tree over a mirror that had since
advanced, and the next diff would re-emit the rollback as operations.
Every cycle now stamps an epoch, and pulls landing under an old one
are dropped.
* fix(ui): apply ratio deltas in the server's clamp band
set_gui_ratio clamped to 0.1-0.9 while the server accepts 0.05-0.95, so
another client's 0.07 arrived as 0.1 — and the next save's ratio diff
pushed the rewrite back at the machine, silently moving their divider.
* fix(core): machine-store hardening around seeds and unreadable files
- A PaneSeed entered the registry live:true unconditionally. A pane
that died between its spawn and its adopting operation had its death
observation dropped (note_pane_facts ignores panes the tree does not
hold), and nothing ever flipped the record back — the leaf claimed a
live pane forever and revival was never offered. The daemon now
installs a liveness probe on the store (registry-backed), consulted
at registration; without one (tests, clients) the seed is trusted.
- seed_ids_past computed max + 1, which panics a debug daemon at
startup when the persisted tree names u64::MAX. saturating_add parks
the counter at the ceiling instead.
- load_machine quarantined an unparseable file but not an unreadable
one: a read failure logged, started empty, and the first mutation
overwrote the very file that could not be read. Read failures now
quarantine too — by rename, since a copy would need the read
permission that just failed.
Also de-flakes the pre-existing spawn_writer test: the first write into
a freshly-closed socket can succeed before the kernel processes the
close, so the poll loop now keeps the writer fed until a write fails.
* feat(control): announce dropped layout deltas so lagged clients resync
A connection whose per-link delta queue overflowed lost an edit it will
never hear again — the server logged the drop, and the client mirrored
a tree it was no longer looking at until something else happened to
fail. The subscriber callback now flags the connection lagged, and the
layout forwarder sends the new ControlEvent::LayoutResync ahead of the
next delta it delivers (the flag is only ever set with a full queue
behind it, so the announcement never waits on a quiet tree). The client
answers by re-pulling the machine mirror and resyncing every window on
that machine — the same recovery an unappliable delta already uses,
announced instead of stumbled into. WatchOverflow is the precedent.
* fix(ui): a pure native-SSH tab is invisible to the tree, not held forever
Held means 'spawns are landing, wait before ordering' — but a remote
window's tab that is native-SSH through and through can never land: its
panes live in this client's daemon and are deliberately unnameable in
the remote machine's tree. Filing it as held made every diff return
before the ordering and active-tab passes, freezing tab order and
activation sync for the whole window for as long as the tab existed —
and a mixed tab whose last remote pane was closed kept its dead leaf on
the machine for ever, because the held id shielded the daemon tab from
the close.
Such tabs are now classified permanently invisible: not desired, not
held. Ordering resumes, and the mixed tab's daemon twin closes when its
last tree-visible pane goes. Pending leaves (a connecting spawn, an
empty slot) still read as held.
* docs(core): drop the dead instance helper, the stale title field, and two doc lies
- local_daemon_instance() lost its last caller when the client-side
pane-identity defenses were retired; deleted.
- DaemonVersion::instance's doc pointed at Workspace::daemon_instance
(deleted with the record store) and claimed pane ids restart from 1 —
no longer true of a tree-carrying daemon, which seeds its ids past
everything the tree names. Rewritten to describe what the field
actually backs now.
- PaneRecord::title claimed to label panes awaiting revival, but no
code ever wrote it: the pane's title is a live foreground-process
query at PaneInfo time, not state the facts path observes. The field
is deleted (serde-compatible: unknown fields are ignored on read) and
the decision recorded where it lived; revival labels derive from cwd
and agent.
* fix(ui): converge the tree after adopting a delta-created tab
Adopting a TabCreated delta whose pane is dead on arrival attaches
nothing and spawns a fresh pane under a new id — and nothing on the
delta path saved afterwards, so the tree kept the dead leaf: other
clients saw a dead tab, and a relaunch would spawn a second successor
beside the leaked first. Reproduced end-to-end (external client creates
a tab with an unspawned pane; the GUI adopted it and the tree never
learned the successor's id).
One sync_window after a clean apply closes it: free when window and
mirror agree (the diff is empty), and exactly the PaneReplace that
spends the dead record when adoption had to spawn.
* fix(core): review follow-ups on the daemon-owned tree
Nine findings from a review pass over the branch. One commit because
they cross the same files, and splitting them would leave an
intermediate that does not build on Windows.
- A dropped delta announced a LayoutResync and then delivered the
backlog behind it. The queue is FIFO, so everything still in it is
*older* than the gap: the peer re-pulled on the notice and was then
walked back through history it had already left — TabRestructured
restoring the shape a tab used to have, with window and mirror
agreeing on the stale answer so nothing recovered a second time. The
forwarder now drops the superseded queue and sends the resync in its
place.
- Pane facts persisted the whole document, with an fsync, from the PTY
reader thread — once per OSC 7, so once per prompt per pane — while
holding the lock that orders every other client's edits. A shell
looping over directories was a write per iteration. Observations
(pane facts, workspace_touch) now take Persist::Soon: the delta still
goes out at once, the file catches up within FACT_FLUSH_INTERVAL, and
the daemon flushes on the way out. The layout itself is never
deferred.
- An ordinary output chunk paid two AgentFacts clones and a
clone-to-compare for facts it could not have changed. Gated on the
signals that can move one, and the compare no longer clones.
- machine.json was created 0644, naming every workspace's directories,
the SSH user and host of every native-SSH pane, and each agent's
session id. It is written owner-only from the first instant the final
name exists, and a second corruption no longer overwrites the rescue
copy of the first.
- Windows had no control listener, so on the one platform where the
tree is the only layout store, tabs did not come back at all. It now
serves the dialect over the transport its pane socket already uses: a
loopback listener whose port and 256-bit token live in a user-private
control.port beside daemon.port — its own token, not the pane
endpoint's — refusing to rebind over a live one, since binding is
what writes the marker. run_daemon and the GUI's local link are one
code path again.
- Workspace names and paths came only from the machine's mirror, so a
laptop shut since Friday listed every row as "Untitled" with a blank
subtitle, in the picker whose whole job is offering workspaces on
machines that are asleep. WindowView carries the label and subject
the machine last gave, stamped on save and on detach; the tree still
wins whenever it answers.
- liveness_of read "the mirror has not been pulled yet" as Stopped,
which tells the user their sessions are gone on the strength of our
own ignorance. Unknown is what that state is for.
- A WorkspaceRemove that never reached its machine was a debug line,
though the client had already forgotten the workspace. It is now a
warning that says what was left where.
- MachineMirrors::install landed a pull without a repaint; the two tests
the record store's retirement took with it (a closed connection stops
being a subscriber, concurrent connections can all write) are back
against the tree; and CHANGELOG records the migration's one-time
layout loss and the Windows gap this closes.
Suites green: tty7-core 675, tty7 819, tty7-server 9/5/3/3/51, fmt and
clippy clean. The Windows listener is unverified by a compiler here — a
C dependency in the tree blocks cross-checking from macOS — so CI's
Windows job is its first build.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: thomas <thomas@thomass-Mini.lan>
|
||
|
|
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> |
||
|
|
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. |
||
|
|
208454e202 |
feat(remote): remote workspaces — a window that is one machine
Split the framework-free half of tty7 into `tty7-core` and add a headless
`tty7-server` built on it, so a workspace's filesystem, git and session state
can live on another machine while the GUI stays where it is.
- `crates/tty7-core`: wire protocol, session daemon, PTY, native SSH engine and
the domain model, with no gpui dependency. Module paths are unchanged.
- `crates/tty7-server`: the same daemon with no GUI attached, linked fully
static against musl and pushed onto the remote box. One dependency, on
purpose — a second one the GUI also needs belongs in core.
- `Host` trait + `HostId`/`HostRegistry`: every fs/git/watch call a workspace
makes goes through the machine it belongs to. `LocalHost` answers on this
box, `RemoteHost` over a routed control connection.
- `ui::host_ops`: the GUI's single door to a `Host`. Host calls block, so all
of them run on the background executor with the result landed on the UI
thread; de-duplication, staleness and error reporting live here rather than
at each call site. Enforced by a CI grep.
- Connect flow: home page → pick a configured SSH host → the machine's own
workspace list → a window bound to one workspace on it. Workspace switcher
groups by machine, this computer included.
- CI: static musl builds of `tty7-server` for x86_64/aarch64 via
cargo-zigbuild, a host-boundary grep, and version stamping factored out of
the nightly workflow. Both new jobs are non-required so branch protection
does not wedge open PRs.
Design and the interface contract it was built to are in
`docs/2026-07-27-remote-workspace-{design,impl-contract}.md`.
|
||
|
|
4107829bf8 |
fix(theme): finish the sweep — plain-button states and midtone grounds
Two gaps in the interaction-state change. `Theme::input` reaches the *outline* button path, which reads the field live, but not the plain one: gpui-component derives `tokens.button_hover` and `tokens.button_active` from `input` once, inside the `apply_config` that `Theme::change` runs — from the stock `#2f2f2f`, before any of `apply_theme`'s overrides exist. A snapshot never sees the fix, so a plain `Button` still hovered and pressed in that grey and `Button::selected` (the terminal search bar's `Aa` / `.*` toggles, the last two left in the app) filled from `tokens.secondary_active` the same way — ~1.03:1 on Dracula, i.e. issue #197 again, one snapshot removed from the field that fixed it. Point those state rungs at the window ladder. `tokens.button`, the resting fill, is deliberately left alone. `legible_ink` picked its extreme with `is_dark`, whose 0.5 luminance threshold is the wrong question: on a midtone ground (luminance 0.18…0.5) it says "dark" while black outreaches white, so a status seed was driven toward white and clamped there *below* the floor with its hue gone — precisely when a user most needs to tell an error from a warning. Take the reachable extreme instead, as `legible_foreground` already does. Every built-in sits far enough from the midpoint that this picks what `is_dark` did, so the derived palettes are unchanged; only an imported scheme could reach the divergent range. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD |
||
|
|
dcb8ba0c77 |
feat(theme): derive interaction state and status colors from the theme
Selected options were indistinguishable from their neighbours on every bundled theme, worst on Dracula where a segmented control's active segment measured 1.03:1 against its siblings (issue #197). The cause was not Dracula: the app had no state model. Interaction state was a set of fixed `mix(bg, fg, t)` ratios scattered across `neutrals()` and `.opacity()` multipliers at call sites, and the fields nobody had noticed — `input`, `ring`, `switch`, `switch_thumb`, `danger`, `warning`, `success`, `link` — silently kept gpui-component's stock values, greys and Tailwind hues with no relationship to the active theme. Introduce the state model the app was missing. `Surface` is the interaction-state ladder for one painting surface, and every rung is derived to hit a *contrast ratio* against that surface rather than a fixed blend ratio. A fixed blend makes the perceived step depend on the seed: selected-vs-resting ranged from 1.20:1 (Catppuccin Latte) to 1.47:1 (Dracula). Every theme now lands on 1.70-1.72:1. The target is 1.70 because that is where the already-signed-off Dracula highlight sits, so the change is a no-op on the theme the look was tuned against and a lift for the rest. Ladders are per surface. Menu rows paint on `popover`, not the window background, which is why the context-menu highlight measured 1.20:1 against the panel it actually sat on while nominally being the fill that reads fine on the terminal ground. A fill alone never communicates selection, so `Surface` carries the label colours too: take a surface, take both channels. The app had already learned this three times locally — the tab chip, the chrome tiles and the sidebar each grew a hand-written fill-plus-text pair — while every site that had not been hand-fixed shipped a fill and nothing else. `ink_on` keeps a selected label readable on its own fill; without it, raising a fill toward the foreground drags the label under the resting ones around it (4.14:1 on Latte). `Semantics` derives danger/warning/success/info/link from each theme's own ANSI-16 instead of stock Tailwind, so a danger marker and an error line of shell output are finally the same red. Conditioning drives a seed toward black or white rather than toward the foreground: routing through Rosé Pine Dawn's purple-grey foreground collapsed its red and yellow into two indistinguishable mauves. Switch toggles read inverted on every dark theme because the knob fell back through two unset fields to a stock near-black. The knob now takes the light end of the theme's axis, the unchecked track comes off the ladder, and the checked track takes the accent — hue is the only axis left once the knob has claimed lightness, which is why every system switch is coloured. `segmented` is hand-rolled. The stock `ButtonGroup::outline()` path gave the selected segment the same border and the same label colour as its siblings, leaving one fill as the entire signal, and exposes no way to vary weight. Nine tests pin the properties rather than the current taste, so retuning the four contrast constants does not force a test edit but abandoning the model does. |
||
|
|
a0d153c546 |
feat(ui): give the right panel and tab rail an overlay scrollbar
The detail panel (Info / Outline / Changes / Files), the remote SFTP listing and the tab rail all scrolled with gpui's `overflow_y_scroll()`, which paints nothing: a deep file tree or a long tab list gave no hint that there was more content, or where in it you were (issue #185). Hang gpui-component's `Scrollbar` over each of those containers through one shared helper (`ui::scrollbar`). The handle stays app-owned so `scroll_to_item` keeps working — that rules out gpui-component's `overflow_y_scrollbar()`, which mints its own handle internally. Theme the bar from the active theme's own background→foreground mix ladder rather than the stock fixed greys, and keep the track transparent so the thumb floats instead of laying a slab down the panel edge. Show mode follows the platform: auto-hide on macOS (and anywhere else the OS uses overlay scrollbars), permanently visible on Windows and Linux. gpui-component's own default picks `Hover` there, which only reveals the bar within 16px of the edge — that is the "no scrollbar at all" the issue reports from Windows. |
||
|
|
985a292513 |
fix(linux): stop theme_follow_system from panicking every launch
On Linux, `"theme_follow_system": true` made the app panic on launch with `RefCell already borrowed` from gpui's Wayland/X11 client, on both backends. Once a user turned "sync with system" on, the setting persisted and every subsequent launch died — a hard brick until `config.json` was hand-edited back. gpui's Linux backends dispatch a window's appearance-changed callback while the platform client's `RefCell` is already mutably borrowed: both XDP handlers hold `client.borrow_mut()` across `set_appearance`, which invokes the callback synchronously. `cx.window_appearance()` re-borrows that same cell. Our appearance observer called straight into `apply_theme` → `effective_preset_id` → `system_dark` → `cx.window_appearance()`, so it re-entered that borrow and panicked. Only while following the system: with the setting off the observer returns before reading the appearance, which is why the default install was fine. It reproduced on *every* launch because the XDG portal source emits one appearance event during startup. Cache the OS appearance in a `SystemAppearance` global instead. The observer fills it from `Window::appearance()` — the window's own cell, released before the callback runs — and `system_dark` reads the cache, so no platform read happens on the re-entrant path. macOS still re-seeds the cache from the platform right after `apply_theme` releases the native appearance pin, since the window reports the pin rather than the OS setting until then. Zed keeps a `SystemAppearance` global for the same reason, and gpui-component switched to `window.appearance()` over the same Linux panic (longbridge/gpui-component#104). Fixes #179 |
||
|
|
4ac7d30178 |
feat(ux): rebuild the menu bar, command palette, and Settings IA
The menu bar shipped four menus in the order App / Shell / Window / View with no Edit menu at all, so Copy and Paste existed nowhere but a right-click, and About / Check for Updates / Hide / Minimize / Help had no home. It now follows the macOS HIG's standard set. The palette listed 47 commands in three competing naming styles, ranked only by catalog order, with no grouping and no way to reach most of what the tab context menu could do. It now has one documented grammar, a scored fuzzy ranker, group bands with a frecency-driven Recent, and the commands it was missing. Settings had a three-row Shell page indistinguishable from Terminal, a seven-group Terminal page that had become a junk drawer, two different groups called "Window", and a search index that had drifted so far from the rendered rows that "opacity" and "completion" returned nothing. Also folds copy / cut / paste / undo onto one code path each, which fixed two real drift bugs: the right-click Paste skipped the image-paste branch that Cmd+V had, and Copy rendered disabled whenever the selection was in the prompt editor rather than the grid. |
||
|
|
7d7d3ce882 |
fix(windows): start fresh workspaces with a shell, and say where a closed one went
Review follow-ups on top of the multi-window work. - A brand-new workspace came up on the home page with no shell, because `claim` always hands back an (empty) session and the window treated that as "restore this". A first run and `New Workspace` now take the first-run path again and spawn a terminal; the launch that exists to show the workspace picker asks for an empty window explicitly (`FreshStart`). - The close-window prompt promised sessions "will be restored the next time you open tty7", which is no longer what happens — the workspace detaches and waits in the picker. Both it and the one-time detach hint now point at the title bar's workspace menu rather than the macOS Window menu, which does not exist on Windows or Linux. - `ToggleSftp` read the panel state off the config, which is now only what a *new* window starts with; it reads this window's own state. - `SelectWorkspace1..9` were unbindable: registered as actions but absent from the keymap tables. Added with no default chord (⌘1–9 is the tab row's). - `theme_commands`' doc comment had been captured by a function inserted above it, and the Window menu's slot→action mapping was a second copy of the title-bar chip's. - CHANGELOG: drop the ⌘1–9 claim (no such binding ships), and document the chrome tile sizing that rode along with this branch. |
||
|
|
e41afaf857 |
feat(windows): one window per workspace
tty7 had exactly one window, so `main` opened it inline and every app-wide duty — tray, menus, the quit hook — lived in `Tty7App`'s constructor. This splits those apart: a *workspace* is the persistent identity (tabs, splits, cwds, name) and a *window* is a transient view onto exactly one of them. - `ui::windows` — the app-level window registry and the single place that opens a window. Exactly one window per workspace is enforced there: the daemon gives each pane a single subscriber, so a second window on the same panes would silently steal the first's output. `open` focuses the existing window instead. New windows cascade so one never lands on top of another. - `WorkspaceStore` owns session.json, so windows never race each other as writers. Closing a window *detaches* — panes keep running in the daemon and the entry stays for the picker; `StopWorkspace` kills the sessions and keeps the layout; `DeleteWorkspace` also forgets it. - Window menu lists every workspace with a monogram badge and a liveness dot, ⌘1–9 for the first nine. Same list in the palette; closed ones also appear in a home-page picker with a coarse relative age. - Sidebar collapse and right-panel visibility move onto `Tty7App`, so toggling one window's chrome leaves the others alone; the config value becomes what a new window starts with. Panel *width* stays shared — a width is a preference, not a view state. - Tray, menus, and the quit hook now walk the registry rather than belonging to a single window. Protocol goes to v2: `RemoteKind::Wsl` is a new enum variant, which is not the additive change it looks like — the enums carry no `#[serde(other)]`, so a v1 peer fails the whole decode and drops the pane's connection. The handshake now catches that skew and offers a restart. |
||
|
|
1cd801e902 |
feat(theme): follow the OS appearance with separate light/dark themes
Add a "Sync with system" mode: when on, the active theme resolves from two user-picked slots (theme_preset_light / theme_preset_dark) by the current OS appearance and switches live when the OS mode flips. - config: theme_follow_system + theme_preset_light/theme_preset_dark - theme: effective_preset_id() resolver; release the native appearance pin while following (it would blind the OS-appearance reads) - app: observe_window_appearance re-applies the theme on OS flips; set_preset writes the slot matching the current appearance while following; explicit set_slot_preset for the Settings cards - settings: sync switch + one card per slot; the picker panel aims at the slot whose card opened it Closes #107 |
||
|
|
2dd817d8cd |
fix(theme): review follow-ups — diff overlay background, hot-reload window effects, docs
- Diff overlay paints the gradient/opacity-aware window background instead of the stale representative solid. - Config hot-reload now re-runs apply_theme with the window (blur flip, traffic-light re-pin) and re-syncs the Appearance opacity slider, so hand-edits to config.json / theme files take effect fully. - Document the theme background/window settings in features docs (EN + zh-CN). |
||
|
|
1a50cf0e60 |
feat(theme): render gradient and image backgrounds, global window opacity/blur
- Gradient backgrounds (vertical/horizontal two-stop) now actually render; previously only the first stop painted as a solid. - Background images composite over the background fill (cover-fit, per-image opacity), under all content. - Window opacity/blur become global Appearance settings (config overrides layered over per-theme defaults), editable for every theme with a follow-theme reset; the theme editor gains a background-image picker and image-opacity slider. - The window is created non-opaque and stays that way: on macOS 26 flipping a window to transparent after creation never reaches the compositor, so translucent themes rendered against black instead of the desktop. Fully opaque themes paint alpha-1.0 content, which is visually identical. - gpui-component's Root no longer paints a second (stale) background layer, and the terminal surface no longer repaints the theme background — the app root is the single owner, so alpha is applied exactly once. - to_yaml now serializes background_image, so editing a theme in-app no longer silently drops its image. |
||
|
|
41fbed1e30 |
fix(ui): SSH auth-sheet polish + softer primary buttons (#79)
* fix(ui): SSH auth-sheet polish + softer primary buttons - Remember toggle: real Checkbox instead of a full-width ghost button whose selected-state fill read as a grey bar across the whole card - auth sheet: tighten to the shared sheet padding (p_4 / gap_3), width 420 - dead-SSH pane: replace the top-left chip + reconnect notice (which overlaid the daemon's red failure line printed at top-left) with a single bottom-centered 'Disconnected — ⌘⇧R · Reconnect' bar, clear of the output - drop the connecting/authenticating top-left SSH chip entirely (the tab status dot carries the phase; the buffer shows connect progress) - soften primary buttons app-wide: fill from foreground nudged ~20% toward the background (a dark charcoal, not pure black) via the primary / button_primary token family Pairs with l0ng-ai/gpui-component@9484cf9 (checkbox: instant check, no fade), picked up by the Cargo.lock bump. * fix(ui): SFTP parent row is a ".." directory entry, not a "Go up" action The leading go-up row used an ArrowUp icon + muted "Go up" label, reading as a toolbar action stranded in the list. Style it like a directory entry — Folder icon + ".." name in the foreground ink — matching the rows below (the file-manager/WinRAR convention). Click still goes to the parent. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
c4bee13d83 |
feat(theme): file-based themes, in-app editor, and a UI/branding refresh (#54)
Replace compiled-in presets + colors.*/ansi_colors.* overrides with a file-based theme system: a serializable seed (bg/fg/accent/cursor/selection + ANSI-16, optional gradient/image/opacity/blur) with all chrome derived, light/dark inferred from luminance behind a WCAG guard, built-ins + user YAML + on-the-fly iTerm2 import via a hot-reloaded registry, and an in-app duplicate-to-edit color editor. Also: prompt-editor shift-click/word-drag selection and cross-platform word keys, ghostty-style tab labels, flat menu highlights, a redesigned app icon, the Background Service -> Daemon rename, and a gated TTY7_PROFILE build-timing probe. |
||
|
|
eeb8eed6ba | feat(theme): support ANSI color overrides (#37) | ||
|
|
c6e071362a |
fix(menu): soften context-menu and dropdown row hover to match the palette (#24)
gpui-component's MenuItemElement highlights the hovered/selected row from `tokens.accent` + `accent_foreground`. tty7's theme never overrode those, so menu rows fell back to the stock saturated accent — a hard highlight out of step with the app's soft mix-based palette. Point the accent tokens (and the plain `accent`/`accent_foreground` fields the input completion / code-action popovers read) at `list_active` (mix 0.17), the same soft fill the command palette uses for its selected row, and keep the hover text at `foreground` so it stays legible on the low-contrast fill. Right- click menu, "+" dropdown and command palette now share one hover language. Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
c2472c3ab3 | Enhance terminal functionality with new keyboard shortcuts and actions. Added ⌘K (Ctrl+K) for clearing the screen and scrollback, and ⌘⏎ for toggling fullscreen, while adjusting the maximize pane shortcut to ⌘⇧⏎. Updated keybindings and command palette to reflect these changes, ensuring a more intuitive user experience. | ||
|
|
22e1ab1694 |
tty7: a GPU-rendered, daemon-backed terminal in pure Rust
tty7 is split into two Rust processes: a persistent daemon that owns the shells and a GPU-rendered client that talks to it over a local socket. Because the shells live in the daemon, quitting and reopening the app leaves the session intact — detach and reattach, no tmux required. - Persistent sessions — the daemon holds the PTYs and child processes, so closing a window or swapping in a new build never takes a shell down. - Performance — an 11 MB `cat` completes in 95 ms and DOOM-fire renders at 888 fps; the daemon drains the PTY at device speed off the render path. - Shell-aware — new tabs and splits open in the current working directory; zsh, bash, fish, and PowerShell are set up automatically. - Enhanced prompt — inline completion, syntax highlighting, history, and in-terminal search, with rich flag/subcommand signatures for common tools. - Tabs, resizable splits, a command palette, click-to-open links, desktop notifications, eight themes, and CJK/IME input. Native builds for macOS, Windows, and Linux. Built on Zed's gpui and Alacritty's VT core. |