A list counts an item it has not laid out yet as zero tall, so the flattened
row list reported a patch of any length as about one viewport of content. The
scrollbar reads that height: its thumb filled the track, and dragging it from
the top to the bottom travelled 248px into an 800-line diff and stopped —
`scrollbar_drag_start_height` freezes the height for the duration of a drag,
so the gesture could not even grow into the rows it uncovered. The overlay
had a bar that could reach anywhere before the rows were virtualised.
`measure_all` would settle it by laying out every row on the first frame,
which is the cost the row list exists to avoid. Count the rows below the fold
at `DIFF_LINE_H` instead — the 19px both views already give a line of a patch
— through `ListState::with_size_hint`, a new gpui fork patch. File and hunk
rows are a few pixels taller, so the estimate runs short by a few percent
until they are measured; a diff is overwhelmingly its lines.
`a_long_patch_builds_only_the_rows_on_screen` now also asserts the bar's
reach. Without the hint it measures 248.5px against 802 rows.
* fix(input-bar): read column widths from unicode-width, not a hand-rolled table
The input bar scored every character against a hand-written list of code-point
ranges. Anything the list missed counted as one plain column, so `🀄`, `⌚` and
every combining mark pulled the rest of the row a column left, and clicks,
wrapping and the caret all landed off by that much (#701).
The grid gets its widths from `unicode-width` by way of `alacritty_terminal`,
so read the same table. Zero-width characters then need a cell to ride in:
group each base with the marks that follow it, so the shaper sees one run and
composes `é` instead of setting `e` and its accent side by side. An emoji
presentation sequence is re-scored as a string the way the grid re-scores it,
so `❤️` is two columns in the bar as well.
A ZWJ sequence stays two cells on purpose — that is what the grid makes of it,
and composing it here would put the bar a column off from where the text lands.
* fix(input-bar): derive click and wrap geometry from the cells the bar draws
`input_cells` re-scores an emoji presentation sequence to two columns and
hands a stranded combining mark a column of its own, but `input_char_positions`
kept walking the text character by character — so `❤️` was drawn two columns
wide and counted as one. Everything geometric read the short count: a click on
`X` in `❤️X` selected past it, wrapping broke a column early, and vertical
caret motion aimed at the wrong column.
Walk the same cells instead. Only the base of a cell carries the width, so a
click still lands on the base rather than a mark riding on it, and the riders
sit at the column the caret takes after the cell.
A cell now also tints as a unit when a selection covers any character in it —
it is one glyph, so half-highlighting it drew a mark unselected next to its
selected base.
* fix(update): surface a failed install instead of silently re-prompting (#540)
The GUI quits as soon as tty7-updater is spawned, so an install that
failed inside the helper left a trace only in update.log — and because
launching the helper had already cleared the prompt state, the next
check offered the same version again, and again. The failure mode the
user saw was an app that nagged about an update it could not install.
The helper now writes update-outcome.json beside update.json on every
terminal path it can still reach, and the next GUI launch folds it into
the update state: a failure shows in Settings with the installer's own
reason until dismissed and stops the version from re-prompting on its
own; a success at the running version retires a failure an earlier
attempt recorded. A leftover result that exists but cannot be parsed is
reported rather than dropped — something ran, and "unreadable" is a
result too.
The same change moves the config directory off the environment and onto
the command line (--config-dir). An elevated child process does not
inherit the spawner's environment, so TTY7_CONFIG_DIR would have fallen
back to the administrator's config directory exactly in the
over-the-shoulder case — the groundwork this lays for #504. The updater
re-exports the variable for the helper children it spawns itself, so
the relaunched app keeps answering for the same config directory.
* feat(update): update an all-users Windows install through one UAC prompt (#504)
An Inno install under C:\Program Files could not be replaced in place:
the updater ran the release Setup as the signed-in user, which either
installed a second, per-user copy beside the real one or let Inno
re-launch itself elevated — a bare UAC prompt for an unsigned
executable in %TEMP%, seconds after the GUI had vanished. So the
layout was refused outright and told to download by hand.
It now updates itself, with the split the design in #504 settled on:
one UAC prompt covering two privileged stages, and one watcher that
is never elevated at all.
- The GUI probes the *installed* updater for the new verbs by running
it ("capabilities"), so a side-loaded or downgraded binary answers
for itself instead of being trusted by version number. An updater
that predates the verbs exits with a usage error, and the install
falls back to pointing at the release page exactly as before — the
first release carrying this still updates the old way, and the one
after it updates itself.
- The prompt dialog says the UAC prompt is coming before the app
quits, and stops offering "Install on Next Launch": nobody is there
to answer a prompt before the first window exists. The same guard
keeps a staged plan from being armed for the next launch, and
apply_pending_at_launch leaves an elevation-needing plan staged
rather than raising a windowless prompt at boot.
- "Install now" spawns the watcher first (medium integrity, the
signed-in user's token, so the relaunched app is never elevated),
then ShellExecuteEx "runas" on the installed updater — the trust
root a medium-integrity process cannot rewrite. Everything the
elevated half needs crosses as command-line arguments, because an
over-the-shoulder child inherits neither the environment nor the
user's profile. The package's expected SHA-256 crosses the same
way, from the checksums the GUI already holds in memory, so a
payload and its checksums file cannot be rewritten together behind
the IL boundary.
- The privileged first stage re-verifies the payload against that
digest, pins its helper byte-for-byte to the installed updater,
stages both in a fresh administrator-only %ProgramData% directory
(an explicit SDDL DACL, swept of stale directories first), and only
then runs the install stage — which runs Setup silently, writes the
outcome file, and never touches the app binary itself. The watcher
follows the chain through the status file and pid liveness
(ERROR_ACCESS_DENIED from OpenProcess still means "alive" across
accounts), then relaunches the app de-elevated and probes that it
actually came up.
- Declining the UAC prompt is not an error: the watcher is reaped,
nothing ran elevated, and the staged package simply waits in
Settings.
Persisted plans from before this protocol serde-default a plan
version that is_usable rejects, so a stale plan is discarded instead
of failing against a helper that would not understand its arguments.
The installer script's explorer-menu registration gains skipifsilent:
a silent run *is* this update path, and launching the app there would
write the menu into the administrator's hive under over-the-shoulder
elevation.
One note on the test suite: ui::remote_connect's
a_routed_auth_prompt_carries_the_machine_that_raised_it fails under
parallel test execution on this machine both with and without this
change — a pre-existing flake, unrelated.
* fix(update): run the UAC request off the UI thread
Real-machine verification of the elevated chain caught this on the
first click: ShellExecuteExW pumps the calling thread's message loop
while the shell raises the consent prompt (its change notifications
re-enter the window), and from the UI thread that re-enters gpui with
its App already borrowed — the process aborts on a RefCell
double-borrow before anything ever elevates. The launch — watcher
spawn included, so the pairing stays atomic — now runs on the
background executor, and only the bookkeeping (quit / decline /
failure) comes back to the UI thread.
* fix(update): throttle a failed version instead of retiring it (#540)
Per the review on #540: a failed install must not keep the version
retired via last_prompted — record last_prompted plus a fresh
remind_after deadline (the same three days "Later" uses), so the
version asks again once the reminder expires. should_prompt already
treats "last_prompted matches, reminder expired" as prompt-again, so
no logic change is needed there, and the pinned
a_failure_lets_the_version_prompt_again test still holds.
Also write update-outcome.json *before* relaunching the previous app
on the macOS/Windows/portable non-elevated paths: the GUI that comes
up next is exactly the process that absorbs the outcome, and it used
to be relaunched before the failure existed on disk. The elevated
chain is unchanged — its watcher already waited for the file.
* fix(update): let only the elevated updater's own image name the trust root
Three holes on the privileged side of the #504 chain, all of the same
shape: a value that decides what runs elevated was taken from the
medium-integrity caller.
- `elevated-stage` pinned the staged helper against
`<install-dir>\tty7-updater.exe`, where `<install-dir>` is a
command-line argument. Both halves of that comparison were the
caller's to choose: name a directory holding two copies of any
binary and the pin passes, then stage 2 runs it elevated. The stage
now derives the installation from its own image — UAC pointed the
prompt at `{app}\tty7-updater.exe`, so `current_exe` is the one path
nothing below the boundary could have written — and passes that on
to stage 2. A caller that named a different directory only gets a
line in the log.
- The staging directory's DACL let no standard user in, but its parent
did: `%ProgramData%` grants Users the right to create directories,
and the creator owns what it creates. A pre-created
`%ProgramData%\tty7` gave its owner delete-child over the
administrator-only staging inside it — enough to rename the verified
staging aside and drop an identical name of their own into the gap
between the digest check and the execute. The root is now created
with the same protected descriptor, taking down whatever holds the
name first; `CreateDirectoryW` applies a descriptor only when it is
the one creating the directory, so succeeding is the proof. The
per-run sweep goes with it — the root's removal takes the leftovers.
- The GUI aimed the prompt at the updater the *plan* named, and
`update.json` sits in the user's config directory. It now aims at
the installation this process runs from, so the binary the prompt
names is the binary that starts.
Also quote the elevated command line the way `CommandLineToArgvW`
reads it back: a backslash escapes only in front of a quote, so a
config directory ending in one used to escape its own closing quote
and swallow every argument after it, `--result-file` — the file the
watcher waits on — included.
* test(update): pin the elevated stage's trust root to its own image
A regression test for the shape of the hole rather than the hole: if
`installed_root` ever goes back to reading an argument, the pin the
elevated stage runs before executing the staged helper stops meaning
anything, and nothing else in the suite would notice.
* fix(update): bring tty7 back when the elevated chain never reports
The watcher's two timeouts returned without relaunching. Every other
way out of the chain ends with the app back on screen, but a stage 1
that died before writing its status or its outcome — killed, crashed,
an AppInfo service that never delivered it — left the user with the
GUI already quit, nothing to replace it, and nothing said. Same for an
install still running an hour later.
Both paths now end the way the others do: an outcome the watcher wrote
itself, then the relaunch. The synthesized outcome is written whether
or not the relaunch succeeds, which also closes the same gap on the
pre-existing "the elevated updater exited without recording a result"
path — the next launch can name what happened instead of silently
offering the version again.
What kept those paths from relaunching was the risk of a second window
beside a GUI that is still up: a declined prompt leaves this process
running, and the kill that reaps its watcher can lose. The watcher now
takes the GUI's pid and opens a handle to it at startup — while the
GUI is provably alive, since it is sitting in ShellExecuteExW waiting
on the prompt — so the number cannot be recycled out from under it.
Before relaunching, a GUI that is still alive is waited out for 30
seconds: one that is quitting (a chain that failed fast can beat it
out the door) is gone well inside that and gets its relaunch, one that
is staying is recognized as staying and gets neither a relaunch nor a
failure record it did not earn. A live process always answers to its
own pid, so the check cannot be wrong in the direction that
double-launches.
Also give the Japanese elevation notice its closing 。
* fix(update): poll the parent out across the elevation account boundary
Under an over-the-shoulder elevation the install stage runs as the
administrator, and OpenProcess on the signed-in user's GUI answers
ERROR_ACCESS_DENIED - the same boundary pid_alive already documents from
the watcher's side. wait_for_exit treated that as a fatal error, so the
chain recovered and reported a failure before Setup ever ran. The wait
now degrades to polling the pid until it stops answering, bounded so a
recycled pid cannot hold the install hostage forever.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: l0ng-ai <l0ng-ai@users.noreply.github.com>
The sidebar's elision measures against real glyph widths but slices by
`char`, so it can satisfy every width check and still hand back a torn
cluster. Scanning budgets from 30px to 200px over emoji fixtures, 48
widths produced output no font can render as intended:
"release-…\u{200d}👩\u{200d}👧" a joiner with nothing in front of it
"lon…\u{fe0f}" the variation selector lands on the ellipsis
"abcdef…🇳ghijklmnopqr" half a flag, which renders as a bare N
A tab title carrying an emoji is not exotic — plenty of TUIs and coding
agents put one there — and the second case is the same U+FE0F this repo
already carries an alacritty patch for.
`elide_keep_edges`, the tail-only fallback, and `short_title`'s 40-glyph
clamp now index grapheme clusters. `elide_path_keep_tail` cuts on `/`
and was already safe. Widths are unchanged: clusters are measured the
same way chars were, so every existing elision test still passes on the
same fixtures.
`unicode-segmentation` is already in the tree via gpui; pinning it here
adds one line to Cargo.lock and no new code.
Tests assert the property rather than the symptom: whatever survives on
either side of the ellipsis has to be a cluster-aligned prefix and
suffix of the input. That catches any tear, not just the three shapes
found here. Written first, confirmed failing on all three cut sites, and
green after.
* 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>
Removing the Outline panel (#374 / #375) took away the only reader of the
client-side command marks. The scanner kept running on every batch of PTY
output, and it was the one scanner that forced the batch to be split before it
reached the emulator, so it was not free.
Gone: `MarkScanner`, `Marks`, `CommandMark`, `record_mark`, `Cut::Mark` and the
tests that only covered them. With the cursor cut as the sole cut left, the
offset sort is a no-op and goes too — `ParkedCursorScanner` already reports in
ascending order — and `Cut` itself collapses into a plain `CursorCut`.
Kept: `zle_tok` and `mode_tok` read the same OSC 133 bytes and are load-bearing
for `zle_reading` / `shell_vi_mode`, including the deliberate live-vs-snapshot
split. Daemon-side OSC 133 handling is untouched.
Dropping `marks().clear()` left `clear_scrollback` with no anchored-state
invalidation at all, and it never had any for the other store that needs it:
kitty image placements are anchored to an absolute scrollback row, so purging
the history moves every anchor and the frame paints over unrelated text or
resolves past the viewport, with no redraw coming since the daemon does not
replay out-of-band image frames. Clear the image store there, as the reattach
path already does, and route the purge through
`Term::clear_screen(ClearMode::Saved)` so a selection reaching into the
discarded rows is invalidated instead of clamping onto the viewport.
Closes#378
ConPTY emits no repaint after a resize; conhost silently re-anchors its
layout and keeps painting with absolute cursor addresses computed against
it. Measured live: growing the window keeps rows and cursor pinned and
opens blank rows below, and shrinking scrolls the last written row to the
new bottom. The grid resized the alacritty way instead, so after a
maximize every absolute-CUP paint landed mid-screen inside the old output.
The vendored alacritty_terminal now has a conpty_resize mode mirroring
conhost's model (fork rev 1276f12); every Windows pane opts in.
Separately, a resize during a burst of output reflowed ahead of the
backlog (up to the gate's 16 MiB of old-width bytes). The daemon now
echoes a Size frame to the controller at the exact stream position where
the PTY geometry changes, and a client that probes the new resize-echo
feature defers its reflow to that marker. Remote routes and older daemons
keep the reflow-at-request-time path.
Desktop notifications now carry the pane they came from: clicking one reveals
that pane's window, tab and split. Windows shows a WinRT toast with an
`Activated` handler, macOS uses mac-notification-sys' click response, and both
route through the existing tray dispatch channel. Linux keeps the plain
notify-rust path.
Titles gained context — an agent name or the machine, then the workspace — and
bodies name the command or agent alongside the duration, all of it translated.
Notification text is sanitized on every path: it comes off the terminal, and a
stray control byte used to make the Windows toast XML fail to parse and lose the
notification outright.
Co-authored-by: Hongwei Qin <exqinhongwei@outlook.com>
The per-window taskbar overlay badge (#355, for #199) is removed, and with
it the in-flight follow-up that was making its green "finished a turn"
state reachable: the feature is not wanted. Nothing shipped — the badge
only ever existed in Unreleased — so this is a plain removal rather than a
deprecation, and its CHANGELOG entry goes with it instead of gaining a
"Removed" counterpart.
What goes: `ui::taskbar` and its `ITaskbarList3::SetOverlayIcon` poll, the
`taskbar_status_icon` config flag and its Settings → Window & Tabs row and
strings, `Tty7App::taskbar_signals`, `TerminalView::shell_busy` /
`RemoteTerminal::shell_busy` (the overlay was their only caller), the
`raw-window-handle` dependency and the `Win32_UI_WindowsAndMessaging`
feature it needed, and the feature docs in both languages. A stale
`taskbar_status_icon` left in someone's `config.json` is ignored, as any
unknown key is.
The tray badge and the in-window status dots are untouched; they were
always the ones the taskbar was mirroring.
ring_system_bell() was macOS-only and returned false everywhere else, so
on Windows and Linux the Audible mode fell straight through to its visual
fallback: Visual, Audible, and the new Both were three names for one
behavior. Windows has MessageBeep, so two of those three now differ.
MB_OK plays the "Default Beep" scheme entry, which follows the user's
choice in Sound Settings rather than synthesizing a fixed tone at the
speaker the way Beep() does. The Win32 metadata files MessageBeep under
Diagnostics::Debug despite it being a user32 export, hence the extra
windows-sys feature; no new crate and no dbghelp.
Linux is left on the flash fallback on purpose: libcanberra and PipeWire
are runtime links away and XBell does nothing under Wayland.
Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* feat(windows): taskbar status overlay per window (#199)
Stamp a colored status dot on each window's taskbar button using the
same palette as the in-window agent dots:
- blue while a shell command or agent is working,
- amber when an agent is waiting on the user,
- green when work finishes while the window is unfocused (cleared on activation).
Adds a `taskbar_status_icon` setting (default on, Windows only) and a
Settings -> Window & Tabs row. The overlay is updated by a foreground
poll that aggregates agent status and shell busy state across each
window's panes, diffing against the current taskbar badge and only
calling ITaskbarList3::SetOverlayIcon when the badge changes.
Includes unit tests for overlay priority and the done-while-unfocused
edge tracking.
* fix(taskbar): retry a failed overlay instead of caching it as drawn
Four fixes on top of the overlay:
- A failed SetOverlayIcon was still recorded in `shown`, so a badge the
taskbar never took was remembered as drawn and never retried. Stamp now
reports success, and a failure drops the interface so the next tick
re-creates it — which is also what an Explorer restart needs.
- `create_failed` was a permanent latch: one CoCreateInstance failure
killed the badge for the whole process, though Explorer may simply not
be up yet when the first window opens. Use the tray's attempts/cooldown
backoff instead, which this module otherwise copies.
- The overlay's accessibility description was hard-coded English in an
app that localizes everything else. Reuse the panel and tray strings.
- Render the dot at 32px, not 16. SetOverlayIcon wants 16x16 at 96 dpi,
so at 150%/200% scaling the shell upscaled a 16px icon; `tray::icon`
already renders at 32 off macOS for the same reason.
Also drops the Win32_Graphics_Gdi feature: CreateIcon, DestroyIcon and
HICON all live in Win32_UI_WindowsAndMessaging, and the build and the
taskbar tests pass without it.
Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
* feat(updater): add windows online updates
* feat(updater): support online updates for windows portable zip builds
f
* feat(updater): support online updates for nightly build
* fix(updater): strengthen post-download update verification
* feat(updater): support explicit stable and nightly channel switching
* fix(i18n): localize update settings ui
* fix(settings): prevent slider value labels from wrapping
* feat(updater): drop the nightly channel, refuse all-users Windows installs
Follow-up to the Windows updater work on this branch, applying maintainer
review.
Nightly is a build channel, not an update channel. The updater consults
`/releases/latest` again and nothing else, so it behaves on Windows exactly
as it already does on macOS: a Nightly build is offered the stable release
that supersedes it and graduates out of the prerelease, and no rolling
prerelease can become a source of code that gets executed on a user's
machine. Removed with it: the `UpdateChannel` enum and its version-string
inference, the `tags/nightly` query, the cross-channel version-ordering
bypass, the Settings → About channel row, the rolling-tag
`update-manifest.json` and the i18n keys that only served them.
`parse_version` and `is_update_available` are byte-identical to main again.
Nightly builds are untouched, and still carry tty7-updater plus the macOS
update archive — a Nightly user needs a working helper to reach the stable
release that replaces their build.
An all-users Windows installation is no longer updated in place. Running the
release Setup silently as the signed-in user cannot replace
`C:\Program Files\tty7`: Inno resolves `{autopf}` to `%LocalAppData%\Programs`
and installs a second copy beside the real one, or re-launches itself
elevated and puts a bare UAC prompt for an unsigned executable in `%TEMP%` in
front of a user whose GUI just vanished. tty7 declines both and points at the
release page. Detection reads Inno's own `HKLM` state for the frozen AppId and
independently probes whether the directory accepts writes, so a relocated or
pruned installation is caught too; the decision is a pure function with unit
tests, and it is re-checked before the download as well as during it.
Release and Nightly now verify the Windows packages they just built, mirroring
the macOS update-archive step: the install marker, tty7-updater.exe, the ZIP
layout the updater will accept and the PE versions it will demand. Every fact
the updater checks on the user's machine after downloading is checked here
instead, so a packaging mistake fails the build.
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>
* fix(windows): brand toast notifications with a tty7 AUMID (#339)
* fix(windows): only write the toast shortcut where it is ours to write
The AUMID shortcut was rewritten on every launch, which broke two cases
the review caught on a real machine.
An elevated install owns `%ProgramData%\...\tty7.lnk`, so writing a
per-user copy listed "tty7" twice in the Start Menu and left an orphan
pointing at a deleted exe once the uninstaller had removed only its own.
And `cargo run` repointed the installed shortcut at `target\debug`,
permanently, for anyone who both installs tty7 and builds it.
So decide before writing. An all-users shortcut settles the question by
itself — branded if the installer stamped our AUMID on it, otherwise we
stay on the PowerShell identity, because the alternative is littering a
Start Menu we cannot clean up. Otherwise we refresh the single per-user
`tty7.lnk` Inno's default install owns anyway, and only when it is not
already ours, and never from a cargo build directory. A dev build still
brands the process for taskbar grouping, and still gets branded toasts
when an install left a stamped shortcut behind — Windows asks that the
AUMID be registered, not that it point at the process using it.
Reading a shortcut back needs `IShellLinkW::GetPath`, hence the
`Win32_Storage_FileSystem` feature; `SLGP_RAWPATH` keeps it from chasing
a moved target over the network.
Also close the window this opened. The shell indexes a new `.lnk`
asynchronously and, for an AUMID it has not seen, `Toast::show()`
reports success and drops the toast — measured, it does not return an
error. A shortcut we wrote seconds ago is therefore not yet proof of
anything, so toasts keep the PowerShell identity for half a minute after
we write one: ugly beats invisible.
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>
* add CLI support for opening directories in new tabs
f
* feat(windows): add optional windows explorer context menus
f
* fix(gui): restore missing windows and reject lossy paths
* fix(windows): harden explorer menu registration and native path handling
* fix(cli): preserve native GUI paths on Windows
---------
Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
The CLI's own --help calls it "built for coding agents", but `capture` handed
back the daemon's raw PTY bytes, which is the least readable thing it emits,
and every verb panicked when its reader hung up.
`capture --plain` replays those bytes through a terminal grid instead of
stripping escapes from them, using the same alacritty_terminal rev the GUI
renders panes with. The difference is not cosmetic: only the grid knows that a
break at the pane's width was a wrap rather than a newline, that a CR meant
"overwrite this line" rather than "end it", and which cell a wide char shares
with its spacer. A regex gets the easy 90% and then invents the rest — on one
real pane it turned 1193 lines into 2806.
The size each segment needs comes for free: the daemon already sends
DaemonMsg::Size right before every Snapshot, and the CLI was discarding it.
Panes here measure 249 and 86 columns, so the hardcoded 120 would have wrapped
both in the wrong places. Observing still resizes nothing.
The pipe fix is two mechanisms with one contract. On Unix SIGPIPE goes back to
its default disposition, which covers every write site at once and ends the
process the way it ends `cat` (141). Windows has no such signal, so stdio::out
recognizes the hung-up write and leaves quietly. Before this, 16 of 19 verbs
printed a panic and a backtrace note for `tty7 ls | head -1`; `run` instead
reported it as a failure with exit 1.
Also adds skills/tty7, the Claude skill for driving this CLI. It shipped with a
Python ANSI stripper, which is what prompted --plain; the script is gone.
alacritty_terminal moves to [workspace.dependencies] so the GUI and the CLI
cannot drift onto two revs of the fork.
The `tty7` CLI was built by every release run and thrown away: all four
bundle scripts copied only `tty7-app`, and the upload glob covers `dist/`,
which the CLI never reached. Nothing put it on PATH either, so the
agent-facing half of the product was unreachable from a shipped install.
Bundle it on all four platforms, and have the GUI link it up itself rather
than hiding the step behind a menu item most people never find.
The install has two halves. The environment half prepends the CLI's
directory to this process's PATH before the daemon is spawned, so every
pane inherits it — that alone makes `tty7` work where agents actually run,
writes nothing to disk, and behaves the same everywhere. The on-disk half
symlinks into a directory already on PATH (Unix) or appends to
HKCU\Environment (Windows), and is allowed to fail.
Candidate directories are a fixed list intersected with PATH, not the first
writable entry on it: pyenv/rbenv/asdf/mise shim directories sit at the
front of PATH on many machines and are writable, and anything dropped there
is deleted on the next rehash — silently, days later.
Debug builds get the environment half only. `target/debug` holds a `tty7`
too, so otherwise a `cargo run` would repoint the developer's real `tty7`
at a debug binary, and each isolated dev-verify instance would rewrite the
PATH of the machine it is meant to stay away from.
- workspace: tty7-cli joins default-members, so a bare root cargo test runs it
- run --keep files the pane into its workspace via TabCreate (and refuses to
keep a pane no workspace would list); --ws help says what it really does
- server start|stop|restart|logs refuse -m instead of silently acting locally
- server start kills the spawned process when it never opens its endpoints
- -m over a down link is refused instead of redialing with auto auth
- capture help tells the truth: raw ANSI bytes, last ring segment by default
- a missed exit-code probe exits 1 with a stderr note, not a fabricated code
- TTY7_SOCKET is honored: control dials it, the pane endpoint is its sibling
- attach's success JSON says attached, not detached_from
- e2e daemons ride a KILL_ON_JOB_CLOSE Job Object on Windows, so a hard-killed
harness cannot leak servers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv
The package name and every display name ("tty7" in menus, tray, .desktop
Name, CFBundleName, installer AppName, shortcuts) stay as they were; only
the executable file is now tty7-app / tty7-app.exe, per docs/cli-design.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014JPaaZVK7rfQPKyrymzsYv
* feat(remote): keep a remote workspace whole across reopens and restarts
Reopening a remote workspace — or coming back to one whose `tty7-server`
had been replaced — landed on a screen of `tty7 — disconnected` panes with
their coding-agent conversations gone. Several independent holes added up
to that; this closes them together, and picks up the surrounding work the
same session produced.
**Telling a restarted server from a blinked link.** `ControlHelloOk` now
carries an `instance` minted once per server *process*. Nothing else in
the handshake changes across a restart — `build` and both dialect numbers
survive it — so a reconnect had no way to know its `pane_id`s were dead.
It does now: a different instance rebuilds the window from its layout
(same tabs and splits, fresh shells in the saved cwds) instead of
re-attaching to a process that is gone. An absent instance means *unknown*
and is never read as a restart.
**An attach can now fail.** `Attach` has no synchronous reply, so the
client returned `Ok` unconditionally and the daemon's `Error` frame was
read much later by the reader thread, which has no arm for it — the pane
then landed in the *link is down* state instead of falling back to a fresh
shell. The client now reads far enough into the reply to classify it on
the kind byte (the snapshot behind it can be megabytes) and hands those
bytes to the reader thread, so a successful attach loses none of its
replay. Local and remote attaches get different waits: the local one is on
the UI thread.
**The agent session survives to be resumed.** `TerminalView` raises
`AgentSessionChanged` when the pane's agent reports a new native session
id, so the layout on file catches up instead of waiting for the user to
happen to open a tab. A pane that is still connecting now carries its
agent through `PendingSpawn` — a save landing in that window used to write
`agent: null` over the record — and `land_pane` sends `--resume` when the
attach turned out to need a fresh shell.
**Ending sessions says so on file.** "End Sessions" kills the panes and
then drops their ids from the record, pushing the cleared layout to the
machine that owns it (design §10: the remote's copy wins, so a local-only
clear would be undone by the next open — the open this exists for).
**The new-tab dropdown lists the window's machine.** `Host::shells` and a
`Shells` control request (dialect v2) make the "+" menu a property of the
machine the window is bound to. A remote window filled from this
computer's `/etc/shells` offered `/bin/zsh` on a box whose zsh is
elsewhere, and every pick failed to spawn.
**An install reports its bytes.** The download and the SFTP upload each
report progress, relayed to the client over the routed connection as a
`RoutePrompt::InstallProgress`, and painted as a bar under the machine's
row in the switcher. ~8 MB across two hops behind the word "connecting…"
was indistinguishable from a hang.
**The installer compares dialects, not version strings.** `tty7-server
--protocol` prints what a binary speaks without starting it, so a connect
adopts an already-running server it can talk to rather than prompting
about a build difference and uploading 8 MB the machine did not need.
**Switcher.** A machine's `⋯` menu holds "New Workspace" (it was a row
under every machine, pushing the list a quarter of a card down) and a new
"Disconnect", which drops the connection and leaves the windows open and
read-only. The suspension lasts exactly as long as that machine has a
window on it.
Also drops three design/contract docs for the now-shipped remote-workspace
work.
* fix(session): stop one workspace's panes from being restored into another
A restart put a copy of one workspace's seven tabs — cwds, layout and
recorded agent sessions — in front of another workspace's own tabs, and
auto-resumed every one of those agents a second time: six `claude
--resume <id>` pairs running in parallel against the same conversations,
one set per window. The record-level corruption that seeded it is still
unattributed, but every mechanism that let it propagate, amplify, or go
unnoticed is closable, and this closes them.
**Panes now know their owner.** `Spawn` can carry the workspace the pane
is created for; the daemon stores it immutably and reports it in
`List`'s `PaneInfo.owner`. Restore refuses to re-attach a pane another
workspace owns (`pane_attachable`) — before this, a saved id landing on
somebody else's live pane attached silently, which is how one window
could pick up another's shells. The field rides a new `SPAWN_OWNED`
frame with a struct payload (the legacy spawn payloads are positional
tuples an old daemon cannot grow), gated on a new `pane-owner` feature
string: a client only sends it to a daemon that advertises it, so the
legacy kinds stay byte-for-byte what old daemons expect. A pane with no
recorded owner stays attachable by anyone — that is the pre-field
behavior, not a new risk.
**Saved pane ids are bound to the daemon process that issued them.**
`DaemonVersion` now carries an `instance` minted once per process (the
local twin of the control hello's), the GUI caches it at the
`ensure_running` handshake, and each local workspace records it as
`daemon_instance` beside its layout. Claiming a workspace whose ids came
from a different instance blanks them first: daemon pane ids restart
from 1, so after a reboot every saved id points at whatever unrelated
shell holds the number now, and the aliveness check cannot tell a
survivor from a squatter. A blank on either side means "cannot tell" and
never trips it. Unlike the duplicate-claim case below, this path keeps
the agent resume — the pane is genuinely gone with its daemon, and the
fresh shell resuming the conversation is the feature.
**A duplicate claim loses its agent resume along with its pane id.**
`dedupe_pane_ids` kept the loser's layout *and* its
`agent_session_id`, so the blanked leaves took restore's spawn-fresh
path and auto-typed `claude --resume` for conversations the winning
workspace's panes were still running — the doubling above. The winner
keeps the panes and the resume; the loser keeps only cwds.
**Cross-workspace saves are caught at the write.** Every terminal view
remembers the workspace whose window created it, and `save_session`
logs an error naming both ids if a window ever records a pane created
for a different workspace — the tripwire for the still-unattributed
seed corruption, so a recurrence is caught in the act instead of
reconstructed from `session.json` archaeology days later.
Wire compatibility both ways: `PaneInfo.owner`, `DaemonVersion.instance`
and `Workspace.daemon_instance` are `#[serde(default)]` struct fields
(old peers' JSON decodes, new fields are ignored by old readers), and
`SPAWN_OWNED` is feature-gated as above. `daemon_instance` is
client-owned in the design-§10 storage split — it names the local
daemon, and the field-census test pins the classification.
* fix(session): resume the agent when a local pane dies mid-restore
`session_to_pane` decided whether to send a coding agent's `--resume`
from `restore.is_none()` — i.e. from whether the pane looked alive when
the restore started. But `alive_panes_on` runs one `List` at the top of
the restore, while the attaches happen per leaf afterwards. A pane that
exited in between failed its attach, fell back to a fresh shell inside
`spawn_shell_terminal_in`, and then landed in the `restore.is_some()`
arm: an empty shell with its conversation dropped.
`ShellParts.restored` already answers this exactly, and the remote path
already reads it in `land_pane`. Carry it onto `TerminalView` so the
synchronous local path can read it too, and branch on that instead of
re-deriving the answer from a set that may be stale by the time it is
used.
No behaviour change on the paths that were already correct: a view that
was never restoring anything reports `restored: false`, which is the
same answer `restore.is_none()` gave them.
* fix(remote): check the server instance against the record, not just memory
A remote workspace's pane ids were only guarded against server restarts
by `RemoteLinks::instances`, an in-memory map. On the first connect after
the client starts, every machine is a first sighting, so `server_restarted`
answers false — and a `tty7-server` that was replaced while the client was
closed sails straight through. Its pane ids restart from 1, so the saved
ones now name unrelated shells, and the reconnect attaches to them: the
exact id-reuse failure the local side already guards against.
`Workspace::daemon_instance` was local-only for the stated reason that a
remote server's identity is tracked live per connection. That tracking is
correct but not sufficient — it cannot survive the client restart that
makes the question worth asking.
So the field now means the same thing on both sides: which process minted
the pane ids in this record. `WorkspaceStore::serving_instance` picks the
local daemon or the far machine's server depending on the workspace, and
`finish_attempt` compares it per workspace before deciding to re-attach or
rebuild. It stays client-owned: it records what *this* client last saw, so
two clients on one remote workspace each keep their own and neither may
overwrite the other's.
An unreachable machine still records nothing, which is what keeps a good
stamp from being erased with `None` — that would disarm the next check.
Also in these three files: the §N references to the deleted design docs,
cleaned up as part of the sweep in the following commit.
* docs: drop the references to the deleted design documents
The three documents this branch removed were cited ~280 times: `design
§10`, `contract §8`, `§17` and friends in comments, five references by
file path in code and manifests, five in CI workflows and one in the
release skill. Every one of them now points at nothing.
Rewritten rather than merely stripped, because most were not decoration:
"design §10 makes the remote's `workspaces.json` the authority" becomes a
statement in its own right, and the several that carried a Chinese phrase
from the document as their justification say the same thing in English
instead. Where the reference was purely parenthetical it is simply gone.
Not touched: `PRD §7.1`, `brief §8` and the like, which name documents
this branch did not remove and were already external before it, and the
`RFC 4648 §10` test-vector citation, which is a real specification.
The `host boundary` CI job loses `(§10.6)` from its name. It is not one of
the required checks, so branch protection is unaffected.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(terminal): stop wide glyphs overlapping after fullwidth punctuation
gpui's apply_force_width_to_layout tells a base glyph from a zero-advance
combining mark by whether the shaped x advanced past half the forced
width, and CJK fullwidth punctuation fails that test (U+FF08 advances
~0.47 em against a 0.6 em half-slot). In a batched wide run the glyph
after such a character was classified as a mark and painted on top of
it. Shape each wide glyph on its own line instead: the first glyph of a
line is unconditionally a base, so the heuristic never misfires.
* perf(terminal): intern wide-segment strings via char_string
Each wide glyph now shapes alone, so its text is a single-char string —
reuse the char_string memo instead of allocating a fresh String per cell
per frame. The interned SharedString is also what keys gpui's line
layout cache, so a CJK-dense repaint allocates nothing.
* perf(terminal): skip MarkScanner's Text state ahead with SIMD memchr
The scanner runs over every batch the client receives, and ordinary
output — where the only byte that matters is ESC — dominates each one.
Skip to the next ESC with memchr instead of stepping per byte, exactly
as tty7-core's OscTokenizer already does: measured on an 8 MB batch of
plausible output, 1.6 GB/s became 8.3 GB/s.
Declare memchr for the root crate — it left with the OSC tokenizer's
move down to tty7-core, and this is the first use since.
* fix(terminal): advance segment_row past each wide glyph
The unbatching change dropped the `col += 2` along with the batching
loop it lived in, so the wide-glyph arm pushed its segment and looped on
the same column forever, growing `segs` until allocation failed — the
6 GiB abort on the Windows CI runner, and a machine-freezing memory
climb under a local `cargo test`.
---------
Co-authored-by: lizhi <lizhi20@xiaomi.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
resvg 0.47 landed in tty7 (#227) and the gpui fork (#237), but
gpui-component still declared its own resvg = 0.45.1, keeping a
legacy resvg/usvg/tiny-skia 0.45/0.11 stack in the tree. The fork now
pins 0.47 (l0ng-ai/gpui-component@2264ff99 — no source changes needed;
its only resvg user, the Windows native-menu rasterizer, uses APIs
unchanged across the bump), so this moves the pin and drops the last
duplicate: the lockfile now carries a single resvg/usvg/tiny-skia
stack at 0.47/0.12, and `cargo tree -i resvg@0.45.1` matches nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #227 bumped tty7's direct resvg to 0.47 while the gpui fork still
pinned 0.45, so the tree compiled two resvg/usvg/tiny-skia stacks. The
fork's tty7 branch now carries resvg 0.47 (l0ng-ai/zed@3aac3ef); move
the gpui pin there so gpui's SVG renderer and tty7's tray-icon
rasterizer share one 0.47 stack again.
gpui-component still declares its own resvg 0.45.1 (semver-incompatible
with 0.47), so one legacy 0.45 stack remains until that fork catches up
- noted in the manifest comments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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`.
Dependabot only rewrote Cargo.lock, but the manifest still required
resvg 0.45, so every `cargo build --locked` CI job failed with
"cannot update the lock file". Bump the requirement to 0.47 and update
the pin-rationale comment: gpui/gpui-component still carry resvg
0.45.x, so a second resvg/usvg/tiny-skia stack now compiles until the
fork catches up (no type conflicts; only image::RgbaImage crosses the
tty7/gpui boundary).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dependabot bumped sha2 to 0.11.0 in Cargo.lock but left the manifest
requiring 0.10, so --locked builds failed on every platform. tty7's
only sha2 usage (Sha512::digest in core::keychain) is unchanged in 0.11.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every character came out as a different character, one for one, consistently
— it read as a broken locale or a mangled encoding, and it was neither.
Hack, the bundled default, has no CJK, so those cells are shaped through the
font-fallback chain. gpui's Windows backend then threw away the face
DirectWrite shaped the run with and looked a fresh one up by family, weight and
style. That round trip mapped DirectWrite's italic to oblique — the enum is
numbered OBLIQUE = 1, ITALIC = 2, and the mapping had them the other way around
— so an italic fallback face resolved to a request for an oblique one, and a
family with no oblique face (Maple Mono NF CN, first in our Windows chain) came
back as its upright face instead. The glyph indices were right; the outlines
they indexed belonged to a different face, at a fixed glyph-id skew.
Fixed upstream in our gpui fork by registering the face DirectWrite actually
chose rather than re-deriving one, which also closes a latent use-after-free in
the same cache: it keyed fonts by a raw pointer to a face nothing held a
reference to, so a released face could be aliased by any later allocation.
Bumps the fork pin; no tty7 code changes. Covered there by two tests in
`gpui_windows::direct_write` — one asserting a shaped run's glyphs round-trip
through the font id the run reports, one asserting every font-face cache key is
owned by the font it maps to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An emoji written as base + U+FE0F rendered wrong twice over. Both halves
came from the variation selector arriving as a zero-width combining mark
after the column budget was already spent.
Width, in the `alacritty_terminal` pin (bumped to the fork's b79e704):
`input` reserves columns one `char` at a time, so a base whose East
Asian Width is Neutral -- U+2764 in `❤️`, U+1F5C2 in `🗂️`, U+26A0 in
`⚠️` -- kept the single column it was given, its glyph bled over the
next cell, and every column after it on the line shifted left by one.
The fork re-scores the sequence with `UnicodeWidthStr`, which is where
UTS #51's width-2 rule lives, and widens the cell to match.
Presentation, here: `snapshot_cell` copied only `cell.c` into
`RenderCell`, so `cell.zerowidth()` was dropped before the shaper ever
saw it. `❤` and `❤\u{FE0F}` reached gpui as the same string and picked
the same text-presentation face -- a black heart where every other
terminal shows a red one. `RenderCell` now carries the marks and a new
`RowSeg::Cluster` shapes them with their base. That restores every
combining mark, not just the selectors: `e` + U+0301 was being dropped
the same way.
A marked cell never joins a batched run. Marks add characters without
adding columns, which is exactly the correspondence `force_width` uses
to pin one glyph per column in a `Run` or `Wide` segment.
Fixes#203.
Option-as-Meta had no effect for anyone typing with a CJK input source.
The whole setting was dead for them: with Pinyin selected, macOS reports
Option chords as printable text (Option+B composes the special
character), so gpui routed them to the IME before the key handler ran.
The IME committed the composed character and swallowed the event --
on_key_down never ran, and reshape_option_keystroke never got a say.
Switching to ABC made it work again, which is why this looked
intermittent. Verified on the wire: with Pinyin active Option+F/Option+B
put c692 / e288ab on the PTY where ESC f / ESC b belong.
The routing decision lives in gpui's macOS backend and is asked once per
view, with no keystroke in hand, so it could not answer "IME for text,
but not for this chord". gpui now comes from our fork, whose one commit
passes the keystroke to prefers_ime_for_printable_keys; the default
implementation ignores it, so no existing handler changes behavior. The
terminal answers per key: an Option chord with the setting on stays on
the dispatch path, everything else still prefers the IME.
Gated on the setting deliberately. With Option-as-Meta off the chord is
text input and the IME is the right owner -- it is what makes dead keys
(Option+E then E -> e-acute) compose at all.
The fork is wired in with [patch] on the source rather than by editing
the gpui pins, because gpui-component declares its own gpui from the
upstream URL and a pin swap would put two incompatible copies of gpui in
the tree. Fetching a repo that size needs the git CLI; cargo's built-in
libgit2 transfer times out partway through.
Fixes#177
Enabling `kitty_keyboard` (#184) made an upstream `alacritty_terminal` bug
reachable from any foreground program. `push_keyboard_mode` caps its stack by
removing from `title_stack` instead of `keyboard_mode_stack` -- a copy-paste
slip from `push_title` that compiles because both are `Vec`s and the removed
value only feeds a `trace!`.
Two consequences. Each overflowing push silently drops a saved window title, so
a later XTPOPTITLE restores the wrong one. And once the title stack is empty,
`Vec::remove(0)` panics: 4097 unpopped `CSI > 1 u` pushes -- roughly 20KB of
output -- kill the `tty7-remote-reader` thread and freeze the pane. The depth
cap never trimmed `keyboard_mode_stack` at all, so it was doing nothing.
Hostile output is not required. A TUI that pushes without popping (per redraw,
per keypress) reaches 4096 on its own in a long session.
Pin our fork of Zed's fork instead: `tty7` is Zed's `fcf32fe` plus the one-word
fix. No `[patch]` section is needed -- tty7 is the only crate in the tree that
depends on `alacritty_terminal`, so a plain URL/rev swap cannot split it into
two incompatible copies the way the gpui pin would.
The regression test guards the pin rather than our own code: it pushes past the
4096 cap and then queries, using the reply as a liveness probe. Verified to fail
against the unpatched rev.
Still present on alacritty master as of 852e971. Drop the fork once it lands
upstream.
Opening a `.rs` file in the code panel silently spawned rust-analyzer,
which then indexed the whole workspace — hundreds of megabytes of RAM and
a busy core — with no setting to turn it off. A terminal emulator should
not do that to its user on a click, and rather than add a flag to disable
something nobody asked for, the integration goes.
Removed: the JSON-RPC client and reader thread (`ui::lsp`), the per-server
registry, the completion / hover / definition providers installed on the
buffer, document sync (didOpen/didChange/didSave/didClose), diagnostics,
Go to Definition (F12), Find References (⇧F12) and its drawer, and the
status bar's server indicator. With them go the `lsp-types`, `ropey` and
`url` dependencies — all three were used only by this code (they remain in
the lock file as transitive deps of gpui-component and gpui, which is
expected).
Kept, and deliberately so:
- **Syntax highlighting**, which is tree-sitter, not LSP: gpui-component's
`tree-sitter-languages` feature, `InputState::code_editor(language)` and
`language_for_path` are all untouched. It is static, in-process, and
costs nothing beyond parsing the open buffer.
- ⌘S save, dirty tracking, the external-change watcher and its conflict
banner, markdown preview, soft wrap, and open-from-the-file-tree.
The module header now records *why* there is no language server, so the
next person to reach for one finds the reasoning instead of a gap.
Net −975 lines.
The branch had `gpui-component` pointed at a sibling checkout by absolute
path, which is why every CI job failed at manifest load. Point it back at
the fork's `tty7` branch (now carrying the custom-button label-color fix
the chrome tiles depend on) with the `tree-sitter-languages` feature, and
re-lock.
Review fixes on top:
- **Changes tab churned.** `right_panel_invalidate` dropped the cached
diff on every `GitStatusCache` notification — including unrelated
repos' — so the list blanked to "Loading…" and spawned a fresh
`git diff` several times a second while a pane produced output.
Replaced by `right_panel_refresh_changes`, which compares branch and
totals first and re-probes in place, mirroring the diff overlay.
- **Changes tab could wedge on "Loading…".** A probe dropped because the
cwd changed mid-flight left `diff_cwd` set and `diff` empty, and the
render path only spawns when the cwd *changes* — so nothing re-probed.
Spawn when nothing is cached and nothing is in flight.
- **Find references blocked the UI thread.** `cx.spawn_in` runs on the
main thread; the up-to-200 `read_to_string`s for the row previews now
run on the background executor, as the comment already claimed.
- **LSP frames could be lost or reordered at startup.** `send` checked
`ready` outside the `queued` lock, so a frame could park behind a
handshake that had just finished and never go out. `ready` now flips
under that lock in `mark_ready_and_flush`.
- `MarkScanner`'s ESC-in-payload branch bypassed the payload cap, so a
stream of bare ESCs inside an unterminated OSC grew the buffer without
bound.
- The file tree's search frontier used `Vec::remove(0)`; a wide tree made
that quadratic. `VecDeque`.
- `procs()` documented a pane check it didn't make; it takes the pane id
and makes it.
- Four doc comments had been orphaned onto newly inserted functions
(`pty`, `smooth_scroll`, `foreground_agent`, `file_expanded`).
jieba's dictionary costs ~55 MB resident and ~130 ms to build, and it was
built eagerly on every terminal-view creation regardless of the
`smart_select` setting or whether the user ever selects CJK text.
macOS already ships a Chinese lexicon in CFStringTokenizer that matches
jieba on most prose, is locale-independent (identical output for current /
NULL / zh_CN / en_US), and segments Japanese and Korean properly where
jieba shreds them into single characters. Make the OS tokenizer the
primary path and keep jieba only as the fallback for platforms with no
such API:
- gate the dependency behind `cfg(not(target_os = "macos"))` so the
embedded dictionary isn't even linked into the macOS build
- never warm eagerly; the first CJK double-click kicks the build off in
the background and settles for the unsegmented run, so the UI thread
never blocks on it
- skip jieba for runs containing kana or hangul, where selecting the
whole run beats per-character tokens
Also honor `Config::smart_select` in the prompt's command editor, which
did bracket pairing, CJK segmentation and mixed-script narrowing even
with the Settings toggle off.
- Double-click expands to the whole URL, email, file path, sci-notation
number, identifier chain, OSC 8 hyperlink run, or matching bracket/quote
pair containing the clicked word. Candidates only ever grow the plain
word selection, so nothing regresses below the stock word behavior.
- Chinese segments with jieba's dictionary on all platforms; Kana/Hangul
use CFStringTokenizer on macOS. The table builds lazily on a background
thread so the first double-click never pays the cost.
- Latin words glued to CJK text narrow to the clicked script's sub-run
instead of selecting the mixed blob.
- Bracket pairs (ASCII and full-width) and symmetric quotes (parity
matched) select through their match, in the grid and prompt editor alike.
- Shift+click extends the existing grid selection instead of restarting.
- Word separators are configurable (word_separators, shared by grid and
prompt editor); new 'Smart selection' toggle in Settings > Terminal.
Replace the orange window-and-cursor identity with the "Duo" mark: two
offset session panes (mint #3FDD8C behind, ink #17171A in front) with a
prompt chevron, on a cream tile following the Big Sur icon grid.
- app-icon.svg is the master; app-icon.png, tty7.icns and favicon.ico
are re-rendered from it (rsvg-convert + iconutil + PIL)
- logo.svg/logo.png/logo@256.png carry the tile-less transparent mark
- tray.svg redrawn as the same mark in template form: solid front pane
with the chevron masked out, back pane at partial alpha
- bare (non-bundled) macOS binaries now set the Dock icon at runtime
via NSApplication.setApplicationIconImage, so cargo dev/run shows the
logo instead of the generic executable icon
- README version badge and the social preview switch to the new brand
green; social preview copy re-aligned with the current README slogan